diff --git a/codemcp/main.py b/codemcp/main.py index 478a37e..d485398 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -7,6 +7,7 @@ from typing import List, Optional import click +import pathspec import uvicorn from fastapi.middleware.cors import CORSMiddleware from mcp.server.fastmcp import FastMCP @@ -396,6 +397,78 @@ def normalize_newlines(s: object) -> object: return "Unknown subtool or operation" +def get_files_respecting_gitignore(dir_path: Path, pattern: str = "**/*") -> List[Path]: + """Get files in a directory respecting .gitignore rules in all subdirectories. + + Args: + dir_path: The directory path to search in + pattern: The glob pattern to match files against (default: "**/*") + + Returns: + A list of Path objects for files that match the pattern and respect .gitignore + """ + # First collect all files and directories + all_paths = list(dir_path.glob(pattern)) + all_files = [p for p in all_paths if p.is_file()] + all_dirs = [dir_path] + [p for p in all_paths if p.is_dir()] + + # Find all .gitignore files in the directory and subdirectories + gitignore_specs = {} + + # Process .gitignore files from root to leaf directories + for directory in sorted(all_dirs, key=lambda d: str(d)): + gitignore_path = directory / ".gitignore" + if gitignore_path.exists() and gitignore_path.is_file(): + try: + with open(gitignore_path, "r") as ignore_file: + ignore_lines = ignore_file.readlines() + gitignore_specs[directory] = pathspec.GitIgnoreSpec.from_lines( + ignore_lines + ) + except Exception as e: + # Log error but continue processing + logging.warning(f"Error reading .gitignore in {directory}: {e}") + + # If no .gitignore files found, return all files + if not gitignore_specs: + return [f for f in all_files if f.is_file()] + + # Helper function to check if a path is ignored by any relevant .gitignore + def is_ignored(path: Path) -> bool: + """ + Check if a path should be ignored according to .gitignore rules. + + This checks the path against all .gitignore files in its parent directories. + """ + # For files, we need to check if any parent directory is ignored first + if path.is_file(): + # Check if any parent directory is ignored + current_dir = path.parent + while current_dir.is_relative_to(dir_path): + if is_ignored(current_dir): + return True + current_dir = current_dir.parent + + # Now check the path against all relevant .gitignore specs + for spec_dir, spec in gitignore_specs.items(): + # Only apply specs from parent directories of the path + if path.is_relative_to(spec_dir): + # Get the path relative to the directory containing the .gitignore + rel_path = str(path.relative_to(spec_dir)) + # Empty string means the directory itself + if not rel_path: + rel_path = "." + # Check if path matches any pattern in the .gitignore + if spec.match_file(rel_path): + return True + + return False + + # Filter out ignored files + result = [f for f in all_files if not is_ignored(f)] + return result + + def configure_logging(log_file: str = "codemcp.log") -> None: """Configure logging to write to both a file and the console. @@ -567,9 +640,10 @@ def process_file(template_file, template_root, output_root): print(f"Created file: {rel_path}") return rel_path - # Recursively process template directory - for template_file in templates_dir.glob("**/*"): - if template_file.is_file() and template_file.name != ".gitkeep": + # Recursively process template directory respecting .gitignore + template_files = get_files_respecting_gitignore(templates_dir, "**/*") + for template_file in template_files: + if template_file.name != ".gitkeep": # Process template file try: rel_path = process_file(template_file, templates_dir, project_path)