diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 109b76b..7b88a4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Run tests with coverage run: | - pytest -v --cov=src/codeindex --cov-report=term-missing --cov-report=xml + pytest -v --cov=src/codeindex --cov-report=term-missing --cov-report=xml --cov-fail-under=78 - name: Upload coverage to Codecov (Ubuntu Python 3.11 only) if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' @@ -60,12 +60,16 @@ jobs: with: python-version: '3.11' - - name: Install ruff - run: pip install ruff + - name: Install lint tools + run: pip install ruff mypy - name: Run ruff check run: ruff check src/ tests/ test_generator/ + - name: Run mypy type check (informational) + run: mypy src/codeindex/parser.py src/codeindex/scanner.py src/codeindex/config.py + continue-on-error: true + build-check: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 977ee79..5f9df25 100644 --- a/Makefile +++ b/Makefile @@ -39,11 +39,9 @@ install: ## Install package in editable mode install-dev: ## Install with dev dependencies pip install -e ".[dev,all]" -install-hooks: ## Install Git hooks (pre-commit, pre-push) +install-hooks: ## Install Git hooks via codeindex CLI @echo "$(CYAN)Installing Git hooks...$(RESET)" - @mkdir -p .git/hooks - @cp scripts/hooks/pre-push .git/hooks/pre-push - @chmod +x .git/hooks/pre-push + @codeindex hooks install --all --force @echo "$(GREEN)✓ Git hooks installed$(RESET)" # ============================================================================ @@ -65,6 +63,9 @@ lint: ## Run linter (ruff) lint-fix: ## Auto-fix linting issues ruff check --fix src/ tests/ +typecheck: ## Run mypy type check on core modules + mypy src/codeindex/parser.py src/codeindex/scanner.py src/codeindex/config.py + format: ## Format code with ruff ruff format src/ tests/ diff --git a/docs/guides/git-hooks-integration.md b/docs/guides/git-hooks-integration.md index eff4f5d..b4655ea 100644 --- a/docs/guides/git-hooks-integration.md +++ b/docs/guides/git-hooks-integration.md @@ -192,6 +192,7 @@ Code Change Commit Shell wrapper (loop guard + venv) ↓ codeindex hooks run post-commit (Python) + ↓ stderr → ~/.codeindex/hooks/post-commit.log ↓ codeindex affected --json → affected directories ↓ diff --git a/pyproject.toml b/pyproject.toml index 0434133..75c01b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ dev = [ "pytest-bdd>=7.0", "pytest-cov>=4.0", "ruff>=0.1", + "mypy>=1.0", ] [project.scripts] @@ -82,7 +83,20 @@ extend-exclude = [ ] [tool.ruff.lint] -select = ["E", "F", "I", "N", "W"] +select = ["E", "F", "I", "N", "W", "T"] + +[tool.ruff.lint.per-file-ignores] +"src/codeindex/cli_*.py" = ["T201"] # CLI files legitimately use print() +"scripts/*.py" = ["T201"] +"tests/**/*.py" = ["T201"] + +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true +check_untyped_defs = false +disallow_untyped_defs = false [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/scripts/hooks/hook-common.sh b/scripts/hooks/hook-common.sh new file mode 100755 index 0000000..0c85e9f --- /dev/null +++ b/scripts/hooks/hook-common.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Common utilities for Git hooks +# Source this file in all hooks to reduce code duplication + +# ============================================ +# Color Definitions +# ============================================ +export RED='\033[0;31m' +export GREEN='\033[0;32m' +export YELLOW='\033[0;33m' +export CYAN='\033[0;36m' +export RESET='\033[0m' + +# ============================================ +# Repository Utilities +# ============================================ + +# Get repository root directory +get_repo_root() { + git rev-parse --show-toplevel +} + +# ============================================ +# Virtual Environment Management +# ============================================ + +# Activate virtual environment (required for all hooks) +# Returns 0 on success, 1 on failure +activate_venv() { + local repo_root=$(get_repo_root) + + if [ -f "$repo_root/.venv/bin/activate" ]; then + source "$repo_root/.venv/bin/activate" + return 0 + elif [ -f "$repo_root/venv/bin/activate" ]; then + source "$repo_root/venv/bin/activate" + return 0 + else + echo -e "${RED}✗ Virtual environment not found${RESET}" + echo -e "${YELLOW}→ Create with: python3 -m venv .venv${RESET}" + echo -e "${YELLOW}→ Install: pip install -e '.[dev,all]'${RESET}" + return 1 + fi +} + +# ============================================ +# Tool Discovery +# ============================================ + +# Find a tool (prefer venv, fallback to system) +# Usage: find_tool ruff +# Returns: path to tool or empty string (sets exit code) +find_tool() { + local tool=$1 + local repo_root=$(get_repo_root) + + if [ -f "$repo_root/.venv/bin/$tool" ]; then + echo "$repo_root/.venv/bin/$tool" + return 0 + elif command -v $tool &> /dev/null; then + echo "$tool" + return 0 + else + echo -e "${RED}✗ $tool not found${RESET}" >&2 + echo -e "${YELLOW}→ Install: pip install $tool${RESET}" >&2 + return 1 + fi +} + +# ============================================ +# Time Measurement +# ============================================ + +# Start timing (returns timestamp) +time_start() { + date +%s +} + +# End timing and return elapsed seconds +# Usage: elapsed=$(time_end $start_time) +time_end() { + local start=$1 + local end=$(date +%s) + echo $((end - start)) +} + +# ============================================ +# Pretty Output +# ============================================ + +# Print section header +section_header() { + local message=$1 + echo -e "${CYAN}${message}${RESET}" +} + +# Print success message +success() { + local message=$1 + echo -e "${GREEN}✓ ${message}${RESET}" +} + +# Print warning message +warning() { + local message=$1 + echo -e "${YELLOW}⚠ ${message}${RESET}" +} + +# Print error message +error() { + local message=$1 + echo -e "${RED}✗ ${message}${RESET}" +} + +# Print completion message with time +completed() { + local hook_name=$1 + local elapsed=$2 + echo "" + echo -e "${GREEN}✓ [$hook_name] Completed in ${elapsed}s${RESET}" +} diff --git a/src/codeindex/README_AI.md b/src/codeindex/README_AI.md index 64f970e..01a4696 100644 --- a/src/codeindex/README_AI.md +++ b/src/codeindex/README_AI.md @@ -1,11 +1,11 @@ - + # codeindex ## Overview - **Files**: 88 -- **Symbols**: 553 +- **Symbols**: 554 ## Files @@ -141,6 +141,7 @@ This module provides: - `def install_hook( self, hook_name: str, backup: bool = True, force: bool = False ) -> bool` +- `def _ensure_hook_common(self) -> None` - `def uninstall_hook( self, hook_name: str, restore_backup: bool = True ) -> bool` @@ -157,9 +158,8 @@ This module provides: - `def detect_existing_hooks(hooks_dir: Path) -> list[str]` - `def install_hook(hook_name: str, repo_path: Optional[Path] = None) -> bool` - `def uninstall_hook(hook_name: str, repo_path: Optional[Path] = None) -> bool` -- `def run_post_commit_hook() -> int` -_... and 5 more symbols_ +_... and 6 more symbols_ ### cli_parse.py _CLI parse command - Parse a single source file and output JSON. @@ -1679,7 +1679,7 @@ including file size issues,_ Lower values indicate higher severity (CRITICAL is m -**class** `class DebtIssu +**class** `class Debt --- _Content truncated due to size limit. See individual module README files for details._ diff --git a/src/codeindex/cli_hooks.py b/src/codeindex/cli_hooks.py index 41371f1..3a5ddd5 100644 --- a/src/codeindex/cli_hooks.py +++ b/src/codeindex/cli_hooks.py @@ -124,8 +124,20 @@ def install_hook( hook_path.write_text(script) hook_path.chmod(0o755) # Make executable + # Ensure hook-common.sh is installed (used by pre-commit/pre-push) + self._ensure_hook_common() + return True + def _ensure_hook_common(self) -> None: + """Copy hook-common.sh to .git/hooks/ if bundled version exists.""" + common_dest = self.hooks_dir / "hook-common.sh" + # Source from scripts/hooks/ in the repo + common_src = self.repo_path / "scripts" / "hooks" / "hook-common.sh" + if common_src.exists(): + shutil.copy(common_src, common_dest) + common_dest.chmod(0o755) + def uninstall_hook( self, hook_name: str, restore_backup: bool = True ) -> bool: @@ -200,7 +212,7 @@ def _generate_pre_commit_script(config: dict) -> str: """Generate pre-commit hook script.""" lint_enabled = config.get("lint_enabled", True) - script = """#!/bin/zsh + script = """#!/usr/bin/env bash # codeindex-managed hook # Pre-commit hook for codeindex # L1: Lint check (ruff) @@ -274,61 +286,9 @@ def _generate_pre_commit_script(config: dict) -> str: """ script += """ -# ============================================ -# L2: Forbid debug code -# ============================================ -echo "\\n${YELLOW}[L2] Checking for debug code...${NC}" - -DEBUG_PATTERNS=( - 'print\\s*\\(' # print() statements - 'breakpoint\\s*\\(' # breakpoint() calls - 'pdb\\.set_trace\\s*\\(' # pdb debugger - 'import\\s+pdb' # pdb import - 'from\\s+pdb\\s+import' # from pdb import -) - -FOUND_DEBUG=0 -for file in $STAGED_PY_FILES; do - # Skip CLI files and modules that use print() for legitimate output - if [[ "$file" == *"/cli"* ]] || [[ "$file" == *"/cli_"* ]] || \\ - [[ "$file" == *"hierarchical.py"* ]] || \\ - [[ "$file" == *"directory_tree.py"* ]] || \\ - [[ "$file" == *"adaptive_selector.py"* ]]; then - continue - fi - - # Get only staged content (not working directory) - STAGED_CONTENT=$(git show ":$file" 2>/dev/null || true) - - if [ -z "$STAGED_CONTENT" ]; then - continue - fi - - for pattern in $DEBUG_PATTERNS; do - # Find matches, excluding console.print() and docstring examples - MATCHES=$(echo "$STAGED_CONTENT" | \\ - grep -n -E "$pattern" | \\ - grep -v "console\\.print" | \\ - grep -v "^[[:space:]]*>>>" || true) - if [ -n "$MATCHES" ]; then - if [ $FOUND_DEBUG -eq 0 ]; then - echo "${RED}✗ Debug code found:${NC}" - FOUND_DEBUG=1 - fi - echo " ${file}:" - echo "$MATCHES" | while read line; do - echo " $line" - done - fi - done -done - -if [ $FOUND_DEBUG -eq 1 ]; then - echo "\\n${RED}✗ Remove debug code before committing.${NC}" - echo " Tip: Use logging module instead of print()" - exit 1 -fi -echo "${GREEN}✓ No debug code found${NC}" +# Note: Debug code detection (print/breakpoint/pdb) is now handled by +# ruff rules T201 (print) and T100 (debugger) in the lint check above. +# Per-file-ignores in pyproject.toml exempt CLI files. # ============================================ # All checks passed @@ -345,13 +305,13 @@ def _generate_post_commit_script(config: dict) -> str: # noqa: E501 auto_update = config.get("auto_update", True) if not auto_update: - return """#!/bin/zsh + return """#!/usr/bin/env bash # codeindex-managed hook # Post-commit hook (disabled) exit 0 """ - return """#!/bin/zsh + return """#!/usr/bin/env bash # codeindex-managed hook # Post-commit hook for codeindex # Thin wrapper — all logic in Python (auto-updated via pip) @@ -375,14 +335,20 @@ def _generate_post_commit_script(config: dict) -> str: # noqa: E501 source "$REPO_ROOT/venv/bin/activate" fi +# Ensure log directory exists +LOG_DIR="$HOME/.codeindex/hooks" +mkdir -p "$LOG_DIR" +LOG_FILE="$LOG_DIR/post-commit.log" + # Delegate to Python (upgradeable via pip) -codeindex hooks run post-commit 2>/dev/null || true +# Errors go to log file instead of being silently discarded +codeindex hooks run post-commit 2>>"$LOG_FILE" || true """ def _generate_pre_push_script(config: dict) -> str: """Generate pre-push hook script.""" - return """#!/bin/zsh + return """#!/usr/bin/env bash # codeindex-managed hook # Pre-push hook for codeindex diff --git a/tests/README_AI.md b/tests/README_AI.md index d3374de..fd70ce6 100644 --- a/tests/README_AI.md +++ b/tests/README_AI.md @@ -1,11 +1,11 @@ - + # tests ## Overview - **Files**: 120 -- **Symbols**: 2071 +- **Symbols**: 2072 ## Files @@ -393,15 +393,15 @@ _Tests for Git Hooks CLI module (Epic 6, P3.1, Task 4.1-4.5)._ - `def test_get_hook_status_exists_codeindex(self, tmp_path)` - `def test_get_hook_status_exists_custom(self, tmp_path)` - `def test_install_hook(self, tmp_path)` +- `def test_install_hook_copies_hook_common(self, tmp_path)` - `def test_install_hook_with_backup(self, tmp_path)` - `def test_uninstall_hook(self, tmp_path)` - `def test_uninstall_hook_restores_backup(self, tmp_path)` - `def test_list_all_hooks_status(self, tmp_path)` - `def test_generate_pre_commit_hook(self)` - `def test_generate_post_commit_hook(self)` -- `def test_generate_hook_with_config(self)` -_... and 7 more symbols_ +_... and 8 more symbols_ ### test_cli_json.py _Tests for CLI JSON output. @@ -1543,9 +1543,7 @@ This test file validates bridging header handling for Swift/Objective-C interop: > Test import extraction from bridging headers. **class** `class TestBridgingHeaderClasses` -> Test class exposure in bridging headers. - -* +> Test class exposure in brid --- _Content truncated due to size limit. See individual module README files for details._ diff --git a/tests/test_cli_hooks.py b/tests/test_cli_hooks.py index 7e5903e..f7c475a 100644 --- a/tests/test_cli_hooks.py +++ b/tests/test_cli_hooks.py @@ -99,6 +99,26 @@ def test_install_hook(self, tmp_path): assert (hooks_dir / "pre-commit").exists() assert (hooks_dir / "pre-commit").stat().st_mode & 0o111 # Executable + def test_install_hook_copies_hook_common(self, tmp_path): + """Should copy hook-common.sh when scripts/hooks/hook-common.sh exists.""" + repo_path = tmp_path / "test_repo" + hooks_dir = repo_path / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + + # Create scripts/hooks/hook-common.sh in the repo + scripts_hooks = repo_path / "scripts" / "hooks" + scripts_hooks.mkdir(parents=True) + common_src = scripts_hooks / "hook-common.sh" + common_src.write_text("#!/bin/bash\n# common utilities\n") + + manager = HookManager(repo_path) + manager.install_hook("pre-commit") + + common_dest = hooks_dir / "hook-common.sh" + assert common_dest.exists() + assert common_dest.read_text() == "#!/bin/bash\n# common utilities\n" + assert common_dest.stat().st_mode & 0o111 # Executable + def test_install_hook_with_backup(self, tmp_path): """Should backup existing custom hook before installing.""" repo_path = tmp_path / "test_repo" @@ -180,7 +200,7 @@ def test_generate_pre_commit_hook(self): """Should generate valid pre-commit hook script.""" script = generate_hook_script("pre-commit") - assert "#!/bin/zsh" in script or "#!/bin/bash" in script + assert script.startswith("#!/") assert "codeindex-managed hook" in script assert "ruff" in script.lower() or "lint" in script.lower() @@ -188,7 +208,7 @@ def test_generate_post_commit_hook(self): """Should generate valid post-commit hook script.""" script = generate_hook_script("post-commit") - assert "#!/bin/zsh" in script or "#!/bin/bash" in script + assert script.startswith("#!/") assert "codeindex-managed hook" in script assert "README_AI.md" in script or "codeindex" in script