mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(projects): fold deleted-worktree subdirs into their parent repo
`_probe_sibling_worktree` recovers a deleted `<repo>-<suffix>` worktree by trimming its name back to a sibling that still resolves, but it only trimmed the LEAF segment. A session's cwd is usually a subdir of the worktree (`<repo>-<suffix>/apps/desktop`), whose basename shares nothing with the repo, so the probe no-oped and the dead path fell through to the path-only heuristic — which minted it as its own main repo root, and then as a top-level project. Walk the ancestors, deepest first, with the probe budget shared across the whole walk so a deeply nested cwd can't fan out into a probe storm.
This commit is contained in:
parent
92549c9a6e
commit
c7fcb73e3d
2 changed files with 60 additions and 8 deletions
|
|
@ -486,6 +486,38 @@ def test_deleted_sibling_worktree_folds_into_parent_home_checkout():
|
|||
assert len(main["sessions"]) == 2
|
||||
|
||||
|
||||
def test_deleted_sibling_worktree_subdir_folds_into_parent_home_checkout():
|
||||
# Same as above, but the session's cwd is a SUBDIR of the deleted worktree
|
||||
# (an agent that cd-ed into `<repo>-<suffix>/apps/desktop`). The leaf name
|
||||
# ("desktop") shares nothing with the repo, so the sibling probe has to walk
|
||||
# the ancestors — otherwise the dead path is minted as its own project.
|
||||
resolve = _resolver({"/www/hermes-agent": ("/www/hermes-agent", "/www/hermes-agent")})
|
||||
sessions = [
|
||||
_session("/www/hermes-agent", branch="main"),
|
||||
_session("/www/hermes-agent-guiperf/apps/desktop"),
|
||||
]
|
||||
|
||||
tree = pt.build_tree([], sessions, [], resolve, hydrate=True)
|
||||
|
||||
assert [p["id"] for p in tree["projects"]] == ["/www/hermes-agent"]
|
||||
project = tree["projects"][0]
|
||||
assert _lane_ids(project) == ["/www/hermes-agent::branch::main"]
|
||||
assert len(project["repos"][0]["groups"][0]["sessions"]) == 2
|
||||
|
||||
|
||||
def test_sibling_probe_is_bounded():
|
||||
# Each miss costs a git invocation, and this runs per session, so a deeply
|
||||
# nested unresolvable cwd must not fan out into an unbounded probe storm.
|
||||
probed = []
|
||||
|
||||
def resolve(cwd):
|
||||
probed.append(cwd)
|
||||
return None
|
||||
|
||||
assert pt._probe_sibling_worktree("/a-b-c/d-e-f/g-h-i/j-k-l", resolve) == ""
|
||||
assert len(probed) <= pt._MAX_SIBLING_PROBES
|
||||
|
||||
|
||||
def test_colliding_repo_basenames_disambiguate_labels():
|
||||
resolve = _resolver(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -151,20 +151,40 @@ def _placement(
|
|||
}
|
||||
|
||||
|
||||
def _parent_dir(path: str) -> str:
|
||||
"""The containing directory of ``path`` (``""`` once the root is passed)."""
|
||||
stripped = re.sub(r"[/\\]+$", "", path or "")
|
||||
return re.sub(r"[/\\]+$", "", re.sub(r"[^/\\]+$", "", stripped))
|
||||
|
||||
|
||||
def _probe_sibling_worktree(cwd: str, resolve: Resolve) -> str:
|
||||
"""The parent repo root of a deleted ``<repo>-<suffix>`` worktree, else ``""``.
|
||||
|
||||
A deleted worktree dir can't be probed, so walk back up its name — trimming
|
||||
one ``-<segment>`` at a time — and return the first sibling that resolves.
|
||||
Probes are bounded and served from the shared git-probe cache.
|
||||
"""
|
||||
parts = base_name(cwd).split("-")
|
||||
floor = max(0, len(parts) - 1 - _MAX_SIBLING_PROBES)
|
||||
|
||||
for i in range(len(parts) - 1, floor, -1):
|
||||
info = resolve(_with_base_name(cwd, "-".join(parts[:i])))
|
||||
if info and info.get("repo_root"):
|
||||
return (info["repo_root"] or "").strip()
|
||||
The session's cwd is frequently a SUBDIR of the deleted worktree (an agent
|
||||
that ``cd``-ed into ``<repo>-<suffix>/apps/desktop``), whose basename shares
|
||||
nothing with the repo. So the trim is applied to each ANCESTOR, deepest
|
||||
first, not just to the leaf — otherwise the probe silently no-ops and the
|
||||
dead path gets minted as its own top-level project. Probes are bounded in
|
||||
total (each costs a git invocation) and served from the shared probe cache.
|
||||
"""
|
||||
probes = 0
|
||||
path = re.sub(r"[/\\]+$", "", cwd or "")
|
||||
|
||||
while path and probes < _MAX_SIBLING_PROBES:
|
||||
parts = base_name(path).split("-")
|
||||
|
||||
for i in range(len(parts) - 1, 0, -1):
|
||||
if probes >= _MAX_SIBLING_PROBES:
|
||||
break
|
||||
probes += 1
|
||||
info = resolve(_with_base_name(path, "-".join(parts[:i])))
|
||||
if info and info.get("repo_root"):
|
||||
return (info["repo_root"] or "").strip()
|
||||
|
||||
path = _parent_dir(path)
|
||||
|
||||
return ""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue