Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a17d201041 |
@@ -11,14 +11,17 @@ import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_latest_commit(project_dir: Path) -> str | None:
|
||||
"""Get the hash of the latest git commit."""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
[git_path, "rev-parse", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -32,8 +35,9 @@ def get_latest_commit(project_dir: Path) -> str | None:
|
||||
def get_commit_count(project_dir: Path) -> int:
|
||||
"""Get the total number of commits."""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-list", "--count", "HEAD"],
|
||||
[git_path, "rev-list", "--count", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -18,6 +18,8 @@ import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check for Claude SDK availability
|
||||
@@ -86,8 +88,9 @@ def get_session_diff(
|
||||
return "(No changes - same commit)"
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", commit_before, commit_after],
|
||||
[git_path, "diff", commit_before, commit_after],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -131,8 +134,9 @@ def get_changed_files(
|
||||
return []
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", commit_before, commit_after],
|
||||
[git_path, "diff", "--name-only", commit_before, commit_after],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -156,8 +160,9 @@ def get_commit_messages(
|
||||
return "(No commits)"
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "log", "--oneline", f"{commit_before}..{commit_after}"],
|
||||
[git_path, "log", "--oneline", f"{commit_before}..{commit_after}"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -9,6 +9,8 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
# Ensure parent directory is in path for imports (before other imports)
|
||||
_PARENT_DIR = Path(__file__).parent.parent
|
||||
if str(_PARENT_DIR) not in sys.path:
|
||||
@@ -58,12 +60,14 @@ def _detect_default_branch(project_dir: Path) -> str:
|
||||
"""
|
||||
import os
|
||||
|
||||
git_path = get_git_executable_path()
|
||||
|
||||
# 1. Check for DEFAULT_BRANCH env var
|
||||
env_branch = os.getenv("DEFAULT_BRANCH")
|
||||
if env_branch:
|
||||
# Verify the branch exists
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--verify", env_branch],
|
||||
[git_path, "rev-parse", "--verify", env_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -74,7 +78,7 @@ def _detect_default_branch(project_dir: Path) -> str:
|
||||
# 2. Auto-detect main/master
|
||||
for branch in ["main", "master"]:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--verify", branch],
|
||||
[git_path, "rev-parse", "--verify", branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -99,9 +103,10 @@ def _get_changed_files_from_git(
|
||||
Returns:
|
||||
List of changed file paths
|
||||
"""
|
||||
git_path = get_git_executable_path()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", f"{base_branch}...HEAD"],
|
||||
[git_path, "diff", "--name-only", f"{base_branch}...HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -119,7 +124,7 @@ def _get_changed_files_from_git(
|
||||
# Fallback: try without the three-dot notation
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", base_branch, "HEAD"],
|
||||
[git_path, "diff", "--name-only", base_branch, "HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -226,8 +231,9 @@ def _generate_and_save_commit_message(project_dir: Path, spec_name: str) -> None
|
||||
diff_summary = ""
|
||||
files_changed = []
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--staged", "--stat"],
|
||||
[git_path, "diff", "--staged", "--stat"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -237,7 +243,7 @@ def _generate_and_save_commit_message(project_dir: Path, spec_name: str) -> None
|
||||
|
||||
# Get list of changed files
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--staged", "--name-only"],
|
||||
[git_path, "diff", "--staged", "--name-only"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -375,6 +381,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
debug(MODULE, "Checking for git-level merge conflicts (non-destructive)...")
|
||||
|
||||
git_path = get_git_executable_path()
|
||||
spec_branch = f"auto-claude/{spec_name}"
|
||||
result = {
|
||||
"has_conflicts": False,
|
||||
@@ -388,7 +395,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
try:
|
||||
# Get the current branch (base branch)
|
||||
base_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -398,7 +405,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
# Get the merge base commit
|
||||
merge_base_result = subprocess.run(
|
||||
["git", "merge-base", result["base_branch"], spec_branch],
|
||||
[git_path, "merge-base", result["base_branch"], spec_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -411,7 +418,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
# Count commits main is ahead
|
||||
ahead_result = subprocess.run(
|
||||
["git", "rev-list", "--count", f"{merge_base}..{result['base_branch']}"],
|
||||
[git_path, "rev-list", "--count", f"{merge_base}..{result['base_branch']}"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -430,7 +437,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
# Note: --write-tree mode only accepts 2 branches (it auto-finds the merge base)
|
||||
merge_tree_result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
git_path,
|
||||
"merge-tree",
|
||||
"--write-tree",
|
||||
"--no-messages",
|
||||
@@ -474,7 +481,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
if not result["conflicting_files"]:
|
||||
# Files changed in main since merge-base
|
||||
main_files_result = subprocess.run(
|
||||
["git", "diff", "--name-only", merge_base, result["base_branch"]],
|
||||
[git_path, "diff", "--name-only", merge_base, result["base_branch"]],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -487,7 +494,7 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
# Files changed in spec branch since merge-base
|
||||
spec_files_result = subprocess.run(
|
||||
["git", "diff", "--name-only", merge_base, spec_branch],
|
||||
[git_path, "diff", "--name-only", merge_base, spec_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -7,10 +7,15 @@ for custom API endpoints.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
from core.git_bash import get_git_bash_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Priority order for auth token resolution
|
||||
# NOTE: We intentionally do NOT fall back to ANTHROPIC_API_KEY.
|
||||
# Auto Claude is designed to use Claude Code OAuth tokens only.
|
||||
@@ -222,6 +227,8 @@ def get_sdk_env_vars() -> dict[str, str]:
|
||||
Collects relevant env vars (ANTHROPIC_BASE_URL, etc.) that should
|
||||
be passed through to the claude-agent-sdk subprocess.
|
||||
|
||||
On Windows, also includes CLAUDE_CODE_GIT_BASH_PATH if git-bash is found.
|
||||
|
||||
Returns:
|
||||
Dict of env var name -> value for non-empty vars
|
||||
"""
|
||||
@@ -230,6 +237,12 @@ def get_sdk_env_vars() -> dict[str, str]:
|
||||
value = os.environ.get(var)
|
||||
if value:
|
||||
env[var] = value
|
||||
|
||||
# On Windows, add git-bash path for Claude SDK sandbox
|
||||
git_bash_env = get_git_bash_env()
|
||||
if git_bash_env:
|
||||
env.update(git_bash_env)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
Windows Git Bash Detection
|
||||
|
||||
Detects git-bash (bash.exe from Git for Windows) for Claude Agent SDK.
|
||||
The SDK requires bash.exe on Windows for its sandbox functionality.
|
||||
|
||||
Detection Priority:
|
||||
1. CLAUDE_CODE_GIT_BASH_PATH environment variable (user override)
|
||||
2. Standard Git installation paths (Program Files)
|
||||
3. User-specific installations (LocalAppData, Scoop, Chocolatey)
|
||||
|
||||
Note: This detects bash.exe, NOT git.exe. They are different executables:
|
||||
- git.exe: The Git CLI
|
||||
- bash.exe: Unix shell bundled with Git for Windows (required by Claude SDK)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Environment variable for user override
|
||||
ENV_VAR_NAME = "CLAUDE_CODE_GIT_BASH_PATH"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GitBashDetectionResult:
|
||||
"""Result of git-bash detection attempt."""
|
||||
found: bool
|
||||
path: Optional[str] = None
|
||||
source: str = "not-found"
|
||||
message: str = ""
|
||||
|
||||
|
||||
# Module-level cache (persists for process lifetime)
|
||||
_cache: Optional[GitBashDetectionResult] = None
|
||||
|
||||
|
||||
def _find_git_executable() -> Optional[str]:
|
||||
"""
|
||||
Find git.exe using multiple methods.
|
||||
|
||||
GUI apps on Windows often have different PATH than terminal sessions.
|
||||
We try multiple approaches to find git.
|
||||
|
||||
Returns:
|
||||
Path to git.exe if found, None otherwise
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
debug = os.environ.get("DEBUG", "").lower() in ("true", "1")
|
||||
|
||||
# Method 1: shutil.which (standard PATH lookup)
|
||||
git_path = shutil.which("git")
|
||||
if git_path:
|
||||
if debug:
|
||||
print(f"[GitBash] Found git via shutil.which: {git_path}")
|
||||
return git_path
|
||||
|
||||
# Method 2: Windows 'where' command (searches PATH differently)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["where", "git"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
git_path = result.stdout.strip().split('\n')[0].strip()
|
||||
if os.path.isfile(git_path):
|
||||
if debug:
|
||||
print(f"[GitBash] Found git via 'where' command: {git_path}")
|
||||
return git_path
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"[GitBash] 'where git' failed: {e}")
|
||||
|
||||
# Method 3: Check Windows Registry for Git install location
|
||||
try:
|
||||
import winreg
|
||||
for hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
|
||||
for key_path in [
|
||||
r"SOFTWARE\GitForWindows",
|
||||
r"SOFTWARE\WOW6432Node\GitForWindows",
|
||||
]:
|
||||
try:
|
||||
with winreg.OpenKey(hive, key_path) as key:
|
||||
install_path, _ = winreg.QueryValueEx(key, "InstallPath")
|
||||
if install_path:
|
||||
# Try multiple possible git.exe locations
|
||||
for subpath in ["cmd\\git.exe", "bin\\git.exe", "mingw64\\bin\\git.exe"]:
|
||||
git_exe = os.path.join(install_path, subpath)
|
||||
if os.path.isfile(git_exe):
|
||||
if debug:
|
||||
print(f"[GitBash] Found git via registry: {git_exe}")
|
||||
return git_exe
|
||||
except (FileNotFoundError, OSError):
|
||||
continue
|
||||
except ImportError:
|
||||
pass # winreg not available (non-Windows)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_bash_from_registry() -> Optional[str]:
|
||||
"""
|
||||
Find bash.exe directly from Windows Registry Git InstallPath.
|
||||
|
||||
This is more reliable than PATH-based detection for GUI apps.
|
||||
|
||||
Returns:
|
||||
Path to bash.exe if found via registry, None otherwise
|
||||
"""
|
||||
debug = os.environ.get("DEBUG", "").lower() in ("true", "1")
|
||||
|
||||
try:
|
||||
import winreg
|
||||
for hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
|
||||
for key_path in [
|
||||
r"SOFTWARE\GitForWindows",
|
||||
r"SOFTWARE\WOW6432Node\GitForWindows",
|
||||
]:
|
||||
try:
|
||||
with winreg.OpenKey(hive, key_path) as key:
|
||||
install_path, _ = winreg.QueryValueEx(key, "InstallPath")
|
||||
if install_path:
|
||||
# Check for bash.exe in standard locations
|
||||
bash_exe = os.path.join(install_path, "bin", "bash.exe")
|
||||
if os.path.isfile(bash_exe):
|
||||
if debug:
|
||||
print(f"[GitBash] Found bash.exe via registry: {bash_exe}")
|
||||
return bash_exe
|
||||
# Try usr/bin as fallback
|
||||
bash_exe = os.path.join(install_path, "usr", "bin", "bash.exe")
|
||||
if os.path.isfile(bash_exe):
|
||||
if debug:
|
||||
print(f"[GitBash] Found bash.exe via registry: {bash_exe}")
|
||||
return bash_exe
|
||||
except (FileNotFoundError, OSError):
|
||||
continue
|
||||
except ImportError:
|
||||
pass # winreg not available (non-Windows)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _derive_bash_from_git(git_path: str) -> Optional[str]:
|
||||
"""
|
||||
Derive bash.exe location from git.exe path.
|
||||
|
||||
Git for Windows structure:
|
||||
<install>/cmd/git.exe
|
||||
<install>/bin/bash.exe
|
||||
<install>/mingw64/bin/git.exe
|
||||
|
||||
Args:
|
||||
git_path: Path to git.exe
|
||||
|
||||
Returns:
|
||||
Path to bash.exe if found, None otherwise
|
||||
"""
|
||||
git_dir = os.path.dirname(git_path)
|
||||
git_parent = os.path.dirname(git_dir)
|
||||
|
||||
# Build list of possible bash locations
|
||||
possible_paths = []
|
||||
|
||||
# If git is in cmd/ folder
|
||||
if git_dir.endswith("cmd"):
|
||||
possible_paths.extend([
|
||||
os.path.join(git_parent, "bin", "bash.exe"),
|
||||
os.path.join(git_parent, "usr", "bin", "bash.exe"),
|
||||
])
|
||||
|
||||
# If git is in mingw64/bin/ folder
|
||||
if "mingw64" in git_dir:
|
||||
git_root = git_parent
|
||||
while git_root and not git_root.endswith("mingw64"):
|
||||
git_root = os.path.dirname(git_root)
|
||||
if git_root:
|
||||
install_root = os.path.dirname(git_root)
|
||||
possible_paths.extend([
|
||||
os.path.join(install_root, "bin", "bash.exe"),
|
||||
os.path.join(install_root, "usr", "bin", "bash.exe"),
|
||||
])
|
||||
|
||||
# Generic fallbacks
|
||||
possible_paths.extend([
|
||||
os.path.join(git_parent, "bin", "bash.exe"),
|
||||
os.path.join(git_parent, "usr", "bin", "bash.exe"),
|
||||
])
|
||||
|
||||
for bash_path in possible_paths:
|
||||
if os.path.isfile(bash_path):
|
||||
return bash_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_git_from_path() -> Optional[str]:
|
||||
"""
|
||||
Find bash.exe by first locating git.exe.
|
||||
|
||||
Returns:
|
||||
Path to bash.exe if found via git location, None otherwise
|
||||
"""
|
||||
git_path = _find_git_executable()
|
||||
if not git_path:
|
||||
return None
|
||||
|
||||
return _derive_bash_from_git(git_path)
|
||||
|
||||
|
||||
def _get_candidate_paths() -> list[str]:
|
||||
"""
|
||||
Get list of candidate paths for bash.exe on Windows.
|
||||
|
||||
Returns paths in priority order (most common first).
|
||||
"""
|
||||
candidates = [
|
||||
# Standard 64-bit Git installation
|
||||
r"C:\Program Files\Git\bin\bash.exe",
|
||||
# Standard 32-bit Git installation
|
||||
r"C:\Program Files (x86)\Git\bin\bash.exe",
|
||||
# User-specific installation (Git installer option)
|
||||
os.path.expandvars(r"%LOCALAPPDATA%\Programs\Git\bin\bash.exe"),
|
||||
# Chocolatey installation
|
||||
r"C:\ProgramData\chocolatey\lib\git\tools\bin\bash.exe",
|
||||
# Scoop installation
|
||||
os.path.expandvars(r"%USERPROFILE%\scoop\apps\git\current\bin\bash.exe"),
|
||||
# Git for Windows SDK
|
||||
r"C:\Git\bin\bash.exe",
|
||||
# Alternative usr/bin location (some Git versions)
|
||||
r"C:\Program Files\Git\usr\bin\bash.exe",
|
||||
]
|
||||
return candidates
|
||||
|
||||
|
||||
def detect_git_bash(force_refresh: bool = False) -> GitBashDetectionResult:
|
||||
"""
|
||||
Detect git-bash installation on Windows.
|
||||
|
||||
Args:
|
||||
force_refresh: If True, bypass cache and re-detect
|
||||
|
||||
Returns:
|
||||
GitBashDetectionResult with detection outcome
|
||||
"""
|
||||
global _cache
|
||||
|
||||
# Non-Windows: not applicable
|
||||
if platform.system() != "Windows":
|
||||
return GitBashDetectionResult(
|
||||
found=False,
|
||||
source="not-applicable",
|
||||
message="Git Bash detection only applies to Windows"
|
||||
)
|
||||
|
||||
# Return cached result if available
|
||||
if _cache is not None and not force_refresh:
|
||||
logger.debug(f"[GitBash] Using cached result: {_cache.path} ({_cache.source})")
|
||||
return _cache
|
||||
|
||||
debug = os.environ.get("DEBUG", "").lower() in ("true", "1")
|
||||
if debug:
|
||||
print("[GitBash] Starting detection...")
|
||||
|
||||
# Priority 1: Environment variable override
|
||||
env_path = os.environ.get(ENV_VAR_NAME)
|
||||
if env_path:
|
||||
if os.path.isfile(env_path):
|
||||
result = GitBashDetectionResult(
|
||||
found=True,
|
||||
path=env_path,
|
||||
source="env-var",
|
||||
message=f"Using {ENV_VAR_NAME}: {env_path}"
|
||||
)
|
||||
print(f"[GitBash] {result.message}")
|
||||
_cache = result
|
||||
return result
|
||||
else:
|
||||
print(f"[GitBash] WARNING: {ENV_VAR_NAME} set but path does not exist: {env_path}")
|
||||
|
||||
# Priority 2: Windows Registry (most reliable for GUI apps)
|
||||
if debug:
|
||||
print("[GitBash] Checking Windows Registry...")
|
||||
|
||||
registry_bash = _find_bash_from_registry()
|
||||
if registry_bash:
|
||||
result = GitBashDetectionResult(
|
||||
found=True,
|
||||
path=registry_bash,
|
||||
source="windows-registry",
|
||||
message=f"Found git-bash via registry: {registry_bash}"
|
||||
)
|
||||
print(f"[GitBash] {result.message}")
|
||||
_cache = result
|
||||
return result
|
||||
|
||||
# Priority 3: Check standard candidate paths
|
||||
candidates = _get_candidate_paths()
|
||||
if debug:
|
||||
print(f"[GitBash] Checking {len(candidates)} standard paths...")
|
||||
|
||||
for candidate_path in candidates:
|
||||
resolved = os.path.expandvars(candidate_path)
|
||||
if os.path.isfile(resolved):
|
||||
result = GitBashDetectionResult(
|
||||
found=True,
|
||||
path=resolved,
|
||||
source="standard-path",
|
||||
message=f"Found git-bash at standard path: {resolved}"
|
||||
)
|
||||
print(f"[GitBash] {result.message}")
|
||||
_cache = result
|
||||
return result
|
||||
elif debug:
|
||||
print(f"[GitBash] Not found: {resolved}")
|
||||
|
||||
# Priority 4: Derive from git.exe in PATH (handles custom PATH setups)
|
||||
if debug:
|
||||
print("[GitBash] Checking PATH-based git installation...")
|
||||
|
||||
path_based_bash = _find_git_from_path()
|
||||
if path_based_bash:
|
||||
result = GitBashDetectionResult(
|
||||
found=True,
|
||||
path=path_based_bash,
|
||||
source="derived-from-path",
|
||||
message=f"Found git-bash via PATH: {path_based_bash}"
|
||||
)
|
||||
print(f"[GitBash] {result.message}")
|
||||
_cache = result
|
||||
return result
|
||||
elif debug:
|
||||
print("[GitBash] Could not derive bash.exe from git PATH")
|
||||
|
||||
# Not found
|
||||
result = GitBashDetectionResult(
|
||||
found=False,
|
||||
source="not-found",
|
||||
message=(
|
||||
"Git Bash not found. Install Git for Windows from https://git-scm.com/downloads/win "
|
||||
f"or set {ENV_VAR_NAME} environment variable."
|
||||
)
|
||||
)
|
||||
print(f"[GitBash] WARNING: {result.message}")
|
||||
_cache = result
|
||||
return result
|
||||
|
||||
|
||||
def get_git_bash_path() -> Optional[str]:
|
||||
"""
|
||||
Get the path to bash.exe if available.
|
||||
|
||||
Convenience function that returns just the path or None.
|
||||
|
||||
Returns:
|
||||
Path to bash.exe or None if not found
|
||||
"""
|
||||
result = detect_git_bash()
|
||||
return result.path if result.found else None
|
||||
|
||||
|
||||
def get_git_bash_env() -> dict[str, str]:
|
||||
"""
|
||||
Get environment variables for Claude SDK with git-bash path.
|
||||
|
||||
Returns a dict suitable for merging into SDK environment variables.
|
||||
Only returns the variable if git-bash was found.
|
||||
|
||||
Returns:
|
||||
Dict with CLAUDE_CODE_GIT_BASH_PATH if found, empty dict otherwise
|
||||
"""
|
||||
# Skip on non-Windows
|
||||
if platform.system() != "Windows":
|
||||
return {}
|
||||
|
||||
result = detect_git_bash()
|
||||
if result.found and result.path:
|
||||
return {ENV_VAR_NAME: result.path}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def clear_cache() -> None:
|
||||
"""Clear the detection cache. Useful for testing."""
|
||||
global _cache
|
||||
_cache = None
|
||||
logger.debug("[GitBash] Cache cleared")
|
||||
|
||||
|
||||
# Module-level cache for git executable path
|
||||
_git_cache: Optional[str] = None
|
||||
|
||||
|
||||
def get_git_executable_path() -> str:
|
||||
"""
|
||||
Get the path to git executable.
|
||||
|
||||
On Windows, uses multi-method detection (shutil.which, registry, standard paths).
|
||||
On other platforms, returns "git" and relies on PATH.
|
||||
|
||||
Returns:
|
||||
Path to git executable, or "git" as fallback
|
||||
"""
|
||||
global _git_cache
|
||||
|
||||
# Return cached result if available
|
||||
if _git_cache is not None:
|
||||
return _git_cache
|
||||
|
||||
# On non-Windows, just use "git" from PATH
|
||||
if platform.system() != "Windows":
|
||||
_git_cache = "git"
|
||||
return _git_cache
|
||||
|
||||
# On Windows, try to find the full path
|
||||
git_path = _find_git_executable()
|
||||
if git_path:
|
||||
_git_cache = git_path
|
||||
debug = os.environ.get("DEBUG", "").lower() in ("true", "1")
|
||||
if debug:
|
||||
print(f"[Git] Using detected git: {git_path}")
|
||||
return _git_cache
|
||||
|
||||
# Fallback to "git" and hope it's in PATH
|
||||
_git_cache = "git"
|
||||
return _git_cache
|
||||
@@ -20,6 +20,7 @@ Public API is exported via workspace/__init__.py for backward compatibility.
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
from ui import (
|
||||
Icons,
|
||||
bold,
|
||||
@@ -180,8 +181,9 @@ def merge_existing_build(
|
||||
# Detect current branch - this is where user wants changes merged
|
||||
# Normal workflow: user is on their feature branch (e.g., version/2.5.5)
|
||||
# and wants to merge the spec changes into it, then PR to main
|
||||
git_path = get_git_executable_path()
|
||||
current_branch_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -545,6 +547,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
"""
|
||||
import re
|
||||
|
||||
git_path = get_git_executable_path()
|
||||
spec_branch = f"auto-claude/{spec_name}"
|
||||
result = {
|
||||
"has_conflicts": False,
|
||||
@@ -556,7 +559,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
try:
|
||||
# Get current branch
|
||||
base_result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -566,7 +569,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
# Get merge base
|
||||
merge_base_result = subprocess.run(
|
||||
["git", "merge-base", result["base_branch"], spec_branch],
|
||||
[git_path, "merge-base", result["base_branch"], spec_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -579,13 +582,13 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
|
||||
# Get commit hashes
|
||||
main_commit_result = subprocess.run(
|
||||
["git", "rev-parse", result["base_branch"]],
|
||||
[git_path, "rev-parse", result["base_branch"]],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
spec_commit_result = subprocess.run(
|
||||
["git", "rev-parse", spec_branch],
|
||||
[git_path, "rev-parse", spec_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -602,7 +605,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
# Note: --write-tree mode only accepts 2 branches (it auto-finds the merge base)
|
||||
merge_tree_result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
git_path,
|
||||
"merge-tree",
|
||||
"--write-tree",
|
||||
"--no-messages",
|
||||
@@ -639,7 +642,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
# Fallback: if we didn't parse conflicts, use diff to find files changed in both branches
|
||||
if not result["conflicting_files"]:
|
||||
main_files_result = subprocess.run(
|
||||
["git", "diff", "--name-only", merge_base, main_commit],
|
||||
[git_path, "diff", "--name-only", merge_base, main_commit],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -651,7 +654,7 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict:
|
||||
)
|
||||
|
||||
spec_files_result = subprocess.run(
|
||||
["git", "diff", "--name-only", merge_base, spec_commit],
|
||||
[git_path, "diff", "--name-only", merge_base, spec_commit],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -728,8 +731,9 @@ def _resolve_git_conflicts_with_ai(
|
||||
)
|
||||
|
||||
# Get merge-base commit
|
||||
git_path = get_git_executable_path()
|
||||
merge_base_result = subprocess.run(
|
||||
["git", "merge-base", base_branch, spec_branch],
|
||||
[git_path, "merge-base", base_branch, spec_branch],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -783,7 +787,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(content, encoding="utf-8")
|
||||
subprocess.run(
|
||||
["git", "add", target_file_path],
|
||||
[git_path, "add", target_file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -904,7 +908,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(merged_content, encoding="utf-8")
|
||||
subprocess.run(
|
||||
["git", "add", file_path], cwd=project_dir, capture_output=True
|
||||
[git_path, "add", file_path], cwd=project_dir, capture_output=True
|
||||
)
|
||||
resolved_files.append(file_path)
|
||||
print(success(f" ✓ {file_path} (new file)"))
|
||||
@@ -914,7 +918,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
if target_path.exists():
|
||||
target_path.unlink()
|
||||
subprocess.run(
|
||||
["git", "add", file_path],
|
||||
[git_path, "add", file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -960,7 +964,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(result.merged_content, encoding="utf-8")
|
||||
subprocess.run(
|
||||
["git", "add", result.file_path],
|
||||
[git_path, "add", result.file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1079,7 +1083,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(result.merged_content, encoding="utf-8")
|
||||
subprocess.run(
|
||||
["git", "add", result.file_path],
|
||||
[git_path, "add", result.file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1112,7 +1116,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
if target_path.exists():
|
||||
target_path.unlink()
|
||||
subprocess.run(
|
||||
["git", "add", target_file_path],
|
||||
[git_path, "add", target_file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1126,7 +1130,7 @@ def _resolve_git_conflicts_with_ai(
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(content, encoding="utf-8")
|
||||
subprocess.run(
|
||||
["git", "add", target_file_path],
|
||||
[git_path, "add", target_file_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
# Constants for merge limits
|
||||
MAX_FILE_LINES_FOR_AI = 5000 # Skip AI for files larger than this
|
||||
MAX_PARALLEL_AI_MERGES = 5 # Limit concurrent AI merge operations
|
||||
@@ -113,9 +115,10 @@ def detect_file_renames(
|
||||
# -M flag enables rename detection
|
||||
# --diff-filter=R shows only renames
|
||||
# --name-status shows status and file names
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
git_path,
|
||||
"log",
|
||||
"--name-status",
|
||||
"-M",
|
||||
@@ -176,8 +179,9 @@ def get_merge_base(project_dir: Path, ref1: str, ref2: str) -> str | None:
|
||||
Merge-base commit hash, or None if not found
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "merge-base", ref1, ref2],
|
||||
[git_path, "merge-base", ref1, ref2],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -191,8 +195,9 @@ def get_merge_base(project_dir: Path, ref1: str, ref2: str) -> str | None:
|
||||
|
||||
def has_uncommitted_changes(project_dir: Path) -> bool:
|
||||
"""Check if user has unsaved work."""
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
[git_path, "status", "--porcelain"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -202,8 +207,9 @@ def has_uncommitted_changes(project_dir: Path) -> bool:
|
||||
|
||||
def get_current_branch(project_dir: Path) -> str:
|
||||
"""Get the current branch name."""
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -239,8 +245,9 @@ def get_file_content_from_ref(
|
||||
project_dir: Path, ref: str, file_path: str
|
||||
) -> str | None:
|
||||
"""Get file content from a git ref (branch, commit, etc.)."""
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "show", f"{ref}:{file_path}"],
|
||||
[git_path, "show", f"{ref}:{file_path}"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -268,8 +275,9 @@ def get_changed_files_from_branch(
|
||||
Returns:
|
||||
List of (file_path, status) tuples
|
||||
"""
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-status", f"{base_branch}...{spec_branch}"],
|
||||
[git_path, "diff", "--name-status", f"{base_branch}...{spec_branch}"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -491,8 +499,9 @@ def create_conflict_file_with_git(
|
||||
try:
|
||||
# git merge-file <current> <base> <other>
|
||||
# Exit codes: 0 = clean merge, 1 = conflicts, >1 = error
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "merge-file", "-p", main_path, base_path, wt_path],
|
||||
[git_path, "merge-file", "-p", main_path, base_path, wt_path],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -12,6 +12,7 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
from merge import FileTimelineTracker
|
||||
from ui import (
|
||||
Icons,
|
||||
@@ -368,8 +369,9 @@ def initialize_timeline_tracking(
|
||||
files_to_modify.extend(subtask.get("files", []))
|
||||
|
||||
# Get the current branch point commit
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
[git_path, "rev-parse", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -22,6 +22,8 @@ import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
|
||||
class WorktreeError(Exception):
|
||||
"""Error during worktree operations."""
|
||||
@@ -70,12 +72,14 @@ class WorktreeManager:
|
||||
Returns:
|
||||
The detected base branch name
|
||||
"""
|
||||
git_path = get_git_executable_path()
|
||||
|
||||
# 1. Check for DEFAULT_BRANCH env var
|
||||
env_branch = os.getenv("DEFAULT_BRANCH")
|
||||
if env_branch:
|
||||
# Verify the branch exists
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--verify", env_branch],
|
||||
[git_path, "rev-parse", "--verify", env_branch],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -92,7 +96,7 @@ class WorktreeManager:
|
||||
# 2. Auto-detect main/master
|
||||
for branch in ["main", "master"]:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--verify", branch],
|
||||
[git_path, "rev-parse", "--verify", branch],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -111,8 +115,9 @@ class WorktreeManager:
|
||||
|
||||
def _get_current_branch(self) -> str:
|
||||
"""Get the current git branch."""
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -127,8 +132,9 @@ class WorktreeManager:
|
||||
self, args: list[str], cwd: Path | None = None
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run a git command and return the result."""
|
||||
git_path = get_git_executable_path()
|
||||
return subprocess.run(
|
||||
["git"] + args,
|
||||
[git_path] + args,
|
||||
cwd=cwd or self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -157,8 +163,9 @@ class WorktreeManager:
|
||||
|
||||
# 1. Check which staged files are gitignored
|
||||
# git check-ignore returns the files that ARE ignored
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "check-ignore", "--stdin"],
|
||||
[git_path, "check-ignore", "--stdin"],
|
||||
cwd=self.project_dir,
|
||||
input="\n".join(staged_files),
|
||||
capture_output=True,
|
||||
|
||||
@@ -15,6 +15,8 @@ import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
from ..types import FileEvolution, TaskSnapshot, compute_content_hash
|
||||
from .storage import EvolutionStorage
|
||||
|
||||
@@ -93,8 +95,9 @@ class BaselineCapture:
|
||||
List of absolute paths to trackable files
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "ls-files"],
|
||||
[git_path, "ls-files"],
|
||||
cwd=self.storage.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -124,8 +127,9 @@ class BaselineCapture:
|
||||
Git commit SHA, or "unknown" if not available
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
[git_path, "rev-parse", "HEAD"],
|
||||
cwd=self.storage.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -15,6 +15,8 @@ import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
from ..semantic_analyzer import SemanticAnalyzer
|
||||
from ..types import FileEvolution, TaskSnapshot, compute_content_hash
|
||||
from .storage import EvolutionStorage
|
||||
@@ -157,9 +159,10 @@ class ModificationTracker:
|
||||
)
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
# Get list of files changed in the worktree vs target branch
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", f"{target_branch}...HEAD"],
|
||||
[git_path, "diff", "--name-only", f"{target_branch}...HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -178,7 +181,7 @@ class ModificationTracker:
|
||||
for file_path in changed_files:
|
||||
# Get the diff for this file
|
||||
diff_result = subprocess.run(
|
||||
["git", "diff", f"{target_branch}...HEAD", "--", file_path],
|
||||
[git_path, "diff", f"{target_branch}...HEAD", "--", file_path],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -188,7 +191,7 @@ class ModificationTracker:
|
||||
# Get content before (from target branch) and after (current)
|
||||
try:
|
||||
show_result = subprocess.run(
|
||||
["git", "show", f"{target_branch}:{file_path}"],
|
||||
[git_path, "show", f"{target_branch}:{file_path}"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -259,10 +262,12 @@ class ModificationTracker:
|
||||
Returns:
|
||||
The detected target branch name, defaults to 'main' if detection fails
|
||||
"""
|
||||
git_path = get_git_executable_path()
|
||||
|
||||
# Try to get the upstream tracking branch
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -280,7 +285,7 @@ class ModificationTracker:
|
||||
for branch in ["main", "master", "develop"]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "merge-base", branch, "HEAD"],
|
||||
[git_path, "merge-base", branch, "HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -15,6 +15,8 @@ from __future__ import annotations
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
|
||||
def find_worktree(project_dir: Path, task_id: str) -> Path | None:
|
||||
"""
|
||||
@@ -57,8 +59,9 @@ def get_file_from_branch(project_dir: Path, file_path: str, branch: str) -> str
|
||||
File content as string, or None if file doesn't exist on branch
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "show", f"{branch}:{file_path}"],
|
||||
[git_path, "show", f"{branch}:{file_path}"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -17,6 +17,8 @@ import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Import debug utilities
|
||||
@@ -56,8 +58,9 @@ class TimelineGitHelper:
|
||||
def get_current_main_commit(self) -> str:
|
||||
"""Get the current HEAD commit on main branch."""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
[git_path, "rev-parse", "HEAD"],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -81,8 +84,9 @@ class TimelineGitHelper:
|
||||
File content as string, or None if file doesn't exist at that commit
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "show", f"{commit_hash}:{file_path}"],
|
||||
[git_path, "show", f"{commit_hash}:{file_path}"],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -104,9 +108,10 @@ class TimelineGitHelper:
|
||||
List of file paths changed in the commit
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
git_path,
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
@@ -134,9 +139,11 @@ class TimelineGitHelper:
|
||||
"""
|
||||
info = {}
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
|
||||
# Get commit message
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--format=%s", commit_hash],
|
||||
[git_path, "log", "-1", "--format=%s", commit_hash],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -146,7 +153,7 @@ class TimelineGitHelper:
|
||||
|
||||
# Get author
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--format=%an", commit_hash],
|
||||
[git_path, "log", "-1", "--format=%an", commit_hash],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -156,7 +163,7 @@ class TimelineGitHelper:
|
||||
|
||||
# Get diff stat
|
||||
result = subprocess.run(
|
||||
["git", "diff-tree", "--stat", "--no-commit-id", commit_hash],
|
||||
[git_path, "diff-tree", "--stat", "--no-commit-id", commit_hash],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -221,8 +228,9 @@ class TimelineGitHelper:
|
||||
target_branch = self._detect_target_branch(worktree_path)
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", f"{target_branch}...HEAD"],
|
||||
[git_path, "diff", "--name-only", f"{target_branch}...HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -254,8 +262,9 @@ class TimelineGitHelper:
|
||||
target_branch = self._detect_target_branch(worktree_path)
|
||||
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "merge-base", target_branch, "HEAD"],
|
||||
[git_path, "merge-base", target_branch, "HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -284,10 +293,12 @@ class TimelineGitHelper:
|
||||
Returns:
|
||||
The detected target branch name, defaults to 'main' if detection fails
|
||||
"""
|
||||
git_path = get_git_executable_path()
|
||||
|
||||
# Try to get the upstream tracking branch
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
||||
[git_path, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -305,7 +316,7 @@ class TimelineGitHelper:
|
||||
for branch in ["main", "master", "develop"]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "merge-base", branch, "HEAD"],
|
||||
[git_path, "merge-base", branch, "HEAD"],
|
||||
cwd=worktree_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -330,8 +341,9 @@ class TimelineGitHelper:
|
||||
Number of commits between the two points
|
||||
"""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "rev-list", "--count", f"{from_commit}..{to_commit}"],
|
||||
[git_path, "rev-list", "--count", f"{from_commit}..{to_commit}"],
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -27,6 +27,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from claude_agent_sdk import AgentDefinition
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
try:
|
||||
from ...core.client import create_client
|
||||
@@ -164,8 +165,9 @@ class ParallelOrchestratorReviewer:
|
||||
)
|
||||
|
||||
# Fetch the commit if not available locally (handles fork PRs)
|
||||
git_path = get_git_executable_path()
|
||||
fetch_result = subprocess.run(
|
||||
["git", "fetch", "origin", head_sha],
|
||||
[git_path, "fetch", "origin", head_sha],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -184,7 +186,7 @@ class ParallelOrchestratorReviewer:
|
||||
|
||||
# Create detached worktree at the PR commit
|
||||
result = subprocess.run(
|
||||
["git", "worktree", "add", "--detach", str(worktree_path), head_sha],
|
||||
[git_path, "worktree", "add", "--detach", str(worktree_path), head_sha],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -245,8 +247,9 @@ class ParallelOrchestratorReviewer:
|
||||
)
|
||||
|
||||
# Try 1: git worktree remove
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "worktree", "remove", "--force", str(worktree_path)],
|
||||
[git_path, "worktree", "remove", "--force", str(worktree_path)],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -267,7 +270,7 @@ class ParallelOrchestratorReviewer:
|
||||
try:
|
||||
shutil.rmtree(worktree_path, ignore_errors=True)
|
||||
subprocess.run(
|
||||
["git", "worktree", "prune"],
|
||||
[git_path, "worktree", "prune"],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
@@ -283,8 +286,9 @@ class ParallelOrchestratorReviewer:
|
||||
return
|
||||
|
||||
# Get registered worktrees from git
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "worktree", "list", "--porcelain"],
|
||||
[git_path, "worktree", "list", "--porcelain"],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -308,7 +312,7 @@ class ParallelOrchestratorReviewer:
|
||||
|
||||
if stale_count > 0:
|
||||
subprocess.run(
|
||||
["git", "worktree", "prune"],
|
||||
[git_path, "worktree", "prune"],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
|
||||
@@ -47,6 +47,8 @@ if sys.version_info < (3, 10): # noqa: UP036
|
||||
import asyncio
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows BEFORE any imports that might print
|
||||
@@ -340,8 +342,15 @@ Examples:
|
||||
print(f" {muted('Running:')} {' '.join(run_cmd)}")
|
||||
print()
|
||||
|
||||
# Execute run.py - replace current process
|
||||
os.execv(sys.executable, run_cmd)
|
||||
# Execute run.py
|
||||
# On Windows, use subprocess.run() to properly handle paths with spaces
|
||||
# os.execv() on Windows has issues with argument quoting for paths containing spaces
|
||||
if platform.system() == "Windows":
|
||||
result = subprocess.run(run_cmd)
|
||||
sys.exit(result.returncode)
|
||||
else:
|
||||
# On Unix, os.execv() replaces the current process (more efficient)
|
||||
os.execv(sys.executable, run_cmd)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
# =============================================================================
|
||||
# SECRET PATTERNS
|
||||
# =============================================================================
|
||||
@@ -364,8 +366,9 @@ def scan_content(content: str, file_path: str) -> list[SecretMatch]:
|
||||
def get_staged_files() -> list[str]:
|
||||
"""Get list of staged files from git (excluding deleted files)."""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
|
||||
[git_path, "diff", "--cached", "--name-only", "--diff-filter=ACM"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
@@ -379,8 +382,9 @@ def get_staged_files() -> list[str]:
|
||||
def get_all_tracked_files() -> list[str]:
|
||||
"""Get all tracked files in the repository."""
|
||||
try:
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "ls-files"],
|
||||
[git_path, "ls-files"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
|
||||
@@ -20,6 +20,8 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from core.git_bash import get_git_executable_path
|
||||
|
||||
|
||||
class FailureType(Enum):
|
||||
"""Types of failures that can occur during autonomous builds."""
|
||||
@@ -425,8 +427,9 @@ class RecoveryManager:
|
||||
"""
|
||||
try:
|
||||
# Use git reset --hard to rollback
|
||||
git_path = get_git_executable_path()
|
||||
result = subprocess.run(
|
||||
["git", "reset", "--hard", commit_hash],
|
||||
[git_path, "reset", "--hard", commit_hash],
|
||||
cwd=self.project_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -377,7 +377,24 @@ class CLIToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. System PATH (augmented)
|
||||
// 3. Windows Registry (most reliable for GUI apps on Windows)
|
||||
if (process.platform === 'win32') {
|
||||
const registryGit = this.findGitFromWindowsRegistry();
|
||||
if (registryGit) {
|
||||
const validation = this.validateGit(registryGit);
|
||||
if (validation.valid) {
|
||||
return {
|
||||
found: true,
|
||||
path: registryGit,
|
||||
version: validation.version,
|
||||
source: 'windows-registry',
|
||||
message: `Using Git from registry: ${registryGit}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. System PATH (augmented)
|
||||
const gitPath = findExecutable('git');
|
||||
if (gitPath) {
|
||||
const validation = this.validateGit(gitPath);
|
||||
@@ -392,7 +409,7 @@ class CLIToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Not found - fallback to 'git'
|
||||
// 5. Not found - fallback to 'git'
|
||||
return {
|
||||
found: false,
|
||||
source: 'fallback',
|
||||
@@ -678,6 +695,64 @@ class CLIToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Git installation from Windows Registry
|
||||
*
|
||||
* Git for Windows registers its install path in the registry.
|
||||
* This is more reliable than PATH for GUI apps.
|
||||
*
|
||||
* @returns Path to git.exe if found via registry, null otherwise
|
||||
*/
|
||||
private findGitFromWindowsRegistry(): string | null {
|
||||
if (process.platform !== 'win32') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use reg.exe to query the registry (works without native modules)
|
||||
const registryPaths = [
|
||||
'HKLM\\SOFTWARE\\GitForWindows',
|
||||
'HKCU\\SOFTWARE\\GitForWindows',
|
||||
'HKLM\\SOFTWARE\\WOW6432Node\\GitForWindows',
|
||||
];
|
||||
|
||||
for (const regPath of registryPaths) {
|
||||
try {
|
||||
const result = execFileSync('reg', ['query', regPath, '/v', 'InstallPath'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
// Parse the registry output to get InstallPath
|
||||
const match = result.match(/InstallPath\s+REG_SZ\s+(.+)/);
|
||||
if (match && match[1]) {
|
||||
const installPath = match[1].trim();
|
||||
// Check for git.exe in cmd folder
|
||||
const gitExe = path.join(installPath, 'cmd', 'git.exe');
|
||||
if (existsSync(gitExe)) {
|
||||
console.warn(`[Git] Found via registry: ${gitExe}`);
|
||||
return gitExe;
|
||||
}
|
||||
// Fallback to bin folder
|
||||
const gitExeBin = path.join(installPath, 'bin', 'git.exe');
|
||||
if (existsSync(gitExeBin)) {
|
||||
console.warn(`[Git] Found via registry: ${gitExeBin}`);
|
||||
return gitExeBin;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Registry key not found, try next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Git] Registry lookup failed:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Git availability and version
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ToolDetectionResult {
|
||||
| 'user-config'
|
||||
| 'venv'
|
||||
| 'homebrew'
|
||||
| 'windows-registry'
|
||||
| 'system-path'
|
||||
| 'bundled'
|
||||
| 'fallback';
|
||||
|
||||
Reference in New Issue
Block a user