feat: discover non-canonical worktrees by branch matching (Closes #528)

This commit is contained in:
2026-07-08 22:53:34 -04:00
parent 9a2e585a9e
commit 7a0de4a8e9
3 changed files with 132 additions and 4 deletions
+46 -2
View File
@@ -36,6 +36,34 @@ def resolve_worktree_path(project_root: str, branch: str) -> str:
return os.path.join(project_root, "branches", branch_worktree_folder(branch))
def discover_local_worktrees(project_root: str) -> dict[str, str]:
"""Return a mapping from branch name to absolute worktree path (#528)."""
res = subprocess.run(
["git", "-C", project_root, "worktree", "list", "--porcelain"],
capture_output=True,
text=True,
check=False,
)
if res.returncode != 0:
return {}
mapping: dict[str, str] = {}
current_worktree = None
for line in res.stdout.splitlines():
line = line.strip()
if not line:
current_worktree = None
continue
if line.startswith("worktree "):
current_worktree = line[9:].strip()
elif line.startswith("branch ") and current_worktree:
ref = line[7:].strip()
if ref.startswith("refs/heads/"):
branch_name = ref[11:]
mapping[branch_name] = current_worktree
return mapping
def read_issue_lock(path: str | None = None) -> dict[str, Any] | None:
if path:
return issue_lock_store.read_lock_file(path.strip())
@@ -216,6 +244,7 @@ def build_pr_cleanup_entry(
delete_capability_allowed: bool,
issue_lock_path: str | None = None,
protected_branches: frozenset[str] | None = None,
worktree_map: dict[str, str] | None = None,
) -> dict[str, Any]:
pr_number = int(pr["number"])
head_branch = pr.get("head") or ""
@@ -224,7 +253,16 @@ def build_pr_cleanup_entry(
title = pr.get("title") or ""
body = pr.get("body") or ""
merged = bool(pr.get("merged_at"))
worktree_path = resolve_worktree_path(project_root, head_branch)
discovered_path = worktree_map.get(head_branch) if worktree_map else None
derived_path = resolve_worktree_path(project_root, head_branch)
if discovered_path:
worktree_path = discovered_path
match_type = "branch"
else:
worktree_path = derived_path
match_type = "path"
worktree_state = read_local_worktree_state(worktree_path)
worktree_state["worktree_path"] = worktree_path
active_lock = has_active_issue_lock(head_branch, issue_lock_path)
@@ -247,6 +285,8 @@ def build_pr_cleanup_entry(
worktree_state=worktree_state,
active_lock=active_lock,
)
local["match_type"] = match_type
return {
"pr_number": pr_number,
"issue_number": extract_linked_issue(title, body),
@@ -272,6 +312,7 @@ def build_reconciliation_report(
protected_branches: frozenset[str] | None = None,
) -> dict[str, Any]:
open_heads = collect_open_pr_heads(open_prs)
worktree_map = discover_local_worktrees(project_root)
entries: list[dict[str, Any]] = []
for pr in closed_prs or []:
if not pr.get("merged_at"):
@@ -290,6 +331,7 @@ def build_reconciliation_report(
delete_capability_allowed=delete_capability_allowed,
issue_lock_path=issue_lock_path,
protected_branches=protected_branches,
worktree_map=worktree_map,
)
)
return {
@@ -302,7 +344,9 @@ def build_reconciliation_report(
def remove_local_worktree(project_root: str, branch: str) -> dict[str, Any]:
worktree_path = resolve_worktree_path(project_root, branch)
worktree_map = discover_local_worktrees(project_root)
discovered_path = worktree_map.get(branch)
worktree_path = discovered_path or resolve_worktree_path(project_root, branch)
if not os.path.isdir(worktree_path):
return {
"success": False,