diff --git a/graphify/hooks.py b/graphify/hooks.py index e78e291f6..3aa372ec1 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -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}" diff --git a/tests/test_hooks.py b/tests/test_hooks.py index bd6d7e521..0910b6816 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -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)