gac 2.2.0__tar.gz → 2.4.0__tar.gz

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.

Potentially problematic release.


This version of gac might be problematic. Click here for more details.

Files changed (47) hide show
  1. {gac-2.2.0 → gac-2.4.0}/PKG-INFO +36 -9
  2. {gac-2.2.0 → gac-2.4.0}/README.md +35 -8
  3. {gac-2.2.0 → gac-2.4.0}/src/gac/__version__.py +1 -1
  4. {gac-2.2.0 → gac-2.4.0}/src/gac/ai.py +26 -0
  5. {gac-2.2.0 → gac-2.4.0}/src/gac/ai_utils.py +28 -13
  6. {gac-2.2.0 → gac-2.4.0}/src/gac/cli.py +7 -1
  7. {gac-2.2.0 → gac-2.4.0}/src/gac/config.py +1 -0
  8. {gac-2.2.0 → gac-2.4.0}/src/gac/constants.py +1 -1
  9. {gac-2.2.0 → gac-2.4.0}/src/gac/git.py +107 -8
  10. gac-2.4.0/src/gac/init_cli.py +435 -0
  11. gac-2.4.0/src/gac/language_cli.py +250 -0
  12. gac-2.4.0/src/gac/main.py +718 -0
  13. {gac-2.2.0 → gac-2.4.0}/src/gac/prompt.py +101 -15
  14. {gac-2.2.0 → gac-2.4.0}/src/gac/security.py +1 -1
  15. {gac-2.2.0 → gac-2.4.0}/src/gac/utils.py +104 -3
  16. gac-2.4.0/src/gac/workflow_utils.py +131 -0
  17. gac-2.2.0/src/gac/init_cli.py +0 -203
  18. gac-2.2.0/src/gac/language_cli.py +0 -82
  19. gac-2.2.0/src/gac/main.py +0 -382
  20. {gac-2.2.0 → gac-2.4.0}/.gitignore +0 -0
  21. {gac-2.2.0 → gac-2.4.0}/LICENSE +0 -0
  22. {gac-2.2.0 → gac-2.4.0}/pyproject.toml +0 -0
  23. {gac-2.2.0 → gac-2.4.0}/src/gac/__init__.py +0 -0
  24. {gac-2.2.0 → gac-2.4.0}/src/gac/config_cli.py +0 -0
  25. {gac-2.2.0 → gac-2.4.0}/src/gac/diff_cli.py +0 -0
  26. {gac-2.2.0 → gac-2.4.0}/src/gac/errors.py +0 -0
  27. {gac-2.2.0 → gac-2.4.0}/src/gac/preprocess.py +0 -0
  28. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/__init__.py +0 -0
  29. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/anthropic.py +0 -0
  30. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/cerebras.py +0 -0
  31. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/chutes.py +0 -0
  32. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/custom_anthropic.py +0 -0
  33. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/custom_openai.py +0 -0
  34. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/deepseek.py +0 -0
  35. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/fireworks.py +0 -0
  36. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/gemini.py +0 -0
  37. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/groq.py +0 -0
  38. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/lmstudio.py +0 -0
  39. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/minimax.py +0 -0
  40. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/mistral.py +0 -0
  41. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/ollama.py +0 -0
  42. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/openai.py +0 -0
  43. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/openrouter.py +0 -0
  44. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/streamlake.py +0 -0
  45. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/synthetic.py +0 -0
  46. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/together.py +0 -0
  47. {gac-2.2.0 → gac-2.4.0}/src/gac/providers/zai.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: LLM-powered Git commit message generator with multi-provider support
5
5
  Project-URL: Homepage, https://github.com/cellwebb/gac
6
6
  Project-URL: Documentation, https://github.com/cellwebb/gac#readme
@@ -41,6 +41,9 @@ Requires-Dist: twine; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  <!-- markdownlint-disable MD013 -->
44
+ <!-- markdownlint-disable MD033 MD036 -->
45
+
46
+ <div align="center">
44
47
 
45
48
  # 🚀 Git Auto Commit (gac)
46
49
 
@@ -50,9 +53,11 @@ Description-Content-Type: text/markdown
50
53
  [![codecov](https://codecov.io/gh/cellwebb/gac/branch/main/graph/badge.svg)](https://app.codecov.io/gh/cellwebb/gac)
51
54
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
52
55
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](https://mypy-lang.org/)
53
- [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/CONTRIBUTING.md)
56
+ [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/en/CONTRIBUTING.md)
54
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
55
58
 
59
+ **English** | [简体中文](README.zh-CN.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [हिन्दी](README.hi.md) | [Français](README.fr.md) | [Русский](README.ru.md) | [Español](README.es.md) | [Português](README.pt.md) | [Deutsch](README.de.md) | [Nederlands](README.nl.md)
60
+
56
61
  **LLM-powered commit messages that understand your code!**
57
62
 
58
63
  **Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
@@ -67,12 +72,19 @@ Intelligent, contextual messages that explain the **why** behind your changes:
67
72
 
68
73
  ---
69
74
 
75
+ </div>
76
+
77
+ <!-- markdownlint-enable MD033 MD036 -->
78
+
79
+ <!-- markdownlint-enable MD033 MD036 -->
80
+
70
81
  ## Quick Start
71
82
 
72
83
  ### Use gac without installing
73
84
 
74
85
  ```bash
75
- uvx gac init # Configure your LLM provider
86
+ uvx gac init # Configure your provider, model, and language
87
+ uvx gac model # Re-run provider/model setup without language prompts
76
88
  uvx gac # Generate and commit with LLM
77
89
  ```
78
90
 
@@ -83,6 +95,7 @@ That's it! Review the generated message and confirm with `y`.
83
95
  ```bash
84
96
  uv tool install gac
85
97
  gac init
98
+ gac model
86
99
  gac
87
100
  ```
88
101
 
@@ -108,6 +121,7 @@ uv tool upgrade gac
108
121
  - **Understands intent**: Analyzes code structure, logic, and patterns to understand the "why" behind your changes, not just what changed
109
122
  - **Semantic awareness**: Recognizes refactoring, bug fixes, features, and breaking changes to generate contextually appropriate messages
110
123
  - **Intelligent filtering**: Prioritizes meaningful changes while ignoring generated files, dependencies, and artifacts
124
+ - **Intelligent commit grouping** - Automatically group related changes into multiple logical commits with `--group`
111
125
 
112
126
  ### 📝 **Multiple Message Formats**
113
127
 
@@ -175,6 +189,9 @@ gac -v -s
175
189
  # Quick one-liner for small changes
176
190
  gac -o
177
191
 
192
+ # Group changes into logically related commits
193
+ gac -ag
194
+
178
195
  # Debug what the LLM sees
179
196
  gac --show-prompt
180
197
 
@@ -216,6 +233,8 @@ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
216
233
 
217
234
  Run `gac init` to configure your provider interactively, or set environment variables:
218
235
 
236
+ Need to change providers or models later without touching language settings? Use `gac model` for a streamlined flow that skips the language prompts.
237
+
219
238
  ```bash
220
239
  # Example configuration
221
240
  GAC_MODEL=anthropic:your-model-name
@@ -227,16 +246,24 @@ See `.gac.env.example` for all available options.
227
246
 
228
247
  **Want commit messages in another language?** Run `gac language` to select from 25+ languages including Español, Français, 日本語, and more.
229
248
 
230
- **Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
249
+ **Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
250
+
251
+ ---
252
+
253
+ ## Project Analytics
254
+
255
+ 📊 **[View live usage analytics and statistics →](https://clickpy.clickhouse.com/dashboard/gac)**
256
+
257
+ Track real-time installation metrics and package download statistics.
231
258
 
232
259
  ---
233
260
 
234
261
  ## Getting Help
235
262
 
236
- - **Full documentation**: [USAGE.md](USAGE.md) - Complete CLI reference
237
- - **Custom prompts**: [CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
238
- - **Troubleshooting**: [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - Common issues and solutions
239
- - **Contributing**: [CONTRIBUTING.md](docs/CONTRIBUTING.md) - Development setup and guidelines
263
+ - **Full documentation**: [docs/USAGE.md](docs/en/USAGE.md) - Complete CLI reference
264
+ - **Custom prompts**: [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
265
+ - **Troubleshooting**: [docs/TROUBLESHOOTING.md](docs/en/TROUBLESHOOTING.md) - Common issues and solutions
266
+ - **Contributing**: [docs/CONTRIBUTING.md](docs/en/CONTRIBUTING.md) - Development setup and guidelines
240
267
 
241
268
  ---
242
269
 
@@ -246,7 +273,7 @@ See `.gac.env.example` for all available options.
246
273
 
247
274
  Made with ❤️ for developers who want better commit messages
248
275
 
249
- [⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](USAGE.md)
276
+ [⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](docs/en/USAGE.md)
250
277
 
251
278
  </div>
252
279
 
@@ -1,4 +1,7 @@
1
1
  <!-- markdownlint-disable MD013 -->
2
+ <!-- markdownlint-disable MD033 MD036 -->
3
+
4
+ <div align="center">
2
5
 
3
6
  # 🚀 Git Auto Commit (gac)
4
7
 
@@ -8,9 +11,11 @@
8
11
  [![codecov](https://codecov.io/gh/cellwebb/gac/branch/main/graph/badge.svg)](https://app.codecov.io/gh/cellwebb/gac)
9
12
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
10
13
  [![mypy](https://img.shields.io/badge/mypy-checked-blue.svg)](https://mypy-lang.org/)
11
- [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/CONTRIBUTING.md)
14
+ [![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg)](docs/en/CONTRIBUTING.md)
12
15
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
16
 
17
+ **English** | [简体中文](README.zh-CN.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [हिन्दी](README.hi.md) | [Français](README.fr.md) | [Русский](README.ru.md) | [Español](README.es.md) | [Português](README.pt.md) | [Deutsch](README.de.md) | [Nederlands](README.nl.md)
18
+
14
19
  **LLM-powered commit messages that understand your code!**
15
20
 
16
21
  **Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
@@ -25,12 +30,19 @@ Intelligent, contextual messages that explain the **why** behind your changes:
25
30
 
26
31
  ---
27
32
 
33
+ </div>
34
+
35
+ <!-- markdownlint-enable MD033 MD036 -->
36
+
37
+ <!-- markdownlint-enable MD033 MD036 -->
38
+
28
39
  ## Quick Start
29
40
 
30
41
  ### Use gac without installing
31
42
 
32
43
  ```bash
33
- uvx gac init # Configure your LLM provider
44
+ uvx gac init # Configure your provider, model, and language
45
+ uvx gac model # Re-run provider/model setup without language prompts
34
46
  uvx gac # Generate and commit with LLM
35
47
  ```
36
48
 
@@ -41,6 +53,7 @@ That's it! Review the generated message and confirm with `y`.
41
53
  ```bash
42
54
  uv tool install gac
43
55
  gac init
56
+ gac model
44
57
  gac
45
58
  ```
46
59
 
@@ -66,6 +79,7 @@ uv tool upgrade gac
66
79
  - **Understands intent**: Analyzes code structure, logic, and patterns to understand the "why" behind your changes, not just what changed
67
80
  - **Semantic awareness**: Recognizes refactoring, bug fixes, features, and breaking changes to generate contextually appropriate messages
68
81
  - **Intelligent filtering**: Prioritizes meaningful changes while ignoring generated files, dependencies, and artifacts
82
+ - **Intelligent commit grouping** - Automatically group related changes into multiple logical commits with `--group`
69
83
 
70
84
  ### 📝 **Multiple Message Formats**
71
85
 
@@ -133,6 +147,9 @@ gac -v -s
133
147
  # Quick one-liner for small changes
134
148
  gac -o
135
149
 
150
+ # Group changes into logically related commits
151
+ gac -ag
152
+
136
153
  # Debug what the LLM sees
137
154
  gac --show-prompt
138
155
 
@@ -174,6 +191,8 @@ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
174
191
 
175
192
  Run `gac init` to configure your provider interactively, or set environment variables:
176
193
 
194
+ Need to change providers or models later without touching language settings? Use `gac model` for a streamlined flow that skips the language prompts.
195
+
177
196
  ```bash
178
197
  # Example configuration
179
198
  GAC_MODEL=anthropic:your-model-name
@@ -185,16 +204,24 @@ See `.gac.env.example` for all available options.
185
204
 
186
205
  **Want commit messages in another language?** Run `gac language` to select from 25+ languages including Español, Français, 日本語, and more.
187
206
 
188
- **Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
207
+ **Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
208
+
209
+ ---
210
+
211
+ ## Project Analytics
212
+
213
+ 📊 **[View live usage analytics and statistics →](https://clickpy.clickhouse.com/dashboard/gac)**
214
+
215
+ Track real-time installation metrics and package download statistics.
189
216
 
190
217
  ---
191
218
 
192
219
  ## Getting Help
193
220
 
194
- - **Full documentation**: [USAGE.md](USAGE.md) - Complete CLI reference
195
- - **Custom prompts**: [CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
196
- - **Troubleshooting**: [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - Common issues and solutions
197
- - **Contributing**: [CONTRIBUTING.md](docs/CONTRIBUTING.md) - Development setup and guidelines
221
+ - **Full documentation**: [docs/USAGE.md](docs/en/USAGE.md) - Complete CLI reference
222
+ - **Custom prompts**: [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
223
+ - **Troubleshooting**: [docs/TROUBLESHOOTING.md](docs/en/TROUBLESHOOTING.md) - Common issues and solutions
224
+ - **Contributing**: [docs/CONTRIBUTING.md](docs/en/CONTRIBUTING.md) - Development setup and guidelines
198
225
 
199
226
  ---
200
227
 
@@ -204,7 +231,7 @@ See `.gac.env.example` for all available options.
204
231
 
205
232
  Made with ❤️ for developers who want better commit messages
206
233
 
207
- [⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](USAGE.md)
234
+ [⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](docs/en/USAGE.md)
208
235
 
209
236
  </div>
210
237
 
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.4.0"
@@ -42,6 +42,8 @@ def generate_commit_message(
42
42
  max_tokens: int = EnvDefaults.MAX_OUTPUT_TOKENS,
43
43
  max_retries: int = EnvDefaults.MAX_RETRIES,
44
44
  quiet: bool = False,
45
+ is_group: bool = False,
46
+ skip_success_message: bool = False,
45
47
  ) -> str:
46
48
  """Generate a commit message using direct API calls to AI providers.
47
49
 
@@ -116,6 +118,8 @@ def generate_commit_message(
116
118
  max_tokens=max_tokens,
117
119
  max_retries=max_retries,
118
120
  quiet=quiet,
121
+ is_group=is_group,
122
+ skip_success_message=skip_success_message,
119
123
  )
120
124
  except AIError:
121
125
  # Re-raise AIError exceptions as-is to preserve error classification
@@ -123,3 +127,25 @@ def generate_commit_message(
123
127
  except Exception as e:
124
128
  logger.error(f"Failed to generate commit message: {e}")
125
129
  raise AIError.model_error(f"Failed to generate commit message: {e}") from e
130
+
131
+
132
+ def generate_grouped_commits(
133
+ model: str,
134
+ prompt: list[dict[str, str]],
135
+ temperature: float,
136
+ max_tokens: int,
137
+ max_retries: int,
138
+ quiet: bool = False,
139
+ skip_success_message: bool = False,
140
+ ) -> str:
141
+ """Generate grouped commits JSON response."""
142
+ return generate_commit_message(
143
+ model=model,
144
+ prompt=prompt,
145
+ temperature=temperature,
146
+ max_tokens=max_tokens,
147
+ max_retries=max_retries,
148
+ quiet=quiet,
149
+ is_group=True,
150
+ skip_success_message=skip_success_message,
151
+ )
@@ -83,6 +83,8 @@ def generate_with_retries(
83
83
  max_tokens: int,
84
84
  max_retries: int,
85
85
  quiet: bool = False,
86
+ is_group: bool = False,
87
+ skip_success_message: bool = False,
86
88
  ) -> str:
87
89
  """Generate content with retry logic using direct API calls."""
88
90
  # Parse model string to determine provider and actual model
@@ -121,10 +123,11 @@ def generate_with_retries(
121
123
  raise AIError.model_error("No messages provided for AI generation")
122
124
 
123
125
  # Set up spinner
126
+ message_type = "commit messages" if is_group else "commit message"
124
127
  if quiet:
125
128
  spinner = None
126
129
  else:
127
- spinner = Halo(text=f"Generating commit message with {provider} {model_name}...", spinner="dots")
130
+ spinner = Halo(text=f"Generating {message_type} with {provider} {model_name}...", spinner="dots")
128
131
  spinner.start()
129
132
 
130
133
  last_exception = None
@@ -132,7 +135,7 @@ def generate_with_retries(
132
135
 
133
136
  for attempt in range(max_retries):
134
137
  try:
135
- if not quiet and attempt > 0:
138
+ if not quiet and not skip_success_message and attempt > 0:
136
139
  if spinner:
137
140
  spinner.text = f"Retry {attempt + 1}/{max_retries} with {provider} {model_name}..."
138
141
  logger.info(f"Retry attempt {attempt + 1}/{max_retries}")
@@ -145,7 +148,10 @@ def generate_with_retries(
145
148
  content = provider_func(model=model_name, messages=messages, temperature=temperature, max_tokens=max_tokens)
146
149
 
147
150
  if spinner:
148
- spinner.succeed(f"Generated commit message with {provider} {model_name}")
151
+ if skip_success_message:
152
+ spinner.stop() # Stop spinner without showing success/failure
153
+ else:
154
+ spinner.succeed(f"Generated {message_type} with {provider} {model_name}")
149
155
 
150
156
  if content is not None and content.strip():
151
157
  return content.strip() # type: ignore[no-any-return]
@@ -160,8 +166,8 @@ def generate_with_retries(
160
166
 
161
167
  # For authentication and model errors, don't retry
162
168
  if error_type in ["authentication", "model"]:
163
- if spinner:
164
- spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
169
+ if spinner and not skip_success_message:
170
+ spinner.fail(f"Failed to generate {message_type} with {provider} {model_name}")
165
171
 
166
172
  # Create the appropriate error type based on classification
167
173
  if error_type == "authentication":
@@ -172,23 +178,32 @@ def generate_with_retries(
172
178
  if attempt < max_retries - 1:
173
179
  # Exponential backoff
174
180
  wait_time = 2**attempt
175
- if not quiet:
176
- logger.warning(f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}")
177
-
178
- if spinner:
181
+ if not quiet and not skip_success_message:
182
+ if attempt == 0:
183
+ logger.warning(f"AI generation failed, retrying in {wait_time}s: {str(e)}")
184
+ else:
185
+ logger.warning(
186
+ f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}"
187
+ )
188
+
189
+ if spinner and not skip_success_message:
179
190
  for i in range(wait_time, 0, -1):
180
191
  spinner.text = f"Retry {attempt + 1}/{max_retries} in {i}s..."
181
192
  time.sleep(1)
182
193
  else:
183
194
  time.sleep(wait_time)
184
195
  else:
185
- logger.error(f"AI generation failed after {max_retries} attempts: {str(e)}")
196
+ num_retries = max_retries
197
+ retry_word = "retry" if num_retries == 1 else "retries"
198
+ logger.error(f"AI generation failed after {num_retries} {retry_word}: {str(e)}")
186
199
 
187
- if spinner:
188
- spinner.fail(f"Failed to generate commit message with {provider} {model_name}")
200
+ if spinner and not skip_success_message:
201
+ spinner.fail(f"Failed to generate {message_type} with {provider} {model_name}")
189
202
 
190
203
  # If we get here, all retries failed - use the last classified error type
191
- error_message = f"Failed to generate commit message after {max_retries} attempts"
204
+ num_retries = max_retries
205
+ retry_word = "retry" if num_retries == 1 else "retries"
206
+ error_message = f"Failed to generate {message_type} after {num_retries} {retry_word}"
192
207
  if last_error_type == "authentication":
193
208
  raise AIError.authentication_error(error_message) from last_exception
194
209
  elif last_error_type == "rate_limit":
@@ -17,6 +17,7 @@ from gac.constants import Languages, Logging
17
17
  from gac.diff_cli import diff as diff_cli
18
18
  from gac.errors import handle_error
19
19
  from gac.init_cli import init as init_cli
20
+ from gac.init_cli import model as model_cli
20
21
  from gac.language_cli import language as language_cli
21
22
  from gac.main import main
22
23
  from gac.utils import setup_logging
@@ -28,6 +29,7 @@ logger = logging.getLogger(__name__)
28
29
  @click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
29
30
  # Git workflow options
30
31
  @click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
32
+ @click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
31
33
  @click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
32
34
  @click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
33
35
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
@@ -70,6 +72,7 @@ logger = logging.getLogger(__name__)
70
72
  def cli(
71
73
  ctx: click.Context,
72
74
  add_all: bool = False,
75
+ group: bool = False,
73
76
  log_level: str = str(config["log_level"]),
74
77
  one_liner: bool = False,
75
78
  push: bool = False,
@@ -109,6 +112,7 @@ def cli(
109
112
  try:
110
113
  main(
111
114
  stage_all=add_all,
115
+ group=group,
112
116
  model=model,
113
117
  hint=hint,
114
118
  one_liner=one_liner,
@@ -131,6 +135,7 @@ def cli(
131
135
 
132
136
  ctx.obj = {
133
137
  "add_all": add_all,
138
+ "group": group,
134
139
  "log_level": log_level,
135
140
  "one_liner": one_liner,
136
141
  "push": push,
@@ -150,9 +155,10 @@ def cli(
150
155
 
151
156
 
152
157
  cli.add_command(config_cli)
158
+ cli.add_command(diff_cli)
153
159
  cli.add_command(init_cli)
154
160
  cli.add_command(language_cli)
155
- cli.add_command(diff_cli)
161
+ cli.add_command(model_cli)
156
162
 
157
163
 
158
164
  @click.command(context_settings=language_cli.context_settings)
@@ -41,6 +41,7 @@ def load_config() -> dict[str, str | int | float | bool | None]:
41
41
  "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
42
42
  "language": os.getenv("GAC_LANGUAGE"),
43
43
  "translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
44
+ "rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
44
45
  }
45
46
 
46
47
  return config
@@ -20,7 +20,7 @@ class EnvDefaults:
20
20
 
21
21
  MAX_RETRIES: int = 3
22
22
  TEMPERATURE: float = 1
23
- MAX_OUTPUT_TOKENS: int = 1024 # includes reasoning tokens
23
+ MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
24
24
  WARNING_LIMIT_TOKENS: int = 32768
25
25
  ALWAYS_INCLUDE_SCOPE: bool = False
26
26
  SKIP_SECRET_SCAN: bool = False
@@ -14,6 +14,63 @@ from gac.utils import run_subprocess
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
+ def run_subprocess_with_encoding_fallback(
18
+ command: list[str], silent: bool = False, timeout: int = 60
19
+ ) -> subprocess.CompletedProcess:
20
+ """Run subprocess with encoding fallback, returning full CompletedProcess object.
21
+
22
+ This is used for cases where we need both stdout and stderr separately,
23
+ like pre-commit and lefthook hook execution.
24
+
25
+ Args:
26
+ command: List of command arguments
27
+ silent: If True, suppress debug logging
28
+ timeout: Command timeout in seconds
29
+
30
+ Returns:
31
+ CompletedProcess object with stdout, stderr, and returncode
32
+ """
33
+ from gac.utils import get_safe_encodings
34
+
35
+ encodings = get_safe_encodings()
36
+ last_exception = None
37
+
38
+ for encoding in encodings:
39
+ try:
40
+ if not silent:
41
+ logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
42
+
43
+ result = subprocess.run(
44
+ command,
45
+ capture_output=True,
46
+ text=True,
47
+ check=False,
48
+ timeout=timeout,
49
+ encoding=encoding,
50
+ errors="replace",
51
+ )
52
+ return result
53
+ except UnicodeError as e:
54
+ last_exception = e
55
+ if not silent:
56
+ logger.debug(f"Failed to decode with {encoding}: {e}")
57
+ continue
58
+ except subprocess.TimeoutExpired:
59
+ raise
60
+ except Exception as e:
61
+ if not silent:
62
+ logger.debug(f"Command error: {e}")
63
+ # Try next encoding for non-timeout errors
64
+ last_exception = e
65
+ continue
66
+
67
+ # If we get here, all encodings failed
68
+ if last_exception:
69
+ raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
70
+ else:
71
+ raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
72
+
73
+
17
74
  def run_git_command(args: list[str], silent: bool = False, timeout: int = 30) -> str:
18
75
  """Run a git command and return the output."""
19
76
  command = ["git"] + args
@@ -50,6 +107,48 @@ def get_staged_files(file_type: str | None = None, existing_only: bool = False)
50
107
  return []
51
108
 
52
109
 
110
+ def get_staged_status() -> str:
111
+ """Get formatted status of staged files only, excluding unstaged/untracked files.
112
+
113
+ Returns:
114
+ Formatted status string with M/A/D/R indicators
115
+ """
116
+ try:
117
+ output = run_git_command(["diff", "--name-status", "--staged"])
118
+ if not output:
119
+ return "No changes staged for commit."
120
+
121
+ status_map = {
122
+ "M": "modified",
123
+ "A": "new file",
124
+ "D": "deleted",
125
+ "R": "renamed",
126
+ "C": "copied",
127
+ "T": "typechange",
128
+ }
129
+
130
+ status_lines = ["Changes to be committed:"]
131
+ for line in output.splitlines():
132
+ line = line.strip()
133
+ if not line:
134
+ continue
135
+
136
+ # Parse status line (e.g., "M\tfile.py" or "R100\told.py\tnew.py")
137
+ parts = line.split("\t")
138
+ if len(parts) < 2:
139
+ continue
140
+
141
+ change_type = parts[0][0] # First char is the status (M, A, D, R, etc.)
142
+ file_path = parts[-1] # Last part is the new/current file path
143
+
144
+ status_label = status_map.get(change_type, "modified")
145
+ status_lines.append(f"\t{status_label}: {file_path}")
146
+
147
+ return "\n".join(status_lines)
148
+ except GitError:
149
+ return "No changes staged for commit."
150
+
151
+
53
152
  def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None, commit2: str | None = None) -> str:
54
153
  """Get the diff between commits or working tree.
55
154
 
@@ -90,20 +189,20 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
90
189
 
91
190
  def get_repo_root() -> str:
92
191
  """Get absolute path of repository root."""
93
- result = subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
94
- return result.decode().strip()
192
+ result = run_git_command(["rev-parse", "--show-toplevel"])
193
+ return result
95
194
 
96
195
 
97
196
  def get_current_branch() -> str:
98
197
  """Get name of current git branch."""
99
- result = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
100
- return result.decode().strip()
198
+ result = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
199
+ return result
101
200
 
102
201
 
103
202
  def get_commit_hash() -> str:
104
203
  """Get SHA-1 hash of current commit."""
105
- result = subprocess.check_output(["git", "rev-parse", "HEAD"])
106
- return result.decode().strip()
204
+ result = run_git_command(["rev-parse", "HEAD"])
205
+ return result
107
206
 
108
207
 
109
208
  def run_pre_commit_hooks() -> bool:
@@ -128,7 +227,7 @@ def run_pre_commit_hooks() -> bool:
128
227
  # Run pre-commit hooks on staged files
129
228
  logger.info("Running pre-commit hooks...")
130
229
  # Run pre-commit and capture both stdout and stderr
131
- result = subprocess.run(["pre-commit", "run"], capture_output=True, text=True, check=False)
230
+ result = run_subprocess_with_encoding_fallback(["pre-commit", "run"])
132
231
 
133
232
  if result.returncode == 0:
134
233
  # All hooks passed
@@ -178,7 +277,7 @@ def run_lefthook_hooks() -> bool:
178
277
  # Run lefthook hooks on staged files
179
278
  logger.info("Running Lefthook hooks...")
180
279
  # Run lefthook and capture both stdout and stderr
181
- result = subprocess.run(["lefthook", "run", "pre-commit"], capture_output=True, text=True, check=False)
280
+ result = run_subprocess_with_encoding_fallback(["lefthook", "run", "pre-commit"])
182
281
 
183
282
  if result.returncode == 0:
184
283
  # All hooks passed