fix(hooks): report hooks written by an older release as out of date (#3771)

Upgrading the package never touches hooks already on disk, so repos
initialised before a hook fix (such as the #2253 console-window launcher)
keep running the old script while `graphify hook status` says
"installed". Status now compares the installed graphify block with the
current template, ignoring the two per-install values (the pinned
interpreter and the viz limit export), and tells the user to run
`graphify hook install`, which already rewrites the block in place.
This commit is contained in:
berce committed 2026-09-30 16:11:06 +03:00
1 parent 1cd9a36c0c
commit 2e43d53ffa
2 files changed
+81 -3

No files matched your search

+26 -3
View File
@@ -905,6 +905,23 @@ def uninstall(path: Path = Path(".")) -> str:
return f"post-commit: {commit_msg}\npost-checkout: {checkout_msg}\nmerge driver: {merge_msg}"
# The two per-install values in a graphify block: the interpreter that ran
# `hook install` and the .graphifyrc viz limit (status checks that one itself).
_PINNED_LINE_RE = re.compile(r"^_PINNED='[^'\n]*'$", re.MULTILINE)
_VIZ_EXPORT_LINE_RE = re.compile(r"^export GRAPHIFY_VIZ_NODE_LIMIT=.*\n", re.MULTILINE)
def _comparable_block(text: str, marker: str, marker_end: str) -> str | None:
"""The graphify block in *text* with its per-install values blanked out, or
None when the block has no end marker (install cannot rewrite those either)."""
start = text.find(marker)
end = text.find(marker_end, start) if start != -1 else -1
if end == -1:
return None
block = text[start:end + len(marker_end)]
return _VIZ_EXPORT_LINE_RE.sub("", _PINNED_LINE_RE.sub("_PINNED=''", block))
def status(path: Path = Path(".")) -> str:
"""Check if graphify hooks are installed."""
root = _git_root(path)
@@ -920,7 +937,7 @@ def status(path: Path = Path(".")) -> str:
print(f" warning: {exc}")
cfg_limit = cfg.get("viz_node_limit")
def _check(name: str, marker: str) -> str:
def _check(name: str, marker: str, marker_end: str, script: str) -> str:
p = hooks_dir / name
if not p.exists():
return "not installed"
@@ -942,10 +959,16 @@ def status(path: Path = Path(".")) -> str:
f"{installed_limit if installed_limit is not None else 'unset'}, "
f".graphifyrc has {cfg_limit})"
)
# Upgrading the package does not touch hooks already on disk, so a block
# from an older release keeps running its old script (#3771).
installed = _comparable_block(text, marker, marker_end)
current = _comparable_block(script.replace("__VIZ_LIMIT_EXPORT__", ""), marker, marker_end)
if installed is not None and installed != current:
return "installed (out of date: run `graphify hook install` to refresh it)"
return "installed"
commit = _check("post-commit", _HOOK_MARKER)
checkout = _check("post-checkout", _CHECKOUT_MARKER)
commit = _check("post-commit", _HOOK_MARKER, _HOOK_MARKER_END, _HOOK_SCRIPT)
checkout = _check("post-checkout", _CHECKOUT_MARKER, _CHECKOUT_MARKER_END, _CHECKOUT_SCRIPT)
merge = _merge_driver_status(root)
res = f"post-commit: {commit}\npost-checkout: {checkout}\nmerge driver: {merge}"
+55
View File
@@ -1533,3 +1533,58 @@ def test_commit_hook_still_rebuilds_for_a_source_change(tmp_path):
result = _emitted_hook_run(repo, _HOOK_SCRIPT, [], {"GRAPHIFY_OUT": "custom-out"})
assert result.returncode == 0, result.stderr
assert _LAUNCH_LINE in result.stdout, result.stdout
# Launcher flags a hook installed before the #2253 fix still carries on disk.
_PRE_2253_FLAGS = "0x00000008 | 0x00000200" # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
def _status_line(result: str, name: str) -> str:
return next(line for line in result.splitlines() if line.startswith(f"{name}:"))
def test_status_flags_a_hook_block_from_an_older_graphify(tmp_path):
"""Upgrading the package never rewrites hooks already on disk, so a block
written by an older release keeps its old launcher (#3771). `status` must
say so and name the command that refreshes it, not report "installed"."""
repo = _make_git_repo(tmp_path)
install(repo)
hook = repo / ".git" / "hooks" / "post-commit"
text = hook.read_text(encoding="utf-8")
assert "0x08000000 | 0x00000200" in text
hook.write_text(text.replace("0x08000000 | 0x00000200", _PRE_2253_FLAGS), encoding="utf-8")
res = status(repo)
assert "out of date" in _status_line(res, "post-commit")
assert "graphify hook install" in _status_line(res, "post-commit")
assert "out of date" not in _status_line(res, "post-checkout")
assert "updated existing post-commit hook" in install(repo)
assert "out of date" not in status(repo)
def test_status_ignores_the_pinned_interpreter(tmp_path):
"""`_PINNED` is whichever interpreter ran `hook install`. A hook pinned to a
different one is not stale, so it must not be reported as out of date."""
repo = _make_git_repo(tmp_path)
install(repo)
hook = repo / ".git" / "hooks" / "post-commit"
text = hook.read_text(encoding="utf-8")
repinned = re.sub(r"(?m)^_PINNED='[^']*'$", "_PINNED='/opt/other/bin/python3'", text)
assert repinned != text
hook.write_text(repinned, encoding="utf-8")
assert "out of date" not in status(repo)
def test_status_compares_only_the_graphify_block(tmp_path):
"""User commands around the graphify block are not graphify's to judge."""
repo = _make_git_repo(tmp_path)
hooks_dir = repo / ".git" / "hooks"
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-commit").write_text("#!/bin/sh\necho 'user before'\n", encoding="utf-8")
install(repo)
hook = hooks_dir / "post-commit"
hook.write_text(hook.read_text(encoding="utf-8") + "echo 'user after'\n", encoding="utf-8")
assert "out of date" not in status(repo)