fix(termux): resolve packed-refs and worktree refs in skill-sync fingerprint
The bundled-skill sync stamp added in the cherry-picked salvage commit parsed .git/HEAD and looked for a loose ref file in the worktree gitdir only, so two real cases hit the unresolved branch: - repos after `git gc` where active refs live in packed-refs - linked worktrees, whose branch ref lives in <commondir>/refs/heads/ (verified on the worktree this salvage was built in) Both fell back to a constant-string fingerprint, so post-commit launches would never re-run the real skill sync. Now we resolve packed-refs and check both the worktree gitdir and the common dir for loose refs. Adds three tests covering: packed-refs resolution, worktree common-dir packed lookup, worktree common-dir loose lookup, and the explicit 'unresolved' marker (still stable + version-fallback-safe).
This commit is contained in:
@@ -275,6 +275,25 @@ def _is_termux_startup_environment(env: dict[str, str] | None = None) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_packed_ref(common_dir: Path, ref: str) -> str | None:
|
||||||
|
"""Look up a ref in .git/packed-refs without spawning git.
|
||||||
|
|
||||||
|
packed-refs lines look like ``<sha> <ref>`` with optional ``^<sha>``
|
||||||
|
peel lines and ``#``-prefixed comments / ``# pack-refs with:`` header.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
text = (common_dir / "packed-refs").read_text(encoding="utf-8", errors="replace")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
for line in text.splitlines():
|
||||||
|
if not line or line.startswith("#") or line.startswith("^"):
|
||||||
|
continue
|
||||||
|
parts = line.split(" ", 1)
|
||||||
|
if len(parts) == 2 and parts[1].strip() == ref:
|
||||||
|
return parts[0].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _read_git_revision_fingerprint(repo_root: Path) -> str | None:
|
def _read_git_revision_fingerprint(repo_root: Path) -> str | None:
|
||||||
"""Return a cheap checkout fingerprint without spawning git."""
|
"""Return a cheap checkout fingerprint without spawning git."""
|
||||||
git_dir = repo_root / ".git"
|
git_dir = repo_root / ".git"
|
||||||
@@ -285,13 +304,36 @@ def _read_git_revision_fingerprint(repo_root: Path) -> str | None:
|
|||||||
if key.strip() == "gitdir" and value.strip():
|
if key.strip() == "gitdir" and value.strip():
|
||||||
git_dir = (repo_root / value.strip()).resolve()
|
git_dir = (repo_root / value.strip()).resolve()
|
||||||
break
|
break
|
||||||
|
# Worktrees point HEAD at a per-worktree gitdir but pack their refs
|
||||||
|
# in the main repo's gitdir (referenced via ``commondir``). Resolve
|
||||||
|
# that up front so packed-refs lookups hit the right file.
|
||||||
|
common_dir = git_dir
|
||||||
|
commondir_file = git_dir / "commondir"
|
||||||
|
if commondir_file.exists():
|
||||||
|
try:
|
||||||
|
rel = commondir_file.read_text(encoding="utf-8", errors="replace").strip()
|
||||||
|
if rel:
|
||||||
|
common_dir = (git_dir / rel).resolve()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
head_file = git_dir / "HEAD"
|
head_file = git_dir / "HEAD"
|
||||||
head = head_file.read_text(encoding="utf-8", errors="replace").strip()
|
head = head_file.read_text(encoding="utf-8", errors="replace").strip()
|
||||||
if head.startswith("ref:"):
|
if head.startswith("ref:"):
|
||||||
ref = head.split(":", 1)[1].strip()
|
ref = head.split(":", 1)[1].strip()
|
||||||
ref_file = git_dir / ref
|
# Loose refs may live in the worktree gitdir OR the common dir
|
||||||
if ref_file.exists():
|
# (branches created via `git worktree add` typically live in the
|
||||||
return f"git:{ref}:{ref_file.read_text(encoding='utf-8', errors='replace').strip()}"
|
# common dir's refs/heads/).
|
||||||
|
for candidate in (git_dir, common_dir):
|
||||||
|
ref_file = candidate / ref
|
||||||
|
if ref_file.exists():
|
||||||
|
return f"git:{ref}:{ref_file.read_text(encoding='utf-8', errors='replace').strip()}"
|
||||||
|
packed_sha = _read_packed_ref(common_dir, ref)
|
||||||
|
if packed_sha:
|
||||||
|
return f"git:{ref}:{packed_sha}"
|
||||||
|
# Ref name is known but unresolved — still stable across launches,
|
||||||
|
# and the version/release fallback in the caller will invalidate
|
||||||
|
# after `hermes update`.
|
||||||
|
return f"git:{ref}:unresolved"
|
||||||
return f"git:HEAD:{head}"
|
return f"git:HEAD:{head}"
|
||||||
except OSError:
|
except OSError:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -428,6 +428,88 @@ def test_termux_forced_bundled_skill_sync_runs(monkeypatch, tmp_path, main_mod):
|
|||||||
assert calls == [True]
|
assert calls == [True]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_git_revision_fingerprint_resolves_packed_refs(tmp_path, main_mod):
|
||||||
|
repo = tmp_path / "repo"
|
||||||
|
git_dir = repo / ".git"
|
||||||
|
git_dir.mkdir(parents=True)
|
||||||
|
(git_dir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
|
||||||
|
packed_sha = "1234567890abcdef1234567890abcdef12345678"
|
||||||
|
(git_dir / "packed-refs").write_text(
|
||||||
|
"# pack-refs with: peeled fully-peeled sorted\n"
|
||||||
|
f"{packed_sha} refs/heads/main\n"
|
||||||
|
"abcdef0000000000000000000000000000000000 refs/tags/v1.0\n"
|
||||||
|
"^99999999aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
fingerprint = main_mod._read_git_revision_fingerprint(repo)
|
||||||
|
|
||||||
|
assert fingerprint == f"git:refs/heads/main:{packed_sha}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_git_revision_fingerprint_packed_refs_in_worktree_common_dir(
|
||||||
|
tmp_path, main_mod
|
||||||
|
):
|
||||||
|
main_repo = tmp_path / "repo"
|
||||||
|
common_git = main_repo / ".git"
|
||||||
|
common_git.mkdir(parents=True)
|
||||||
|
packed_sha = "fedcba9876543210fedcba9876543210fedcba98"
|
||||||
|
(common_git / "packed-refs").write_text(
|
||||||
|
f"{packed_sha} refs/heads/main\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
worktree = tmp_path / "wt"
|
||||||
|
worktree.mkdir()
|
||||||
|
wt_gitdir = common_git / "worktrees" / "wt"
|
||||||
|
wt_gitdir.mkdir(parents=True)
|
||||||
|
(wt_gitdir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
|
||||||
|
(wt_gitdir / "commondir").write_text("../..\n", encoding="utf-8")
|
||||||
|
(worktree / ".git").write_text(f"gitdir: {wt_gitdir}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
fingerprint = main_mod._read_git_revision_fingerprint(worktree)
|
||||||
|
|
||||||
|
assert fingerprint == f"git:refs/heads/main:{packed_sha}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_git_revision_fingerprint_loose_ref_in_worktree_common_dir(
|
||||||
|
tmp_path, main_mod
|
||||||
|
):
|
||||||
|
"""`git worktree add -b NAME` writes the new branch ref to the common dir,
|
||||||
|
not the per-worktree gitdir. The fingerprint must still resolve it."""
|
||||||
|
main_repo = tmp_path / "repo"
|
||||||
|
common_git = main_repo / ".git"
|
||||||
|
common_git.mkdir(parents=True)
|
||||||
|
loose_sha = "0123456789abcdef0123456789abcdef01234567"
|
||||||
|
(common_git / "refs" / "heads").mkdir(parents=True)
|
||||||
|
(common_git / "refs" / "heads" / "feature").write_text(
|
||||||
|
loose_sha + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
worktree = tmp_path / "wt"
|
||||||
|
worktree.mkdir()
|
||||||
|
wt_gitdir = common_git / "worktrees" / "wt"
|
||||||
|
wt_gitdir.mkdir(parents=True)
|
||||||
|
(wt_gitdir / "HEAD").write_text("ref: refs/heads/feature\n", encoding="utf-8")
|
||||||
|
(wt_gitdir / "commondir").write_text("../..\n", encoding="utf-8")
|
||||||
|
(worktree / ".git").write_text(f"gitdir: {wt_gitdir}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
fingerprint = main_mod._read_git_revision_fingerprint(worktree)
|
||||||
|
|
||||||
|
assert fingerprint == f"git:refs/heads/feature:{loose_sha}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_git_revision_fingerprint_unresolved_ref_is_stable(tmp_path, main_mod):
|
||||||
|
repo = tmp_path / "repo"
|
||||||
|
git_dir = repo / ".git"
|
||||||
|
git_dir.mkdir(parents=True)
|
||||||
|
(git_dir / "HEAD").write_text("ref: refs/heads/missing\n", encoding="utf-8")
|
||||||
|
|
||||||
|
fingerprint = main_mod._read_git_revision_fingerprint(repo)
|
||||||
|
|
||||||
|
assert fingerprint == "git:refs/heads/missing:unresolved"
|
||||||
|
|
||||||
|
|
||||||
def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
|
def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user