diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7af93..dab2dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Leftover post-commit hooks surfaced** (GH #167 follow-up). A + `.git/hooks/post-commit` installed by codeindex < 0.37 survives the + upgrade silently — the dead wrapper's errors go to + `~/.codeindex/hooks/post-commit.log` while every commit still pays one + Python startup. `codeindex hooks status` now flags it with the exact + cleanup command, and `hooks uninstall --all` removes it alongside the + supported hooks. - **JS/TS test-file excludes suggested by wizard + config help** (GH #165). Unexcluded co-located test files (`*.spec.ts` / `*.test.ts` / `__tests__`) were the upstream root cause of graph-export edge pollution (lh-enterprise: diff --git a/docs/guides/git-hooks-integration.md b/docs/guides/git-hooks-integration.md index d9b42f0..a2db683 100644 --- a/docs/guides/git-hooks-integration.md +++ b/docs/guides/git-hooks-integration.md @@ -16,8 +16,10 @@ codeindex provides built-in Git Hooks management to automate: > it returned. README_AI refresh is now **release-time or manual**: run > `codeindex scan-all` whenever you want fresh indexes, or (in the codeindex > repo itself) let `scripts/release.sh` step 6.5 refresh before each tag. -> **Migration**: `codeindex hooks uninstall post-commit` — the command still -> accepts `post-commit` to clean up an existing install. +> **Migration**: a leftover hook from an older install is silent (its errors +> go to `~/.codeindex/hooks/post-commit.log`) but costs one Python startup +> per commit — `codeindex hooks status` flags it, and +> `codeindex hooks uninstall post-commit` (or `uninstall --all`) removes it. No manual hook creation needed - install with one command! diff --git a/src/codeindex/cli_hooks.py b/src/codeindex/cli_hooks.py index efa268f..e961275 100644 --- a/src/codeindex/cli_hooks.py +++ b/src/codeindex/cli_hooks.py @@ -33,6 +33,11 @@ class HookManager: CODEINDEX_MARKER = "# codeindex-managed hook" SUPPORTED_HOOKS = ["pre-commit", "pre-push"] + # GH #167: hooks removed from the product. Still uninstallable (cleanup + # of installs from older codeindex), and surfaced as leftovers by + # `hooks status` so they don't sit silent (dead wrapper = one Python + # startup per commit, errors buried in ~/.codeindex/hooks/). + RETIRED_HOOKS = ["post-commit"] def __init__(self, repo_path: Optional[Path] = None): """ @@ -529,6 +534,13 @@ def uninstall(hook_name: Optional[str], uninstall_all: bool, keep_backup: bool): for name, status in statuses.items() if status == HookStatus.INSTALLED ] + # Retired leftovers are ours too — clean them with the rest + hooks_to_uninstall += [ + name + for name in manager.RETIRED_HOOKS + if name not in hooks_to_uninstall + and manager.get_hook_status(name) == HookStatus.INSTALLED + ] elif hook_name: hooks_to_uninstall = [hook_name] else: @@ -619,6 +631,20 @@ def status(): console.print() + # Retired-hook leftovers (GH #167): dead wrappers from older + # installs — silent on every commit, so surface them here. + for name in manager.RETIRED_HOOKS: + hook_path = manager.hooks_dir / name + if hook_path.exists(): + content = hook_path.read_text() + if manager.CODEINDEX_MARKER in content: + console.print( + f"[yellow]⚠[/yellow] {name}: leftover from a removed " + "codeindex feature — does nothing but still runs on " + f"every commit. Remove with " + f"[bold]codeindex hooks uninstall {name}[/bold]" + ) + # Summary installed = sum(1 for s in statuses.values() if s == HookStatus.INSTALLED) custom = sum(1 for s in statuses.values() if s == HookStatus.CUSTOM) diff --git a/tests/test_cli_hooks.py b/tests/test_cli_hooks.py index 44d0e80..d8dee45 100644 --- a/tests/test_cli_hooks.py +++ b/tests/test_cli_hooks.py @@ -288,3 +288,68 @@ def test_cli_hooks_status_command(self, mock_run, tmp_path): """Should provide hooks status CLI command.""" # This will be implemented with Click pass + + +class TestRetiredHookLeftover: + """GH #167: post-commit removed from the product — detect the leftover + installed by codeindex < 0.37 so it doesn't sit silent (dead wrapper, + ~hundreds of ms startup tax per commit, errors buried in the log).""" + + def _make_repo(self, tmp_path: Path) -> Path: + repo = tmp_path / "repo" + (repo / ".git" / "hooks").mkdir(parents=True) + return repo + + def _write_leftover(self, repo: Path) -> None: + hook = repo / ".git" / "hooks" / "post-commit" + hook.write_text("#!/usr/bin/env bash\n# codeindex-managed hook\nexit 0\n") + + def _run_cli(self, repo: Path, args: list) -> "object": + from click.testing import CliRunner + + from codeindex.cli_hooks import hooks + + original = Path.cwd() + try: + os.chdir(repo) + return CliRunner().invoke(hooks, args) + finally: + os.chdir(original) + + def test_status_warns_on_leftover(self, tmp_path): + repo = self._make_repo(tmp_path) + self._write_leftover(repo) + + result = self._run_cli(repo, ["status"]) + + assert result.exit_code == 0 + assert "post-commit" in result.output + assert "uninstall post-commit" in result.output + + def test_status_silent_on_custom_post_commit(self, tmp_path): + repo = self._make_repo(tmp_path) + (repo / ".git" / "hooks" / "post-commit").write_text( + "#!/bin/sh\necho own hook\n" + ) + + result = self._run_cli(repo, ["status"]) + + assert result.exit_code == 0 + assert "leftover" not in result.output + + def test_status_silent_without_leftover(self, tmp_path): + repo = self._make_repo(tmp_path) + + result = self._run_cli(repo, ["status"]) + + assert result.exit_code == 0 + assert "post-commit" not in result.output + + def test_uninstall_all_removes_leftover(self, tmp_path): + repo = self._make_repo(tmp_path) + self._write_leftover(repo) + + result = self._run_cli(repo, ["uninstall", "--all"]) + + assert result.exit_code == 0 + assert not (repo / ".git" / "hooks" / "post-commit").exists()