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.
|
e5478085
Keenan Nemetz
11d ago
|
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" | |
|
92d8afc8
Keenan Nemetz
1h ago
|
10 | "io" |
| 11 | "sort" | |
|
e5478085
Keenan Nemetz
11d ago
|
12 | "strings" |
| 13 | "time" | |
| 14 | ||
| 15 | xssh "golang.org/x/crypto/ssh" | |
| 16 | ||
|
d37d301f
Keenan Nemetz
19h ago
|
17 | "rodent.software/vole/ci/runs" |
| 18 | "rodent.software/vole/forge/access" | |
| 19 | "rodent.software/vole/forge/landing" | |
| 20 | "rodent.software/vole/internal/format" | |
| 21 | "rodent.software/vole/metadata" | |
|
e5478085
Keenan Nemetz
11d ago
|
22 | ) |
| 23 | ||
| 24 | func (s *Server) serveChangeStatus(ch xssh.Channel, argv []string, user, remote string) int { | |
| 25 | if len(argv) != 3 { | |
| 26 | fmt.Fprint(ch, "vole: usage: vole-change-status <repository> <request-id>\n") | |
| 27 | return 129 | |
| 28 | } | |
| 29 | name, ok := repoName(argv[1]) | |
| 30 | if !ok { | |
| 31 | fmt.Fprint(ch, "vole: invalid repository name\n") | |
| 32 | return 129 | |
| 33 | } | |
| 34 | id := strings.TrimSpace(argv[2]) | |
| 35 | m, err := s.cfg.Repos.Open(name) | |
| 36 | if err != nil { | |
| 37 | fmt.Fprintf(ch, "vole: repository %q not found\n", name) | |
| 38 | return 1 | |
| 39 | } | |
| 40 | a, err := m.ACL() | |
| 41 | if err != nil { | |
| 42 | fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name) | |
| 43 | return 1 | |
| 44 | } | |
| 45 | role := a.EffectiveRole(user, s.cfg.Identity != nil && s.cfg.Identity.IsAdmin(user)) | |
| 46 | if !role.AtLeast(access.Read) { | |
| 47 | fmt.Fprintf(ch, "vole: %s may not read change requests in %s\n", user, name) | |
| 48 | return 1 | |
| 49 | } | |
| 50 | req, err := metadata.GetRequest(m.Git(), id) | |
| 51 | if err != nil { | |
| 52 | fmt.Fprintf(ch, "vole: %v\n", err) | |
| 53 | return 1 | |
| 54 | } | |
|
92d8afc8
Keenan Nemetz
1h ago
|
55 | // The runs for the head are the ones a reviewer opens first, and they are true |
| 56 | // whether the change is open or already landed. The run id is the whole point: | |
| 57 | // without it `vole run show` and `vole run logs` have nothing to take. | |
| 58 | writeRunLines(ch, s.cfg.Runs, name, req.Head, "head") | |
|
e5478085
Keenan Nemetz
11d ago
|
59 | if !req.Open() { |
| 60 | fmt.Fprintf(ch, "combined: not needed (%s)\n", req.State) | |
| 61 | return 0 | |
| 62 | } | |
| 63 | gate := landing.Gate{ | |
| 64 | Repo: m.Git(), Name: m.Name, ACL: a, | |
| 65 | Actor: user, Role: role, | |
| 66 | Runs: func() (*runs.Store, error) { return s.cfg.Runs, nil }, | |
| 67 | } | |
| 68 | plan, err := landing.Prepare(gate, req.ID, user, time.Now().UTC()) | |
| 69 | if err != nil { | |
| 70 | fmt.Fprintf(ch, "combined: cannot tell (%v)\n", err) | |
| 71 | return 0 | |
| 72 | } | |
| 73 | switch { | |
| 74 | case len(plan.Conflicts) > 0: | |
| 75 | fmt.Fprintf(ch, "combined: conflict with %s in %s\n", plan.Base, strings.Join(plan.Conflicts, ", ")) | |
| 76 | case !plan.MergeCommit: | |
| 77 | fmt.Fprint(ch, "combined: not needed (fast-forward)\n") | |
| 78 | case plan.Ready: | |
| 79 | fmt.Fprintf(ch, "combined: passed %s\n", format.SHA(plan.NewTip)) | |
| 80 | case !plan.Checks: | |
| 81 | fmt.Fprintf(ch, "combined: no checks required %s\n", format.SHA(plan.NewTip)) | |
| 82 | case len(plan.Gaps) == 0: | |
| 83 | fmt.Fprintf(ch, "combined: untested %s\n", format.SHA(plan.NewTip)) | |
| 84 | case plan.Pending: | |
| 85 | fmt.Fprintf(ch, "combined: testing %s\n", format.SHA(plan.NewTip)) | |
| 86 | default: | |
| 87 | fmt.Fprintf(ch, "combined: failed %s\n", format.SHA(plan.NewTip)) | |
| 88 | } | |
| 89 | if plan.Blocked != "" { | |
| 90 | fmt.Fprintf(ch, "landing: blocked (%s)\n", plan.Blocked) | |
| 91 | } else if plan.Ready { | |
| 92 | fmt.Fprint(ch, "landing: ready\n") | |
| 93 | } else if len(plan.Gaps) > 0 { | |
| 94 | fmt.Fprint(ch, "landing: waiting for the combined result\n") | |
| 95 | } | |
|
92d8afc8
Keenan Nemetz
1h ago
|
96 | // The combined result is a different commit once the base has moved, so its |
| 97 | // runs are not the head's. They are the ones that explain a failed combined | |
| 98 | // line, and a fast-forward has no separate commit to ask about. | |
| 99 | if plan.NewTip != "" && plan.NewTip != req.Head { | |
| 100 | writeRunLines(ch, s.cfg.Runs, name, plan.NewTip, "combined") | |
| 101 | } | |
|
e5478085
Keenan Nemetz
11d ago
|
102 | return 0 |
| 103 | } | |
|
92d8afc8
Keenan Nemetz
1h ago
|
104 | |
| 105 | // writeRunLines lists the newest run per job for one commit, in the same order the | |
| 106 | // change page's Runs tab uses. It is display text, not a wire format: the id is what | |
| 107 | // a reader carries to `vole run show`, and the status says whether that is the run to | |
| 108 | // open. A missing queue is not an error here; the rest of the status still answers. | |
| 109 | func writeRunLines(out io.Writer, store *runs.Store, repo, sha, where string) { | |
| 110 | if store == nil || sha == "" { | |
| 111 | return | |
| 112 | } | |
| 113 | byKey, err := store.ForSHA(repo, sha) | |
| 114 | if err != nil { | |
| 115 | return | |
| 116 | } | |
| 117 | keys := make([]string, 0, len(byKey)) | |
| 118 | for key := range byKey { | |
| 119 | keys = append(keys, key) | |
| 120 | } | |
| 121 | sort.Strings(keys) | |
| 122 | for _, key := range keys { | |
| 123 | r := byKey[key] | |
| 124 | exit := "" | |
| 125 | switch r.Status { | |
| 126 | case runs.StatusSuccess, runs.StatusFailed, runs.StatusCanceled, runs.StatusError: | |
| 127 | exit = fmt.Sprintf(" exit %d", r.ExitCode) | |
| 128 | } | |
| 129 | fmt.Fprintf(out, "run: %s %s %s/%s%s (%s)\n", | |
| 130 | r.ID, r.Status, r.Workflow, r.Job, exit, where) | |
| 131 | } | |
| 132 | } |