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