e2e/mrrebase_test.go

3bcdce33fb9a2309312854331359d376171c7368
gitbay/e2e/mrrebase_test.go history · blame · raw

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}