gac 3.8.1__py3-none-any.whl → 3.10.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gac/__init__.py +4 -6
- gac/__version__.py +1 -1
- gac/ai_utils.py +18 -49
- gac/cli.py +14 -10
- gac/commit_executor.py +59 -0
- gac/config.py +28 -3
- gac/config_cli.py +19 -7
- gac/constants/__init__.py +34 -0
- gac/constants/commit.py +63 -0
- gac/constants/defaults.py +40 -0
- gac/constants/file_patterns.py +110 -0
- gac/constants/languages.py +119 -0
- gac/diff_cli.py +0 -22
- gac/errors.py +8 -2
- gac/git.py +6 -6
- gac/git_state_validator.py +193 -0
- gac/grouped_commit_workflow.py +458 -0
- gac/init_cli.py +2 -1
- gac/interactive_mode.py +179 -0
- gac/language_cli.py +0 -1
- gac/main.py +222 -959
- gac/model_cli.py +2 -1
- gac/model_identifier.py +70 -0
- gac/oauth/claude_code.py +2 -2
- gac/oauth/qwen_oauth.py +4 -0
- gac/oauth/token_store.py +2 -2
- gac/oauth_retry.py +161 -0
- gac/postprocess.py +155 -0
- gac/prompt.py +20 -490
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +70 -81
- gac/providers/anthropic.py +12 -56
- gac/providers/azure_openai.py +48 -92
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +10 -43
- gac/providers/chutes.py +16 -72
- gac/providers/claude_code.py +64 -97
- gac/providers/custom_anthropic.py +51 -85
- gac/providers/custom_openai.py +29 -87
- gac/providers/deepseek.py +10 -43
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +10 -43
- gac/providers/gemini.py +66 -73
- gac/providers/groq.py +10 -62
- gac/providers/kimi_coding.py +19 -59
- gac/providers/lmstudio.py +62 -52
- gac/providers/minimax.py +10 -43
- gac/providers/mistral.py +10 -43
- gac/providers/moonshot.py +10 -43
- gac/providers/ollama.py +54 -41
- gac/providers/openai.py +30 -46
- gac/providers/openrouter.py +15 -62
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +55 -67
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +137 -91
- gac/providers/streamlake.py +26 -56
- gac/providers/synthetic.py +35 -47
- gac/providers/together.py +10 -43
- gac/providers/zai.py +21 -59
- gac/py.typed +0 -0
- gac/security.py +1 -1
- gac/templates/__init__.py +1 -0
- gac/templates/question_generation.txt +60 -0
- gac/templates/system_prompt.txt +224 -0
- gac/templates/user_prompt.txt +28 -0
- gac/utils.py +6 -5
- gac/workflow_context.py +162 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
- gac-3.10.10.dist-info/RECORD +79 -0
- gac/constants.py +0 -328
- gac-3.8.1.dist-info/RECORD +0 -56
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""File pattern constants for identifying special file types and importance weighting."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FilePatterns:
|
|
5
|
+
"""Patterns for identifying special file types."""
|
|
6
|
+
|
|
7
|
+
# Regex patterns to detect binary file changes in git diffs (e.g., images or other non-text files)
|
|
8
|
+
BINARY: list[str] = [
|
|
9
|
+
r"Binary files .* differ",
|
|
10
|
+
r"GIT binary patch",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
# Regex patterns to detect minified files in git diffs (e.g., JavaScript or CSS files)
|
|
14
|
+
MINIFIED_EXTENSIONS: list[str] = [
|
|
15
|
+
".min.js",
|
|
16
|
+
".min.css",
|
|
17
|
+
".bundle.js",
|
|
18
|
+
".bundle.css",
|
|
19
|
+
".compressed.js",
|
|
20
|
+
".compressed.css",
|
|
21
|
+
".opt.js",
|
|
22
|
+
".opt.css",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Regex patterns to detect build directories in git diffs (e.g., dist, build, vendor, etc.)
|
|
26
|
+
BUILD_DIRECTORIES: list[str] = [
|
|
27
|
+
"/dist/",
|
|
28
|
+
"/build/",
|
|
29
|
+
"/vendor/",
|
|
30
|
+
"/node_modules/",
|
|
31
|
+
"/assets/vendor/",
|
|
32
|
+
"/public/build/",
|
|
33
|
+
"/static/dist/",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FileTypeImportance:
|
|
38
|
+
"""Importance multipliers for different file types."""
|
|
39
|
+
|
|
40
|
+
EXTENSIONS: dict[str, float] = {
|
|
41
|
+
# Programming languages
|
|
42
|
+
".py": 5.0, # Python
|
|
43
|
+
".js": 4.5, # JavaScript
|
|
44
|
+
".ts": 4.5, # TypeScript
|
|
45
|
+
".jsx": 4.8, # React JS
|
|
46
|
+
".tsx": 4.8, # React TS
|
|
47
|
+
".go": 4.5, # Go
|
|
48
|
+
".rs": 4.5, # Rust
|
|
49
|
+
".java": 4.2, # Java
|
|
50
|
+
".c": 4.2, # C
|
|
51
|
+
".h": 4.2, # C/C++ header
|
|
52
|
+
".cpp": 4.2, # C++
|
|
53
|
+
".rb": 4.2, # Ruby
|
|
54
|
+
".php": 4.0, # PHP
|
|
55
|
+
".scala": 4.0, # Scala
|
|
56
|
+
".swift": 4.0, # Swift
|
|
57
|
+
".kt": 4.0, # Kotlin
|
|
58
|
+
# Configuration
|
|
59
|
+
".json": 3.5, # JSON config
|
|
60
|
+
".yaml": 3.8, # YAML config
|
|
61
|
+
".yml": 3.8, # YAML config
|
|
62
|
+
".toml": 3.8, # TOML config
|
|
63
|
+
".ini": 3.5, # INI config
|
|
64
|
+
".env": 3.5, # Environment variables
|
|
65
|
+
# Documentation
|
|
66
|
+
".md": 2.5, # Markdown (reduced to prioritize code changes)
|
|
67
|
+
".rst": 2.5, # reStructuredText (reduced to prioritize code changes)
|
|
68
|
+
# Web
|
|
69
|
+
".html": 3.5, # HTML
|
|
70
|
+
".css": 3.5, # CSS
|
|
71
|
+
".scss": 3.5, # SCSS
|
|
72
|
+
".svg": 2.5, # SVG graphics
|
|
73
|
+
# Build & CI
|
|
74
|
+
"Dockerfile": 4.0, # Docker
|
|
75
|
+
".github/workflows": 4.0, # GitHub Actions
|
|
76
|
+
"CMakeLists.txt": 3.8, # CMake
|
|
77
|
+
"Makefile": 3.8, # Make
|
|
78
|
+
"package.json": 4.2, # NPM package
|
|
79
|
+
"pyproject.toml": 4.2, # Python project
|
|
80
|
+
"requirements.txt": 4.0, # Python requirements
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CodePatternImportance:
|
|
85
|
+
"""Importance multipliers for different code patterns."""
|
|
86
|
+
|
|
87
|
+
# Regex patterns to detect code structure changes in git diffs (e.g., class, function, import)
|
|
88
|
+
# Note: The patterns are prefixed with "+" to match only added and modified lines
|
|
89
|
+
PATTERNS: dict[str, float] = {
|
|
90
|
+
# Structure changes
|
|
91
|
+
r"\+\s*(class|interface|enum)\s+\w+": 1.8, # Class/interface/enum definitions
|
|
92
|
+
r"\+\s*(def|function|func)\s+\w+\s*\(": 1.5, # Function definitions
|
|
93
|
+
r"\+\s*(import|from .* import)": 1.3, # Imports
|
|
94
|
+
r"\+\s*(public|private|protected)\s+\w+": 1.2, # Access modifiers
|
|
95
|
+
# Configuration changes
|
|
96
|
+
r"\+\s*\"(dependencies|devDependencies)\"": 1.4, # Package dependencies
|
|
97
|
+
r"\+\s*version[\"\s:=]+[0-9.]+": 1.3, # Version changes
|
|
98
|
+
# Logic changes
|
|
99
|
+
r"\+\s*(if|else|elif|switch|case|for|while)[\s(]": 1.2, # Control structures
|
|
100
|
+
r"\+\s*(try|catch|except|finally)[\s:]": 1.2, # Exception handling
|
|
101
|
+
r"\+\s*return\s+": 1.1, # Return statements
|
|
102
|
+
r"\+\s*await\s+": 1.1, # Async/await
|
|
103
|
+
# Comments & docs
|
|
104
|
+
r"\+\s*(//|#|/\*|\*\*)\s*TODO": 1.2, # TODOs
|
|
105
|
+
r"\+\s*(//|#|/\*|\*\*)\s*FIX": 1.3, # FIXes
|
|
106
|
+
r"\+\s*(\"\"\"|\'\'\')": 1.1, # Docstrings
|
|
107
|
+
# Test code
|
|
108
|
+
r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
|
|
109
|
+
r"\+\s*(assert|expect)": 1.0, # Assertions
|
|
110
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Language code mappings and utilities for internationalization."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Languages:
|
|
5
|
+
"""Language code mappings and utilities."""
|
|
6
|
+
|
|
7
|
+
# Language code to full name mapping
|
|
8
|
+
# Supports ISO 639-1 codes and common variants
|
|
9
|
+
CODE_MAP: dict[str, str] = {
|
|
10
|
+
# English
|
|
11
|
+
"en": "English",
|
|
12
|
+
# Chinese
|
|
13
|
+
"zh": "Simplified Chinese",
|
|
14
|
+
"zh-cn": "Simplified Chinese",
|
|
15
|
+
"zh-hans": "Simplified Chinese",
|
|
16
|
+
"zh-tw": "Traditional Chinese",
|
|
17
|
+
"zh-hant": "Traditional Chinese",
|
|
18
|
+
# Japanese
|
|
19
|
+
"ja": "Japanese",
|
|
20
|
+
# Korean
|
|
21
|
+
"ko": "Korean",
|
|
22
|
+
# Spanish
|
|
23
|
+
"es": "Spanish",
|
|
24
|
+
# Portuguese
|
|
25
|
+
"pt": "Portuguese",
|
|
26
|
+
# French
|
|
27
|
+
"fr": "French",
|
|
28
|
+
# German
|
|
29
|
+
"de": "German",
|
|
30
|
+
# Russian
|
|
31
|
+
"ru": "Russian",
|
|
32
|
+
# Hindi
|
|
33
|
+
"hi": "Hindi",
|
|
34
|
+
# Italian
|
|
35
|
+
"it": "Italian",
|
|
36
|
+
# Polish
|
|
37
|
+
"pl": "Polish",
|
|
38
|
+
# Turkish
|
|
39
|
+
"tr": "Turkish",
|
|
40
|
+
# Dutch
|
|
41
|
+
"nl": "Dutch",
|
|
42
|
+
# Vietnamese
|
|
43
|
+
"vi": "Vietnamese",
|
|
44
|
+
# Thai
|
|
45
|
+
"th": "Thai",
|
|
46
|
+
# Indonesian
|
|
47
|
+
"id": "Indonesian",
|
|
48
|
+
# Swedish
|
|
49
|
+
"sv": "Swedish",
|
|
50
|
+
# Arabic
|
|
51
|
+
"ar": "Arabic",
|
|
52
|
+
# Hebrew
|
|
53
|
+
"he": "Hebrew",
|
|
54
|
+
# Greek
|
|
55
|
+
"el": "Greek",
|
|
56
|
+
# Danish
|
|
57
|
+
"da": "Danish",
|
|
58
|
+
# Norwegian
|
|
59
|
+
"no": "Norwegian",
|
|
60
|
+
"nb": "Norwegian",
|
|
61
|
+
"nn": "Norwegian",
|
|
62
|
+
# Finnish
|
|
63
|
+
"fi": "Finnish",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# List of languages with display names and English names for CLI selection
|
|
67
|
+
# Format: (display_name, english_name)
|
|
68
|
+
LANGUAGES: list[tuple[str, str]] = [
|
|
69
|
+
("English", "English"),
|
|
70
|
+
("简体中文", "Simplified Chinese"),
|
|
71
|
+
("繁體中文", "Traditional Chinese"),
|
|
72
|
+
("日本語", "Japanese"),
|
|
73
|
+
("한국어", "Korean"),
|
|
74
|
+
("Español", "Spanish"),
|
|
75
|
+
("Português", "Portuguese"),
|
|
76
|
+
("Français", "French"),
|
|
77
|
+
("Deutsch", "German"),
|
|
78
|
+
("Русский", "Russian"),
|
|
79
|
+
("हिन्दी", "Hindi"),
|
|
80
|
+
("Italiano", "Italian"),
|
|
81
|
+
("Polski", "Polish"),
|
|
82
|
+
("Türkçe", "Turkish"),
|
|
83
|
+
("Nederlands", "Dutch"),
|
|
84
|
+
("Tiếng Việt", "Vietnamese"),
|
|
85
|
+
("ไทย", "Thai"),
|
|
86
|
+
("Bahasa Indonesia", "Indonesian"),
|
|
87
|
+
("Svenska", "Swedish"),
|
|
88
|
+
("العربية", "Arabic"),
|
|
89
|
+
("עברית", "Hebrew"),
|
|
90
|
+
("Ελληνικά", "Greek"),
|
|
91
|
+
("Dansk", "Danish"),
|
|
92
|
+
("Norsk", "Norwegian"),
|
|
93
|
+
("Suomi", "Finnish"),
|
|
94
|
+
("Custom", "Custom"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def resolve_code(language: str) -> str:
|
|
99
|
+
"""Resolve a language code to its full name.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Full language name (e.g., 'Spanish', 'Simplified Chinese')
|
|
106
|
+
|
|
107
|
+
If the input is already a full language name, it's returned as-is.
|
|
108
|
+
If it's a recognized code, it's converted to the full name.
|
|
109
|
+
Otherwise, the input is returned unchanged (for custom languages).
|
|
110
|
+
"""
|
|
111
|
+
# Normalize the code to lowercase for lookup
|
|
112
|
+
code_lower = language.lower().strip()
|
|
113
|
+
|
|
114
|
+
# Check if it's a recognized code
|
|
115
|
+
if code_lower in Languages.CODE_MAP:
|
|
116
|
+
return Languages.CODE_MAP[code_lower]
|
|
117
|
+
|
|
118
|
+
# Return as-is (could be a full name or custom language)
|
|
119
|
+
return language
|
gac/diff_cli.py
CHANGED
|
@@ -157,25 +157,3 @@ def diff(
|
|
|
157
157
|
commit1=commit1,
|
|
158
158
|
commit2=commit2,
|
|
159
159
|
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# Function for testing only
|
|
163
|
-
def _callback_for_testing(
|
|
164
|
-
filter: bool,
|
|
165
|
-
truncate: bool,
|
|
166
|
-
max_tokens: int | None,
|
|
167
|
-
staged: bool,
|
|
168
|
-
color: bool,
|
|
169
|
-
commit1: str | None = None,
|
|
170
|
-
commit2: str | None = None,
|
|
171
|
-
) -> None:
|
|
172
|
-
"""A version of the diff command callback that can be called directly from tests."""
|
|
173
|
-
_diff_implementation(
|
|
174
|
-
filter=filter,
|
|
175
|
-
truncate=truncate,
|
|
176
|
-
max_tokens=max_tokens,
|
|
177
|
-
staged=staged,
|
|
178
|
-
color=color,
|
|
179
|
-
commit1=commit1,
|
|
180
|
-
commit2=commit2,
|
|
181
|
-
)
|
gac/errors.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
-
from typing import TypeVar
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
7
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
@@ -113,6 +113,12 @@ class SecurityError(GacError):
|
|
|
113
113
|
exit_code = 6
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
class HookError(GacError):
|
|
117
|
+
"""Error when pre-commit or lefthook hooks fail."""
|
|
118
|
+
|
|
119
|
+
exit_code = 1
|
|
120
|
+
|
|
121
|
+
|
|
116
122
|
# Simplified error hierarchy - we use a single AIError class with error codes
|
|
117
123
|
# instead of multiple subclasses for better maintainability
|
|
118
124
|
|
|
@@ -216,7 +222,7 @@ def with_error_handling(
|
|
|
216
222
|
"""
|
|
217
223
|
|
|
218
224
|
def decorator(func: Callable[..., T]) -> Callable[..., T | None]:
|
|
219
|
-
def wrapper(*args, **kwargs) -> T | None:
|
|
225
|
+
def wrapper(*args: Any, **kwargs: Any) -> T | None:
|
|
220
226
|
try:
|
|
221
227
|
return func(*args, **kwargs)
|
|
222
228
|
except Exception as e:
|
gac/git.py
CHANGED
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
def run_subprocess_with_encoding_fallback(
|
|
18
18
|
command: list[str], silent: bool = False, timeout: int = 60
|
|
19
|
-
) -> subprocess.CompletedProcess:
|
|
19
|
+
) -> subprocess.CompletedProcess[str]:
|
|
20
20
|
"""Run subprocess with encoding fallback, returning full CompletedProcess object.
|
|
21
21
|
|
|
22
22
|
This is used for cases where we need both stdout and stderr separately,
|
|
@@ -57,7 +57,7 @@ def run_subprocess_with_encoding_fallback(
|
|
|
57
57
|
continue
|
|
58
58
|
except subprocess.TimeoutExpired:
|
|
59
59
|
raise
|
|
60
|
-
except
|
|
60
|
+
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
|
|
61
61
|
if not silent:
|
|
62
62
|
logger.debug(f"Command error: {e}")
|
|
63
63
|
# Try next encoding for non-timeout errors
|
|
@@ -182,7 +182,7 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
|
|
|
182
182
|
|
|
183
183
|
output = run_git_command(args)
|
|
184
184
|
return output
|
|
185
|
-
except
|
|
185
|
+
except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
|
|
186
186
|
logger.error(f"Failed to get diff: {str(e)}")
|
|
187
187
|
raise GitError(f"Failed to get diff: {str(e)}") from e
|
|
188
188
|
|
|
@@ -246,7 +246,7 @@ def run_pre_commit_hooks(hook_timeout: int = 120) -> bool:
|
|
|
246
246
|
else:
|
|
247
247
|
logger.error(f"Pre-commit hooks failed with exit code {result.returncode}")
|
|
248
248
|
return False
|
|
249
|
-
except
|
|
249
|
+
except (subprocess.SubprocessError, OSError, FileNotFoundError, PermissionError) as e:
|
|
250
250
|
logger.debug(f"Error running pre-commit: {e}")
|
|
251
251
|
# If pre-commit isn't available, don't block the commit
|
|
252
252
|
return True
|
|
@@ -296,7 +296,7 @@ def run_lefthook_hooks(hook_timeout: int = 120) -> bool:
|
|
|
296
296
|
else:
|
|
297
297
|
logger.error(f"Lefthook hooks failed with exit code {result.returncode}")
|
|
298
298
|
return False
|
|
299
|
-
except
|
|
299
|
+
except (subprocess.SubprocessError, OSError, FileNotFoundError, PermissionError) as e:
|
|
300
300
|
logger.debug(f"Error running Lefthook: {e}")
|
|
301
301
|
# If lefthook isn't available, don't block the commit
|
|
302
302
|
return True
|
|
@@ -320,7 +320,7 @@ def push_changes() -> bool:
|
|
|
320
320
|
else:
|
|
321
321
|
logger.error(f"Failed to push changes: {error_msg}")
|
|
322
322
|
return False
|
|
323
|
-
except
|
|
323
|
+
except (subprocess.SubprocessError, OSError, ConnectionError) as e:
|
|
324
324
|
logger.error(f"Failed to push changes: {e}")
|
|
325
325
|
return False
|
|
326
326
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Git state validation and management for gac."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any, NamedTuple
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from gac.config import GACConfig
|
|
11
|
+
from gac.errors import ConfigError, GitError, handle_error
|
|
12
|
+
from gac.git import get_staged_files, get_staged_status, run_git_command
|
|
13
|
+
from gac.preprocess import preprocess_diff
|
|
14
|
+
from gac.security import get_affected_files, scan_staged_diff
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GitState(NamedTuple):
|
|
21
|
+
"""Structured representation of git repository state."""
|
|
22
|
+
|
|
23
|
+
repo_root: str
|
|
24
|
+
staged_files: list[str]
|
|
25
|
+
status: str
|
|
26
|
+
diff: str
|
|
27
|
+
diff_stat: str
|
|
28
|
+
processed_diff: str
|
|
29
|
+
has_secrets: bool
|
|
30
|
+
secrets: list[Any]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GitStateValidator:
|
|
34
|
+
"""Validates and manages git repository state."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: GACConfig):
|
|
37
|
+
self.config = config
|
|
38
|
+
|
|
39
|
+
def validate_repository(self) -> str:
|
|
40
|
+
"""Validate that we're in a git repository and return the repo root."""
|
|
41
|
+
try:
|
|
42
|
+
git_dir = run_git_command(["rev-parse", "--show-toplevel"])
|
|
43
|
+
if not git_dir:
|
|
44
|
+
raise GitError("Not in a git repository")
|
|
45
|
+
return git_dir
|
|
46
|
+
except (subprocess.SubprocessError, GitError, OSError) as e:
|
|
47
|
+
logger.error(f"Error checking git repository: {e}")
|
|
48
|
+
handle_error(GitError("Not in a git repository"), exit_program=True)
|
|
49
|
+
return "" # Never reached, but required for type safety
|
|
50
|
+
|
|
51
|
+
def stage_all_if_requested(self, stage_all: bool, dry_run: bool) -> None:
|
|
52
|
+
"""Stage all changes if requested and not in dry run mode."""
|
|
53
|
+
if stage_all and (not dry_run):
|
|
54
|
+
logger.info("Staging all changes")
|
|
55
|
+
run_git_command(["add", "--all"])
|
|
56
|
+
|
|
57
|
+
def get_git_state(
|
|
58
|
+
self,
|
|
59
|
+
stage_all: bool = False,
|
|
60
|
+
dry_run: bool = False,
|
|
61
|
+
skip_secret_scan: bool = False,
|
|
62
|
+
quiet: bool = False,
|
|
63
|
+
model: str | None = None,
|
|
64
|
+
hint: str = "",
|
|
65
|
+
one_liner: bool = False,
|
|
66
|
+
infer_scope: bool = False,
|
|
67
|
+
verbose: bool = False,
|
|
68
|
+
language: str | None = None,
|
|
69
|
+
) -> GitState | None:
|
|
70
|
+
"""Get complete git state including validation and processing.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
GitState if staged changes exist, None if no staged changes found.
|
|
74
|
+
"""
|
|
75
|
+
from gac.constants import Utility
|
|
76
|
+
|
|
77
|
+
# Validate repository
|
|
78
|
+
repo_root = self.validate_repository()
|
|
79
|
+
|
|
80
|
+
# Stage files if requested
|
|
81
|
+
self.stage_all_if_requested(stage_all, dry_run)
|
|
82
|
+
|
|
83
|
+
# Get staged files
|
|
84
|
+
staged_files = get_staged_files(existing_only=False)
|
|
85
|
+
|
|
86
|
+
if not staged_files:
|
|
87
|
+
console.print(
|
|
88
|
+
"[yellow]No staged changes found. Stage your changes with git add first or use --add-all.[/yellow]"
|
|
89
|
+
)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# Get git status and diffs
|
|
93
|
+
status = get_staged_status()
|
|
94
|
+
diff = run_git_command(["diff", "--staged"])
|
|
95
|
+
diff_stat = " " + run_git_command(["diff", "--stat", "--cached"])
|
|
96
|
+
|
|
97
|
+
# Scan for secrets
|
|
98
|
+
has_secrets = False
|
|
99
|
+
secrets = []
|
|
100
|
+
if not skip_secret_scan:
|
|
101
|
+
logger.info("Scanning staged changes for potential secrets...")
|
|
102
|
+
secrets = scan_staged_diff(diff)
|
|
103
|
+
has_secrets = bool(secrets)
|
|
104
|
+
|
|
105
|
+
# Process diff for AI consumption
|
|
106
|
+
logger.debug(f"Preprocessing diff ({len(diff)} characters)")
|
|
107
|
+
if model is None:
|
|
108
|
+
raise ConfigError("Model must be specified via GAC_MODEL environment variable or --model flag")
|
|
109
|
+
processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model)
|
|
110
|
+
logger.debug(f"Processed diff ({len(processed_diff)} characters)")
|
|
111
|
+
|
|
112
|
+
return GitState(
|
|
113
|
+
repo_root=repo_root,
|
|
114
|
+
staged_files=staged_files,
|
|
115
|
+
status=status,
|
|
116
|
+
diff=diff,
|
|
117
|
+
diff_stat=diff_stat,
|
|
118
|
+
processed_diff=processed_diff,
|
|
119
|
+
has_secrets=has_secrets,
|
|
120
|
+
secrets=secrets,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def handle_secret_detection(self, secrets: list[Any], quiet: bool = False) -> bool | None:
|
|
124
|
+
"""Handle secret detection and user interaction.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True: Continue with commit
|
|
128
|
+
False: Re-get git state (files were removed)
|
|
129
|
+
None: Abort workflow
|
|
130
|
+
"""
|
|
131
|
+
if not secrets:
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
if not quiet:
|
|
135
|
+
console.print("\n[bold red]⚠️ SECURITY WARNING: Potential secrets detected![/bold red]")
|
|
136
|
+
console.print("[red]The following sensitive information was found in your staged changes:[/red]\n")
|
|
137
|
+
|
|
138
|
+
for secret in secrets:
|
|
139
|
+
location = f"{secret.file_path}:{secret.line_number}" if secret.line_number else secret.file_path
|
|
140
|
+
if not quiet:
|
|
141
|
+
console.print(f" • [yellow]{secret.secret_type}[/yellow] in [cyan]{location}[/cyan]")
|
|
142
|
+
console.print(f" Match: [dim]{secret.matched_text}[/dim]\n")
|
|
143
|
+
|
|
144
|
+
if not quiet:
|
|
145
|
+
console.print("\n[bold]Options:[/bold]")
|
|
146
|
+
console.print(" \\[a] Abort commit (recommended)")
|
|
147
|
+
console.print(" \\[c] [yellow]Continue anyway[/yellow] (not recommended)")
|
|
148
|
+
console.print(" \\[r] Remove affected file(s) and continue")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
import click
|
|
152
|
+
|
|
153
|
+
choice = (
|
|
154
|
+
click.prompt(
|
|
155
|
+
"\nChoose an option",
|
|
156
|
+
type=click.Choice(["a", "c", "r"], case_sensitive=False),
|
|
157
|
+
default="a",
|
|
158
|
+
show_choices=True,
|
|
159
|
+
show_default=True,
|
|
160
|
+
)
|
|
161
|
+
.strip()
|
|
162
|
+
.lower()
|
|
163
|
+
)
|
|
164
|
+
except (EOFError, KeyboardInterrupt):
|
|
165
|
+
console.print("\n[red]Aborted by user.[/red]")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
if choice == "a":
|
|
169
|
+
console.print("[yellow]Commit aborted.[/yellow]")
|
|
170
|
+
return None
|
|
171
|
+
elif choice == "c":
|
|
172
|
+
console.print("[bold yellow]⚠️ Continuing with potential secrets in commit...[/bold yellow]")
|
|
173
|
+
logger.warning("User chose to continue despite detected secrets")
|
|
174
|
+
return True
|
|
175
|
+
elif choice == "r":
|
|
176
|
+
affected_files = get_affected_files(secrets)
|
|
177
|
+
for file_path in affected_files:
|
|
178
|
+
try:
|
|
179
|
+
run_git_command(["reset", "HEAD", file_path])
|
|
180
|
+
console.print(f"[green]Unstaged: {file_path}[/green]")
|
|
181
|
+
except GitError as e:
|
|
182
|
+
console.print(f"[red]Failed to unstage {file_path}: {e}[/red]")
|
|
183
|
+
|
|
184
|
+
# Check if there are still staged files
|
|
185
|
+
remaining_staged = get_staged_files(existing_only=False)
|
|
186
|
+
if not remaining_staged:
|
|
187
|
+
console.print("[yellow]No files remain staged. Commit aborted.[/yellow]")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
console.print(f"[green]Continuing with {len(remaining_staged)} staged file(s)...[/green]")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
return True
|