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.
Files changed (76) hide show
  1. gac/__init__.py +4 -6
  2. gac/__version__.py +1 -1
  3. gac/ai_utils.py +18 -49
  4. gac/cli.py +14 -10
  5. gac/commit_executor.py +59 -0
  6. gac/config.py +28 -3
  7. gac/config_cli.py +19 -7
  8. gac/constants/__init__.py +34 -0
  9. gac/constants/commit.py +63 -0
  10. gac/constants/defaults.py +40 -0
  11. gac/constants/file_patterns.py +110 -0
  12. gac/constants/languages.py +119 -0
  13. gac/diff_cli.py +0 -22
  14. gac/errors.py +8 -2
  15. gac/git.py +6 -6
  16. gac/git_state_validator.py +193 -0
  17. gac/grouped_commit_workflow.py +458 -0
  18. gac/init_cli.py +2 -1
  19. gac/interactive_mode.py +179 -0
  20. gac/language_cli.py +0 -1
  21. gac/main.py +222 -959
  22. gac/model_cli.py +2 -1
  23. gac/model_identifier.py +70 -0
  24. gac/oauth/claude_code.py +2 -2
  25. gac/oauth/qwen_oauth.py +4 -0
  26. gac/oauth/token_store.py +2 -2
  27. gac/oauth_retry.py +161 -0
  28. gac/postprocess.py +155 -0
  29. gac/prompt.py +20 -490
  30. gac/prompt_builder.py +88 -0
  31. gac/providers/README.md +437 -0
  32. gac/providers/__init__.py +70 -81
  33. gac/providers/anthropic.py +12 -56
  34. gac/providers/azure_openai.py +48 -92
  35. gac/providers/base.py +329 -0
  36. gac/providers/cerebras.py +10 -43
  37. gac/providers/chutes.py +16 -72
  38. gac/providers/claude_code.py +64 -97
  39. gac/providers/custom_anthropic.py +51 -85
  40. gac/providers/custom_openai.py +29 -87
  41. gac/providers/deepseek.py +10 -43
  42. gac/providers/error_handler.py +139 -0
  43. gac/providers/fireworks.py +10 -43
  44. gac/providers/gemini.py +66 -73
  45. gac/providers/groq.py +10 -62
  46. gac/providers/kimi_coding.py +19 -59
  47. gac/providers/lmstudio.py +62 -52
  48. gac/providers/minimax.py +10 -43
  49. gac/providers/mistral.py +10 -43
  50. gac/providers/moonshot.py +10 -43
  51. gac/providers/ollama.py +54 -41
  52. gac/providers/openai.py +30 -46
  53. gac/providers/openrouter.py +15 -62
  54. gac/providers/protocol.py +71 -0
  55. gac/providers/qwen.py +55 -67
  56. gac/providers/registry.py +58 -0
  57. gac/providers/replicate.py +137 -91
  58. gac/providers/streamlake.py +26 -56
  59. gac/providers/synthetic.py +35 -47
  60. gac/providers/together.py +10 -43
  61. gac/providers/zai.py +21 -59
  62. gac/py.typed +0 -0
  63. gac/security.py +1 -1
  64. gac/templates/__init__.py +1 -0
  65. gac/templates/question_generation.txt +60 -0
  66. gac/templates/system_prompt.txt +224 -0
  67. gac/templates/user_prompt.txt +28 -0
  68. gac/utils.py +6 -5
  69. gac/workflow_context.py +162 -0
  70. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
  71. gac-3.10.10.dist-info/RECORD +79 -0
  72. gac/constants.py +0 -328
  73. gac-3.8.1.dist-info/RECORD +0 -56
  74. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
  75. {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
  76. {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 Exception as e:
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 Exception as e:
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 Exception as e:
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 Exception as e:
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 Exception as e:
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