diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d92d7c..a73fed47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.44 (unreleased) +- Feature: `graphify hook install` reads a committed `.graphifyrc` (`viz_node_limit=`) and bakes the visualization node limit into the generated git hooks, so a project-wide limit is shared via version control and survives hook regeneration; `hook status` reports it (#2760, thanks @hopstreax). The baked value uses a `${GRAPHIFY_VIZ_NODE_LIMIT:-}` default so an explicit per-run env var still wins, and `hook status` degrades gracefully on a malformed `.graphifyrc`. - Fix: `graphify install` (Claude always-on) now writes the CLAUDE.md registration into `$CLAUDE_CONFIG_DIR` when that env var relocates the Claude profile, instead of always mutating the default `~/.claude/CLAUDE.md` (part of #2694, thanks @AromalBiju1). - Fix: a JS/TS inline or nested function expression — including a generator function expression (`function*(k){…}`) — no longer fabricates an INFERRED `indirect_call` when one of its parameters/locals shares a name with an unrelated callable; the expression's own bindings now shadow the name (#2752, thanks @imagineers-tyler), completing the shadow family alongside catch/arrow/loop/external-import (#2757). - Fix: a git-tracked file that also matches a `.gitignore` pattern (a committed file later added to `.gitignore`, or a force-added one) is no longer dropped from the corpus, matching git's own behavior of never un-tracking such a file; `.graphifyignore`/`--exclude` stay authoritative and a non-git corpus is unaffected (#2759, thanks @NithishKumar04). The `git ls-files` probe is skipped entirely when no `.gitignore` is in play, so ordinary corpora pay nothing for it. diff --git a/graphify/hooks.py b/graphify/hooks.py index 47c34530..5be58d12 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -721,7 +721,11 @@ def install(path: Path = Path(".")) -> str: cfg = _load_graphifyrc(root) viz_limit = cfg.get("viz_node_limit") if viz_limit is not None: - viz_export = f'export GRAPHIFY_VIZ_NODE_LIMIT="{viz_limit}"\n' + # Use the `:-` default form (like GRAPHIFY_MAX_WORKERS below) so an + # explicit `GRAPHIFY_VIZ_NODE_LIMIT=... git commit` still wins over the + # baked project default — persisting config must not clobber a per-run + # override. + viz_export = f'export GRAPHIFY_VIZ_NODE_LIMIT="${{GRAPHIFY_VIZ_NODE_LIMIT:-{viz_limit}}}"\n' else: viz_export = "" @@ -756,7 +760,13 @@ def status(path: Path = Path(".")) -> str: if root is None: return "Not in a git repository." hooks_dir = _user_hooks_dir(_hooks_dir(root)) - cfg = _load_graphifyrc(root) + # status is a read-only diagnostic: a malformed .graphifyrc must not turn it + # into a traceback. Report the config problem and continue with no limit. + try: + cfg = _load_graphifyrc(root) + except ValueError as exc: + cfg = {} + print(f" warning: {exc}") cfg_limit = cfg.get("viz_node_limit") def _check(name: str, marker: str) -> str: @@ -767,8 +777,14 @@ def status(path: Path = Path(".")) -> str: if marker not in text: return "not installed (hook exists but graphify not found)" if cfg_limit is not None: - m = re.search(r'export GRAPHIFY_VIZ_NODE_LIMIT="(\d+)"', text) - installed_limit = int(m.group(1)) if m else None + # Baked as `"${GRAPHIFY_VIZ_NODE_LIMIT:-}"` so a per-run override + # wins; match the default , and still accept the older bare + # `""` form from hooks installed before that change. + m = re.search( + r'export GRAPHIFY_VIZ_NODE_LIMIT="(?:\$\{GRAPHIFY_VIZ_NODE_LIMIT:-(\d+)\}|(\d+))"', + text, + ) + installed_limit = int(m.group(1) or m.group(2)) if m else None if installed_limit != cfg_limit: return ( f"installed (out of date: hook has limit " diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 4bcf1cd8..9fffb32b 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -874,8 +874,40 @@ def test_config_baked_into_generated_hook(tmp_path): commit_hook = (repo / ".git" / "hooks" / "post-commit").read_text() checkout_hook = (repo / ".git" / "hooks" / "post-checkout").read_text() - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="0"' in commit_hook - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="0"' in checkout_hook + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-0}"' in commit_hook + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-0}"' in checkout_hook + + +def test_baked_viz_limit_yields_to_an_explicit_per_run_override(tmp_path): + """Persisting the project default must not clobber an explicit per-run + GRAPHIFY_VIZ_NODE_LIMIT: the baked line uses the `${VAR:-}` default form, + so an already-set env value wins (mirrors GRAPHIFY_MAX_WORKERS).""" + repo = _make_git_repo(tmp_path) + (repo / ".graphifyrc").write_text("viz_node_limit=100\n", encoding="utf-8") + install(repo) + commit_hook = (repo / ".git" / "hooks" / "post-commit").read_text() + + # default form, not an unconditional assignment that would override the env + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-100}"' in commit_hook + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="100"' not in commit_hook + # prove the shell semantics: an explicit env value survives the export line + line = 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-100}"' + out = subprocess.run( + ["sh", "-c", f'GRAPHIFY_VIZ_NODE_LIMIT=7; {line}; echo "$GRAPHIFY_VIZ_NODE_LIMIT"'], + capture_output=True, text=True, check=True, + ) + assert out.stdout.strip() == "7" + + +def test_status_survives_a_malformed_graphifyrc(tmp_path): + """A typo in the committed .graphifyrc must not turn the read-only `status` + diagnostic into a traceback; it reports the problem and continues.""" + repo = _make_git_repo(tmp_path) + install(repo) + (repo / ".graphifyrc").write_text("viz_node_limit=not-an-int\n", encoding="utf-8") + + result = status(repo) # must not raise + assert "installed" in result def test_changing_config_updates_existing_hook(tmp_path): @@ -890,8 +922,8 @@ def test_changing_config_updates_existing_hook(tmp_path): assert "updated existing" in result commit_hook = (repo / ".git" / "hooks" / "post-commit").read_text() - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="0"' in commit_hook - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="5000"' not in commit_hook + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-0}"' in commit_hook + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-5000}"' not in commit_hook assert commit_hook.count("# graphify-hook-start") == 1 @@ -914,7 +946,7 @@ def test_user_hook_content_survives_update(tmp_path): content = post_commit.read_text() assert "echo 'user content before'" in content assert "echo 'user content after'" in content - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="0"' in content + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-0}"' in content assert "old block" not in content @@ -943,4 +975,4 @@ def test_both_hooks_configured(tmp_path): for name in ("post-commit", "post-checkout"): hook_text = (repo / ".git" / "hooks" / name).read_text() - assert 'export GRAPHIFY_VIZ_NODE_LIMIT="42"' in hook_text + assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-42}"' in hook_text