Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a202970fd | ||
|
|
f6b2adac4d | ||
|
|
7a0de4a8e9 |
@@ -90,6 +90,17 @@ def list_local_worktrees(project_root: str) -> list[dict[str, Any]]:
|
||||
return entries
|
||||
|
||||
|
||||
def discover_local_worktrees(project_root: str) -> dict[str, str]:
|
||||
"""Return a mapping from branch name to absolute worktree path (#528)."""
|
||||
mapping: dict[str, str] = {}
|
||||
for entry in list_local_worktrees(project_root):
|
||||
branch = entry.get("branch")
|
||||
path = entry.get("path")
|
||||
if branch and path:
|
||||
mapping[str(branch)] = str(path)
|
||||
return mapping
|
||||
|
||||
|
||||
def _is_under_branches(project_root: str, path: str) -> bool:
|
||||
branches_root = os.path.realpath(os.path.join(project_root, "branches"))
|
||||
real_path = os.path.realpath(path)
|
||||
@@ -148,12 +159,15 @@ def resolve_cleanup_worktree_state(
|
||||
issue_match = wanted_issue is not None and entry_issue == wanted_issue
|
||||
if not (branch_match or issue_match):
|
||||
continue
|
||||
if not _head_is_safe_for_cleanup(
|
||||
safe_head = _head_is_safe_for_cleanup(
|
||||
project_root=project_root,
|
||||
candidate_head=entry.get("head_sha"),
|
||||
pr_head_sha=pr_head_sha,
|
||||
target_ref=target_ref,
|
||||
):
|
||||
)
|
||||
if not safe_head and branch_match and not pr_head_sha and not target_ref:
|
||||
safe_head = True
|
||||
if not safe_head:
|
||||
unsafe_matches.append(path)
|
||||
continue
|
||||
candidates.append(entry)
|
||||
@@ -165,7 +179,9 @@ def resolve_cleanup_worktree_state(
|
||||
alias_state["worktree_path"] = path
|
||||
alias_state["expected_worktree_path"] = expected_path
|
||||
alias_state["discovered_worktree_path"] = path
|
||||
alias_state["match_type"] = "alias"
|
||||
alias_state["match_type"] = (
|
||||
"branch" if candidates[0].get("branch") == head_branch else "alias"
|
||||
)
|
||||
alias_state["candidate_paths"] = [path]
|
||||
alias_state["alias_block_reasons"] = []
|
||||
alias_state["alias_verified"] = True
|
||||
|
||||
@@ -154,6 +154,70 @@ class TestMergedCleanupReport(unittest.TestCase):
|
||||
finally:
|
||||
os.remove(lock_path)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_discover_local_worktrees(self, mock_run):
|
||||
mock_output = (
|
||||
"worktree /repo/Gitea-Tools\n"
|
||||
"bare\n\n"
|
||||
"worktree /repo/Gitea-Tools/branches/custom-name\n"
|
||||
"branch refs/heads/feat/issue-11-x\n"
|
||||
"HEAD cafebabe\n\n"
|
||||
)
|
||||
mock_run.return_value = Mock(returncode=0, stdout=mock_output)
|
||||
res = mcr.discover_local_worktrees("/repo/Gitea-Tools")
|
||||
self.assertEqual(res, {"feat/issue-11-x": "/repo/Gitea-Tools/branches/custom-name"})
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("merged_cleanup_reconcile.read_local_worktree_state")
|
||||
def test_build_report_discovers_non_canonical_worktree_by_branch(self, mock_state, mock_run):
|
||||
mock_output = (
|
||||
"worktree /repo/Gitea-Tools\n"
|
||||
"bare\n\n"
|
||||
"worktree /repo/Gitea-Tools/branches/custom-name\n"
|
||||
"branch refs/heads/feat/issue-11-x\n"
|
||||
"HEAD cafebabe\n\n"
|
||||
)
|
||||
mock_run.return_value = Mock(returncode=0, stdout=mock_output)
|
||||
mock_state.side_effect = [
|
||||
{
|
||||
"exists": False,
|
||||
"clean": None,
|
||||
"current_branch": None,
|
||||
"head_sha": None,
|
||||
"dirty_files": [],
|
||||
},
|
||||
{
|
||||
"exists": True,
|
||||
"clean": True,
|
||||
"current_branch": "feat/issue-11-x",
|
||||
"head_sha": "cafebabe",
|
||||
"dirty_files": [],
|
||||
},
|
||||
]
|
||||
|
||||
report = mcr.build_reconciliation_report(
|
||||
project_root="/repo/Gitea-Tools",
|
||||
closed_prs=[
|
||||
{
|
||||
"number": 11,
|
||||
"title": "merged",
|
||||
"body": "Closes #11",
|
||||
"head": {"ref": "feat/issue-11-x"},
|
||||
"merged_at": "2026-07-06T12:00:00Z",
|
||||
"merge_commit_sha": "abc123",
|
||||
}
|
||||
],
|
||||
open_prs=[],
|
||||
remote_branch_exists={"feat/issue-11-x": True},
|
||||
head_on_master={11: True},
|
||||
delete_capability_allowed=True,
|
||||
)
|
||||
self.assertEqual(report["merged_pr_count"], 1)
|
||||
entry = report["entries"][0]
|
||||
local = entry["local_worktree"]
|
||||
self.assertEqual(local["worktree_path"], "/repo/Gitea-Tools/branches/custom-name")
|
||||
self.assertEqual(local["match_type"], "branch")
|
||||
|
||||
@patch("merged_cleanup_reconcile.read_local_worktree_state")
|
||||
@patch("subprocess.run")
|
||||
def test_build_report_discovers_issue_alias_worktree(self, mock_run, mock_state):
|
||||
@@ -209,7 +273,7 @@ class TestMergedCleanupReport(unittest.TestCase):
|
||||
|
||||
local = report["entries"][0]["local_worktree"]
|
||||
self.assertTrue(local["safe_to_remove_worktree"])
|
||||
self.assertEqual(local["match_type"], "alias")
|
||||
self.assertEqual(local["match_type"], "branch")
|
||||
self.assertEqual(
|
||||
local["expected_worktree_path"],
|
||||
"/repo/Gitea-Tools/branches/feat-issue-510-workspace-binding-isolation",
|
||||
@@ -308,7 +372,7 @@ class TestMergedCleanupReport(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertTrue(state["exists"])
|
||||
self.assertEqual(state["match_type"], "alias")
|
||||
self.assertEqual(state["match_type"], "branch")
|
||||
self.assertEqual(
|
||||
state["discovered_worktree_path"],
|
||||
"/repo/Gitea-Tools/branches/issue-510-workspace-binding-isolation",
|
||||
|
||||
Reference in New Issue
Block a user