gac 2.2.0__py3-none-any.whl → 2.4.0__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.
Potentially problematic release.
This version of gac might be problematic. Click here for more details.
- gac/__version__.py +1 -1
- gac/ai.py +26 -0
- gac/ai_utils.py +28 -13
- gac/cli.py +7 -1
- gac/config.py +1 -0
- gac/constants.py +1 -1
- gac/git.py +107 -8
- gac/init_cli.py +305 -73
- gac/language_cli.py +177 -9
- gac/main.py +505 -169
- gac/prompt.py +101 -15
- gac/security.py +1 -1
- gac/utils.py +104 -3
- gac/workflow_utils.py +131 -0
- {gac-2.2.0.dist-info → gac-2.4.0.dist-info}/METADATA +36 -9
- {gac-2.2.0.dist-info → gac-2.4.0.dist-info}/RECORD +19 -18
- {gac-2.2.0.dist-info → gac-2.4.0.dist-info}/WHEEL +0 -0
- {gac-2.2.0.dist-info → gac-2.4.0.dist-info}/entry_points.txt +0 -0
- {gac-2.2.0.dist-info → gac-2.4.0.dist-info}/licenses/LICENSE +0 -0
gac/main.py
CHANGED
|
@@ -17,6 +17,7 @@ from gac.constants import EnvDefaults, Utility
|
|
|
17
17
|
from gac.errors import AIError, GitError, handle_error
|
|
18
18
|
from gac.git import (
|
|
19
19
|
get_staged_files,
|
|
20
|
+
get_staged_status,
|
|
20
21
|
push_changes,
|
|
21
22
|
run_git_command,
|
|
22
23
|
run_lefthook_hooks,
|
|
@@ -25,7 +26,13 @@ from gac.git import (
|
|
|
25
26
|
from gac.preprocess import preprocess_diff
|
|
26
27
|
from gac.prompt import build_prompt, clean_commit_message
|
|
27
28
|
from gac.security import get_affected_files, scan_staged_diff
|
|
28
|
-
from gac.
|
|
29
|
+
from gac.workflow_utils import (
|
|
30
|
+
check_token_warning,
|
|
31
|
+
display_commit_message,
|
|
32
|
+
execute_commit,
|
|
33
|
+
handle_confirmation_loop,
|
|
34
|
+
restore_staging,
|
|
35
|
+
)
|
|
29
36
|
|
|
30
37
|
logger = logging.getLogger(__name__)
|
|
31
38
|
|
|
@@ -33,8 +40,450 @@ config = load_config()
|
|
|
33
40
|
console = Console() # Initialize console globally to prevent undefined access
|
|
34
41
|
|
|
35
42
|
|
|
43
|
+
def _validate_grouped_files_or_feedback(staged: set[str], grouped_result: dict) -> tuple[bool, str, str]:
|
|
44
|
+
from collections import Counter
|
|
45
|
+
|
|
46
|
+
commits = grouped_result.get("commits", []) if isinstance(grouped_result, dict) else []
|
|
47
|
+
all_files: list[str] = []
|
|
48
|
+
for commit in commits:
|
|
49
|
+
files = commit.get("files", []) if isinstance(commit, dict) else []
|
|
50
|
+
all_files.extend([str(p) for p in files])
|
|
51
|
+
|
|
52
|
+
counts = Counter(all_files)
|
|
53
|
+
union_set = set(all_files)
|
|
54
|
+
|
|
55
|
+
duplicates = sorted([f for f, c in counts.items() if c > 1])
|
|
56
|
+
missing = sorted(staged - union_set)
|
|
57
|
+
unexpected = sorted(union_set - staged)
|
|
58
|
+
|
|
59
|
+
if not duplicates and not missing and not unexpected:
|
|
60
|
+
return True, "", ""
|
|
61
|
+
|
|
62
|
+
problems: list[str] = []
|
|
63
|
+
if missing:
|
|
64
|
+
problems.append(f"Missing: {', '.join(missing)}")
|
|
65
|
+
if unexpected:
|
|
66
|
+
problems.append(f"Not staged: {', '.join(unexpected)}")
|
|
67
|
+
if duplicates:
|
|
68
|
+
problems.append(f"Duplicates: {', '.join(duplicates)}")
|
|
69
|
+
|
|
70
|
+
feedback = f"{'; '.join(problems)}. Required files: {', '.join(sorted(staged))}. Respond with ONLY valid JSON."
|
|
71
|
+
return False, feedback, "; ".join(problems)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_model_identifier(model: str) -> tuple[str, str]:
|
|
75
|
+
"""Validate and split model identifier into provider and model name."""
|
|
76
|
+
normalized = model.strip()
|
|
77
|
+
if ":" not in normalized:
|
|
78
|
+
message = (
|
|
79
|
+
f"Invalid model format: '{model}'. Expected 'provider:model', e.g. 'openai:gpt-4o-mini'. "
|
|
80
|
+
"Use 'gac config set model <provider:model>' to update your configuration."
|
|
81
|
+
)
|
|
82
|
+
logger.error(message)
|
|
83
|
+
console.print(f"[red]{message}[/red]")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
provider, model_name = normalized.split(":", 1)
|
|
87
|
+
if not provider or not model_name:
|
|
88
|
+
message = (
|
|
89
|
+
f"Invalid model format: '{model}'. Both provider and model name are required "
|
|
90
|
+
"(example: 'anthropic:claude-3-5-haiku-latest')."
|
|
91
|
+
)
|
|
92
|
+
logger.error(message)
|
|
93
|
+
console.print(f"[red]{message}[/red]")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
return provider, model_name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _handle_validation_retry(
|
|
100
|
+
attempts: int,
|
|
101
|
+
content_retry_budget: int,
|
|
102
|
+
raw_response: str,
|
|
103
|
+
feedback_message: str,
|
|
104
|
+
error_message: str,
|
|
105
|
+
conversation_messages: list[dict[str, str]],
|
|
106
|
+
quiet: bool,
|
|
107
|
+
retry_context: str,
|
|
108
|
+
) -> bool:
|
|
109
|
+
"""Handle validation retry logic. Returns True if should exit, False if should retry."""
|
|
110
|
+
conversation_messages.append({"role": "assistant", "content": raw_response})
|
|
111
|
+
conversation_messages.append({"role": "user", "content": feedback_message})
|
|
112
|
+
if attempts >= content_retry_budget:
|
|
113
|
+
logger.error(error_message)
|
|
114
|
+
console.print(f"\n[red]{error_message}[/red]")
|
|
115
|
+
console.print("\n[yellow]Raw model output:[/yellow]")
|
|
116
|
+
console.print(Panel(raw_response, title="Model Output", border_style="yellow"))
|
|
117
|
+
return True
|
|
118
|
+
if not quiet:
|
|
119
|
+
console.print(f"[yellow]Retry {attempts} of {content_retry_budget - 1}: {retry_context}[/yellow]")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def execute_grouped_commits_workflow(
|
|
124
|
+
*,
|
|
125
|
+
system_prompt: str,
|
|
126
|
+
user_prompt: str,
|
|
127
|
+
model: str,
|
|
128
|
+
temperature: float,
|
|
129
|
+
max_output_tokens: int,
|
|
130
|
+
max_retries: int,
|
|
131
|
+
require_confirmation: bool,
|
|
132
|
+
quiet: bool,
|
|
133
|
+
no_verify: bool,
|
|
134
|
+
dry_run: bool,
|
|
135
|
+
push: bool,
|
|
136
|
+
show_prompt: bool,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Execute the grouped commits workflow."""
|
|
139
|
+
import json
|
|
140
|
+
|
|
141
|
+
from gac.ai import generate_grouped_commits
|
|
142
|
+
|
|
143
|
+
provider, model_name = _parse_model_identifier(model)
|
|
144
|
+
|
|
145
|
+
if show_prompt:
|
|
146
|
+
full_prompt = f"SYSTEM PROMPT:\n{system_prompt}\n\nUSER PROMPT:\n{user_prompt}"
|
|
147
|
+
console.print(Panel(full_prompt, title="Prompt for LLM", border_style="bright_blue"))
|
|
148
|
+
|
|
149
|
+
conversation_messages: list[dict[str, str]] = []
|
|
150
|
+
if system_prompt:
|
|
151
|
+
conversation_messages.append({"role": "system", "content": system_prompt})
|
|
152
|
+
conversation_messages.append({"role": "user", "content": user_prompt})
|
|
153
|
+
|
|
154
|
+
_parse_model_identifier(model)
|
|
155
|
+
|
|
156
|
+
first_iteration = True
|
|
157
|
+
content_retry_budget = max(3, int(max_retries))
|
|
158
|
+
attempts = 0
|
|
159
|
+
|
|
160
|
+
grouped_result: dict | None = None
|
|
161
|
+
raw_response: str = ""
|
|
162
|
+
|
|
163
|
+
while True:
|
|
164
|
+
prompt_tokens = count_tokens(conversation_messages, model)
|
|
165
|
+
|
|
166
|
+
if first_iteration:
|
|
167
|
+
warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
168
|
+
assert warning_limit_val is not None
|
|
169
|
+
warning_limit = int(warning_limit_val)
|
|
170
|
+
if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
first_iteration = False
|
|
173
|
+
|
|
174
|
+
raw_response = generate_grouped_commits(
|
|
175
|
+
model=model,
|
|
176
|
+
prompt=conversation_messages,
|
|
177
|
+
temperature=temperature,
|
|
178
|
+
max_tokens=max_output_tokens,
|
|
179
|
+
max_retries=max_retries,
|
|
180
|
+
quiet=quiet,
|
|
181
|
+
skip_success_message=True,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
parsed: dict | None = None
|
|
185
|
+
extract = raw_response
|
|
186
|
+
first_brace = raw_response.find("{")
|
|
187
|
+
last_brace = raw_response.rfind("}")
|
|
188
|
+
if first_brace != -1 and last_brace != -1 and first_brace < last_brace:
|
|
189
|
+
extract = raw_response[first_brace : last_brace + 1]
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
parsed = json.loads(extract)
|
|
193
|
+
except json.JSONDecodeError as e:
|
|
194
|
+
parsed = None
|
|
195
|
+
logger.debug(
|
|
196
|
+
f"JSON parsing failed: {e}. Extract length: {len(extract)}, Response length: {len(raw_response)}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if parsed is None:
|
|
200
|
+
attempts += 1
|
|
201
|
+
feedback = "Your response was not valid JSON. Respond with ONLY valid JSON matching the expected schema. Do not include any commentary or code fences."
|
|
202
|
+
error_msg = f"Failed to parse LLM response as JSON after {attempts} retries."
|
|
203
|
+
if _handle_validation_retry(
|
|
204
|
+
attempts,
|
|
205
|
+
content_retry_budget,
|
|
206
|
+
raw_response,
|
|
207
|
+
feedback,
|
|
208
|
+
error_msg,
|
|
209
|
+
conversation_messages,
|
|
210
|
+
quiet,
|
|
211
|
+
"JSON parsing failed, asking model to fix...",
|
|
212
|
+
):
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
if "commits" not in parsed or not isinstance(parsed["commits"], list):
|
|
218
|
+
raise ValueError("Response missing 'commits' array")
|
|
219
|
+
if len(parsed["commits"]) == 0:
|
|
220
|
+
raise ValueError("No commits in response")
|
|
221
|
+
for idx, commit in enumerate(parsed["commits"]):
|
|
222
|
+
if "files" not in commit or not isinstance(commit["files"], list):
|
|
223
|
+
raise ValueError(f"Commit {idx + 1} missing 'files' array")
|
|
224
|
+
if "message" not in commit or not isinstance(commit["message"], str):
|
|
225
|
+
raise ValueError(f"Commit {idx + 1} missing 'message' string")
|
|
226
|
+
if len(commit["files"]) == 0:
|
|
227
|
+
raise ValueError(f"Commit {idx + 1} has empty files list")
|
|
228
|
+
if not commit["message"].strip():
|
|
229
|
+
raise ValueError(f"Commit {idx + 1} has empty message")
|
|
230
|
+
except (ValueError, TypeError) as e:
|
|
231
|
+
attempts += 1
|
|
232
|
+
feedback = f"Invalid response structure: {e}. Please return ONLY valid JSON following the schema with a non-empty 'commits' array of objects containing 'files' and 'message'."
|
|
233
|
+
error_msg = f"Invalid grouped commits structure after {attempts} retries: {e}"
|
|
234
|
+
if _handle_validation_retry(
|
|
235
|
+
attempts,
|
|
236
|
+
content_retry_budget,
|
|
237
|
+
raw_response,
|
|
238
|
+
feedback,
|
|
239
|
+
error_msg,
|
|
240
|
+
conversation_messages,
|
|
241
|
+
quiet,
|
|
242
|
+
"Structure validation failed, asking model to fix...",
|
|
243
|
+
):
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
staged_set = set(get_staged_files(existing_only=False))
|
|
248
|
+
ok, feedback, detail_msg = _validate_grouped_files_or_feedback(staged_set, parsed)
|
|
249
|
+
if not ok:
|
|
250
|
+
attempts += 1
|
|
251
|
+
error_msg = (
|
|
252
|
+
f"Grouped commits file set mismatch after {attempts} retries{': ' + detail_msg if detail_msg else ''}"
|
|
253
|
+
)
|
|
254
|
+
if _handle_validation_retry(
|
|
255
|
+
attempts,
|
|
256
|
+
content_retry_budget,
|
|
257
|
+
raw_response,
|
|
258
|
+
feedback,
|
|
259
|
+
error_msg,
|
|
260
|
+
conversation_messages,
|
|
261
|
+
quiet,
|
|
262
|
+
"File coverage mismatch, asking model to fix...",
|
|
263
|
+
):
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
grouped_result = parsed
|
|
268
|
+
conversation_messages.append({"role": "assistant", "content": raw_response})
|
|
269
|
+
|
|
270
|
+
if not quiet:
|
|
271
|
+
console.print(f"[green]✔ Generated commit messages with {provider} {model_name}[/green]")
|
|
272
|
+
num_commits = len(grouped_result["commits"])
|
|
273
|
+
console.print(f"[bold green]Proposed Commits ({num_commits}):[/bold green]\n")
|
|
274
|
+
for idx, commit in enumerate(grouped_result["commits"], 1):
|
|
275
|
+
files = commit["files"]
|
|
276
|
+
files_display = ", ".join(files)
|
|
277
|
+
console.print(f"[dim]{files_display}[/dim]")
|
|
278
|
+
commit_msg = commit["message"]
|
|
279
|
+
console.print(Panel(commit_msg, title=f"Commit Message {idx}/{num_commits}", border_style="cyan"))
|
|
280
|
+
console.print()
|
|
281
|
+
|
|
282
|
+
completion_tokens = count_tokens(raw_response, model)
|
|
283
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
284
|
+
console.print(
|
|
285
|
+
f"[dim]Token usage: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} total[/dim]"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if require_confirmation:
|
|
289
|
+
accepted = False
|
|
290
|
+
num_commits = len(grouped_result["commits"]) if grouped_result else 0
|
|
291
|
+
while True:
|
|
292
|
+
response = click.prompt(
|
|
293
|
+
f"Proceed with {num_commits} commits above? [y/n/r/<feedback>]",
|
|
294
|
+
type=str,
|
|
295
|
+
show_default=False,
|
|
296
|
+
).strip()
|
|
297
|
+
response_lower = response.lower()
|
|
298
|
+
|
|
299
|
+
if response_lower in ["y", "yes"]:
|
|
300
|
+
accepted = True
|
|
301
|
+
break
|
|
302
|
+
if response_lower in ["n", "no"]:
|
|
303
|
+
console.print("[yellow]Commits not accepted. Exiting...[/yellow]")
|
|
304
|
+
sys.exit(0)
|
|
305
|
+
if response == "":
|
|
306
|
+
continue
|
|
307
|
+
if response_lower in ["r", "reroll"]:
|
|
308
|
+
feedback_message = "Please provide alternative commit groupings using the same repository context."
|
|
309
|
+
console.print("[cyan]Regenerating commit groups...[/cyan]")
|
|
310
|
+
conversation_messages.append({"role": "user", "content": feedback_message})
|
|
311
|
+
console.print()
|
|
312
|
+
attempts = 0
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
feedback_message = f"Please revise the commit groupings based on this feedback: {response}"
|
|
316
|
+
console.print(f"[cyan]Regenerating commit groups with feedback: {response}[/cyan]")
|
|
317
|
+
conversation_messages.append({"role": "user", "content": feedback_message})
|
|
318
|
+
console.print()
|
|
319
|
+
attempts = 0
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if not accepted:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
num_commits = len(grouped_result["commits"]) if grouped_result else 0
|
|
326
|
+
if dry_run:
|
|
327
|
+
console.print(f"[yellow]Dry run: Would create {num_commits} commits[/yellow]")
|
|
328
|
+
for idx, commit in enumerate(grouped_result["commits"], 1):
|
|
329
|
+
console.print(f"\n[cyan]Commit {idx}/{num_commits}:[/cyan]")
|
|
330
|
+
console.print(f" Files: {', '.join(commit['files'])}")
|
|
331
|
+
console.print(f" Message: {commit['message'][:50]}...")
|
|
332
|
+
else:
|
|
333
|
+
original_staged_files = get_staged_files(existing_only=False)
|
|
334
|
+
original_staged_diff = run_git_command(["diff", "--cached", "--binary"], silent=True)
|
|
335
|
+
run_git_command(["reset", "HEAD"])
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
for idx, commit in enumerate(grouped_result["commits"], 1):
|
|
339
|
+
try:
|
|
340
|
+
for file_path in commit["files"]:
|
|
341
|
+
run_git_command(["add", "-A", file_path])
|
|
342
|
+
execute_commit(commit["message"], no_verify)
|
|
343
|
+
console.print(f"[green]✓ Commit {idx}/{num_commits} created[/green]")
|
|
344
|
+
except Exception as e:
|
|
345
|
+
console.print(f"[red]✗ Failed at commit {idx}/{num_commits}: {e}[/red]")
|
|
346
|
+
console.print(f"[yellow]Completed {idx - 1}/{num_commits} commits.[/yellow]")
|
|
347
|
+
if idx == 1:
|
|
348
|
+
console.print("[yellow]Restoring original staging area...[/yellow]")
|
|
349
|
+
restore_staging(original_staged_files, original_staged_diff)
|
|
350
|
+
console.print("[green]Original staging area restored.[/green]")
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
except KeyboardInterrupt:
|
|
353
|
+
console.print("\n[yellow]Interrupted by user. Restoring original staging area...[/yellow]")
|
|
354
|
+
restore_staging(original_staged_files, original_staged_diff)
|
|
355
|
+
console.print("[green]Original staging area restored.[/green]")
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
if push:
|
|
359
|
+
try:
|
|
360
|
+
if dry_run:
|
|
361
|
+
console.print("[yellow]Dry run: Would push changes[/yellow]")
|
|
362
|
+
sys.exit(0)
|
|
363
|
+
if push_changes():
|
|
364
|
+
logger.info("Changes pushed successfully")
|
|
365
|
+
console.print("[green]Changes pushed successfully[/green]")
|
|
366
|
+
else:
|
|
367
|
+
console.print(
|
|
368
|
+
"[red]Failed to push changes. Check your remote configuration and network connection.[/red]"
|
|
369
|
+
)
|
|
370
|
+
sys.exit(1)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
console.print(f"[red]Error pushing changes: {e}[/red]")
|
|
373
|
+
sys.exit(1)
|
|
374
|
+
|
|
375
|
+
sys.exit(0)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def execute_single_commit_workflow(
|
|
379
|
+
*,
|
|
380
|
+
system_prompt: str,
|
|
381
|
+
user_prompt: str,
|
|
382
|
+
model: str,
|
|
383
|
+
temperature: float,
|
|
384
|
+
max_output_tokens: int,
|
|
385
|
+
max_retries: int,
|
|
386
|
+
require_confirmation: bool,
|
|
387
|
+
quiet: bool,
|
|
388
|
+
no_verify: bool,
|
|
389
|
+
dry_run: bool,
|
|
390
|
+
push: bool,
|
|
391
|
+
show_prompt: bool,
|
|
392
|
+
) -> None:
|
|
393
|
+
if show_prompt:
|
|
394
|
+
full_prompt = f"SYSTEM PROMPT:\n{system_prompt}\n\nUSER PROMPT:\n{user_prompt}"
|
|
395
|
+
console.print(Panel(full_prompt, title="Prompt for LLM", border_style="bright_blue"))
|
|
396
|
+
|
|
397
|
+
conversation_messages: list[dict[str, str]] = []
|
|
398
|
+
if system_prompt:
|
|
399
|
+
conversation_messages.append({"role": "system", "content": system_prompt})
|
|
400
|
+
conversation_messages.append({"role": "user", "content": user_prompt})
|
|
401
|
+
|
|
402
|
+
_parse_model_identifier(model)
|
|
403
|
+
|
|
404
|
+
first_iteration = True
|
|
405
|
+
while True:
|
|
406
|
+
prompt_tokens = count_tokens(conversation_messages, model)
|
|
407
|
+
if first_iteration:
|
|
408
|
+
warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
409
|
+
assert warning_limit_val is not None
|
|
410
|
+
warning_limit = int(warning_limit_val)
|
|
411
|
+
if not check_token_warning(prompt_tokens, warning_limit, require_confirmation):
|
|
412
|
+
sys.exit(0)
|
|
413
|
+
first_iteration = False
|
|
414
|
+
|
|
415
|
+
raw_commit_message = generate_commit_message(
|
|
416
|
+
model=model,
|
|
417
|
+
prompt=conversation_messages,
|
|
418
|
+
temperature=temperature,
|
|
419
|
+
max_tokens=max_output_tokens,
|
|
420
|
+
max_retries=max_retries,
|
|
421
|
+
quiet=quiet,
|
|
422
|
+
)
|
|
423
|
+
commit_message = clean_commit_message(raw_commit_message)
|
|
424
|
+
logger.info("Generated commit message:")
|
|
425
|
+
logger.info(commit_message)
|
|
426
|
+
conversation_messages.append({"role": "assistant", "content": commit_message})
|
|
427
|
+
display_commit_message(commit_message, prompt_tokens, model, quiet)
|
|
428
|
+
|
|
429
|
+
if require_confirmation:
|
|
430
|
+
decision, commit_message, conversation_messages = handle_confirmation_loop(
|
|
431
|
+
commit_message, conversation_messages, quiet, model
|
|
432
|
+
)
|
|
433
|
+
if decision == "no":
|
|
434
|
+
console.print("[yellow]Prompt not accepted. Exiting...[/yellow]")
|
|
435
|
+
sys.exit(0)
|
|
436
|
+
elif decision == "yes":
|
|
437
|
+
break
|
|
438
|
+
else:
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
if dry_run:
|
|
442
|
+
console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
|
|
443
|
+
console.print("Would commit with message:")
|
|
444
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
445
|
+
staged_files = get_staged_files(existing_only=False)
|
|
446
|
+
console.print(f"Would commit {len(staged_files)} files")
|
|
447
|
+
logger.info(f"Would commit {len(staged_files)} files")
|
|
448
|
+
else:
|
|
449
|
+
execute_commit(commit_message, no_verify)
|
|
450
|
+
|
|
451
|
+
if push:
|
|
452
|
+
try:
|
|
453
|
+
if dry_run:
|
|
454
|
+
staged_files = get_staged_files(existing_only=False)
|
|
455
|
+
logger.info("Dry run: Would push changes")
|
|
456
|
+
logger.info("Would push with message:")
|
|
457
|
+
logger.info(commit_message)
|
|
458
|
+
logger.info(f"Would push {len(staged_files)} files")
|
|
459
|
+
console.print("[yellow]Dry run: Would push changes[/yellow]")
|
|
460
|
+
console.print("Would push with message:")
|
|
461
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
462
|
+
console.print(f"Would push {len(staged_files)} files")
|
|
463
|
+
sys.exit(0)
|
|
464
|
+
if push_changes():
|
|
465
|
+
logger.info("Changes pushed successfully")
|
|
466
|
+
console.print("[green]Changes pushed successfully[/green]")
|
|
467
|
+
else:
|
|
468
|
+
console.print(
|
|
469
|
+
"[red]Failed to push changes. Check your remote configuration and network connection.[/red]"
|
|
470
|
+
)
|
|
471
|
+
sys.exit(1)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
console.print(f"[red]Error pushing changes: {e}[/red]")
|
|
474
|
+
sys.exit(1)
|
|
475
|
+
|
|
476
|
+
if not quiet:
|
|
477
|
+
logger.info("Successfully committed changes with message:")
|
|
478
|
+
logger.info(commit_message)
|
|
479
|
+
if push:
|
|
480
|
+
logger.info("Changes pushed to remote.")
|
|
481
|
+
sys.exit(0)
|
|
482
|
+
|
|
483
|
+
|
|
36
484
|
def main(
|
|
37
485
|
stage_all: bool = False,
|
|
486
|
+
group: bool = False,
|
|
38
487
|
model: str | None = None,
|
|
39
488
|
hint: str = "",
|
|
40
489
|
one_liner: bool = False,
|
|
@@ -85,33 +534,35 @@ def main(
|
|
|
85
534
|
logger.info("Staging all changes")
|
|
86
535
|
run_git_command(["add", "--all"])
|
|
87
536
|
|
|
88
|
-
# Check for staged files
|
|
89
537
|
staged_files = get_staged_files(existing_only=False)
|
|
538
|
+
|
|
539
|
+
if group:
|
|
540
|
+
num_files = len(staged_files)
|
|
541
|
+
multiplier = min(5, 2 + (num_files // 10))
|
|
542
|
+
max_output_tokens *= multiplier
|
|
543
|
+
logger.debug(f"Grouped mode: scaling max_output_tokens by {multiplier}x for {num_files} files")
|
|
544
|
+
|
|
90
545
|
if not staged_files:
|
|
91
546
|
console.print(
|
|
92
547
|
"[yellow]No staged changes found. Stage your changes with git add first or use --add-all.[/yellow]"
|
|
93
548
|
)
|
|
94
549
|
sys.exit(0)
|
|
95
550
|
|
|
96
|
-
# Run pre-commit and lefthook hooks before doing expensive operations
|
|
97
551
|
if not no_verify and not dry_run:
|
|
98
|
-
# Run lefthook hooks
|
|
99
552
|
if not run_lefthook_hooks():
|
|
100
553
|
console.print("[red]Lefthook hooks failed. Please fix the issues and try again.[/red]")
|
|
101
554
|
console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
|
|
102
555
|
sys.exit(1)
|
|
103
556
|
|
|
104
|
-
# Run pre-commit hooks
|
|
105
557
|
if not run_pre_commit_hooks():
|
|
106
558
|
console.print("[red]Pre-commit hooks failed. Please fix the issues and try again.[/red]")
|
|
107
559
|
console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
|
|
108
560
|
sys.exit(1)
|
|
109
561
|
|
|
110
|
-
status =
|
|
562
|
+
status = get_staged_status()
|
|
111
563
|
diff = run_git_command(["diff", "--staged"])
|
|
112
564
|
diff_stat = " " + run_git_command(["diff", "--stat", "--cached"])
|
|
113
565
|
|
|
114
|
-
# Security scan for secrets
|
|
115
566
|
if not skip_secret_scan:
|
|
116
567
|
logger.info("Scanning staged changes for potential secrets...")
|
|
117
568
|
secrets = scan_staged_diff(diff)
|
|
@@ -170,14 +621,12 @@ def main(
|
|
|
170
621
|
sys.exit(0)
|
|
171
622
|
|
|
172
623
|
console.print(f"[green]Continuing with {len(remaining_staged)} staged file(s)...[/green]")
|
|
173
|
-
|
|
174
|
-
status = run_git_command(["status"])
|
|
624
|
+
status = get_staged_status()
|
|
175
625
|
diff = run_git_command(["diff", "--staged"])
|
|
176
626
|
diff_stat = " " + run_git_command(["diff", "--stat", "--cached"])
|
|
177
627
|
else:
|
|
178
628
|
logger.info("No secrets detected in staged changes")
|
|
179
629
|
|
|
180
|
-
# Preprocess the diff before passing to build_prompt
|
|
181
630
|
logger.debug(f"Preprocessing diff ({len(diff)} characters)")
|
|
182
631
|
assert model is not None
|
|
183
632
|
processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model)
|
|
@@ -188,7 +637,6 @@ def main(
|
|
|
188
637
|
system_template_path_value if isinstance(system_template_path_value, str) else None
|
|
189
638
|
)
|
|
190
639
|
|
|
191
|
-
# Use language parameter if provided, otherwise fall back to config
|
|
192
640
|
if language is None:
|
|
193
641
|
language_value = config.get("language")
|
|
194
642
|
language = language_value if isinstance(language_value, str) else None
|
|
@@ -209,174 +657,62 @@ def main(
|
|
|
209
657
|
translate_prefixes=translate_prefixes,
|
|
210
658
|
)
|
|
211
659
|
|
|
212
|
-
if
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
660
|
+
if group:
|
|
661
|
+
from gac.prompt import build_group_prompt
|
|
662
|
+
|
|
663
|
+
system_prompt, user_prompt = build_group_prompt(
|
|
664
|
+
status=status,
|
|
665
|
+
processed_diff=processed_diff,
|
|
666
|
+
diff_stat=diff_stat,
|
|
667
|
+
one_liner=one_liner,
|
|
668
|
+
hint=hint,
|
|
669
|
+
infer_scope=infer_scope,
|
|
670
|
+
verbose=verbose,
|
|
671
|
+
system_template_path=system_template_path,
|
|
672
|
+
language=language,
|
|
673
|
+
translate_prefixes=translate_prefixes,
|
|
221
674
|
)
|
|
222
675
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
first_iteration = True
|
|
230
|
-
|
|
231
|
-
while True:
|
|
232
|
-
prompt_tokens = count_tokens(conversation_messages, model)
|
|
233
|
-
|
|
234
|
-
if first_iteration:
|
|
235
|
-
warning_limit_val = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
236
|
-
assert warning_limit_val is not None
|
|
237
|
-
warning_limit = int(warning_limit_val)
|
|
238
|
-
if warning_limit and prompt_tokens > warning_limit:
|
|
239
|
-
console.print(
|
|
240
|
-
f"[yellow]⚠️ WARNING: Prompt contains {prompt_tokens} tokens, which exceeds the warning limit of "
|
|
241
|
-
f"{warning_limit} tokens.[/yellow]"
|
|
242
|
-
)
|
|
243
|
-
if require_confirmation:
|
|
244
|
-
proceed = click.confirm("Do you want to continue anyway?", default=True)
|
|
245
|
-
if not proceed:
|
|
246
|
-
console.print("[yellow]Aborted due to token limit.[/yellow]")
|
|
247
|
-
sys.exit(0)
|
|
248
|
-
|
|
249
|
-
first_iteration = False
|
|
250
|
-
|
|
251
|
-
raw_commit_message = generate_commit_message(
|
|
676
|
+
try:
|
|
677
|
+
execute_grouped_commits_workflow(
|
|
678
|
+
system_prompt=system_prompt,
|
|
679
|
+
user_prompt=user_prompt,
|
|
252
680
|
model=model,
|
|
253
|
-
prompt=conversation_messages,
|
|
254
681
|
temperature=temperature,
|
|
255
|
-
|
|
682
|
+
max_output_tokens=max_output_tokens,
|
|
256
683
|
max_retries=max_retries,
|
|
684
|
+
require_confirmation=require_confirmation,
|
|
257
685
|
quiet=quiet,
|
|
686
|
+
no_verify=no_verify,
|
|
687
|
+
dry_run=dry_run,
|
|
688
|
+
push=push,
|
|
689
|
+
show_prompt=show_prompt,
|
|
258
690
|
)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
conversation_messages.append({"role": "assistant", "content": commit_message})
|
|
266
|
-
|
|
267
|
-
console.print("[bold green]Generated commit message:[/bold green]")
|
|
268
|
-
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
269
|
-
|
|
270
|
-
if not quiet:
|
|
271
|
-
completion_tokens = count_tokens(commit_message, model)
|
|
272
|
-
total_tokens = prompt_tokens + completion_tokens
|
|
273
|
-
console.print(
|
|
274
|
-
f"[dim]Token usage: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} "
|
|
275
|
-
"total[/dim]"
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
if require_confirmation:
|
|
279
|
-
while True:
|
|
280
|
-
response = click.prompt(
|
|
281
|
-
"Proceed with commit above? [y/n/r/e/<feedback>]",
|
|
282
|
-
type=str,
|
|
283
|
-
show_default=False,
|
|
284
|
-
).strip()
|
|
285
|
-
response_lower = response.lower()
|
|
286
|
-
|
|
287
|
-
if response_lower in ["y", "yes"]:
|
|
288
|
-
break
|
|
289
|
-
if response_lower in ["n", "no"]:
|
|
290
|
-
console.print("[yellow]Prompt not accepted. Exiting...[/yellow]")
|
|
291
|
-
sys.exit(0)
|
|
292
|
-
if response == "":
|
|
293
|
-
continue
|
|
294
|
-
if response_lower in ["e", "edit"]:
|
|
295
|
-
edited_message = edit_commit_message_inplace(commit_message)
|
|
296
|
-
if edited_message:
|
|
297
|
-
commit_message = edited_message
|
|
298
|
-
conversation_messages[-1] = {"role": "assistant", "content": commit_message}
|
|
299
|
-
logger.info("Commit message edited by user")
|
|
300
|
-
console.print("\n[bold green]Edited commit message:[/bold green]")
|
|
301
|
-
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
302
|
-
else:
|
|
303
|
-
console.print("[yellow]Using previous message.[/yellow]")
|
|
304
|
-
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
305
|
-
continue
|
|
306
|
-
if response_lower in ["r", "reroll"]:
|
|
307
|
-
feedback_message = (
|
|
308
|
-
"Please provide an alternative commit message using the same repository context."
|
|
309
|
-
)
|
|
310
|
-
console.print("[cyan]Regenerating commit message...[/cyan]")
|
|
311
|
-
conversation_messages.append({"role": "user", "content": feedback_message})
|
|
312
|
-
console.print()
|
|
313
|
-
break
|
|
314
|
-
|
|
315
|
-
feedback_message = f"Please revise the commit message based on this feedback: {response}"
|
|
316
|
-
console.print(f"[cyan]Regenerating commit message with feedback: {response}[/cyan]")
|
|
317
|
-
conversation_messages.append({"role": "user", "content": feedback_message})
|
|
318
|
-
console.print()
|
|
319
|
-
break
|
|
320
|
-
|
|
321
|
-
if response_lower in ["y", "yes"]:
|
|
322
|
-
break
|
|
323
|
-
else:
|
|
324
|
-
break
|
|
325
|
-
|
|
326
|
-
if dry_run:
|
|
327
|
-
console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
|
|
328
|
-
console.print("Would commit with message:")
|
|
329
|
-
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
330
|
-
staged_files = get_staged_files(existing_only=False)
|
|
331
|
-
console.print(f"Would commit {len(staged_files)} files")
|
|
332
|
-
logger.info(f"Would commit {len(staged_files)} files")
|
|
333
|
-
else:
|
|
334
|
-
commit_args = ["commit", "-m", commit_message]
|
|
335
|
-
if no_verify:
|
|
336
|
-
commit_args.append("--no-verify")
|
|
337
|
-
run_git_command(commit_args)
|
|
338
|
-
logger.info("Commit created successfully")
|
|
339
|
-
console.print("[green]Commit created successfully[/green]")
|
|
340
|
-
except AIError as e:
|
|
341
|
-
logger.error(str(e))
|
|
342
|
-
console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
|
|
343
|
-
sys.exit(1)
|
|
344
|
-
|
|
345
|
-
if push:
|
|
691
|
+
except AIError as e:
|
|
692
|
+
logger.error(str(e))
|
|
693
|
+
console.print(f"[red]Failed to generate grouped commits: {str(e)}[/red]")
|
|
694
|
+
sys.exit(1)
|
|
695
|
+
else:
|
|
346
696
|
try:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
else:
|
|
365
|
-
console.print(
|
|
366
|
-
"[red]Failed to push changes. Check your remote configuration and network connection.[/red]"
|
|
367
|
-
)
|
|
368
|
-
sys.exit(1)
|
|
369
|
-
except Exception as e:
|
|
370
|
-
console.print(f"[red]Error pushing changes: {e}[/red]")
|
|
697
|
+
execute_single_commit_workflow(
|
|
698
|
+
system_prompt=system_prompt,
|
|
699
|
+
user_prompt=user_prompt,
|
|
700
|
+
model=model,
|
|
701
|
+
temperature=temperature,
|
|
702
|
+
max_output_tokens=max_output_tokens,
|
|
703
|
+
max_retries=max_retries,
|
|
704
|
+
require_confirmation=require_confirmation,
|
|
705
|
+
quiet=quiet,
|
|
706
|
+
no_verify=no_verify,
|
|
707
|
+
dry_run=dry_run,
|
|
708
|
+
push=push,
|
|
709
|
+
show_prompt=show_prompt,
|
|
710
|
+
)
|
|
711
|
+
except AIError as e:
|
|
712
|
+
logger.error(str(e))
|
|
713
|
+
console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
|
|
371
714
|
sys.exit(1)
|
|
372
715
|
|
|
373
|
-
if not quiet:
|
|
374
|
-
logger.info("Successfully committed changes with message:")
|
|
375
|
-
logger.info(commit_message)
|
|
376
|
-
if push:
|
|
377
|
-
logger.info("Changes pushed to remote.")
|
|
378
|
-
sys.exit(0)
|
|
379
|
-
|
|
380
716
|
|
|
381
717
|
if __name__ == "__main__":
|
|
382
718
|
main()
|