e2e/merge_strategies_test.go

v1.37.0
gitbay/e2e/merge_strategies_test.go history · blame · raw

198 lines · 9253 bytes

  1package e2e
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"testing"
  8)
  9
 10func TestSquashAndRebaseMerges(t *testing.T) {
 11	t.Parallel()
 12	inst := startInstance(t)
 13	aliceKey := inst.newKey(t, "alice")
 14	bobKey := inst.newKey(t, "bob")
 15	inst.admin(t, "admin", "user", "create", "alice",
 16		"--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
 17	inst.admin(t, "admin", "user", "create", "bob",
 18		"--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
 19
 20	// Repo with bob granted write; bob authors branches, alice merges.
 21	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/lib"); code != 0 {
 22		t.Fatalf("repo create: %s", errOut)
 23	}
 24	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/lib", "bob", "write"); code != 0 {
 25		t.Fatal("grant failed")
 26	}
 27
 28	aliceEnv := inst.gitEnv(aliceKey)
 29	bobEnv := inst.gitEnv(bobKey)
 30	work := t.TempDir()
 31	mustGit(t, work, aliceEnv, "clone", inst.sshURL("alice/lib"), "w")
 32	dir := filepath.Join(work, "w")
 33	os.WriteFile(filepath.Join(dir, "base.txt"), []byte("base\n"), 0o644)
 34	mustGit(t, dir, aliceEnv, "checkout", "-q", "-b", "main")
 35	mustGit(t, dir, aliceEnv, "add", ".")
 36	mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "base")
 37	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
 38
 39	// --- squash: two bob commits, diverged target -> one new commit ---
 40	bobWork := t.TempDir()
 41	mustGit(t, bobWork, bobEnv, "clone", inst.sshURL("alice/lib"), "w")
 42	bobDir := filepath.Join(bobWork, "w")
 43	mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "feat1", "origin/main")
 44	os.WriteFile(filepath.Join(bobDir, "a.txt"), []byte("a\n"), 0o644)
 45	mustGit(t, bobDir, bobEnv, "add", ".")
 46	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "wip 1")
 47	os.WriteFile(filepath.Join(bobDir, "b.txt"), []byte("b\n"), 0o644)
 48	mustGit(t, bobDir, bobEnv, "add", ".")
 49	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "wip 2")
 50	mustGit(t, bobDir, bobEnv, "push", "-q", "origin", "feat1")
 51	if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/lib",
 52		"--source", "feat1", "--target", "main", "--title", "'squash me'", "--body", "'two wips'"); code != 0 {
 53		t.Fatalf("mr create: %s", errOut)
 54	}
 55	// Target advances so ff is impossible.
 56	mustGit(t, dir, aliceEnv, "commit", "-q", "--allow-empty", "-m", "mainline")
 57	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
 58
 59	before := strings.TrimSpace(mustGit(t, dir, aliceEnv, "rev-parse", "origin/main"))
 60	out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1", "--strategy", "squash", "--json")
 61	if code != 0 {
 62		t.Fatalf("squash merge: %s", errOut)
 63	}
 64	if !strings.Contains(out, `"strategy":"squash"`) {
 65		t.Fatalf("squash output: %s", out)
 66	}
 67	mustGit(t, dir, aliceEnv, "fetch", "-q", "origin")
 68	// Exactly one commit landed on top of the old tip.
 69	count := strings.TrimSpace(mustGit(t, dir, aliceEnv, "rev-list", "--count", before+"..origin/main"))
 70	if count != "1" {
 71		t.Fatalf("squash added %s commits, want 1", count)
 72	}
 73	// Single parent, author = MR author (bob), committer = merger (alice).
 74	ident := strings.TrimSpace(mustGit(t, dir, aliceEnv, "log", "-1",
 75		"--format=%an <%ae>|%cn <%ce>|%p|%s", "origin/main"))
 76	parts := strings.Split(ident, "|")
 77	if parts[0] != "bob <bob@example.test>" || parts[1] != "alice <alice@example.test>" {
 78		t.Fatalf("squash identities: %s", ident)
 79	}
 80	if strings.Contains(parts[2], " ") {
 81		t.Fatalf("squash commit has multiple parents: %s", ident)
 82	}
 83	if parts[3] != "squash me (!1)" {
 84		t.Fatalf("squash subject: %s", ident)
 85	}
 86	// Both files present.
 87	mustGit(t, dir, aliceEnv, "checkout", "-q", "main")
 88	mustGit(t, dir, aliceEnv, "pull", "-q", "origin", "main")
 89	for _, f := range []string{"a.txt", "b.txt"} {
 90		if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
 91			t.Fatalf("squashed content missing %s", f)
 92		}
 93	}
 94
 95	// --- rebase: two commits replayed onto a diverged target ---
 96	mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "feat2", "origin/main")
 97	mustGit(t, bobDir, bobEnv, "fetch", "-q", "origin")
 98	mustGit(t, bobDir, bobEnv, "reset", "-q", "--hard", "origin/main")
 99	os.WriteFile(filepath.Join(bobDir, "c.txt"), []byte("c\n"), 0o644)
100	mustGit(t, bobDir, bobEnv, "add", ".")
101	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "step one")
102	os.WriteFile(filepath.Join(bobDir, "d.txt"), []byte("d\n"), 0o644)
103	mustGit(t, bobDir, bobEnv, "add", ".")
104	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "step two")
105	mustGit(t, bobDir, bobEnv, "push", "-q", "origin", "feat2")
106	if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/lib",
107		"--source", "feat2", "--target", "main", "--title", "'rebase me'"); code != 0 {
108		t.Fatalf("mr2 create: %s", errOut)
109	}
110	mustGit(t, dir, aliceEnv, "commit", "-q", "--allow-empty", "-m", "mainline again")
111	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
112
113	before = strings.TrimSpace(mustGit(t, dir, aliceEnv, "rev-parse", "origin/main"))
114	out, errOut, code = inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "2", "--strategy", "rebase", "--json")
115	if code != 0 {
116		t.Fatalf("rebase merge: %s", errOut)
117	}
118	mustGit(t, dir, aliceEnv, "fetch", "-q", "origin")
119	// Two commits, linear (no merges), authors preserved, committer alice.
120	count = strings.TrimSpace(mustGit(t, dir, aliceEnv, "rev-list", "--count", before+"..origin/main"))
121	if count != "2" {
122		t.Fatalf("rebase added %s commits, want 2", count)
123	}
124	merges := strings.TrimSpace(mustGit(t, dir, aliceEnv, "rev-list", "--merges", "--count", before+"..origin/main"))
125	if merges != "0" {
126		t.Fatal("rebase produced a merge commit")
127	}
128	logOut := mustGit(t, dir, aliceEnv, "log", "--format=%ae|%ce|%s", before+"..origin/main")
129	for _, line := range strings.Split(strings.TrimSpace(logOut), "\n") {
130		p := strings.Split(line, "|")
131		if p[0] != "t@example.test" || p[1] != "alice@example.test" {
132			t.Fatalf("rebase identities: %s", line)
133		}
134	}
135	if !strings.Contains(logOut, "step one") || !strings.Contains(logOut, "step two") {
136		t.Fatalf("rebase messages: %s", logOut)
137	}
138
139	// --- rebase refuses merge commits in the source ---
140	mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "feat3")
141	mustGit(t, bobDir, bobEnv, "fetch", "-q", "origin")
142	mustGit(t, bobDir, bobEnv, "reset", "-q", "--hard", "origin/main")
143	mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "side")
144	os.WriteFile(filepath.Join(bobDir, "e.txt"), []byte("e\n"), 0o644)
145	mustGit(t, bobDir, bobEnv, "add", ".")
146	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "side work")
147	mustGit(t, bobDir, bobEnv, "checkout", "-q", "feat3")
148	os.WriteFile(filepath.Join(bobDir, "f.txt"), []byte("f\n"), 0o644)
149	mustGit(t, bobDir, bobEnv, "add", ".")
150	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "main work")
151	mustGit(t, bobDir, bobEnv, "merge", "-q", "--no-ff", "-m", "internal merge", "side")
152	mustGit(t, bobDir, bobEnv, "push", "-q", "origin", "feat3")
153	if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/lib",
154		"--source", "feat3", "--target", "main", "--title", "'has a merge'"); code != 0 {
155		t.Fatalf("mr3 create: %s", errOut)
156	}
157	mustGit(t, dir, aliceEnv, "fetch", "-q", "origin")
158	mustGit(t, dir, aliceEnv, "reset", "-q", "--hard", "origin/main")
159	mustGit(t, dir, aliceEnv, "commit", "-q", "--allow-empty", "-m", "diverge again")
160	mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
161	_, errOut, code = inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "3", "--strategy", "rebase")
162	if code != 2 || !strings.Contains(errOut, "linear history") {
163		t.Fatalf("rebase with merge commit: exit %d, %s", code, errOut)
164	}
165
166	// --- require_signed_commits refuses squash outright ---
167	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "require-signed", "alice/lib", "on"); code != 0 {
168		t.Fatal("require-signed failed")
169	}
170	_, errOut, code = inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "3", "--strategy", "squash")
171	if code != 4 || !strings.Contains(errOut, "only fast-forward") {
172		t.Fatalf("squash on require-signed: exit %d, %s", code, errOut)
173	}
174	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "require-signed", "alice/lib", "off"); code != 0 {
175		t.Fatal("require-signed off failed")
176	}
177
178	// --- rebase when ff is possible IS a fast-forward: shas preserved ---
179	mustGit(t, bobDir, bobEnv, "checkout", "-q", "-b", "feat4")
180	mustGit(t, bobDir, bobEnv, "fetch", "-q", "origin")
181	mustGit(t, bobDir, bobEnv, "reset", "-q", "--hard", "origin/main")
182	os.WriteFile(filepath.Join(bobDir, "g.txt"), []byte("g\n"), 0o644)
183	mustGit(t, bobDir, bobEnv, "add", ".")
184	mustGit(t, bobDir, bobEnv, "commit", "-q", "-m", "clean on top")
185	tip := strings.TrimSpace(mustGit(t, bobDir, bobEnv, "rev-parse", "HEAD"))
186	mustGit(t, bobDir, bobEnv, "push", "-q", "origin", "feat4")
187	if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "create", "alice/lib",
188		"--source", "feat4", "--target", "main", "--title", "'ff-able'"); code != 0 {
189		t.Fatalf("mr4 create: %s", errOut)
190	}
191	out, errOut, code = inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "4", "--strategy", "rebase", "--json")
192	if code != 0 {
193		t.Fatalf("ff-able rebase: %s", errOut)
194	}
195	if !strings.Contains(out, `"strategy":"ff"`) || !strings.Contains(out, tip) {
196		t.Fatalf("ff-able rebase should fast-forward to %s: %s", tip, out)
197	}
198}