Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 29m ago 32290114
history blame raw 4.3 KiB
1package 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
8import (
9 "fmt"
10 "io"
11 "sort"
12 "strings"
13 "time"
14
15 xssh "golang.org/x/crypto/ssh"
16
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"
22)
23
24func (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 }
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")
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 }
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 }
102 return 0
103}
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.
109func 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}
133