aboutsummaryrefslogtreecommitdiffziptar.gz
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 09:57:12 +0200
committergodosa <godosa@godosa.eu>2026-10-07 09:57:12 +0200
commit6f559437fe95b3a952b63b6672db7d89d0ce86c2 (patch)
tree03eb1c00d219ed8698e005a6dfaf30075cc9ca0d
parent959ba8df813ceda4df6466f2c35eea3bcce82271 (diff)
downloadworkflow-6f559437fe95b3a952b63b6672db7d89d0ce86c2.tar.gz
workflow-6f559437fe95b3a952b63b6672db7d89d0ce86c2.zip
wf start: refuse worktree of another repo; .wf-home not stray
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2
-rw-r--r--tests/test_split.py30
-rwxr-xr-xwf.py10
2 files changed, 39 insertions, 1 deletions
diff --git a/tests/test_split.py b/tests/test_split.py
index 934bf6f..9d19308 100644
--- a/tests/test_split.py
+++ b/tests/test_split.py
@@ -91,6 +91,26 @@ class StartSplitTest(SplitCli):
self.assertIn(f"cd {wt} && (make test) && ", out)
self.assertFalse((self.root / ".worktrees").exists()) # nothing in the private repo
+ def test_start_refuses_private_repo_worktree(self):
+ wt = self.root / ".worktrees" / "slow"
+ git(self.root, "worktree", "add", "-q", str(wt), "-b", "slow/old")
+ code, out, err = self.wf("start", "t-three", "--worktree", str(wt), "--branch", "slow/t-three",
+ project=False, env=IDENT)
+ self.assertNotEqual(code, 0)
+ self.assertIn("is not a worktree of", err)
+ self.assertFalse((wt / ".wf-home").exists())
+ self.assertEqual(subprocess.run(["git", "branch", "--show-current"], cwd=wt, capture_output=True,
+ text=True).stdout.strip(), "slow/old") # not switched
+ self.assertIn("- **t-three** [P2] (<1h): Third", (self.root / "TASKS.md").read_text()) # no progress
+
+ def test_start_reuses_existing_code_worktree(self):
+ wt = self.code / ".worktrees" / "slow"
+ git(self.code, "worktree", "add", "-q", str(wt), "-b", "slow/old")
+ code, out, err = self.wf("start", "t-three", "--worktree", str(wt), "--branch", "slow/t-three",
+ project=False, env=IDENT)
+ self.assertEqual(code, 0, out + err)
+ self.assertEqual((wt / ".wf-home").read_text(), f"{self.root}\n")
+
def test_setup_in_code_worktree(self):
wt = self.add_wt()
code, out, err = self.wf("setup", project=False, cwd=wt)
@@ -152,6 +172,16 @@ class FinishSplitTest(SplitCli):
self.assertEqual(subprocess.run(["git", "branch", "--list", "sonnet/t-three"], cwd=self.code,
capture_output=True, text=True).stdout, "")
+ def test_unignored_wf_home_does_not_block(self):
+ (self.code / ".gitignore").write_text(".worktrees/\n")
+ git(self.code, "commit", "-qam", "no wf-home ignore")
+ git(self.wt, "rebase", "-q", "master")
+ (self.wt / "code.txt").write_text("x\n")
+ code, out, err = self.finish("t-three", "-m", "ok", "--commit", "impl", "code.txt")
+ self.assertEqual(code, 0, out + err)
+ self.assertNotIn(".wf-home", subprocess.run(["git", "show", "--stat", "master"], cwd=self.code,
+ capture_output=True, text=True).stdout)
+
def test_stray_code_file_refused_task_open(self):
(self.wt / "code.txt").write_text("x\n")
(self.wt / "stray.txt").write_text("s\n")
diff --git a/wf.py b/wf.py
index 7b5ae12..dda4914 100755
--- a/wf.py
+++ b/wf.py
@@ -1231,7 +1231,7 @@ def cmd_merge(args) -> int:
bmain = config.git_top(cfg.root) or main # books repo: the private repo of a split project, else main
private = [str(Path(x).resolve().relative_to(bmain.resolve())) for x in getattr(args, "private", None) or []]
files = [str(f.relative_to(bmain)) for f in (cfg.tasks, cfg.archive)] + private
- if git_run(top, "status", "--porcelain", "-uall").stdout.strip():
+ if _dirty_outside(top, []):
raise Failure("worktree has uncommitted changes: commit them first")
had = unmerged_count(top, master)
if not branch and not had:
@@ -1326,6 +1326,8 @@ def _dirty_outside(top: Path, keep: list[Path]) -> list[str]:
for line in git_run(top, "status", "--porcelain", "-uall").stdout.splitlines():
rel = line[3:].split(" -> ")[-1].strip('"').rstrip("/")
path = (top / rel).resolve()
+ if rel == config.WF_HOME: # wf start's pointer in a code worktree, never committed
+ continue
if not any(path == k or k in path.parents for k in keep):
stray.append(rel)
return stray
@@ -1531,6 +1533,12 @@ def cmd_start(args) -> int:
git_ok(repo, "worktree", "add", "-q", str(wt), "-b", b, master)
how = f"new, branch {b} from {master}"
else:
+ own = git_run(wt, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
+ want = git_run(repo, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
+ if not own or Path(own).resolve() != Path(want).resolve():
+ raise Failure(f"{shown} is not a worktree of {repo}"
+ + (f" (it belongs to {Path(own).resolve().parent})" if own else "")
+ + f": pass a worktree path of {repo} (e.g. {repo}/.worktrees/<lane>)")
dirty = git_run(wt, "status", "--porcelain").stdout.rstrip("\n")
cur = config.git_branch(Path(git_run(wt, "rev-parse", "--absolute-git-dir").stdout.strip()))
if dirty and not args.recovery: