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.

Blame · server/ssh/merge.go

Line attribution on main · 115 line(s)

b14a7819
Keenan Nemetz
13d ago
1 package 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
14 import (
15 "fmt"
16 "strings"
17 "time"
18
19 xssh "golang.org/x/crypto/ssh"
20
d37d301f
Keenan Nemetz
16h ago
21 "rodent.software/vole/ci/runs"
22 "rodent.software/vole/forge/landing"
23 "rodent.software/vole/internal/format"
b14a7819
Keenan Nemetz
13d ago
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.
28 func (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 }
e5478085
Keenan Nemetz
11d ago
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)
b14a7819
Keenan Nemetz
13d ago
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 }
183d48e5
Keenan Nemetz
13d ago
109 for _, issue := range res.ClosedIssues {
110 fmt.Fprintf(ch, "closed issue %s\n", issue)
111 }
b14a7819
Keenan Nemetz
13d ago
112 s.cfg.Log.Info("ssh: merged a change request", "user", user, "repo", name,
183d48e5
Keenan Nemetz
13d ago
113 "request", id, "base", res.Base, "tip", res.NewTip, "closed", res.ClosedIssues)
b14a7819
Keenan Nemetz
13d ago
114 return 0
115 }