e2e/mrretarget_test.go
127 lines · 5411 bytes
1package e2e
2
3import (
4 "net/url"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// TestMRRetarget moves an open merge request onto another branch, over
12// SSH and from the browser. Retargeting changes which diff a review was
13// of, so the existing approvals have to go stale with it.
14func TestMRRetarget(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",
19 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
20 inst.admin(t, "admin", "user", "create", "bob",
21 "--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
22
23 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/lib"); code != 0 {
24 t.Fatalf("repo create: %s", errOut)
25 }
26 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/lib", "bob", "write"); code != 0 {
27 t.Fatalf("grant: %s", errOut)
28 }
29
30 env := inst.gitEnv(aliceKey)
31 work := t.TempDir()
32 mustGit(t, work, env, "clone", inst.sshURL("alice/lib"), "w")
33 dir := filepath.Join(work, "w")
34 os.WriteFile(filepath.Join(dir, "lib.txt"), []byte("v1\n"), 0o644)
35 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
36 mustGit(t, dir, env, "add", ".")
37 mustGit(t, dir, env, "commit", "-q", "-m", "base")
38 mustGit(t, dir, env, "push", "-q", "origin", "main")
39 // A second long-lived branch to retarget onto.
40 mustGit(t, dir, env, "checkout", "-q", "-b", "release")
41 os.WriteFile(filepath.Join(dir, "release.txt"), []byte("1.0\n"), 0o644)
42 mustGit(t, dir, env, "add", ".")
43 mustGit(t, dir, env, "commit", "-q", "-m", "cut release")
44 mustGit(t, dir, env, "push", "-q", "origin", "release")
45 mustGit(t, dir, env, "checkout", "-q", "-b", "feat", "main")
46 os.WriteFile(filepath.Join(dir, "feat.txt"), []byte("work\n"), 0o644)
47 mustGit(t, dir, env, "add", ".")
48 mustGit(t, dir, env, "commit", "-q", "-m", "add feat")
49 mustGit(t, dir, env, "push", "-q", "origin", "feat")
50
51 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/lib",
52 "--source", "feat", "--target", "main", "--title", "'feature'"); code != 0 {
53 t.Fatalf("mr create: %s", errOut)
54 }
55 if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "review", "alice/lib", "1", "--approve"); code != 0 {
56 t.Fatalf("review: %s", errOut)
57 }
58
59 // Refusals first: an unknown branch, the branch it already targets,
60 // and its own source.
61 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "retarget", "alice/lib", "1", "nope"); code != 3 ||
62 !strings.Contains(errOut, "not found") {
63 t.Fatalf("unknown branch: exit %d, %s", code, errOut)
64 }
65 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "retarget", "alice/lib", "1", "main"); code != 2 ||
66 !strings.Contains(errOut, "already targets") {
67 t.Fatalf("same branch: exit %d, %s", code, errOut)
68 }
69 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "retarget", "alice/lib", "1", "feat"); code != 2 ||
70 !strings.Contains(errOut, "source branch") {
71 t.Fatalf("source branch: exit %d, %s", code, errOut)
72 }
73 // Nobody outside the repo can move it.
74 eveKey := inst.newKey(t, "eve")
75 inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
76 if _, _, code := inst.ssh(t, eveKey, "", "mr", "retarget", "alice/lib", "1", "release"); code == 0 {
77 t.Fatal("a stranger retargeted the merge request")
78 }
79
80 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "retarget", "alice/lib", "1", "release"); code != 0 {
81 t.Fatalf("retarget: %s", errOut)
82 }
83 show := inst.mrShow(t, aliceKey, "alice/lib", "1")
84 if show.TargetRef != "release" {
85 t.Fatalf("target not moved: %q", show.TargetRef)
86 }
87 if len(show.Reviews) != 1 || !show.Reviews[0].Stale {
88 t.Fatalf("approval survived the retarget: %+v", show.Reviews)
89 }
90 // The diff follows the new base: release.txt is on the target now, so
91 // it is no longer part of the change.
92 out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "diff", "alice/lib", "1")
93 if code != 0 {
94 t.Fatalf("mr diff: %s", errOut)
95 }
96 if !strings.Contains(out, "feat.txt") || strings.Contains(out, "release.txt") {
97 t.Fatalf("diff not rebased on the new target:\n%s", out)
98 }
99
100 // The move is recorded on the conversation.
101 if out, _, _ := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", "1"); !strings.Contains(out, "retargeted from main to release") {
102 t.Fatalf("no system comment for the move:\n%s", out)
103 }
104
105 // And the browser can do it, through the same command.
106 mrURL := inst.base() + "/alice/lib/mrs/1"
107 alice := inst.login(t, aliceKey)
108 _, body := browserGet(t, alice, mrURL)
109 if !strings.Contains(body, `action="/alice/lib/mrs/1/retarget"`) {
110 t.Fatalf("no retarget control on the MR page:\n%s", body)
111 }
112 if status, _ := browserPost(t, alice, mrURL+"/retarget", url.Values{"target": {"main"}}); status != 200 {
113 t.Fatalf("retarget post: %d", status)
114 }
115 if got := inst.mrShow(t, aliceKey, "alice/lib", "1").TargetRef; got != "main" {
116 t.Fatalf("web retarget did not land: %q", got)
117 }
118
119 // A merged merge request is settled.
120 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1"); code != 0 {
121 t.Fatalf("merge: %s", errOut)
122 }
123 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "retarget", "alice/lib", "1", "release"); code != 2 ||
124 !strings.Contains(errOut, "only an open merge request") {
125 t.Fatalf("merged MR retargeted: exit %d, %s", code, errOut)
126 }
127}