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