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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 27m ago 4b8c9e1d
history blame raw 4.0 KiB
1package ssh
2
3// `vole change merge` over the SSH channel.
4//
5// Landing moves a branch, so it has to happen where the rules live: the ACL, the
6// approval files and the run results are all instance-side, and a clone holds none
7// of them. The CLI therefore asks the server, over the key it already pushes with,
8// and the server runs forge/landing.Land — the same operation the browser's merge
9// button runs and the same gate the pre-receive hook applies to a push.
10//
11// This is not a shell. The verb takes a repository name and a request id, nothing
12// else, and answers one line.
13
14import (
15 "fmt"
16 "strings"
17 "time"
18
19 xssh "golang.org/x/crypto/ssh"
20
21 "rodent.software/vole/ci/runs"
22 "rodent.software/vole/forge/landing"
23 "rodent.software/vole/internal/format"
24)
25
26// serveChangeMerge runs one landing for the caller. Every refusal is written to the
27// channel with a "vole:" prefix, which is what the client turns into the error.
28func (s *Server) serveChangeMerge(ch xssh.Channel, argv []string, user, remote string) int {
29 if len(argv) != 3 {
30 fmt.Fprint(ch, "vole: usage: vole-change-merge <repository> <request-id>\n")
31 return 129
32 }
33 name, ok := repoName(argv[1])
34 if !ok {
35 fmt.Fprint(ch, "vole: invalid repository name\n")
36 return 129
37 }
38 id := strings.TrimSpace(argv[2])
39 m, err := s.cfg.Repos.Open(name)
40 if err != nil {
41 fmt.Fprintf(ch, "vole: repository %q not found\n", name)
42 return 1
43 }
44 a, err := m.ACL()
45 if err != nil {
46 fmt.Fprintf(ch, "vole: cannot read access rules for %s\n", name)
47 return 1
48 }
49 // Merging is the approve capability: landing is the act of accepting the review.
50 // The author may do it once somebody else approved; whether enough approvals
51 // exist is the gate's question, asked inside Land.
52 if !a.CanTriage(user) {
53 fmt.Fprintf(ch, "vole: %s may not merge change requests in %s\n", user, name)
54 return 1
55 }
56 gate := landing.Gate{
57 Repo: m.Git(), Name: m.Name, ACL: a, Actor: user,
58 Role: a.EffectiveRole(user, s.cfg.Identity.IsAdmin(user)),
59 Runs: func() (*runs.Store, error) { return s.cfg.Runs, nil },
60 }
61 now := time.Now().UTC()
62 // Ask the same plan the page does. If the combined result is still building, a
63 // CLI merge becomes the same "merge when ready" wait rather than an error the
64 // caller has to come back and retry.
65 plan, err := landing.Prepare(gate, id, user, now)
66 if err != nil {
67 fmt.Fprintf(ch, "vole: %v\n", err)
68 return 1
69 }
70 if len(plan.Conflicts) > 0 {
71 fmt.Fprintf(ch, "vole: %s does not merge into %s: %s conflict. Resolve it on the branch and push again\n",
72 format.SHA(plan.Head), plan.Base, strings.Join(plan.Conflicts, ", "))
73 return 1
74 }
75 if plan.Blocked != "" {
76 fmt.Fprintf(ch, "vole: %s\n", plan.Blocked)
77 return 1
78 }
79 if !plan.Ready {
80 if plan.Pending || len(plan.Gaps) == 0 {
81 if s.cfg.Runs == nil {
82 fmt.Fprint(ch, "vole: this instance has no CI queue, so the combined result cannot be tested\n")
83 return 1
84 }
85 if err := s.cfg.Runs.ArmAutoMerge(name, id, user, now); err != nil {
86 fmt.Fprintf(ch, "vole: cannot arm merge: %v\n", err)
87 return 1
88 }
89 fmt.Fprintf(ch, "merging %s into %s when the combined result passes\n", id, plan.Base)
90 return 0
91 }
92 fmt.Fprintf(ch, "vole: the combined result failed:\n %s\n", strings.Join(plan.Gaps, "\n "))
93 return 1
94 }
95 res, err := landing.Land(gate, id, user, now)
96 if err != nil {
97 fmt.Fprintf(ch, "vole: %v\n", err)
98 return 1
99 }
100 switch {
101 case res.AlreadyLanded:
102 fmt.Fprintf(ch, "already merged: %s at %s\n", id, format.SHA(res.Request.MergedHead))
103 case res.FastForward:
104 fmt.Fprintf(ch, "merged %s into %s at %s\n", id, res.Base, format.SHA(res.NewTip))
105 default:
106 fmt.Fprintf(ch, "merged %s into %s at %s (merge commit %s)\n",
107 id, res.Base, format.SHA(res.NewTip), format.SHA(res.Request.Head))
108 }
109 for _, issue := range res.ClosedIssues {
110 fmt.Fprintf(ch, "closed issue %s\n", issue)
111 }
112 s.cfg.Log.Info("ssh: merged a change request", "user", user, "repo", name,
113 "request", id, "base", res.Base, "tip", res.NewTip, "closed", res.ClosedIssues)
114 return 0
115}
116