Skip to content
Closed
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
80 changes: 77 additions & 3 deletions codemcp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
Expand Down
Loading