diff --git a/src/auto_commit_service/__pycache__/app.cpython-312.pyc b/src/auto_commit_service/__pycache__/app.cpython-312.pyc index ecec300..abf5ccf 100644 Binary files a/src/auto_commit_service/__pycache__/app.cpython-312.pyc and b/src/auto_commit_service/__pycache__/app.cpython-312.pyc differ diff --git a/src/auto_commit_service/__pycache__/config.cpython-312.pyc b/src/auto_commit_service/__pycache__/config.cpython-312.pyc index 5279235..02cf655 100644 Binary files a/src/auto_commit_service/__pycache__/config.cpython-312.pyc and b/src/auto_commit_service/__pycache__/config.cpython-312.pyc differ diff --git a/src/auto_commit_service/__pycache__/models.cpython-312.pyc b/src/auto_commit_service/__pycache__/models.cpython-312.pyc index 84d56ed..be12b0a 100644 Binary files a/src/auto_commit_service/__pycache__/models.cpython-312.pyc and b/src/auto_commit_service/__pycache__/models.cpython-312.pyc differ diff --git a/src/auto_commit_service/git/operations.py b/src/auto_commit_service/git/operations.py index d2cdda4..d5cb050 100644 --- a/src/auto_commit_service/git/operations.py +++ b/src/auto_commit_service/git/operations.py @@ -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: diff --git a/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc b/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc index fa73ed5..ca40ec1 100644 Binary files a/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc and b/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc differ