Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions docs/guides/git-hooks-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down
26 changes: 26 additions & 0 deletions src/codeindex/cli_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions tests/test_cli_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading