From ecf39ec27307f36d9458a22a332c8848bffe79a5 Mon Sep 17 00:00:00 2001 From: godosa Date: Wed, 7 Oct 2026 09:24:14 +0200 Subject: split projects 3/6: wf finish/merge/wip across code + private repo (report: commit books ) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JEAjUkQRrCYX5MZhWxdtj2 --- wf.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 17 deletions(-) (limited to 'wf.py') diff --git a/wf.py b/wf.py index 444cd37..f3b7f28 100755 --- a/wf.py +++ b/wf.py @@ -1194,7 +1194,8 @@ def worktree_done_lines(p: Project, args) -> list[str]: return [] branch, main = config.git_branch(wt[1]), wt[2] master = config.git_branch(main / ".git") or "master" - files = " ".join(str(f.relative_to(main)) for f in (p.cfg.tasks, p.cfg.archive)) + bmain = config.git_top(p.cfg.root) or main # books repo (private repo of a split project) + files = " ".join(str(f.relative_to(bmain)) for f in (p.cfg.tasks, p.cfg.archive)) warn = [] if not branch and (n := unmerged_count(wt[0], master)): warn = [f"detached HEAD has {n} commit{'s' * (n != 1)} not in {master}: wf merge merges " @@ -1224,14 +1225,19 @@ def cmd_merge(args) -> int: branch = config.git_branch(gitdir) master = config.git_branch(main / ".git") or "master" cfg = config.load_at(start) - files = [str(f.relative_to(main)) for f in (cfg.tasks, cfg.archive)] + 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(): raise Failure("worktree has uncommitted changes: commit them first") - if not branch and not unmerged_count(top, master): + had = unmerged_count(top, master) + if not branch and not had: # after a merge: notes / follow-ups written since → bookkeeping commit only - if not git_run(main, "status", "--porcelain", "--", *files).stdout.strip(): + if not git_run(bmain, "status", "--porcelain", "--", *files).stdout.strip(): raise Failure("worktree is on a detached HEAD: nothing to merge") - r = git_run(main, "commit", "-q", "-m", args.m or "bookkeeping", "--", *files) + if private and (r := git_run(bmain, "add", "--", *private)).returncode: + raise Failure(f"git add failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") + r = git_run(bmain, "commit", "-q", "-m", args.m or "bookkeeping", "--", *files) if r.returncode: raise Failure(f"bookkeeping commit failed: {(r.stderr.strip() or r.stdout.strip() or 'git error').splitlines()[-1]}") print(f"committed {' '.join(files)}") @@ -1241,21 +1247,28 @@ def cmd_merge(args) -> int: git_run(top, "rebase", "--abort") raise Failure(f"rebase onto {master} conflicts: git rebase {master}, resolve, verify, then wf merge again") out.append(f"rebased onto {master}") - args.merged_sha = git_run(top, "rev-parse", "--short", "HEAD").stdout.strip() + args.merged_sha = (git_run(top, "rev-parse", "--short", "HEAD").stdout.strip() if had or bmain == main + else "-") r = git_run(main, "merge", "--ff-only", branch or git_run(top, "rev-parse", "HEAD").stdout.strip()) if r.returncode: raise Failure(f"ff-merge into {master} failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") out.append(f"fast-forwarded {master}") - if git_run(main, "status", "--porcelain", "--", *files).stdout.strip(): - msg = args.m or (f"{branch.rsplit('/', 1)[-1]} done" if branch else "bookkeeping") - r = git_run(main, "commit", "-q", "-m", msg, "--", *files) + if private and (r := git_run(bmain, "add", "--", *private)).returncode: + raise Failure(f"git add failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") + if git_run(bmain, "status", "--porcelain", "--", *files).stdout.strip(): + msg = (getattr(args, "private_msg", None) if private else None) or args.m \ + or (f"{branch.rsplit('/', 1)[-1]} done" if branch else "bookkeeping") + if bmain != main and args.merged_sha != "-": + msg += f" (code {args.merged_sha})" + r = git_run(bmain, "commit", "-q", "-m", msg, "--", *files) if r.returncode: raise Failure(f"bookkeeping commit failed: {(r.stderr.strip() or r.stdout.strip() or 'git error').splitlines()[-1]}") out.append(f"committed {' '.join(files)}") + args.books_sha = git_run(bmain, "rev-parse", "--short", "HEAD").stdout.strip() git_run(top, "switch", "-q", "--detach", master) if branch: git_run(top, "branch", "-q", "-d", branch) - if not args.no_push and _push_home(main): + if not args.no_push and any([_push_home(main), bmain != main and _push_home(bmain)]): out.append("pushed home") out.append(f"merged {branch or 'detached HEAD'} into {master}") print("\n".join(out)) @@ -1304,15 +1317,28 @@ def cmd_finish(args) -> int: top = Path(git_run(start, "rev-parse", "--show-toplevel").stdout.strip() or start) paths = [(start / f).resolve() for f in args.paths] books = [] if wt else [cfg.tasks.resolve(), cfg.archive.resolve()] + btop = (config.git_top(cfg.root) or cfg.root).resolve() + split = bool(wt) and btop != wt[2].resolve() # code worktree of a split project: books in another repo + proot, priv, code = cfg.root.resolve(), [], [] for f, path in zip(args.paths, paths): + if split and (path == proot or proot in path.parents): + if not path.exists() and git_run(btop, "ls-files", "--error-unmatch", "--", str(path)).returncode: + raise Failure(f"path {f} does not exist and is not tracked: nothing to commit") + priv.append(path) + continue + code.append(path) if path != top and top not in path.parents: + if split: + raise Failure(f"path {f} is in neither repo ({top}, {cfg.root}): commit it in its own repo " + "first, then wf finish without it") raise Failure(f"path {f} is outside this repo ({top}): commit it in its own repo first, " "then wf finish without it") - if wt and path in (top / cfg.tasks.relative_to(wt[2]), top / cfg.archive.relative_to(wt[2])): + if wt and not split and path in (top / cfg.tasks.relative_to(wt[2]), top / cfg.archive.relative_to(wt[2])): raise Failure(f"path {f} = this worktree's copy of the books: wf finish writes the main tree's " "and wf merge commits them; drop it") if not path.exists() and git_run(top, "ls-files", "--error-unmatch", "--", str(path)).returncode: raise Failure(f"path {f} does not exist and is not tracked: nothing to commit") + paths = code if wt and paths and not git_run(top, "status", "--porcelain", "--", *map(str, paths)).stdout.strip(): raise Failure("nothing to commit in the --commit paths: drop them (wf finish without paths) or fix them") if stray := _dirty_outside(top, paths + books): @@ -1348,11 +1374,13 @@ def cmd_finish(args) -> int: if not wt and not args.no_push and _push_home(top): print("pushed home", flush=True) if wt: - ns = argparse.Namespace(project=args.project, m=None, no_push=args.no_push) + ns = argparse.Namespace(project=args.project, m=None, no_push=args.no_push, + private=priv, private_msg=args.commit) rc = cmd_merge(ns) if not rc and getattr(ns, "merged_sha", ""): tool = getattr(args, "tool_commit", None) - print(f"report: commit {ns.merged_sha}" + (f" tool {tool}" if tool else ""), flush=True) + books_sha = f" books {ns.books_sha}" if split else "" + print(f"report: commit {ns.merged_sha}{books_sha}" + (f" tool {tool}" if tool else ""), flush=True) return rc return 0 @@ -1360,10 +1388,25 @@ def cmd_finish(args) -> int: def cmd_wip(args) -> int: """Wrap-up in one call (lane worktree): commit the given paths on the branch, note the state, status clear.""" start = Path(args.project or Path.cwd()).resolve() - if not config.linked_worktree(start): + wt = config.linked_worktree(start) + if not wt: raise Failure("wip runs inside a lane worktree") top = Path(git_run(start, "rev-parse", "--show-toplevel").stdout.strip() or start) - paths = [str((start / f).resolve()) for f in args.paths] + p = load_project(args) + id = p.resolve(args.id) + btop = (config.git_top(p.cfg.root) or p.cfg.root).resolve() + proot, priv, paths = p.cfg.root.resolve(), [], [] + for f in args.paths: # split project: paths under the private root are committed there + path = (start / f).resolve() + (priv if btop != wt[2].resolve() and (path == proot or proot in path.parents) else paths).append(str(path)) + if priv: + r = git_run(btop, "add", "--", *priv) + if r.returncode: + raise Failure(f"git add failed: {(r.stderr.strip() or 'git error').splitlines()[-1]}") + r = git_run(btop, "commit", "-q", "-m", f"{id} wip: {args.m}", "--", *priv) + if r.returncode: + raise Failure(f"commit failed: {(r.stderr.strip() or r.stdout.strip() or 'git error').splitlines()[-1]}") + print("committed " + " ".join(os.path.relpath(c, btop) for c in priv), flush=True) if paths: r = git_run(top, "add", "--", *paths) if r.returncode: @@ -1372,8 +1415,6 @@ def cmd_wip(args) -> int: if r.returncode: raise Failure(f"commit failed: {(r.stderr.strip() or r.stdout.strip() or 'git error').splitlines()[-1]}") print("committed " + " ".join(os.path.relpath(c, top) for c in paths), flush=True) - p = load_project(args) - id = p.resolve(args.id) with project_lock(p.cfg.root): cmd_note(argparse.Namespace(project=args.project, id=id, line=args.m, dry_run=False)) cmd_status(argparse.Namespace(project=args.project, id=id, kind="clear", value=[], dry_run=False, -- cgit