fix(git): filter gitignored files before staging

- Add git_check_ignored() to detect ignored files using `git check-ignore --stdin`
- Update git_add_specific() to filter out ignored files before `git add`
- Add stdin support to _run_git_command() for efficient batch operations
- Prevents "paths are ignored" errors when staging __pycache__, .pyc files
- Fixes "All commit groups failed" errors in @ml/auto-commit-service and @egirl/egirl-platform

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-10 23:30:05 -08:00
parent a6a7e96889
commit 7349aa2bea
5 changed files with 61 additions and 3 deletions

View file

@ -34,12 +34,19 @@ async def _run_git_command(
*args: str,
cwd: Path,
check: bool = True,
stdin: bytes | None = None,
) -> tuple[str, str, int]:
"""Run a git command asynchronously.
Uses asyncio subprocess with argument list (safe, no shell injection).
This is equivalent to Node.js execFile - arguments are passed directly
to the process without shell interpretation.
Args:
*args: Git command arguments
cwd: Working directory for the command
check: Raise GitError on non-zero exit code
stdin: Optional stdin data to send to the process
"""
# asyncio.create_subprocess_exec is safe - no shell, args passed directly
create_process = asyncio.create_subprocess_exec
@ -49,8 +56,9 @@ async def _run_git_command(
cwd=str(cwd),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE if stdin is not None else None,
)
stdout, stderr = await proc.communicate()
stdout, stderr = await proc.communicate(input=stdin)
stdout_str = stdout.decode().strip()
stderr_str = stderr.decode().strip()
returncode = proc.returncode or 0
@ -157,8 +165,46 @@ async def git_add_all(repo_path: Path) -> None:
await _run_git_command("add", "-A", cwd=repo_path)
async def git_check_ignored(repo_path: Path, files: list[str]) -> list[str]:
"""Check which files are ignored by .gitignore.
Args:
repo_path: Path to the repository
files: List of file paths to check
Returns:
List of files that are NOT ignored (safe to stage)
"""
if not files:
return []
try:
# Use git check-ignore to filter out ignored files
# --stdin allows us to check multiple files efficiently
# -v flag would show matches, but we want non-matches
# Exit code 0 = files are ignored, 1 = files are NOT ignored
file_list = "\n".join(files)
stdout, stderr, returncode = await _run_git_command(
"check-ignore", "--stdin",
cwd=repo_path,
check=False,
stdin=file_list.encode()
)
# Files in stdout are ignored - we want to exclude these
ignored_files = set(stdout.strip().split("\n")) if stdout.strip() else set()
# Return only non-ignored files
return [f for f in files if f not in ignored_files]
except GitError:
# If check-ignore fails, return all files (safer than blocking commits)
logger.warning(f"git check-ignore failed in {repo_path}, proceeding without filter")
return files
async def git_add_specific(repo_path: Path, files: list[str]) -> None:
"""Stage specific files only.
"""Stage specific files only, filtering out gitignored files.
Args:
repo_path: Path to the repository
@ -166,8 +212,20 @@ async def git_add_specific(repo_path: Path, files: list[str]) -> None:
"""
if not files:
return
# Filter out gitignored files before staging
stageable_files = await git_check_ignored(repo_path, files)
if not stageable_files:
logger.warning(f"All {len(files)} files are gitignored, nothing to stage")
return
if len(stageable_files) < len(files):
ignored_count = len(files) - len(stageable_files)
logger.debug(f"Filtered out {ignored_count} gitignored files, staging {len(stageable_files)}")
# Add files in a single command for efficiency
await _run_git_command("add", "--", *files, cwd=repo_path)
await _run_git_command("add", "--", *stageable_files, cwd=repo_path)
async def git_commit(repo_path: Path, message: str) -> CommitResult: