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/prompt.py
CHANGED
|
@@ -246,30 +246,30 @@ DEFAULT_USER_TEMPLATE = """<hint>
|
|
|
246
246
|
Additional context provided by the user: <hint_text></hint_text>
|
|
247
247
|
</hint>
|
|
248
248
|
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
</
|
|
249
|
+
<git_diff>
|
|
250
|
+
<diff></diff>
|
|
251
|
+
</git_diff>
|
|
252
252
|
|
|
253
253
|
<git_diff_stat>
|
|
254
254
|
<diff_stat></diff_stat>
|
|
255
255
|
</git_diff_stat>
|
|
256
256
|
|
|
257
|
-
<
|
|
258
|
-
<
|
|
259
|
-
</
|
|
257
|
+
<git_status>
|
|
258
|
+
<status></status>
|
|
259
|
+
</git_status>
|
|
260
|
+
|
|
261
|
+
<language_instructions>
|
|
262
|
+
IMPORTANT: You MUST write the entire commit message in <language_name></language_name>.
|
|
263
|
+
All text in the commit message, including the summary line and body, must be in <language_name></language_name>.
|
|
264
|
+
<prefix_instruction></prefix_instruction>
|
|
265
|
+
</language_instructions>
|
|
260
266
|
|
|
261
|
-
<
|
|
267
|
+
<format_instructions>
|
|
262
268
|
IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
|
|
263
269
|
DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
|
|
264
270
|
DO NOT use markdown formatting, headers, or code blocks.
|
|
265
271
|
The entire response will be passed directly to 'git commit -m'.
|
|
266
|
-
|
|
267
|
-
<language>
|
|
268
|
-
IMPORTANT: You MUST write the entire commit message in <language_name></language_name>.
|
|
269
|
-
All text in the commit message, including the summary line and body, must be in <language_name></language_name>.
|
|
270
|
-
<prefix_instruction></prefix_instruction>
|
|
271
|
-
</language>
|
|
272
|
-
</instructions>"""
|
|
272
|
+
</format_instructions>"""
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
# ============================================================================
|
|
@@ -518,7 +518,7 @@ The ENTIRE commit message, including the prefix, must be in {language}."""
|
|
|
518
518
|
|
|
519
519
|
user_template = user_template.replace("<prefix_instruction></prefix_instruction>", prefix_instruction)
|
|
520
520
|
else:
|
|
521
|
-
user_template = _remove_template_section(user_template, "
|
|
521
|
+
user_template = _remove_template_section(user_template, "language_instructions")
|
|
522
522
|
logger.debug("Using default language (English)")
|
|
523
523
|
|
|
524
524
|
user_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", user_template)
|
|
@@ -526,6 +526,92 @@ The ENTIRE commit message, including the prefix, must be in {language}."""
|
|
|
526
526
|
return system_template.strip(), user_template.strip()
|
|
527
527
|
|
|
528
528
|
|
|
529
|
+
def build_group_prompt(
|
|
530
|
+
status: str,
|
|
531
|
+
processed_diff: str,
|
|
532
|
+
diff_stat: str,
|
|
533
|
+
one_liner: bool,
|
|
534
|
+
hint: str,
|
|
535
|
+
infer_scope: bool,
|
|
536
|
+
verbose: bool,
|
|
537
|
+
system_template_path: str | None,
|
|
538
|
+
language: str | None,
|
|
539
|
+
translate_prefixes: bool,
|
|
540
|
+
) -> tuple[str, str]:
|
|
541
|
+
"""Build prompt for grouped commit generation (JSON output with multiple commits)."""
|
|
542
|
+
system_prompt, user_prompt = build_prompt(
|
|
543
|
+
status=status,
|
|
544
|
+
processed_diff=processed_diff,
|
|
545
|
+
diff_stat=diff_stat,
|
|
546
|
+
one_liner=one_liner,
|
|
547
|
+
hint=hint,
|
|
548
|
+
infer_scope=infer_scope,
|
|
549
|
+
verbose=verbose,
|
|
550
|
+
system_template_path=system_template_path,
|
|
551
|
+
language=language,
|
|
552
|
+
translate_prefixes=translate_prefixes,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
user_prompt = _remove_template_section(user_prompt, "format_instructions")
|
|
556
|
+
|
|
557
|
+
grouping_instructions = """
|
|
558
|
+
<format_instructions>
|
|
559
|
+
Your task is to split the changed files into separate, logical commits. Think of this like sorting files into different folders where each file belongs in exactly one folder.
|
|
560
|
+
|
|
561
|
+
CRITICAL REQUIREMENT - Every File Used Exactly Once:
|
|
562
|
+
You must assign EVERY file from the diff to exactly ONE commit.
|
|
563
|
+
- NO file should be left out
|
|
564
|
+
- NO file should appear in multiple commits
|
|
565
|
+
- EVERY file must be used once and ONLY once
|
|
566
|
+
|
|
567
|
+
Think of it like dealing cards: Once you've dealt a card to a player, that card cannot be dealt to another player.
|
|
568
|
+
|
|
569
|
+
HOW TO SPLIT THE FILES:
|
|
570
|
+
1. Review all changed files in the diff
|
|
571
|
+
2. Group files by logical relationship (e.g., related features, bug fixes, documentation)
|
|
572
|
+
3. Assign each file to exactly one commit based on what makes the most sense
|
|
573
|
+
4. If a file could fit in multiple commits, pick the best fit and move on - do NOT duplicate it
|
|
574
|
+
5. Continue until every single file has been assigned to a commit
|
|
575
|
+
|
|
576
|
+
ORDERING:
|
|
577
|
+
Order the commits in a logical sequence considering dependencies, natural progression, and overall workflow.
|
|
578
|
+
|
|
579
|
+
YOUR RESPONSE FORMAT:
|
|
580
|
+
Respond with valid JSON following this structure:
|
|
581
|
+
```json
|
|
582
|
+
{
|
|
583
|
+
"commits": [
|
|
584
|
+
{
|
|
585
|
+
"files": ["src/auth/login.ts", "src/auth/logout.ts"],
|
|
586
|
+
"message": "<commit_message_conforming_to_prescribed_structure_and_format>"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
"files": ["src/db/schema.sql", "src/db/migrations/001.sql"],
|
|
590
|
+
"message": "<commit_message_conforming_to_prescribed_structure_and_format>"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"files": ["tests/auth.test.ts", "tests/db.test.ts", "README.md"],
|
|
594
|
+
"message": "<commit_message_conforming_to_prescribed_structure_and_format>"
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
☝️ Notice how EVERY file path in the example above appears exactly ONCE across all commits. "src/auth/login.ts" appears once. "tests/auth.test.ts" appears once. No file is repeated.
|
|
601
|
+
|
|
602
|
+
VALIDATION CHECKLIST - Before responding, verify:
|
|
603
|
+
□ Total files across all commits = Total files in the diff
|
|
604
|
+
□ Each file appears in exactly 1 commit (no duplicates, no omissions)
|
|
605
|
+
□ Every commit has at least one file
|
|
606
|
+
□ If you list all files from all commits and count them, you get the same count as unique files in the diff
|
|
607
|
+
</format_instructions>
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
user_prompt = user_prompt + grouping_instructions
|
|
611
|
+
|
|
612
|
+
return system_prompt, user_prompt
|
|
613
|
+
|
|
614
|
+
|
|
529
615
|
# ============================================================================
|
|
530
616
|
# Message Cleaning Helpers
|
|
531
617
|
# ============================================================================
|
gac/security.py
CHANGED
|
@@ -54,7 +54,7 @@ class SecretPatterns:
|
|
|
54
54
|
PRIVATE_KEY = re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")
|
|
55
55
|
|
|
56
56
|
# Bearer Tokens (require actual token with specific characteristics)
|
|
57
|
-
BEARER_TOKEN = re.compile(r"Bearer\s+[A-Za-z0-9]{20,}[/=]
|
|
57
|
+
BEARER_TOKEN = re.compile(r"Bearer\s+[A-Za-z0-9]{20,}[/=]*(?:\s|$)", re.IGNORECASE)
|
|
58
58
|
|
|
59
59
|
# JWT Tokens
|
|
60
60
|
JWT_TOKEN = re.compile(r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+")
|
gac/utils.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Utility functions for gac."""
|
|
2
2
|
|
|
3
|
+
import locale
|
|
3
4
|
import logging
|
|
4
5
|
import subprocess
|
|
6
|
+
import sys
|
|
5
7
|
|
|
6
8
|
from rich.console import Console
|
|
7
9
|
from rich.theme import Theme
|
|
@@ -65,18 +67,47 @@ def print_message(message: str, level: str = "info") -> None:
|
|
|
65
67
|
console.print(message, style=level)
|
|
66
68
|
|
|
67
69
|
|
|
68
|
-
def
|
|
70
|
+
def get_safe_encodings() -> list[str]:
|
|
71
|
+
"""Get a list of safe encodings to try for subprocess calls, in order of preference.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of encoding strings to try, with UTF-8 first
|
|
75
|
+
"""
|
|
76
|
+
encodings = ["utf-8"]
|
|
77
|
+
|
|
78
|
+
# Add locale encoding as fallback
|
|
79
|
+
locale_encoding = locale.getpreferredencoding(False)
|
|
80
|
+
if locale_encoding and locale_encoding not in encodings:
|
|
81
|
+
encodings.append(locale_encoding)
|
|
82
|
+
|
|
83
|
+
# Windows-specific fallbacks
|
|
84
|
+
if sys.platform == "win32":
|
|
85
|
+
windows_encodings = ["cp65001", "cp936", "cp1252"] # UTF-8, GBK, Windows-1252
|
|
86
|
+
for enc in windows_encodings:
|
|
87
|
+
if enc not in encodings:
|
|
88
|
+
encodings.append(enc)
|
|
89
|
+
|
|
90
|
+
# Final fallback to system default
|
|
91
|
+
if "utf-8" not in encodings:
|
|
92
|
+
encodings.append("utf-8")
|
|
93
|
+
|
|
94
|
+
return encodings
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_subprocess_with_encoding(
|
|
69
98
|
command: list[str],
|
|
99
|
+
encoding: str,
|
|
70
100
|
silent: bool = False,
|
|
71
101
|
timeout: int = 60,
|
|
72
102
|
check: bool = True,
|
|
73
103
|
strip_output: bool = True,
|
|
74
104
|
raise_on_error: bool = True,
|
|
75
105
|
) -> str:
|
|
76
|
-
"""Run a
|
|
106
|
+
"""Run subprocess with a specific encoding, handling encoding errors gracefully.
|
|
77
107
|
|
|
78
108
|
Args:
|
|
79
109
|
command: List of command arguments
|
|
110
|
+
encoding: Specific encoding to use
|
|
80
111
|
silent: If True, suppress debug logging
|
|
81
112
|
timeout: Command timeout in seconds
|
|
82
113
|
check: Whether to check return code (for compatibility)
|
|
@@ -91,7 +122,7 @@ def run_subprocess(
|
|
|
91
122
|
subprocess.CalledProcessError: If the command fails and raise_on_error is True
|
|
92
123
|
"""
|
|
93
124
|
if not silent:
|
|
94
|
-
logger.debug(f"Running command: {' '.join(command)}")
|
|
125
|
+
logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
|
|
95
126
|
|
|
96
127
|
try:
|
|
97
128
|
result = subprocess.run(
|
|
@@ -100,6 +131,8 @@ def run_subprocess(
|
|
|
100
131
|
text=True,
|
|
101
132
|
check=False,
|
|
102
133
|
timeout=timeout,
|
|
134
|
+
encoding=encoding,
|
|
135
|
+
errors="replace", # Replace problematic characters instead of crashing
|
|
103
136
|
)
|
|
104
137
|
|
|
105
138
|
should_raise = result.returncode != 0 and (check or raise_on_error)
|
|
@@ -123,6 +156,11 @@ def run_subprocess(
|
|
|
123
156
|
if raise_on_error:
|
|
124
157
|
raise
|
|
125
158
|
return ""
|
|
159
|
+
except UnicodeError as e:
|
|
160
|
+
# This should be rare with errors="replace", but handle it just in case
|
|
161
|
+
if not silent:
|
|
162
|
+
logger.debug(f"Encoding error with {encoding}: {e}")
|
|
163
|
+
raise
|
|
126
164
|
except Exception as e:
|
|
127
165
|
if not silent:
|
|
128
166
|
logger.debug(f"Command error: {e}")
|
|
@@ -132,6 +170,69 @@ def run_subprocess(
|
|
|
132
170
|
return ""
|
|
133
171
|
|
|
134
172
|
|
|
173
|
+
def run_subprocess(
|
|
174
|
+
command: list[str],
|
|
175
|
+
silent: bool = False,
|
|
176
|
+
timeout: int = 60,
|
|
177
|
+
check: bool = True,
|
|
178
|
+
strip_output: bool = True,
|
|
179
|
+
raise_on_error: bool = True,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Run a subprocess command safely and return the output, trying multiple encodings.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
command: List of command arguments
|
|
185
|
+
silent: If True, suppress debug logging
|
|
186
|
+
timeout: Command timeout in seconds
|
|
187
|
+
check: Whether to check return code (for compatibility)
|
|
188
|
+
strip_output: Whether to strip whitespace from output
|
|
189
|
+
raise_on_error: Whether to raise an exception on error
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Command output as string
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
GacError: If the command times out
|
|
196
|
+
subprocess.CalledProcessError: If the command fails and raise_on_error is True
|
|
197
|
+
|
|
198
|
+
Note:
|
|
199
|
+
Tries multiple encodings in order: utf-8, locale encoding, platform-specific fallbacks
|
|
200
|
+
This prevents UnicodeDecodeError on systems with non-UTF-8 locales (e.g., Chinese Windows)
|
|
201
|
+
"""
|
|
202
|
+
encodings = get_safe_encodings()
|
|
203
|
+
last_exception = None
|
|
204
|
+
|
|
205
|
+
for encoding in encodings:
|
|
206
|
+
try:
|
|
207
|
+
return run_subprocess_with_encoding(
|
|
208
|
+
command=command,
|
|
209
|
+
encoding=encoding,
|
|
210
|
+
silent=silent,
|
|
211
|
+
timeout=timeout,
|
|
212
|
+
check=check,
|
|
213
|
+
strip_output=strip_output,
|
|
214
|
+
raise_on_error=raise_on_error,
|
|
215
|
+
)
|
|
216
|
+
except UnicodeError as e:
|
|
217
|
+
last_exception = e
|
|
218
|
+
if not silent:
|
|
219
|
+
logger.debug(f"Failed to decode with {encoding}: {e}")
|
|
220
|
+
continue
|
|
221
|
+
except (subprocess.CalledProcessError, GacError, subprocess.TimeoutExpired):
|
|
222
|
+
# These are not encoding-related errors, so don't retry with other encodings
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
# If we get here, all encodings failed with UnicodeError
|
|
226
|
+
if not silent:
|
|
227
|
+
logger.error(f"Failed to decode command output with any encoding: {encodings}")
|
|
228
|
+
|
|
229
|
+
# Raise the last UnicodeError we encountered
|
|
230
|
+
if last_exception:
|
|
231
|
+
raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
|
|
232
|
+
else:
|
|
233
|
+
raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
|
|
234
|
+
|
|
235
|
+
|
|
135
236
|
def edit_commit_message_inplace(message: str) -> str | None:
|
|
136
237
|
"""Edit commit message in-place using rich terminal editing.
|
|
137
238
|
|
gac/workflow_utils.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_confirmation_loop(
|
|
13
|
+
commit_message: str,
|
|
14
|
+
conversation_messages: list[dict[str, str]],
|
|
15
|
+
quiet: bool,
|
|
16
|
+
model: str,
|
|
17
|
+
) -> tuple[str, str, list[dict[str, str]]]:
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
|
|
20
|
+
from gac.utils import edit_commit_message_inplace
|
|
21
|
+
|
|
22
|
+
while True:
|
|
23
|
+
response = click.prompt(
|
|
24
|
+
"Proceed with commit above? [y/n/r/e/<feedback>]",
|
|
25
|
+
type=str,
|
|
26
|
+
show_default=False,
|
|
27
|
+
).strip()
|
|
28
|
+
response_lower = response.lower()
|
|
29
|
+
|
|
30
|
+
if response_lower in ["y", "yes"]:
|
|
31
|
+
return ("yes", commit_message, conversation_messages)
|
|
32
|
+
if response_lower in ["n", "no"]:
|
|
33
|
+
return ("no", commit_message, conversation_messages)
|
|
34
|
+
if response == "":
|
|
35
|
+
continue
|
|
36
|
+
if response_lower in ["e", "edit"]:
|
|
37
|
+
edited_message = edit_commit_message_inplace(commit_message)
|
|
38
|
+
if edited_message:
|
|
39
|
+
commit_message = edited_message
|
|
40
|
+
conversation_messages[-1] = {"role": "assistant", "content": commit_message}
|
|
41
|
+
logger.info("Commit message edited by user")
|
|
42
|
+
console.print("\n[bold green]Edited commit message:[/bold green]")
|
|
43
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
44
|
+
else:
|
|
45
|
+
console.print("[yellow]Using previous message.[/yellow]")
|
|
46
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
47
|
+
continue
|
|
48
|
+
if response_lower in ["r", "reroll"]:
|
|
49
|
+
msg = "Please provide an alternative commit message using the same repository context."
|
|
50
|
+
conversation_messages.append({"role": "user", "content": msg})
|
|
51
|
+
console.print("[cyan]Regenerating commit message...[/cyan]")
|
|
52
|
+
return ("regenerate", commit_message, conversation_messages)
|
|
53
|
+
|
|
54
|
+
msg = f"Please revise the commit message based on this feedback: {response}"
|
|
55
|
+
conversation_messages.append({"role": "user", "content": msg})
|
|
56
|
+
console.print(f"[cyan]Regenerating commit message with feedback: {response}[/cyan]")
|
|
57
|
+
return ("regenerate", commit_message, conversation_messages)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def execute_commit(commit_message: str, no_verify: bool) -> None:
|
|
61
|
+
from gac.git import run_git_command
|
|
62
|
+
|
|
63
|
+
commit_args = ["commit", "-m", commit_message]
|
|
64
|
+
if no_verify:
|
|
65
|
+
commit_args.append("--no-verify")
|
|
66
|
+
run_git_command(commit_args)
|
|
67
|
+
logger.info("Commit created successfully")
|
|
68
|
+
console.print("[green]Commit created successfully[/green]")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check_token_warning(
|
|
72
|
+
prompt_tokens: int,
|
|
73
|
+
warning_limit: int,
|
|
74
|
+
require_confirmation: bool,
|
|
75
|
+
) -> bool:
|
|
76
|
+
if warning_limit and prompt_tokens > warning_limit:
|
|
77
|
+
console.print(f"[yellow]⚠️ WARNING: Prompt has {prompt_tokens} tokens (limit: {warning_limit})[/yellow]")
|
|
78
|
+
if require_confirmation:
|
|
79
|
+
proceed = click.confirm("Do you want to continue anyway?", default=True)
|
|
80
|
+
if not proceed:
|
|
81
|
+
console.print("[yellow]Aborted due to token limit.[/yellow]")
|
|
82
|
+
return False
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def display_commit_message(commit_message: str, prompt_tokens: int, model: str, quiet: bool) -> None:
|
|
87
|
+
from rich.panel import Panel
|
|
88
|
+
|
|
89
|
+
from gac.ai_utils import count_tokens
|
|
90
|
+
|
|
91
|
+
console.print("[bold green]Generated commit message:[/bold green]")
|
|
92
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
93
|
+
|
|
94
|
+
if not quiet:
|
|
95
|
+
completion_tokens = count_tokens(commit_message, model)
|
|
96
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
97
|
+
console.print(
|
|
98
|
+
f"[dim]Token usage: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} total[/dim]"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def restore_staging(staged_files: list[str], staged_diff: str | None = None) -> None:
|
|
103
|
+
"""Restore the git staging area to a previous state.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
staged_files: List of file paths that should be staged
|
|
107
|
+
staged_diff: Optional staged diff to reapply for partial staging
|
|
108
|
+
"""
|
|
109
|
+
from gac.git import run_git_command
|
|
110
|
+
|
|
111
|
+
run_git_command(["reset", "HEAD"])
|
|
112
|
+
|
|
113
|
+
if staged_diff:
|
|
114
|
+
temp_path: Path | None = None
|
|
115
|
+
try:
|
|
116
|
+
with tempfile.NamedTemporaryFile("w", delete=False) as tmp:
|
|
117
|
+
tmp.write(staged_diff)
|
|
118
|
+
temp_path = Path(tmp.name)
|
|
119
|
+
run_git_command(["apply", "--cached", str(temp_path)])
|
|
120
|
+
return
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Failed to reapply staged diff, falling back to file list: {e}")
|
|
123
|
+
finally:
|
|
124
|
+
if temp_path:
|
|
125
|
+
temp_path.unlink(missing_ok=True)
|
|
126
|
+
|
|
127
|
+
for file_path in staged_files:
|
|
128
|
+
try:
|
|
129
|
+
run_git_command(["add", file_path])
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.warning(f"Failed to restore staging for {file_path}: {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 2.
|
|
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
|
[](https://app.codecov.io/gh/cellwebb/gac)
|
|
51
54
|
[](https://github.com/astral-sh/ruff)
|
|
52
55
|
[](https://mypy-lang.org/)
|
|
53
|
-
[](docs/CONTRIBUTING.md)
|
|
56
|
+
[](docs/en/CONTRIBUTING.md)
|
|
54
57
|
[](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
|
|
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,21 +1,22 @@
|
|
|
1
1
|
gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
|
|
2
|
-
gac/__version__.py,sha256=
|
|
3
|
-
gac/ai.py,sha256=
|
|
4
|
-
gac/ai_utils.py,sha256=
|
|
5
|
-
gac/cli.py,sha256=
|
|
6
|
-
gac/config.py,sha256=
|
|
2
|
+
gac/__version__.py,sha256=D_ezOWkaq6NqXOifgxVl-YxQ5yWx8XUH0WiBeGaE5As,66
|
|
3
|
+
gac/ai.py,sha256=6SQK4axBE0uEbF3eKVTvQtGL9X1TbxoBOrY7NuYIfiM,5009
|
|
4
|
+
gac/ai_utils.py,sha256=reJINfsKlX0HAg5HPlH4ImO6FvibzgRZ0ruG9u1cxa8,8312
|
|
5
|
+
gac/cli.py,sha256=ecmlZkejIM7uQgUun3S4dVht3f43_nCyibP3jm2hufQ,6057
|
|
6
|
+
gac/config.py,sha256=yyhcQjmWv5c0-AVg6U1X9gE89x-e0369IYjieqKDjW0,2078
|
|
7
7
|
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
8
|
-
gac/constants.py,sha256=
|
|
8
|
+
gac/constants.py,sha256=yyvYycIfRJ9SZZIMIhwn1s6yohjcaNM-fGXl_R9w1dI,9586
|
|
9
9
|
gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
|
|
10
10
|
gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
|
|
11
|
-
gac/git.py,sha256=
|
|
12
|
-
gac/init_cli.py,sha256=
|
|
13
|
-
gac/language_cli.py,sha256=
|
|
14
|
-
gac/main.py,sha256=
|
|
11
|
+
gac/git.py,sha256=ss-ztVyNnU3zwigHTHUqUAx8RiTzhcrSxHMrdjU1VgE,11117
|
|
12
|
+
gac/init_cli.py,sha256=ZfC6oci9xiRVKHuOipH7RQvVI-79O_OymG7_ZZ6CyLM,19606
|
|
13
|
+
gac/language_cli.py,sha256=FEQf-k5KrhIpvx4XJvjqxTeE4qumNO00d-tgOAf83W4,8488
|
|
14
|
+
gac/main.py,sha256=r5zwAYBBn-qz9kix2Zn5yJ-ac3-42u6HaIoirybOaTQ,28784
|
|
15
15
|
gac/preprocess.py,sha256=hk2p2X4-xVDvuy-T1VMzMa9k5fTUbhlWDyw89DCf81Q,15379
|
|
16
|
-
gac/prompt.py,sha256=
|
|
17
|
-
gac/security.py,sha256=
|
|
18
|
-
gac/utils.py,sha256=
|
|
16
|
+
gac/prompt.py,sha256=ofumb6DmxJceqZLUlUyLE9b7Mwx9BpIEHweKEV9eicw,31841
|
|
17
|
+
gac/security.py,sha256=QT91mBEo2Y7la-aXvKuF2zhWuoOSXb6PWKLJ93kSy2k,9926
|
|
18
|
+
gac/utils.py,sha256=gTpc9zLzy3-3L5k-V5uSeFI-NnSPabA1GGVwSgpeMSk,11709
|
|
19
|
+
gac/workflow_utils.py,sha256=fc0yPRBeA-7P2WiVPFa7A_NjXTW68UlbUv6_AXVYAzA,5023
|
|
19
20
|
gac/providers/__init__.py,sha256=pT1xcKoZkPm6BWaxcAQ299-Ca5zZ31kf4DeQYAim9Tw,1367
|
|
20
21
|
gac/providers/anthropic.py,sha256=VK5d1s1PmBNDwh_x7illQ2CIZIHNIYU28btVfizwQPs,2036
|
|
21
22
|
gac/providers/cerebras.py,sha256=Ik8lhlsliGJVkgDgqlThfpra9tqbdYQZkaC4eNxRd9w,1648
|
|
@@ -36,8 +37,8 @@ gac/providers/streamlake.py,sha256=KAA2ZnpuEI5imzvdWVWUhEBHSP0BMnprKXte6CbwBWY,2
|
|
|
36
37
|
gac/providers/synthetic.py,sha256=sRMIJTS9LpcXd9A7qp_ZjZxdqtTKRn9fl1W4YwJZP4c,1855
|
|
37
38
|
gac/providers/together.py,sha256=1bUIVHfYzcEDw4hQPE8qV6hjc2JNHPv_khVgpk2IJxI,1667
|
|
38
39
|
gac/providers/zai.py,sha256=kywhhrCfPBu0rElZyb-iENxQxxpVGykvePuL4xrXlaU,2739
|
|
39
|
-
gac-2.
|
|
40
|
-
gac-2.
|
|
41
|
-
gac-2.
|
|
42
|
-
gac-2.
|
|
43
|
-
gac-2.
|
|
40
|
+
gac-2.4.0.dist-info/METADATA,sha256=aNQRaKtaYr-aEmsrT769NVFoH4gJKI8CxmN_sESUrpo,10784
|
|
41
|
+
gac-2.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
42
|
+
gac-2.4.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
43
|
+
gac-2.4.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
|
|
44
|
+
gac-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|