* 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:
+101
-5
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user