e2e/stack_test.go
157 lines · 5886 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// A three-deep stack, merged bottom-up. Each merge moves the stack above
12// it onto the merged target with its reviews intact; a squash under a
13// stack is refused.
14func TestStackedMergeRequests(t *testing.T) {
15 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
16 aliceKey := inst.newKey(t, "alice")
17 bobKey := inst.newKey(t, "bob")
18 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
19 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
20 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/lib"); code != 0 {
21 t.Fatalf("repo create: %s", errOut)
22 }
23 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/lib", "bob", "write"); code != 0 {
24 t.Fatal("grant failed")
25 }
26 work := t.TempDir()
27 env := inst.gitEnv(aliceKey)
28 mustGit(t, work, env, "clone", inst.sshURL("alice/lib"), "w")
29 dir := filepath.Join(work, "w")
30 commit := func(branch, from, file string) {
31 t.Helper()
32 if from == "" {
33 mustGit(t, dir, env, "checkout", "-q", "-b", branch)
34 } else {
35 mustGit(t, dir, env, "checkout", "-q", "-b", branch, from)
36 }
37 os.WriteFile(filepath.Join(dir, file), []byte(file+"\n"), 0o644)
38 mustGit(t, dir, env, "add", ".")
39 mustGit(t, dir, env, "commit", "-q", "-m", file)
40 mustGit(t, dir, env, "push", "-q", "origin", branch)
41 }
42 commit("main", "", "base.txt")
43 commit("feat-a", "main", "a.txt")
44 commit("feat-b", "feat-a", "b.txt")
45 commit("feat-c", "feat-b", "c.txt")
46
47 create := func(src, dst, title string) string {
48 t.Helper()
49 out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/lib", "--source", src, "--target", dst, "--title", title, "--json")
50 if code != 0 {
51 t.Fatalf("mr create %s: %s", src, errOut)
52 }
53 return out
54 }
55 if out := create("feat-a", "main", "A"); strings.Contains(out, "stacked_on") {
56 t.Fatalf("A is not stacked:\n%s", out)
57 }
58 if out := create("feat-b", "feat-a", "B"); !strings.Contains(out, `"stacked_on":{"number":1`) {
59 t.Fatalf("B create does not report its stack:\n%s", out)
60 }
61 create("feat-c", "feat-b", "C")
62
63 type stackShow struct {
64 mrShow
65 StackedOn *struct {
66 Number int64 `json:"number"`
67 } `json:"stacked_on"`
68 Stacked []struct {
69 Number int64 `json:"number"`
70 } `json:"stacked"`
71 Comments []struct {
72 Body string `json:"body"`
73 } `json:"comments"`
74 }
75 show := func(n string) stackShow {
76 t.Helper()
77 out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", n, "--json")
78 if code != 0 {
79 t.Fatalf("mr show %s: %s", n, errOut)
80 }
81 var env struct {
82 Data stackShow `json:"data"`
83 }
84 if err := json.Unmarshal([]byte(out), &env); err != nil {
85 t.Fatalf("show: %v\n%s", err, out)
86 }
87 return env.Data
88 }
89 a, b, c := show("1"), show("2"), show("3")
90 if a.StackedOn != nil || len(a.Stacked) != 1 || a.Stacked[0].Number != 2 {
91 t.Fatalf("A stack: %+v", a)
92 }
93 if b.StackedOn == nil || b.StackedOn.Number != 1 || len(b.Stacked) != 1 || b.Stacked[0].Number != 3 {
94 t.Fatalf("B stack: %+v", b)
95 }
96 if c.StackedOn == nil || c.StackedOn.Number != 2 || len(c.Stacked) != 0 {
97 t.Fatalf("C stack: %+v", c)
98 }
99 if out, _, _ := inst.ssh(t, aliceKey, "", "mr", "list", "alice/lib"); !strings.Contains(out, "stacked on !1") || !strings.Contains(out, "stacked on !2") {
100 t.Fatalf("mr list lacks the stack:\n%s", out)
101 }
102
103 // Bob approves B; the approval must survive the retarget.
104 if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "review", "alice/lib", "2", "--approve"); code != 0 {
105 t.Fatalf("review: %s", errOut)
106 }
107
108 // A squash under the stack is refused, naming the stack.
109 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1", "--strategy", "squash"); code != 2 || !strings.Contains(errOut, "!2") {
110 t.Fatalf("squash under a stack: exit %d %s", code, errOut)
111 }
112 // Merge A: B moves onto main, C stays on feat-b.
113 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1"); code != 0 {
114 t.Fatalf("merge A: %s", errOut)
115 }
116 b = show("2")
117 if b.TargetRef != "main" || b.StackedOn != nil || len(b.Reviews) != 1 || b.Reviews[0].Stale {
118 t.Fatalf("B after A merged: %+v", b)
119 }
120 found := false
121 for _, cm := range b.Comments {
122 if strings.Contains(cm.Body, "retargeted from feat-a to main: !1 merged") {
123 found = true
124 }
125 }
126 if !found {
127 t.Fatalf("B carries no retarget comment: %+v", b.Comments)
128 }
129 if c = show("3"); c.TargetRef != "feat-b" || c.StackedOn == nil || c.StackedOn.Number != 2 {
130 t.Fatalf("C after A merged: %+v", c)
131 }
132 // The web page says both directions.
133 root := inst.login(t, aliceKey)
134 if _, body := browserGet(t, root, inst.base()+"/alice/lib/mrs/2"); !strings.Contains(body, "Builds on this") || !strings.Contains(body, "/alice/lib/mrs/3") {
135 t.Fatalf("B page lacks its stack:\n%s", body)
136 }
137 if _, body := browserGet(t, root, inst.base()+"/alice/lib/mrs/3"); !strings.Contains(body, "Stacked on") || !strings.Contains(body, "/alice/lib/mrs/2") {
138 t.Fatalf("C page lacks its parent:\n%s", body)
139 }
140 // Merge B, then C: each lands its own commit only.
141 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "2"); code != 0 {
142 t.Fatalf("merge B: %s", errOut)
143 }
144 if c = show("3"); c.TargetRef != "main" || c.StackedOn != nil {
145 t.Fatalf("C after B merged: %+v", c)
146 }
147 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "3"); code != 0 {
148 t.Fatalf("merge C: %s", errOut)
149 }
150 mustGit(t, dir, env, "checkout", "-q", "main")
151 mustGit(t, dir, env, "pull", "-q", "origin", "main")
152 for _, f := range []string{"a.txt", "b.txt", "c.txt"} {
153 if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
154 t.Fatalf("%s missing from main after the stack merged", f)
155 }
156 }
157}