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.

Add vole change merge, over an SSH verb

b14a78191909907be833d27d82d215a0319de9e3 · Keenan Nemetz · 14d ago · parent 38b30251

Add vole change merge, over an SSH verb

The landing operation existed but only the browser could reach it. The CLI
cannot do it from a clone: whether a landing is allowed depends on the
repository's ACL, the approvals filed against the request, and the run
results, and none of those are in the working tree.

So the CLI asks the instance, over the key it already pushes with. The new
verb takes a repository path and a request id and answers one line; the
server resolves the rules, runs forge/landing.Land, and writes the merge
line to the audit log the same way a push does.

Merging is the approve capability, and the author is not excluded: landing
your own request once somebody else approved it is the point of separating
the two. Whether an approval exists is the gate's question, asked inside
Land, so the CLI, the browser button and the pre-receive hook cannot
disagree about it.

The verb accepts the same remote path shape as the git verbs and runs it
through the same repository-name check, so a name that would be refused on
a push is refused here too.

Closes #01M5XF4XWHVMMCQEYENA8X32GK.

server/ssh/merge_test.go +78

@@ -0,0 +1,78 @@
1 package ssh
2
3 // `vole change merge` is a verb that moves a branch, so the two things worth
4 // pinning over the wire are that it is not a shell and that it asks the repository's
5 // rules rather than the caller's word. The landing rules themselves are pinned in
6 // forge/landing; these are about who gets to ask and what a refusal looks like.
7
8 import (
9 "strings"
10 "testing"
11 )
12
13 func TestMergeVerbRefusesNonMaintainers(t *testing.T) {
14 t.Parallel()
15 h := newHarness(t)
16 // reader holds the read role: it may clone, and it may not merge.
17 c := h.dial(t, h.roKey)
18
19 sess, err := c.NewSession()
20 if err != nil {
21 t.Fatal(err)
22 }
23 out, err := sess.CombinedOutput("vole-change-merge /demo.git 01M5XF4XWHVMMCQEYENA8X32GK")
24 sess.Close()
25 if err == nil {
26 t.Fatalf("a reader merged a change request: %s", out)
27 }
28 if !strings.Contains(string(out), "may not merge") {
29 t.Fatalf("refusal = %q, want it to name the missing capability", out)
30 }
31 }
32
33 func TestMergeVerbShowsTheGateRefusal(t *testing.T) {
34 t.Parallel()
35 h := newHarness(t)
36 c := h.dial(t, h.keyPath) // the writer, a maintainer
37
38 sess, err := c.NewSession()
39 if err != nil {
40 t.Fatal(err)
41 }
42 // A request id nobody opened: the landing refuses and says so, on the channel,
43 // with the prefix the client turns into an error.
44 out, err := sess.CombinedOutput("vole-change-merge /demo.git 01M5XF4XWHVMMCQEYENA8X32GK")
45 sess.Close()
46 if err == nil {
47 t.Fatalf("merging an unknown request succeeded: %s", out)
48 }
49 if !strings.HasPrefix(string(out), "vole: ") {
50 t.Fatalf("refusal = %q, want the vole: prefix the client reads", out)
51 }
52 }
53
54 func TestMergeVerbIsNotAShell(t *testing.T) {
55 t.Parallel()
56 h := newHarness(t)
57 c := h.dial(t, h.keyPath)
58
59 for _, command := range []string{
60 "vole-change-merge",
61 "vole-change-merge /demo.git",
62 "vole-change-merge /demo.git 01M5XF4XWHVMMCQEYENA8X32GK; cat /etc/passwd",
63 "vole-change-merge /../demo.git 01M5XF4XWHVMMCQEYENA8X32GK",
64 } {
65 sess, err := c.NewSession()
66 if err != nil {
67 t.Fatal(err)
68 }
69 out, err := sess.CombinedOutput(command)
70 sess.Close()
71 if err == nil {
72 t.Fatalf("%q succeeded, want a refusal (output: %s)", command, out)
73 }
74 if strings.Contains(string(out), "root:") {
75 t.Fatalf("%q executed something: %s", command, out)
76 }
77 }
78 }