e2e/mrrebase_test.go
147 lines · 6124 bytes
1package e2e
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// `mr rebase` replays a merge request's branch onto its target and
12// re-pushes it, which is what makes a fast-forward merge possible again
13// after the target has moved. The git work is local, so the replayed
14// commits are signed by whatever the user's git config signs with and the
15// server is never asked to vouch for a commit it did not receive already
16// signed (#175).
17func TestCLIMRRebase(t *testing.T) {
18 t.Parallel()
19 inst := startInstance(t)
20 aliceKey := inst.newKey(t, "alice")
21 inst.admin(t, "admin", "user", "create", "alice",
22 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
23
24 c := &cli{bin: buildGitbayCLI(t), configDir: t.TempDir(), inst: inst, key: aliceKey}
25 c.must(t, "", "", "remote", "add", "test", "127.0.0.1",
26 "--port", fmt.Sprint(inst.port),
27 "--ssh-option", "-i", "--ssh-option", aliceKey,
28 "--ssh-option", "-oIdentitiesOnly=yes",
29 "--ssh-option", "-oStrictHostKeyChecking=no",
30 "--ssh-option", "-oUserKnownHostsFile="+filepath.Join(inst.sshDir, "kh"),
31 "--ssh-option", "-oBatchMode=yes",
32 "--default")
33 c.must(t, "", "", "repo", "create", "alice/app")
34
35 env := inst.gitEnv(aliceKey)
36 work := t.TempDir()
37 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
38 dir := filepath.Join(work, "w")
39 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
40 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
41 mustGit(t, dir, env, "add", ".")
42 mustGit(t, dir, env, "commit", "-q", "-m", "base")
43 mustGit(t, dir, env, "push", "-q", "origin", "main")
44
45 // A branch off main, and a merge request for it.
46 mustGit(t, dir, env, "checkout", "-q", "-b", "feat")
47 os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b\n"), 0o644)
48 mustGit(t, dir, env, "add", ".")
49 mustGit(t, dir, env, "commit", "-q", "-m", "the change")
50 mustGit(t, dir, env, "push", "-q", "origin", "feat")
51 c.must(t, dir, "", "mr", "create", "alice/app",
52 "--source", "feat", "--target", "main", "--title", "change")
53
54 // main moves on, so feat is no longer a fast-forward.
55 mustGit(t, dir, env, "checkout", "-q", "main")
56 os.WriteFile(filepath.Join(dir, "c.txt"), []byte("c\n"), 0o644)
57 mustGit(t, dir, env, "add", ".")
58 mustGit(t, dir, env, "commit", "-q", "-m", "moved on")
59 mustGit(t, dir, env, "push", "-q", "origin", "main")
60 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "1", "--strategy", "ff"); code == 0 {
61 t.Fatal("fast-forward merged a diverged branch")
62 } else if !strings.Contains(errOut, "fast-forward not possible") {
63 t.Fatalf("unexpected refusal: %s", errOut)
64 }
65
66 // Rebase, and the same merge now lands.
67 out, errOut, code := c.run(t, dir, "", "mr", "rebase", "1")
68 if code != 0 {
69 t.Fatalf("mr rebase: exit %d\n%s\n%s", code, out, errOut)
70 }
71 if !strings.Contains(out, "gitbay mr merge 1") {
72 t.Errorf("rebase does not name the next step: %s", out)
73 }
74 if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/app", "1", "--strategy", "ff"); code != 0 {
75 t.Fatalf("fast-forward still refused after a rebase: %s", errOut)
76 }
77
78 // The rebase moved the branch rather than merging main into it: the
79 // change is one commit on top of what main had.
80 out, _, _ = inst.ssh(t, aliceKey, "", "mr", "show", "alice/app", "1", "--json")
81 if !strings.Contains(out, `"state":"merged"`) {
82 t.Fatalf("merge request not merged:\n%s", out)
83 }
84}
85
86// The guards: a dirty tree, and a source in a fork this clone cannot push.
87func TestCLIMRRebaseRefusals(t *testing.T) {
88 t.Parallel()
89 inst := startInstance(t)
90 aliceKey := inst.newKey(t, "alice")
91 bobKey := inst.newKey(t, "bob")
92 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
93 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
94
95 c := &cli{bin: buildGitbayCLI(t), configDir: t.TempDir(), inst: inst, key: aliceKey}
96 c.must(t, "", "", "remote", "add", "test", "127.0.0.1",
97 "--port", fmt.Sprint(inst.port),
98 "--ssh-option", "-i", "--ssh-option", aliceKey,
99 "--ssh-option", "-oIdentitiesOnly=yes",
100 "--ssh-option", "-oStrictHostKeyChecking=no",
101 "--ssh-option", "-oUserKnownHostsFile="+filepath.Join(inst.sshDir, "kh"),
102 "--ssh-option", "-oBatchMode=yes",
103 "--default")
104 c.must(t, "", "", "repo", "create", "alice/app")
105
106 env := inst.gitEnv(aliceKey)
107 work := t.TempDir()
108 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
109 dir := filepath.Join(work, "w")
110 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
111 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
112 mustGit(t, dir, env, "add", ".")
113 mustGit(t, dir, env, "commit", "-q", "-m", "base")
114 mustGit(t, dir, env, "push", "-q", "origin", "main")
115
116 // bob forks, pushes, and opens a merge request from the fork.
117 inst.ssh(t, bobKey, "", "repo", "fork", "alice/app")
118 benv := inst.gitEnv(bobKey)
119 bwork := t.TempDir()
120 mustGit(t, bwork, benv, "clone", inst.sshURL("bob/app"), "w")
121 bdir := filepath.Join(bwork, "w")
122 mustGit(t, bdir, benv, "checkout", "-q", "-b", "feat")
123 os.WriteFile(filepath.Join(bdir, "b.txt"), []byte("b\n"), 0o644)
124 mustGit(t, bdir, benv, "add", ".")
125 mustGit(t, bdir, benv, "commit", "-q", "-m", "from the fork")
126 mustGit(t, bdir, benv, "push", "-q", "origin", "feat")
127 inst.ssh(t, bobKey, "", "mr", "create", "alice/app",
128 "--source", "bob/app:feat", "--target", "main", "--title", "forked")
129
130 // Alice's clone of the target cannot rebase a branch that lives in
131 // bob's fork, and says which repository to do it in.
132 _, errOut, code := c.run(t, dir, "", "mr", "rebase", "1")
133 if code == 0 {
134 t.Fatal("rebased a fork's branch from the target's clone")
135 }
136 if !strings.Contains(errOut, "bob/app:feat") {
137 t.Errorf("refusal does not name the fork: %s", errOut)
138 }
139
140 // A dirty tree is refused before any round trip, so the merge request
141 // number never has to be valid for this one.
142 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("edited\n"), 0o644)
143 if _, errOut, code := c.run(t, dir, "", "mr", "rebase", "1"); code == 0 ||
144 !strings.Contains(errOut, "uncommitted changes") {
145 t.Errorf("dirty tree not refused: exit %d %s", code, errOut)
146 }
147}