fix(bootstrap): path-shaped branches ancestry without isdir (#850 review #551)

resolve_canonical_repo_root fallback now uses commonpath only — no
string split and no os.path.isdir gate — so MCP project_root =
branches/<wt> still resolves to the repo root when the path is not yet
on disk (#274 / review #551 regression).
This commit is contained in:
2026-07-24 07:54:07 -04:00
parent 06e95254f0
commit e1d844bfed
2 changed files with 22 additions and 12 deletions
+10 -7
View File
@@ -93,9 +93,10 @@ def resolve_canonical_repo_root(workspace_path: str, fallback_project_root: str)
pass pass
# Fallback when git metadata is unavailable. Never string-split on # Fallback when git metadata is unavailable. Never string-split on
# "/branches/" (review #531 Finding 2 / F-6): recover the repo root only # "/branches/" (review #531 F2 / #551): recover the repo root only via
# via resolved-path commonpath ancestry against a parent that owns a # resolved-path commonpath ancestry. Do **not** require on-disk isdir —
# real ``branches`` directory containing the fallback path. # MCP may launch with project_root = branches/<wt> before that path
# exists, and #274 path-shaped worktree-as-project-root must still resolve.
fallback = os.path.realpath(fallback_project_root or workspace_path or ".") fallback = os.path.realpath(fallback_project_root or workspace_path or ".")
cur = fallback cur = fallback
for _ in range(64): for _ in range(64):
@@ -103,16 +104,18 @@ def resolve_canonical_repo_root(workspace_path: str, fallback_project_root: str)
if parent == cur: if parent == cur:
break break
branches_dir = os.path.realpath(os.path.join(parent, "branches")) branches_dir = os.path.realpath(os.path.join(parent, "branches"))
if os.path.isdir(branches_dir):
try: try:
# Path-shaped: fallback is under parent/branches/ (commonpath).
if os.path.commonpath([branches_dir, fallback]) == branches_dir: if os.path.commonpath([branches_dir, fallback]) == branches_dir:
return parent return parent
except ValueError: except ValueError:
pass pass
# Also accept fallback itself being the branches directory. # Fallback path itself is the branches directory.
if os.path.basename(cur) == "branches" and os.path.isdir(cur): if os.path.basename(os.path.realpath(cur)) == "branches":
try: try:
if os.path.commonpath([cur, fallback]) == cur: if os.path.commonpath([os.path.realpath(cur), fallback]) == os.path.realpath(
cur
):
return parent return parent
except ValueError: except ValueError:
pass pass
+7
View File
@@ -86,6 +86,13 @@ class TestAssessAuthorMutationWorktree(unittest.TestCase):
self.assertTrue(result["proven"]) self.assertTrue(result["proven"])
self.assertFalse(result["block"]) self.assertFalse(result["block"])
def test_path_shaped_branches_ancestry_without_isdir(self):
"""Review #551: commonpath recovery must not require on-disk isdir."""
fake_wt = "/repo/Gitea-Tools/branches/issue-274"
root = amw.resolve_canonical_repo_root(fake_wt, fake_wt)
self.assertEqual(root, "/repo/Gitea-Tools")
self.assertNotIn('split("/branches/")', open(amw.__file__).read())
class TestPreflightIntegration(unittest.TestCase): class TestPreflightIntegration(unittest.TestCase):
def test_verify_preflight_blocks_control_checkout_with_test_porcelain(self): def test_verify_preflight_blocks_control_checkout_with_test_porcelain(self):