vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Test the merge result before landing it
e5478085e5b53a2892d0649d6f07c6f0a5653378 · Keenan Nemetz · 11d ago · parent db5e4382
Test the merge result before landing it A protected branch could receive a non-fast-forward landing whose tree was never built: the gate asked for checks on the request head, and the merge commit was made at landing time. A pushed merge with the right parents but extra content was accepted too. Publish the merge commit under refs/vole/candidates/, test it as the base branch, and make the gate require its results. Approvals name the head and the base branch, so a moved base retests without a new approval. A merger who clicks while that run is still going waits for it instead of clicking again. A pushed merge commit now has to carry exactly the tree a clean merge would produce. Closes: 01M61QQ56TESQ1HTMB1Z5C3733
server/ssh/status.go +91
| @@ -0,0 +1,91 @@ | |||
| 1 | package ssh | ||
| 2 | |||
| 3 | // `vole change show` asks the instance whether the landing tree has been tested. | ||
| 4 | // A clone can see the commits and the refs, but not the approvals or the run | ||
| 5 | // results that decide whether a landing is allowed, so the answer comes from the | ||
| 6 | // same plan the merge button and the push hook use. | ||
| 7 | |||
| 8 | import ( | ||
| 9 | "fmt" | ||
| 10 | "strings" | ||
| 11 | "time" | ||
| 12 | |||
| 13 | xssh "golang.org/x/crypto/ssh" | ||
| 14 | |||
| 15 | "github.com/rodent-software/vole/ci/runs" | ||
| 16 | "github.com/rodent-software/vole/forge/access" | ||
| 17 | "github.com/rodent-software/vole/forge/landing" | ||
| 18 | "github.com/rodent-software/vole/internal/format" | ||
| 19 | "github.com/rodent-software/vole/metadata" | ||
| 20 | ) | ||
| 21 | |||
| 22 | func (s *Server) serveChangeStatus(ch xssh.Channel, argv []string, user, remote string) int { | ||
| 23 | if len(argv) != 3 { | ||
| 24 | fmt.Fprint(ch, "vole: usage: vole-change-status <repository> <request-id>\n") | ||
| 25 | return 129 | ||
| 26 | } | ||
| 27 | name, ok := repoName(argv[1]) | ||
| 28 | if !ok { | ||
| 29 | fmt.Fprint(ch, "vole: invalid repository name\n") | ||
| 30 | return 129 | ||
| 31 | } | ||
| 32 | id := strings.TrimSpace(argv[2]) | ||
| 33 | m, err := s.cfg.Repos.Open(name) | ||
| 34 | if err != nil { | ||
| 35 | fmt.Fprintf(ch, "vole: repository %q not found\n", name) | ||
| 36 | return 1 | ||
| 37 | } | ||
| 38 | a, err := m.ACL() | ||
| 39 | if err != nil { | ||
| 40 | fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name) | ||
| 41 | return 1 | ||
| 42 | } | ||
| 43 | role := a.EffectiveRole(user, s.cfg.Identity != nil && s.cfg.Identity.IsAdmin(user)) | ||
| 44 | if !role.AtLeast(access.Read) { | ||
| 45 | fmt.Fprintf(ch, "vole: %s may not read change requests in %s\n", user, name) | ||
| 46 | return 1 | ||
| 47 | } | ||
| 48 | req, err := metadata.GetRequest(m.Git(), id) | ||
| 49 | if err != nil { | ||
| 50 | fmt.Fprintf(ch, "vole: %v\n", err) | ||
| 51 | return 1 | ||
| 52 | } | ||
| 53 | if !req.Open() { | ||
| 54 | fmt.Fprintf(ch, "combined: not needed (%s)\n", req.State) | ||
| 55 | return 0 | ||
| 56 | } | ||
| 57 | gate := landing.Gate{ | ||
| 58 | Repo: m.Git(), Name: m.Name, ACL: a, | ||
| 59 | Actor: user, Role: role, | ||
| 60 | Runs: func() (*runs.Store, error) { return s.cfg.Runs, nil }, | ||
| 61 | } | ||
| 62 | plan, err := landing.Prepare(gate, req.ID, user, time.Now().UTC()) | ||
| 63 | if err != nil { | ||
| 64 | fmt.Fprintf(ch, "combined: cannot tell (%v)\n", err) | ||
| 65 | return 0 | ||
| 66 | } | ||
| 67 | switch { | ||
| 68 | case len(plan.Conflicts) > 0: | ||
| 69 | fmt.Fprintf(ch, "combined: conflict with %s in %s\n", plan.Base, strings.Join(plan.Conflicts, ", ")) | ||
| 70 | case !plan.MergeCommit: | ||
| 71 | fmt.Fprint(ch, "combined: not needed (fast-forward)\n") | ||
| 72 | case plan.Ready: | ||
| 73 | fmt.Fprintf(ch, "combined: passed %s\n", format.SHA(plan.NewTip)) | ||
| 74 | case !plan.Checks: | ||
| 75 | fmt.Fprintf(ch, "combined: no checks required %s\n", format.SHA(plan.NewTip)) | ||
| 76 | case len(plan.Gaps) == 0: | ||
| 77 | fmt.Fprintf(ch, "combined: untested %s\n", format.SHA(plan.NewTip)) | ||
| 78 | case plan.Pending: | ||
| 79 | fmt.Fprintf(ch, "combined: testing %s\n", format.SHA(plan.NewTip)) | ||
| 80 | default: | ||
| 81 | fmt.Fprintf(ch, "combined: failed %s\n", format.SHA(plan.NewTip)) | ||
| 82 | } | ||
| 83 | if plan.Blocked != "" { | ||
| 84 | fmt.Fprintf(ch, "landing: blocked (%s)\n", plan.Blocked) | ||
| 85 | } else if plan.Ready { | ||
| 86 | fmt.Fprint(ch, "landing: ready\n") | ||
| 87 | } else if len(plan.Gaps) > 0 { | ||
| 88 | fmt.Fprint(ch, "landing: waiting for the combined result\n") | ||
| 89 | } | ||
| 90 | return 0 | ||
| 91 | } | ||