fix: auto-commit .gitignore changes during project initialization (#1087) (#1124)

* fix: auto-commit .gitignore changes during project initialization (#1087)

When Auto-Claude modifies .gitignore to add its own entries, those
changes were not being committed. This caused merge failures with
"local changes would be overwritten by merge: .gitignore".

Changes:
- Add _is_git_repo() helper to check if directory is a git repo
- Add _commit_gitignore() helper to commit .gitignore changes
- Update ensure_all_gitignore_entries() to accept auto_commit parameter
- Update init_auto_claude_dir() and repair_gitignore() to auto-commit

The commit message "chore: add auto-claude entries to .gitignore" is
used for these automatic commits.

Fixes #1087

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: fix ruff formatting for function signature

Split long function signature across multiple lines to satisfy
ruff format requirements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add subprocess timeouts and improve error handling

- Add timeout=10 to git rev-parse call in _is_git_repo
- Add timeout=30 to git add and git commit calls in _commit_gitignore
- Check both stdout and stderr for "nothing to commit" message
  (location varies by git version/locale)
- Log warning when auto-commit fails to help diagnose merge issues
- Catch subprocess.TimeoutExpired explicitly

Addresses CodeRabbit MAJOR review comments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: youngmrz <elliott.zach@gmail.com>

* fix: use LC_ALL=C for locale-independent git output parsing

Set LC_ALL=C environment variable when running git commands to
ensure English output messages regardless of user locale. This
prevents "nothing to commit" detection from failing in non-English
environments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: only commit .gitignore file, not all staged changes

Previously, `git commit -m "..."` would commit ALL staged files,
potentially including unrelated user changes. This fix explicitly
specifies .gitignore as the file to commit.

Addresses CRITICAL bot feedback about unintentionally committing
user's staged changes during project initialization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: format git commit args per ruff

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use get_git_executable() for cross-platform git resolution

Use the platform abstraction module (core.git_executable) to resolve
the git executable path instead of hardcoding "git". This ensures
proper operation on Windows where git may not be in PATH.

Addresses CodeRabbit suggestion for consistent cross-platform behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add debug logging for exception handling in git operations

Address CodeRabbit feedback to log exceptions at DEBUG level in
_is_git_repo and _commit_gitignore functions for better debugging.
Also update repair_gitignore docstring to document auto-commit behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Signed-off-by: youngmrz <elliott.zach@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
youngmrz
2026-01-17 16:03:38 -05:00
committed by GitHub
parent 75a3684cae
commit ba089c5b0c
+101 -5
View File
@@ -4,8 +4,15 @@ Auto Claude project initialization utilities.
Handles first-time setup of .auto-claude directory and ensures proper gitignore configuration.
"""
import logging
import os
import subprocess
from pathlib import Path
from core.git_executable import get_git_executable
logger = logging.getLogger(__name__)
# All entries that should be added to .gitignore for auto-claude projects
AUTO_CLAUDE_GITIGNORE_ENTRIES = [
".auto-claude/",
@@ -76,7 +83,83 @@ def ensure_gitignore_entry(project_dir: Path, entry: str = ".auto-claude/") -> b
return True
def ensure_all_gitignore_entries(project_dir: Path) -> list[str]:
def _is_git_repo(project_dir: Path) -> bool:
"""Check if the directory is a git repository."""
try:
result = subprocess.run(
[get_git_executable(), "rev-parse", "--is-inside-work-tree"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, Exception) as e:
logger.debug("Git repo check failed: %s", e)
return False
def _commit_gitignore(project_dir: Path) -> bool:
"""
Commit .gitignore changes with a standard message.
FIX (#1087): Auto-commit .gitignore changes to prevent merge failures.
Without this, merging tasks fails with "local changes would be overwritten".
Args:
project_dir: The project root directory
Returns:
True if commit succeeded, False otherwise
"""
if not _is_git_repo(project_dir):
return False
try:
# Use LC_ALL=C to ensure English git output for reliable parsing
git_env = {**os.environ, "LC_ALL": "C"}
# Stage .gitignore
result = subprocess.run(
[get_git_executable(), "add", ".gitignore"],
cwd=project_dir,
capture_output=True,
text=True,
timeout=30,
env=git_env,
)
if result.returncode != 0:
return False
# Commit with standard message - explicitly specify .gitignore to avoid
# committing other staged files the user may have
result = subprocess.run(
[
get_git_executable(),
"commit",
".gitignore",
"-m",
"chore: add auto-claude entries to .gitignore",
],
cwd=project_dir,
capture_output=True,
text=True,
timeout=30,
env=git_env,
)
# Return True even if commit "fails" due to nothing to commit
# Check both stdout and stderr as message location varies by git version
combined_output = result.stdout + result.stderr
return result.returncode == 0 or "nothing to commit" in combined_output
except (subprocess.TimeoutExpired, Exception) as e:
logger.debug("Git commit failed: %s", e)
return False
def ensure_all_gitignore_entries(
project_dir: Path, auto_commit: bool = False
) -> list[str]:
"""
Ensure all auto-claude related entries exist in the project's .gitignore file.
@@ -84,6 +167,7 @@ def ensure_all_gitignore_entries(project_dir: Path) -> list[str]:
Args:
project_dir: The project root directory
auto_commit: If True, automatically commit the .gitignore changes
Returns:
List of entries that were added (empty if all already existed)
@@ -120,6 +204,16 @@ def ensure_all_gitignore_entries(project_dir: Path) -> list[str]:
added_entries.append(entry)
gitignore_path.write_text(content)
# Auto-commit if requested and entries were added
if auto_commit and added_entries:
if not _commit_gitignore(project_dir):
logger.warning(
"Failed to auto-commit .gitignore changes in %s. "
"Manual commit may be required to avoid merge conflicts.",
project_dir,
)
return added_entries
@@ -143,16 +237,17 @@ def init_auto_claude_dir(project_dir: Path) -> tuple[Path, bool]:
auto_claude_dir.mkdir(parents=True, exist_ok=True)
# Ensure all auto-claude entries are in .gitignore (only on first creation)
# FIX (#1087): Auto-commit the changes to prevent merge failures
gitignore_updated = False
if dir_created:
added = ensure_all_gitignore_entries(project_dir)
added = ensure_all_gitignore_entries(project_dir, auto_commit=True)
gitignore_updated = len(added) > 0
else:
# Even if dir exists, check gitignore on first run
# Use a marker file to track if we've already checked
marker = auto_claude_dir / ".gitignore_checked"
if not marker.exists():
added = ensure_all_gitignore_entries(project_dir)
added = ensure_all_gitignore_entries(project_dir, auto_commit=True)
gitignore_updated = len(added) > 0
marker.touch()
@@ -185,6 +280,7 @@ def repair_gitignore(project_dir: Path) -> list[str]:
or when gitignore entries were manually removed.
Also resets the .gitignore_checked marker to allow future updates.
Changes are automatically committed if the project is a git repository.
Args:
project_dir: The project root directory
@@ -200,8 +296,8 @@ def repair_gitignore(project_dir: Path) -> list[str]:
if marker.exists():
marker.unlink()
# Add all missing entries
added = ensure_all_gitignore_entries(project_dir)
# Add all missing entries and auto-commit
added = ensure_all_gitignore_entries(project_dir, auto_commit=True)
# Re-create the marker
if auto_claude_dir.exists():