aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_merge.py
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 07:27:17 +0200
committergodosa <godosa@godosa.eu>2026-10-07 07:27:17 +0200
commit81d4e80fd5aabe4e80f58e960affa795cf7d34ec (patch)
treee98eeac2af6af63aa4287bba1f6d4a3af26b5727 /tests/test_merge.py
downloadworkflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.tar.gz
workflow-81d4e80fd5aabe4e80f58e960affa795cf7d34ec.zip
workflow: initial public history
Diffstat (limited to 'tests/test_merge.py')
-rw-r--r--tests/test_merge.py159
1 files changed, 159 insertions, 0 deletions
diff --git a/tests/test_merge.py b/tests/test_merge.py
new file mode 100644
index 0000000..cb14a20
--- /dev/null
+++ b/tests/test_merge.py
@@ -0,0 +1,159 @@
+import fcntl
+import subprocess
+import sys
+import time
+import unittest
+
+from test_cli import Cli, WF
+from test_claims import git
+
+IDENT = {"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
+ "CLAUDE_CODE_MESSAGING_SOCKET": ""}
+
+
+class MergeTest(Cli):
+ def setUp(self):
+ super().setUp()
+ git(self.root, "init", "-q", "-b", "master")
+ (self.root / ".gitignore").write_text(".worktrees/\n.wf/\n")
+ git(self.root, "add", "-A")
+ git(self.root, "commit", "-qm", "init")
+ self.wt = self.root / ".worktrees" / "sonnet"
+ git(self.root, "worktree", "add", "-q", str(self.wt), "-b", "sonnet/t-three")
+
+ def merge(self, *args):
+ return self.wf("merge", *args, project=False, cwd=self.wt, env=IDENT)
+
+ def out(self, *args, cwd=None):
+ return subprocess.run(["git", *args], cwd=cwd or self.root, capture_output=True, text=True).stdout
+
+ def commit_code(self):
+ (self.wt / "code.txt").write_text("x\n")
+ git(self.wt, "add", "code.txt")
+ git(self.wt, "commit", "-qm", "code")
+
+ def test_merge_ff_commits_bookkeeping_and_detaches(self):
+ self.commit_code()
+ self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT)
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (0, ""), out)
+ self.assertEqual(out, "rebased onto master\nfast-forwarded master\n"
+ "committed TASKS.md tasks/archive.md\nmerged sonnet/t-three into master\n")
+ self.assertEqual(self.out("log", "--format=%s", "master"), "t-three done\ncode\ninit\n")
+ self.assertEqual(self.out("branch", "--list", "sonnet/t-three"), "")
+ self.assertEqual(self.out("status", "--porcelain"), "")
+ self.assertEqual((self.root / "code.txt").read_text(), "x\n")
+
+ def test_merge_commits_notes_and_followups_after_done(self):
+ self.commit_code()
+ self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT)
+ r = self.wf("add", "-p", "2", "-e", "1h", "follow up", project=False, cwd=self.wt, env=IDENT); self.assertEqual(r[0], 0, r)
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (0, ""), out)
+ self.assertEqual(self.out("status", "--porcelain"), "")
+ self.assertIn("follow up", self.out("show", "master:TASKS.md"))
+
+ def test_merge_rebases_onto_moved_master(self):
+ self.commit_code()
+ (self.root / "other.txt").write_text("o\n")
+ git(self.root, "add", "other.txt")
+ git(self.root, "commit", "-qm", "other")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (0, ""), out)
+ self.assertEqual(self.out("log", "--format=%s", "master"), "code\nother\ninit\n")
+
+ def test_merge_conflict_aborts_and_keeps_branch(self):
+ (self.root / "DESIGN.md").write_text("main side\n")
+ git(self.root, "commit", "-qam", "main edit")
+ (self.wt / "DESIGN.md").write_text("branch side\n")
+ git(self.wt, "commit", "-qam", "branch edit")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (1, "wf: rebase onto master conflicts: git rebase master, resolve, verify, "
+ "then wf merge again\n"))
+ self.assertEqual((self.wt / "DESIGN.md").read_text(), "branch side\n")
+ self.assertEqual(self.out("status", "--porcelain", cwd=self.wt), "")
+ self.assertEqual(self.out("branch", "--show-current", cwd=self.wt), "sonnet/t-three\n")
+
+ def test_merge_refuses_dirty_worktree(self):
+ (self.wt / "DESIGN.md").write_text("dirty\n")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (1, "wf: worktree has uncommitted changes: commit them first\n"))
+
+ def test_merge_outside_worktree_fails(self):
+ self.assertEqual(self.fails("merge"), "wf: merge runs inside a linked git worktree (lane worktree)\n")
+
+ def test_merge_detached_fails(self):
+ git(self.wt, "switch", "-q", "--detach", "master")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (1, "wf: worktree is on a detached HEAD: nothing to merge\n"))
+
+ def test_merge_detached_commits_leftover_bookkeeping(self):
+ self.commit_code()
+ self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT)
+ self.merge("--no-push")
+ self.wf("add", "-p", "2", "-e", "1h", "late follow up", project=False, cwd=self.wt, env=IDENT)
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err, out), (0, "", "committed TASKS.md tasks/archive.md\n"))
+ self.assertEqual(self.out("status", "--porcelain"), "")
+ self.assertIn("late follow up", self.out("show", "master:TASKS.md"))
+
+ def test_merge_detached_with_impl_commit_merges_it(self):
+ git(self.wt, "switch", "-q", "--detach", "master")
+ (self.root / "other.txt").write_text("o\n")
+ git(self.root, "add", "other.txt")
+ git(self.root, "commit", "-qm", "other")
+ self.commit_code()
+ r = self.wf("done", "t-three", "-m", "ok", project=False, cwd=self.wt, env=IDENT)
+ self.assertIn("detached HEAD has 1 commit not in master: wf merge merges it", r[1])
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (0, ""), out)
+ self.assertEqual(out, "rebased onto master\nfast-forwarded master\n"
+ "committed TASKS.md tasks/archive.md\nmerged detached HEAD into master\n")
+ self.assertEqual(self.out("log", "--format=%s", "master"), "bookkeeping\ncode\nother\ninit\n")
+ self.assertEqual((self.root / "code.txt").read_text(), "x\n")
+ self.assertEqual(self.out("rev-parse", "HEAD", cwd=self.wt), self.out("rev-parse", "master"))
+ self.assertEqual(self.out("status", "--porcelain"), "")
+
+ def test_merge_detached_refuses_dirty_worktree(self):
+ git(self.wt, "switch", "-q", "--detach", "master")
+ (self.wt / "code.txt").write_text("x\n")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (1, "wf: worktree has uncommitted changes: commit them first\n"))
+
+ def test_merge_detached_conflict_aborts_and_keeps_commit(self):
+ git(self.wt, "switch", "-q", "--detach", "master")
+ (self.root / "DESIGN.md").write_text("main side\n")
+ git(self.root, "commit", "-qam", "main edit")
+ (self.wt / "DESIGN.md").write_text("branch side\n")
+ git(self.wt, "commit", "-qam", "branch edit")
+ code, out, err = self.merge("--no-push")
+ self.assertEqual((code, err), (1, "wf: rebase onto master conflicts: git rebase master, resolve, verify, "
+ "then wf merge again\n"))
+ self.assertEqual(self.out("log", "-1", "--format=%s", cwd=self.wt), "branch edit\n")
+
+ def test_merge_pushes_to_home(self):
+ bare = self.root.parent / "home.git"
+ subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True)
+ git(self.root, "remote", "add", "home", str(bare))
+ self.commit_code()
+ code, out, err = self.merge()
+ self.assertEqual((code, err), (0, ""), out)
+ self.assertIn("pushed home\n", out)
+ self.assertEqual(self.out("log", "--format=%s", "master", cwd=bare), "code\ninit\n")
+
+ def test_merge_waits_for_lock(self):
+ self.commit_code()
+ (self.root / ".wf").mkdir(exist_ok=True)
+ with open(self.root / ".wf" / "lock", "w") as held:
+ fcntl.flock(held, fcntl.LOCK_EX)
+ import os
+ p = subprocess.Popen([sys.executable, str(WF), "merge", "--no-push"], cwd=self.wt, text=True,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={**os.environ, **IDENT})
+ time.sleep(0.5)
+ self.assertIsNone(p.poll(), "merge ran while the lock was held")
+ out, err = p.communicate(timeout=20)
+ self.assertEqual((p.returncode, err), (0, ""))
+
+
+if __name__ == "__main__":
+ unittest.main()