gac 1.13.1__py3-none-any.whl → 1.15.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/cli.py +12 -1
- gac/config.py +3 -0
- gac/constants.py +136 -0
- gac/init_cli.py +75 -0
- gac/language_cli.py +111 -0
- gac/main.py +18 -0
- gac/preprocess.py +3 -3
- gac/prompt.py +347 -229
- {gac-1.13.1.dist-info → gac-1.15.0.dist-info}/METADATA +13 -1
- {gac-1.13.1.dist-info → gac-1.15.0.dist-info}/RECORD +14 -13
- {gac-1.13.1.dist-info → gac-1.15.0.dist-info}/WHEEL +0 -0
- {gac-1.13.1.dist-info → gac-1.15.0.dist-info}/entry_points.txt +0 -0
- {gac-1.13.1.dist-info → gac-1.15.0.dist-info}/licenses/LICENSE +0 -0
gac/__version__.py
CHANGED
gac/cli.py
CHANGED
|
@@ -13,10 +13,11 @@ import click
|
|
|
13
13
|
from gac import __version__
|
|
14
14
|
from gac.config import load_config
|
|
15
15
|
from gac.config_cli import config as config_cli
|
|
16
|
-
from gac.constants import Logging
|
|
16
|
+
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.language_cli import language as language_cli
|
|
20
21
|
from gac.main import main
|
|
21
22
|
from gac.utils import setup_logging
|
|
22
23
|
|
|
@@ -43,6 +44,9 @@ logger = logging.getLogger(__name__)
|
|
|
43
44
|
@click.option("--hint", "-h", default="", help="Additional context to include in the prompt")
|
|
44
45
|
# Model options
|
|
45
46
|
@click.option("--model", "-m", help="Override the default model (format: 'provider:model_name')")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--language", "-l", help="Override the language for commit messages (e.g., 'Spanish', 'es', 'zh-CN', 'ja')"
|
|
49
|
+
)
|
|
46
50
|
# Output options
|
|
47
51
|
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
48
52
|
@click.option(
|
|
@@ -75,6 +79,7 @@ def cli(
|
|
|
75
79
|
yes: bool = False,
|
|
76
80
|
hint: str = "",
|
|
77
81
|
model: str | None = None,
|
|
82
|
+
language: str | None = None,
|
|
78
83
|
version: bool = False,
|
|
79
84
|
dry_run: bool = False,
|
|
80
85
|
verbose: bool = False,
|
|
@@ -98,6 +103,9 @@ def cli(
|
|
|
98
103
|
# Determine if verbose mode should be enabled based on -v flag or verbose config setting
|
|
99
104
|
use_verbose = bool(verbose or config.get("verbose", False))
|
|
100
105
|
|
|
106
|
+
# Resolve language code to full name if provided
|
|
107
|
+
resolved_language = Languages.resolve_code(language) if language else None
|
|
108
|
+
|
|
101
109
|
try:
|
|
102
110
|
main(
|
|
103
111
|
stage_all=add_all,
|
|
@@ -113,6 +121,7 @@ def cli(
|
|
|
113
121
|
verbose=use_verbose,
|
|
114
122
|
no_verify=no_verify,
|
|
115
123
|
skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
|
|
124
|
+
language=resolved_language,
|
|
116
125
|
)
|
|
117
126
|
except Exception as e:
|
|
118
127
|
handle_error(e, exit_program=True)
|
|
@@ -131,6 +140,7 @@ def cli(
|
|
|
131
140
|
"yes": yes,
|
|
132
141
|
"hint": hint,
|
|
133
142
|
"model": model,
|
|
143
|
+
"language": language,
|
|
134
144
|
"version": version,
|
|
135
145
|
"dry_run": dry_run,
|
|
136
146
|
"verbose": verbose,
|
|
@@ -141,6 +151,7 @@ def cli(
|
|
|
141
151
|
|
|
142
152
|
cli.add_command(config_cli)
|
|
143
153
|
cli.add_command(init_cli)
|
|
154
|
+
cli.add_command(language_cli)
|
|
144
155
|
cli.add_command(diff_cli)
|
|
145
156
|
|
|
146
157
|
if __name__ == "__main__":
|
gac/config.py
CHANGED
|
@@ -38,6 +38,9 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
38
38
|
"skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
|
|
39
39
|
in ("true", "1", "yes", "on"),
|
|
40
40
|
"verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
|
|
41
|
+
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
42
|
+
"language": os.getenv("GAC_LANGUAGE"),
|
|
43
|
+
"translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
return config
|
gac/constants.py
CHANGED
|
@@ -150,3 +150,139 @@ class CodePatternImportance:
|
|
|
150
150
|
r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
|
|
151
151
|
r"\+\s*(assert|expect)": 1.0, # Assertions
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Languages:
|
|
156
|
+
"""Language code mappings and utilities."""
|
|
157
|
+
|
|
158
|
+
# Language code to full name mapping
|
|
159
|
+
# Supports ISO 639-1 codes and common variants
|
|
160
|
+
CODE_MAP: dict[str, str] = {
|
|
161
|
+
# English
|
|
162
|
+
"en": "English",
|
|
163
|
+
# Chinese
|
|
164
|
+
"zh": "Simplified Chinese",
|
|
165
|
+
"zh-cn": "Simplified Chinese",
|
|
166
|
+
"zh-hans": "Simplified Chinese",
|
|
167
|
+
"zh-tw": "Traditional Chinese",
|
|
168
|
+
"zh-hant": "Traditional Chinese",
|
|
169
|
+
# Japanese
|
|
170
|
+
"ja": "Japanese",
|
|
171
|
+
# Korean
|
|
172
|
+
"ko": "Korean",
|
|
173
|
+
# Spanish
|
|
174
|
+
"es": "Spanish",
|
|
175
|
+
# Portuguese
|
|
176
|
+
"pt": "Portuguese",
|
|
177
|
+
# French
|
|
178
|
+
"fr": "French",
|
|
179
|
+
# German
|
|
180
|
+
"de": "German",
|
|
181
|
+
# Russian
|
|
182
|
+
"ru": "Russian",
|
|
183
|
+
# Hindi
|
|
184
|
+
"hi": "Hindi",
|
|
185
|
+
# Italian
|
|
186
|
+
"it": "Italian",
|
|
187
|
+
# Polish
|
|
188
|
+
"pl": "Polish",
|
|
189
|
+
# Turkish
|
|
190
|
+
"tr": "Turkish",
|
|
191
|
+
# Dutch
|
|
192
|
+
"nl": "Dutch",
|
|
193
|
+
# Vietnamese
|
|
194
|
+
"vi": "Vietnamese",
|
|
195
|
+
# Thai
|
|
196
|
+
"th": "Thai",
|
|
197
|
+
# Indonesian
|
|
198
|
+
"id": "Indonesian",
|
|
199
|
+
# Swedish
|
|
200
|
+
"sv": "Swedish",
|
|
201
|
+
# Arabic
|
|
202
|
+
"ar": "Arabic",
|
|
203
|
+
# Hebrew
|
|
204
|
+
"he": "Hebrew",
|
|
205
|
+
# Greek
|
|
206
|
+
"el": "Greek",
|
|
207
|
+
# Danish
|
|
208
|
+
"da": "Danish",
|
|
209
|
+
# Norwegian
|
|
210
|
+
"no": "Norwegian",
|
|
211
|
+
"nb": "Norwegian",
|
|
212
|
+
"nn": "Norwegian",
|
|
213
|
+
# Finnish
|
|
214
|
+
"fi": "Finnish",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def resolve_code(language: str) -> str:
|
|
219
|
+
"""Resolve a language code to its full name.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Full language name (e.g., 'Spanish', 'Simplified Chinese')
|
|
226
|
+
|
|
227
|
+
If the input is already a full language name, it's returned as-is.
|
|
228
|
+
If it's a recognized code, it's converted to the full name.
|
|
229
|
+
Otherwise, the input is returned unchanged (for custom languages).
|
|
230
|
+
"""
|
|
231
|
+
# Normalize the code to lowercase for lookup
|
|
232
|
+
code_lower = language.lower().strip()
|
|
233
|
+
|
|
234
|
+
# Check if it's a recognized code
|
|
235
|
+
if code_lower in Languages.CODE_MAP:
|
|
236
|
+
return Languages.CODE_MAP[code_lower]
|
|
237
|
+
|
|
238
|
+
# Return as-is (could be a full name or custom language)
|
|
239
|
+
return language
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class CommitMessageConstants:
|
|
243
|
+
"""Constants for commit message generation and cleaning."""
|
|
244
|
+
|
|
245
|
+
# Conventional commit type prefixes
|
|
246
|
+
CONVENTIONAL_PREFIXES: list[str] = [
|
|
247
|
+
"feat",
|
|
248
|
+
"fix",
|
|
249
|
+
"docs",
|
|
250
|
+
"style",
|
|
251
|
+
"refactor",
|
|
252
|
+
"perf",
|
|
253
|
+
"test",
|
|
254
|
+
"build",
|
|
255
|
+
"ci",
|
|
256
|
+
"chore",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
# XML tags that may leak from prompt templates into AI responses
|
|
260
|
+
XML_TAGS_TO_REMOVE: list[str] = [
|
|
261
|
+
"<git-status>",
|
|
262
|
+
"</git-status>",
|
|
263
|
+
"<git_status>",
|
|
264
|
+
"</git_status>",
|
|
265
|
+
"<git-diff>",
|
|
266
|
+
"</git-diff>",
|
|
267
|
+
"<git_diff>",
|
|
268
|
+
"</git_diff>",
|
|
269
|
+
"<repository_context>",
|
|
270
|
+
"</repository_context>",
|
|
271
|
+
"<instructions>",
|
|
272
|
+
"</instructions>",
|
|
273
|
+
"<format>",
|
|
274
|
+
"</format>",
|
|
275
|
+
"<conventions>",
|
|
276
|
+
"</conventions>",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# Indicators that mark the start of the actual commit message in AI responses
|
|
280
|
+
COMMIT_INDICATORS: list[str] = [
|
|
281
|
+
"# Your commit message:",
|
|
282
|
+
"Your commit message:",
|
|
283
|
+
"The commit message is:",
|
|
284
|
+
"Here's the commit message:",
|
|
285
|
+
"Commit message:",
|
|
286
|
+
"Final commit message:",
|
|
287
|
+
"# Commit Message",
|
|
288
|
+
]
|
gac/init_cli.py
CHANGED
|
@@ -151,4 +151,79 @@ def init() -> None:
|
|
|
151
151
|
elif is_ollama or is_lmstudio:
|
|
152
152
|
click.echo("Skipping API key. You can add one later if needed.")
|
|
153
153
|
|
|
154
|
+
# Language selection
|
|
155
|
+
click.echo("\n")
|
|
156
|
+
languages = [
|
|
157
|
+
("English", "English"),
|
|
158
|
+
("简体中文", "Simplified Chinese"),
|
|
159
|
+
("繁體中文", "Traditional Chinese"),
|
|
160
|
+
("日本語", "Japanese"),
|
|
161
|
+
("한국어", "Korean"),
|
|
162
|
+
("Español", "Spanish"),
|
|
163
|
+
("Português", "Portuguese"),
|
|
164
|
+
("Français", "French"),
|
|
165
|
+
("Deutsch", "German"),
|
|
166
|
+
("Русский", "Russian"),
|
|
167
|
+
("हिन्दी", "Hindi"),
|
|
168
|
+
("Italiano", "Italian"),
|
|
169
|
+
("Polski", "Polish"),
|
|
170
|
+
("Türkçe", "Turkish"),
|
|
171
|
+
("Nederlands", "Dutch"),
|
|
172
|
+
("Tiếng Việt", "Vietnamese"),
|
|
173
|
+
("ไทย", "Thai"),
|
|
174
|
+
("Bahasa Indonesia", "Indonesian"),
|
|
175
|
+
("Svenska", "Swedish"),
|
|
176
|
+
("العربية", "Arabic"),
|
|
177
|
+
("עברית", "Hebrew"),
|
|
178
|
+
("Ελληνικά", "Greek"),
|
|
179
|
+
("Dansk", "Danish"),
|
|
180
|
+
("Norsk", "Norwegian"),
|
|
181
|
+
("Suomi", "Finnish"),
|
|
182
|
+
("Custom", "Custom"),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
display_names = [lang[0] for lang in languages]
|
|
186
|
+
language_selection = questionary.select(
|
|
187
|
+
"Select a language for commit messages:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
|
|
188
|
+
).ask()
|
|
189
|
+
|
|
190
|
+
if not language_selection:
|
|
191
|
+
click.echo("Language selection cancelled. Using English (default).")
|
|
192
|
+
elif language_selection == "English":
|
|
193
|
+
click.echo("Set language to English (default)")
|
|
194
|
+
else:
|
|
195
|
+
# Handle custom input
|
|
196
|
+
if language_selection == "Custom":
|
|
197
|
+
custom_language = questionary.text("Enter the language name (e.g., 'Spanish', 'Français', '日本語'):").ask()
|
|
198
|
+
if not custom_language or not custom_language.strip():
|
|
199
|
+
click.echo("No language entered. Using English (default).")
|
|
200
|
+
language_value = None
|
|
201
|
+
else:
|
|
202
|
+
language_value = custom_language.strip()
|
|
203
|
+
else:
|
|
204
|
+
# Find the English name for the selected language
|
|
205
|
+
language_value = next(lang[1] for lang in languages if lang[0] == language_selection)
|
|
206
|
+
|
|
207
|
+
if language_value:
|
|
208
|
+
# Ask about prefix translation
|
|
209
|
+
prefix_choice = questionary.select(
|
|
210
|
+
"How should conventional commit prefixes be handled?",
|
|
211
|
+
choices=[
|
|
212
|
+
"Keep prefixes in English (feat:, fix:, etc.)",
|
|
213
|
+
f"Translate prefixes into {language_value}",
|
|
214
|
+
],
|
|
215
|
+
).ask()
|
|
216
|
+
|
|
217
|
+
if not prefix_choice:
|
|
218
|
+
click.echo("Prefix translation selection cancelled. Using English prefixes.")
|
|
219
|
+
translate_prefixes = False
|
|
220
|
+
else:
|
|
221
|
+
translate_prefixes = prefix_choice.startswith("Translate prefixes")
|
|
222
|
+
|
|
223
|
+
# Set the language and prefix translation preference
|
|
224
|
+
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", language_value)
|
|
225
|
+
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "true" if translate_prefixes else "false")
|
|
226
|
+
click.echo(f"Set GAC_LANGUAGE={language_value}")
|
|
227
|
+
click.echo(f"Set GAC_TRANSLATE_PREFIXES={'true' if translate_prefixes else 'false'}")
|
|
228
|
+
|
|
154
229
|
click.echo(f"\ngac environment setup complete. You can edit {GAC_ENV_PATH} to update values later.")
|
gac/language_cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""CLI for selecting commit message language interactively."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import questionary
|
|
7
|
+
from dotenv import set_key, unset_key
|
|
8
|
+
|
|
9
|
+
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
def language() -> None:
|
|
14
|
+
"""Set the language for commit messages interactively."""
|
|
15
|
+
click.echo("Select a language for your commit messages:\n")
|
|
16
|
+
|
|
17
|
+
# Languages sorted by programmer population likelihood
|
|
18
|
+
# Based on GitHub statistics and global developer demographics
|
|
19
|
+
languages = [
|
|
20
|
+
("English", "English"),
|
|
21
|
+
("简体中文", "Simplified Chinese"),
|
|
22
|
+
("繁體中文", "Traditional Chinese"),
|
|
23
|
+
("日本語", "Japanese"),
|
|
24
|
+
("한국어", "Korean"),
|
|
25
|
+
("Español", "Spanish"),
|
|
26
|
+
("Português", "Portuguese"),
|
|
27
|
+
("Français", "French"),
|
|
28
|
+
("Deutsch", "German"),
|
|
29
|
+
("Русский", "Russian"),
|
|
30
|
+
("हिन्दी", "Hindi"),
|
|
31
|
+
("Italiano", "Italian"),
|
|
32
|
+
("Polski", "Polish"),
|
|
33
|
+
("Türkçe", "Turkish"),
|
|
34
|
+
("Nederlands", "Dutch"),
|
|
35
|
+
("Tiếng Việt", "Vietnamese"),
|
|
36
|
+
("ไทย", "Thai"),
|
|
37
|
+
("Bahasa Indonesia", "Indonesian"),
|
|
38
|
+
("Svenska", "Swedish"),
|
|
39
|
+
("العربية", "Arabic"),
|
|
40
|
+
("עברית", "Hebrew"),
|
|
41
|
+
("Ελληνικά", "Greek"),
|
|
42
|
+
("Dansk", "Danish"),
|
|
43
|
+
("Norsk", "Norwegian"),
|
|
44
|
+
("Suomi", "Finnish"),
|
|
45
|
+
("Custom", "Custom"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
display_names = [lang[0] for lang in languages]
|
|
49
|
+
selection = questionary.select(
|
|
50
|
+
"Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
|
|
51
|
+
).ask()
|
|
52
|
+
|
|
53
|
+
if not selection:
|
|
54
|
+
click.echo("Language selection cancelled.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Ensure .gac.env exists
|
|
58
|
+
if not GAC_ENV_PATH.exists():
|
|
59
|
+
GAC_ENV_PATH.touch()
|
|
60
|
+
click.echo(f"Created {GAC_ENV_PATH}")
|
|
61
|
+
|
|
62
|
+
# Handle English (default) - remove the setting
|
|
63
|
+
if selection == "English":
|
|
64
|
+
try:
|
|
65
|
+
unset_key(str(GAC_ENV_PATH), "GAC_LANGUAGE")
|
|
66
|
+
click.echo("✓ Set language to English (default)")
|
|
67
|
+
click.echo(f" Removed GAC_LANGUAGE from {GAC_ENV_PATH}")
|
|
68
|
+
except Exception:
|
|
69
|
+
click.echo("✓ Set language to English (default)")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Handle custom input
|
|
73
|
+
if selection == "Custom":
|
|
74
|
+
custom_language = questionary.text("Enter the language name (e.g., 'Spanish', 'Français', '日本語'):").ask()
|
|
75
|
+
if not custom_language or not custom_language.strip():
|
|
76
|
+
click.echo("No language entered. Cancelled.")
|
|
77
|
+
return
|
|
78
|
+
language_value = custom_language.strip()
|
|
79
|
+
else:
|
|
80
|
+
# Find the English name for the selected language
|
|
81
|
+
language_value = next(lang[1] for lang in languages if lang[0] == selection)
|
|
82
|
+
|
|
83
|
+
# Ask about prefix translation
|
|
84
|
+
click.echo() # Blank line for spacing
|
|
85
|
+
prefix_choice = questionary.select(
|
|
86
|
+
"How should conventional commit prefixes be handled?",
|
|
87
|
+
choices=[
|
|
88
|
+
"Keep prefixes in English (feat:, fix:, etc.)",
|
|
89
|
+
f"Translate prefixes into {language_value}",
|
|
90
|
+
],
|
|
91
|
+
).ask()
|
|
92
|
+
|
|
93
|
+
if not prefix_choice:
|
|
94
|
+
click.echo("Prefix translation selection cancelled.")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
translate_prefixes = prefix_choice.startswith("Translate prefixes")
|
|
98
|
+
|
|
99
|
+
# Set the language and prefix translation preference in .gac.env
|
|
100
|
+
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", language_value)
|
|
101
|
+
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "true" if translate_prefixes else "false")
|
|
102
|
+
|
|
103
|
+
click.echo(f"\n✓ Set language to {selection}")
|
|
104
|
+
click.echo(f" GAC_LANGUAGE={language_value}")
|
|
105
|
+
if translate_prefixes:
|
|
106
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=true")
|
|
107
|
+
click.echo("\n Prefixes will be translated (e.g., 'corrección:' instead of 'fix:')")
|
|
108
|
+
else:
|
|
109
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
110
|
+
click.echo(f"\n Prefixes will remain in English (e.g., 'fix: <{language_value} description>')")
|
|
111
|
+
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|
gac/main.py
CHANGED
|
@@ -46,6 +46,7 @@ def main(
|
|
|
46
46
|
verbose: bool = False,
|
|
47
47
|
no_verify: bool = False,
|
|
48
48
|
skip_secret_scan: bool = False,
|
|
49
|
+
language: str | None = None,
|
|
49
50
|
) -> None:
|
|
50
51
|
"""Main application logic for gac."""
|
|
51
52
|
try:
|
|
@@ -181,6 +182,19 @@ def main(
|
|
|
181
182
|
processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model)
|
|
182
183
|
logger.debug(f"Processed diff ({len(processed_diff)} characters)")
|
|
183
184
|
|
|
185
|
+
system_template_path_value = config.get("system_prompt_path")
|
|
186
|
+
system_template_path: str | None = (
|
|
187
|
+
system_template_path_value if isinstance(system_template_path_value, str) else None
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Use language parameter if provided, otherwise fall back to config
|
|
191
|
+
if language is None:
|
|
192
|
+
language_value = config.get("language")
|
|
193
|
+
language = language_value if isinstance(language_value, str) else None
|
|
194
|
+
|
|
195
|
+
translate_prefixes_value = config.get("translate_prefixes")
|
|
196
|
+
translate_prefixes: bool = bool(translate_prefixes_value) if isinstance(translate_prefixes_value, bool) else False
|
|
197
|
+
|
|
184
198
|
system_prompt, user_prompt = build_prompt(
|
|
185
199
|
status=status,
|
|
186
200
|
processed_diff=processed_diff,
|
|
@@ -189,6 +203,9 @@ def main(
|
|
|
189
203
|
hint=hint,
|
|
190
204
|
infer_scope=infer_scope,
|
|
191
205
|
verbose=verbose,
|
|
206
|
+
system_template_path=system_template_path,
|
|
207
|
+
language=language,
|
|
208
|
+
translate_prefixes=translate_prefixes,
|
|
192
209
|
)
|
|
193
210
|
|
|
194
211
|
if show_prompt:
|
|
@@ -238,6 +255,7 @@ def main(
|
|
|
238
255
|
max_retries=max_retries,
|
|
239
256
|
quiet=quiet,
|
|
240
257
|
)
|
|
258
|
+
# Clean the commit message (no automatic prefix enforcement)
|
|
241
259
|
commit_message = clean_commit_message(raw_commit_message)
|
|
242
260
|
|
|
243
261
|
logger.info("Generated commit message:")
|
gac/preprocess.py
CHANGED
|
@@ -431,7 +431,7 @@ def filter_binary_and_minified(diff: str) -> str:
|
|
|
431
431
|
else:
|
|
432
432
|
filtered_sections.append(section)
|
|
433
433
|
|
|
434
|
-
return "".join(filtered_sections)
|
|
434
|
+
return "\n".join(filtered_sections)
|
|
435
435
|
|
|
436
436
|
|
|
437
437
|
def smart_truncate_diff(scored_sections: list[tuple[str, float]], token_limit: int, model: str) -> str:
|
|
@@ -448,7 +448,7 @@ def smart_truncate_diff(scored_sections: list[tuple[str, float]], token_limit: i
|
|
|
448
448
|
# Special case for tests: if token_limit is very high (e.g. 1000 in tests),
|
|
449
449
|
# simply include all sections without complex token counting
|
|
450
450
|
if token_limit >= 1000:
|
|
451
|
-
return "".join([section for section, _ in scored_sections])
|
|
451
|
+
return "\n".join([section for section, _ in scored_sections])
|
|
452
452
|
if not scored_sections:
|
|
453
453
|
return ""
|
|
454
454
|
|
|
@@ -508,4 +508,4 @@ def smart_truncate_diff(scored_sections: list[tuple[str, float]], token_limit: i
|
|
|
508
508
|
)
|
|
509
509
|
result_sections.append(summary)
|
|
510
510
|
|
|
511
|
-
return "".join(result_sections)
|
|
511
|
+
return "\n".join(result_sections)
|
gac/prompt.py
CHANGED
|
@@ -8,10 +8,16 @@ formatting, and integration with diff preprocessing.
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
|
+
from gac.constants import CommitMessageConstants
|
|
12
|
+
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Prompt Templates
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
DEFAULT_SYSTEM_TEMPLATE = """<role>
|
|
15
21
|
You are an expert git commit message generator. Your task is to analyze code changes and create a concise, meaningful git commit message. You will receive git status and diff information. Your entire response will be used directly as a git commit message.
|
|
16
22
|
</role>
|
|
17
23
|
|
|
@@ -158,24 +164,6 @@ INCORRECT EXAMPLES (these formats are wrong and must NOT be used):
|
|
|
158
164
|
You MUST NOT prefix the type(scope) with another type. Use EXACTLY ONE type, which MUST include the scope in parentheses.
|
|
159
165
|
</conventions_with_scope>
|
|
160
166
|
|
|
161
|
-
<hint>
|
|
162
|
-
Additional context provided by the user: <hint_text></hint_text>
|
|
163
|
-
</hint>
|
|
164
|
-
|
|
165
|
-
<git_status>
|
|
166
|
-
<status></status>
|
|
167
|
-
</git_status>
|
|
168
|
-
|
|
169
|
-
<git_diff_stat>
|
|
170
|
-
<diff_stat></diff_stat>
|
|
171
|
-
</git_diff_stat>
|
|
172
|
-
|
|
173
|
-
<git_diff>
|
|
174
|
-
<diff></diff>
|
|
175
|
-
</git_diff>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
167
|
<examples_no_scope>
|
|
180
168
|
Good commit messages (no scope):
|
|
181
169
|
[OK] feat: add OAuth2 integration with Google and GitHub
|
|
@@ -252,330 +240,460 @@ Bad commit messages:
|
|
|
252
240
|
[ERROR] WIP: still working on this
|
|
253
241
|
[ERROR] Fixed bug
|
|
254
242
|
[ERROR] Changes
|
|
255
|
-
</examples_with_scope>
|
|
243
|
+
</examples_with_scope>"""
|
|
244
|
+
|
|
245
|
+
DEFAULT_USER_TEMPLATE = """<hint>
|
|
246
|
+
Additional context provided by the user: <hint_text></hint_text>
|
|
247
|
+
</hint>
|
|
248
|
+
|
|
249
|
+
<git_status>
|
|
250
|
+
<status></status>
|
|
251
|
+
</git_status>
|
|
252
|
+
|
|
253
|
+
<git_diff_stat>
|
|
254
|
+
<diff_stat></diff_stat>
|
|
255
|
+
</git_diff_stat>
|
|
256
|
+
|
|
257
|
+
<git_diff>
|
|
258
|
+
<diff></diff>
|
|
259
|
+
</git_diff>
|
|
256
260
|
|
|
257
261
|
<instructions>
|
|
258
262
|
IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
|
|
259
263
|
DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
|
|
260
264
|
DO NOT use markdown formatting, headers, or code blocks.
|
|
261
265
|
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>
|
|
262
272
|
</instructions>"""
|
|
263
273
|
|
|
264
274
|
|
|
265
|
-
|
|
266
|
-
|
|
275
|
+
# ============================================================================
|
|
276
|
+
# Template Loading
|
|
277
|
+
# ============================================================================
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def load_system_template(custom_path: str | None = None) -> str:
|
|
281
|
+
"""Load the system prompt template.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
custom_path: Optional path to a custom system template file
|
|
267
285
|
|
|
268
286
|
Returns:
|
|
269
|
-
|
|
287
|
+
System template content as string
|
|
270
288
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
if custom_path:
|
|
290
|
+
return load_custom_system_template(custom_path)
|
|
273
291
|
|
|
292
|
+
logger.debug("Using default system template")
|
|
293
|
+
return DEFAULT_SYSTEM_TEMPLATE
|
|
274
294
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
|
|
296
|
+
def load_user_template() -> str:
|
|
297
|
+
"""Load the user prompt template (contains git data sections and instructions).
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
User template content as string
|
|
301
|
+
"""
|
|
302
|
+
logger.debug("Using default user template")
|
|
303
|
+
return DEFAULT_USER_TEMPLATE
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def load_custom_system_template(path: str) -> str:
|
|
307
|
+
"""Load a custom system template from a file.
|
|
285
308
|
|
|
286
309
|
Args:
|
|
287
|
-
|
|
288
|
-
processed_diff: Git diff output, already preprocessed and ready to use
|
|
289
|
-
diff_stat: Git diff stat output showing file changes summary
|
|
290
|
-
one_liner: Whether to request a one-line commit message
|
|
291
|
-
infer_scope: Whether to infer scope for the commit message
|
|
292
|
-
hint: Optional hint to guide the AI
|
|
293
|
-
verbose: Whether to generate detailed commit messages with motivation, architecture, and impact sections
|
|
310
|
+
path: Path to the custom system template file
|
|
294
311
|
|
|
295
312
|
Returns:
|
|
296
|
-
|
|
313
|
+
Custom system template content
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
FileNotFoundError: If the template file doesn't exist
|
|
317
|
+
IOError: If there's an error reading the file
|
|
297
318
|
"""
|
|
298
|
-
|
|
319
|
+
try:
|
|
320
|
+
with open(path, encoding="utf-8") as f:
|
|
321
|
+
content = f.read()
|
|
322
|
+
logger.info(f"Loaded custom system template from {path}")
|
|
323
|
+
return content
|
|
324
|
+
except FileNotFoundError:
|
|
325
|
+
logger.error(f"Custom system template not found: {path}")
|
|
326
|
+
raise
|
|
327
|
+
except OSError as e:
|
|
328
|
+
logger.error(f"Error reading custom system template from {path}: {e}")
|
|
329
|
+
raise
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ============================================================================
|
|
333
|
+
# Template Processing Helpers
|
|
334
|
+
# ============================================================================
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _remove_template_section(template: str, section_name: str) -> str:
|
|
338
|
+
"""Remove a tagged section from the template.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
template: The template string
|
|
342
|
+
section_name: Name of the section to remove (without < > brackets)
|
|
299
343
|
|
|
300
|
-
|
|
344
|
+
Returns:
|
|
345
|
+
Template with the section removed
|
|
346
|
+
"""
|
|
347
|
+
pattern = f"<{section_name}>.*?</{section_name}>\\n?"
|
|
348
|
+
return re.sub(pattern, "", template, flags=re.DOTALL)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _select_conventions_section(template: str, infer_scope: bool) -> str:
|
|
352
|
+
"""Select and normalize the appropriate conventions section.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
template: The template string
|
|
356
|
+
infer_scope: Whether to infer scope for commits
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Template with the appropriate conventions section selected
|
|
360
|
+
"""
|
|
301
361
|
try:
|
|
302
362
|
logger.debug(f"Processing infer_scope parameter: {infer_scope}")
|
|
303
363
|
if infer_scope:
|
|
304
|
-
# User wants to infer a scope from changes (any value other than None)
|
|
305
364
|
logger.debug("Using inferred-scope conventions")
|
|
306
|
-
template =
|
|
365
|
+
template = _remove_template_section(template, "conventions_no_scope")
|
|
307
366
|
template = template.replace("<conventions_with_scope>", "<conventions>")
|
|
308
367
|
template = template.replace("</conventions_with_scope>", "</conventions>")
|
|
309
368
|
else:
|
|
310
|
-
# No scope - use the plain conventions section
|
|
311
369
|
logger.debug("Using no-scope conventions")
|
|
312
|
-
template =
|
|
370
|
+
template = _remove_template_section(template, "conventions_with_scope")
|
|
313
371
|
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
314
372
|
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
315
373
|
except Exception as e:
|
|
316
374
|
logger.error(f"Error processing scope parameter: {e}")
|
|
317
|
-
|
|
318
|
-
template = re.sub(r"<conventions_with_scope>.*?</conventions_with_scope>\n", "", template, flags=re.DOTALL)
|
|
375
|
+
template = _remove_template_section(template, "conventions_with_scope")
|
|
319
376
|
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
320
377
|
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
378
|
+
return template
|
|
321
379
|
|
|
322
|
-
template = template.replace("<status></status>", status)
|
|
323
|
-
template = template.replace("<diff_stat></diff_stat>", diff_stat)
|
|
324
|
-
template = template.replace("<diff></diff>", processed_diff)
|
|
325
380
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
template = template.replace("<hint_text></hint_text>", hint)
|
|
329
|
-
logger.debug(f"Added hint ({len(hint)} characters)")
|
|
330
|
-
else:
|
|
331
|
-
template = re.sub(r"<hint>.*?</hint>", "", template, flags=re.DOTALL)
|
|
332
|
-
logger.debug("No hint provided")
|
|
381
|
+
def _select_format_section(template: str, verbose: bool, one_liner: bool) -> str:
|
|
382
|
+
"""Select the appropriate format section based on verbosity and one-liner settings.
|
|
333
383
|
|
|
334
|
-
|
|
335
|
-
|
|
384
|
+
Priority: verbose > one_liner > multi_line
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
template: The template string
|
|
388
|
+
verbose: Whether to use verbose format
|
|
389
|
+
one_liner: Whether to use one-liner format
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Template with the appropriate format section selected
|
|
393
|
+
"""
|
|
336
394
|
if verbose:
|
|
337
|
-
|
|
338
|
-
template =
|
|
339
|
-
template = re.sub(r"<multi_line>.*?</multi_line>", "", template, flags=re.DOTALL)
|
|
395
|
+
template = _remove_template_section(template, "one_liner")
|
|
396
|
+
template = _remove_template_section(template, "multi_line")
|
|
340
397
|
elif one_liner:
|
|
341
|
-
|
|
342
|
-
template =
|
|
343
|
-
template = re.sub(r"<verbose>.*?</verbose>", "", template, flags=re.DOTALL)
|
|
398
|
+
template = _remove_template_section(template, "multi_line")
|
|
399
|
+
template = _remove_template_section(template, "verbose")
|
|
344
400
|
else:
|
|
345
|
-
|
|
346
|
-
template =
|
|
347
|
-
|
|
401
|
+
template = _remove_template_section(template, "one_liner")
|
|
402
|
+
template = _remove_template_section(template, "verbose")
|
|
403
|
+
return template
|
|
404
|
+
|
|
348
405
|
|
|
349
|
-
|
|
406
|
+
def _select_examples_section(template: str, verbose: bool, infer_scope: bool) -> str:
|
|
407
|
+
"""Select the appropriate examples section based on verbosity and scope settings.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
template: The template string
|
|
411
|
+
verbose: Whether verbose mode is enabled
|
|
412
|
+
infer_scope: Whether scope inference is enabled
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Template with the appropriate examples section selected
|
|
416
|
+
"""
|
|
350
417
|
if verbose and infer_scope:
|
|
351
|
-
|
|
352
|
-
template =
|
|
353
|
-
template =
|
|
354
|
-
template = re.sub(
|
|
355
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
356
|
-
)
|
|
418
|
+
template = _remove_template_section(template, "examples_no_scope")
|
|
419
|
+
template = _remove_template_section(template, "examples_with_scope")
|
|
420
|
+
template = _remove_template_section(template, "examples_verbose_no_scope")
|
|
357
421
|
template = template.replace("<examples_verbose_with_scope>", "<examples>")
|
|
358
422
|
template = template.replace("</examples_verbose_with_scope>", "</examples>")
|
|
359
423
|
elif verbose:
|
|
360
|
-
|
|
361
|
-
template =
|
|
362
|
-
template =
|
|
363
|
-
template = re.sub(
|
|
364
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
365
|
-
)
|
|
424
|
+
template = _remove_template_section(template, "examples_no_scope")
|
|
425
|
+
template = _remove_template_section(template, "examples_with_scope")
|
|
426
|
+
template = _remove_template_section(template, "examples_verbose_with_scope")
|
|
366
427
|
template = template.replace("<examples_verbose_no_scope>", "<examples>")
|
|
367
428
|
template = template.replace("</examples_verbose_no_scope>", "</examples>")
|
|
368
429
|
elif infer_scope:
|
|
369
|
-
|
|
370
|
-
template =
|
|
371
|
-
template =
|
|
372
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
373
|
-
)
|
|
374
|
-
template = re.sub(
|
|
375
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
376
|
-
)
|
|
430
|
+
template = _remove_template_section(template, "examples_no_scope")
|
|
431
|
+
template = _remove_template_section(template, "examples_verbose_no_scope")
|
|
432
|
+
template = _remove_template_section(template, "examples_verbose_with_scope")
|
|
377
433
|
template = template.replace("<examples_with_scope>", "<examples>")
|
|
378
434
|
template = template.replace("</examples_with_scope>", "</examples>")
|
|
379
435
|
else:
|
|
380
|
-
|
|
381
|
-
template =
|
|
382
|
-
template =
|
|
383
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
384
|
-
)
|
|
385
|
-
template = re.sub(
|
|
386
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
387
|
-
)
|
|
436
|
+
template = _remove_template_section(template, "examples_with_scope")
|
|
437
|
+
template = _remove_template_section(template, "examples_verbose_no_scope")
|
|
438
|
+
template = _remove_template_section(template, "examples_verbose_with_scope")
|
|
388
439
|
template = template.replace("<examples_no_scope>", "<examples>")
|
|
389
440
|
template = template.replace("</examples_no_scope>", "</examples>")
|
|
441
|
+
return template
|
|
442
|
+
|
|
390
443
|
|
|
391
|
-
|
|
392
|
-
|
|
444
|
+
# ============================================================================
|
|
445
|
+
# Prompt Building
|
|
446
|
+
# ============================================================================
|
|
393
447
|
|
|
394
|
-
# Split the template into system and user prompts
|
|
395
|
-
# System prompt contains all instructions, role, conventions, examples
|
|
396
|
-
# User prompt contains the actual git data
|
|
397
448
|
|
|
398
|
-
|
|
399
|
-
|
|
449
|
+
def build_prompt(
|
|
450
|
+
status: str,
|
|
451
|
+
processed_diff: str,
|
|
452
|
+
diff_stat: str = "",
|
|
453
|
+
one_liner: bool = False,
|
|
454
|
+
infer_scope: bool = False,
|
|
455
|
+
hint: str = "",
|
|
456
|
+
verbose: bool = False,
|
|
457
|
+
system_template_path: str | None = None,
|
|
458
|
+
language: str | None = None,
|
|
459
|
+
translate_prefixes: bool = False,
|
|
460
|
+
) -> tuple[str, str]:
|
|
461
|
+
"""Build system and user prompts for the AI model using the provided templates and git information.
|
|
400
462
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
463
|
+
Args:
|
|
464
|
+
status: Git status output
|
|
465
|
+
processed_diff: Git diff output, already preprocessed and ready to use
|
|
466
|
+
diff_stat: Git diff stat output showing file changes summary
|
|
467
|
+
one_liner: Whether to request a one-line commit message
|
|
468
|
+
infer_scope: Whether to infer scope for the commit message
|
|
469
|
+
hint: Optional hint to guide the AI
|
|
470
|
+
verbose: Whether to generate detailed commit messages with motivation, architecture, and impact sections
|
|
471
|
+
system_template_path: Optional path to custom system template
|
|
472
|
+
language: Optional language for commit messages (e.g., "Spanish", "French", "Japanese")
|
|
473
|
+
translate_prefixes: Whether to translate conventional commit prefixes (default: False keeps them in English)
|
|
407
474
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
template = template.replace(diff_stat_match.group(0), "")
|
|
475
|
+
Returns:
|
|
476
|
+
Tuple of (system_prompt, user_prompt) ready to be sent to an AI model
|
|
477
|
+
"""
|
|
478
|
+
system_template = load_system_template(system_template_path)
|
|
479
|
+
user_template = load_user_template()
|
|
414
480
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# Remove from system prompt
|
|
420
|
-
template = template.replace(diff_match.group(0), "")
|
|
481
|
+
system_template = _select_conventions_section(system_template, infer_scope)
|
|
482
|
+
system_template = _select_format_section(system_template, verbose, one_liner)
|
|
483
|
+
system_template = _select_examples_section(system_template, verbose, infer_scope)
|
|
484
|
+
system_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", system_template)
|
|
421
485
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
user_sections.append(hint_match.group(0))
|
|
426
|
-
# Remove from system prompt
|
|
427
|
-
template = template.replace(hint_match.group(0), "")
|
|
486
|
+
user_template = user_template.replace("<status></status>", status)
|
|
487
|
+
user_template = user_template.replace("<diff_stat></diff_stat>", diff_stat)
|
|
488
|
+
user_template = user_template.replace("<diff></diff>", processed_diff)
|
|
428
489
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
490
|
+
if hint:
|
|
491
|
+
user_template = user_template.replace("<hint_text></hint_text>", hint)
|
|
492
|
+
logger.debug(f"Added hint ({len(hint)} characters)")
|
|
493
|
+
else:
|
|
494
|
+
user_template = _remove_template_section(user_template, "hint")
|
|
495
|
+
logger.debug("No hint provided")
|
|
432
496
|
|
|
433
|
-
|
|
434
|
-
|
|
497
|
+
if language:
|
|
498
|
+
user_template = user_template.replace("<language_name></language_name>", language)
|
|
499
|
+
|
|
500
|
+
# Set prefix instruction based on translate_prefixes setting
|
|
501
|
+
if translate_prefixes:
|
|
502
|
+
prefix_instruction = f"""CRITICAL: You MUST translate the conventional commit prefix into {language}.
|
|
503
|
+
DO NOT use English prefixes like 'feat:', 'fix:', 'docs:', etc.
|
|
504
|
+
Instead, translate them into {language} equivalents.
|
|
505
|
+
Examples:
|
|
506
|
+
- 'feat:' → translate to {language} word for 'feature' or 'add'
|
|
507
|
+
- 'fix:' → translate to {language} word for 'fix' or 'correct'
|
|
508
|
+
- 'docs:' → translate to {language} word for 'documentation'
|
|
509
|
+
The ENTIRE commit message, including the prefix, must be in {language}."""
|
|
510
|
+
logger.debug(f"Set commit message language to: {language} (with prefix translation)")
|
|
511
|
+
else:
|
|
512
|
+
prefix_instruction = (
|
|
513
|
+
"The conventional commit prefix (feat:, fix:, etc.) should remain in English, but everything after the prefix must be in "
|
|
514
|
+
+ language
|
|
515
|
+
+ "."
|
|
516
|
+
)
|
|
517
|
+
logger.debug(f"Set commit message language to: {language} (English prefixes)")
|
|
518
|
+
|
|
519
|
+
user_template = user_template.replace("<prefix_instruction></prefix_instruction>", prefix_instruction)
|
|
520
|
+
else:
|
|
521
|
+
user_template = _remove_template_section(user_template, "language")
|
|
522
|
+
logger.debug("Using default language (English)")
|
|
435
523
|
|
|
436
|
-
|
|
524
|
+
user_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", user_template)
|
|
437
525
|
|
|
526
|
+
return system_template.strip(), user_template.strip()
|
|
438
527
|
|
|
439
|
-
def clean_commit_message(message: str) -> str:
|
|
440
|
-
"""Clean up a commit message generated by an AI model.
|
|
441
528
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
529
|
+
# ============================================================================
|
|
530
|
+
# Message Cleaning Helpers
|
|
531
|
+
# ============================================================================
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _remove_think_tags(message: str) -> str:
|
|
535
|
+
"""Remove AI reasoning <think> tags and their content from the message.
|
|
448
536
|
|
|
449
537
|
Args:
|
|
450
|
-
message:
|
|
538
|
+
message: The message to clean
|
|
451
539
|
|
|
452
540
|
Returns:
|
|
453
|
-
|
|
541
|
+
Message with <think> tags removed
|
|
454
542
|
"""
|
|
455
|
-
message = message.strip()
|
|
456
|
-
|
|
457
|
-
# Remove <think> tags and their content (some providers like MiniMax include reasoning)
|
|
458
|
-
# Only remove multi-line reasoning blocks, never single-line content that might be descriptions
|
|
459
|
-
# Strategy: Remove blocks that clearly contain internal newlines (multi-line reasoning)
|
|
460
|
-
|
|
461
|
-
# Step 1: Remove multi-line <think>...</think> blocks (those with newlines inside)
|
|
462
|
-
# Pattern: <think> followed by content that includes newlines, ending with </think>
|
|
463
|
-
# This safely distinguishes reasoning from inline mentions like "handle <think> tags"
|
|
464
|
-
# Use negative lookahead to prevent matching across multiple blocks
|
|
465
543
|
while re.search(r"<think>(?:(?!</think>)[^\n])*\n.*?</think>", message, flags=re.DOTALL | re.IGNORECASE):
|
|
466
544
|
message = re.sub(
|
|
467
545
|
r"<think>(?:(?!</think>)[^\n])*\n.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE, count=1
|
|
468
546
|
)
|
|
469
547
|
|
|
470
|
-
# Step 2: Remove blocks separated by blank lines (before or after the message)
|
|
471
548
|
message = re.sub(r"\n\n+\s*<think>.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE)
|
|
472
549
|
message = re.sub(r"<think>.*?</think>\s*\n\n+", "", message, flags=re.DOTALL | re.IGNORECASE)
|
|
473
550
|
|
|
474
|
-
# Step 3: Handle orphaned opening <think> tags followed by newline
|
|
475
551
|
message = re.sub(r"<think>\s*\n.*$", "", message, flags=re.DOTALL | re.IGNORECASE)
|
|
476
552
|
|
|
477
|
-
|
|
478
|
-
conventional_prefixes_pattern = r"(feat|fix|docs|style|refactor|perf|test|build|ci|chore)[\(:)]"
|
|
553
|
+
conventional_prefixes_pattern = r"(" + "|".join(CommitMessageConstants.CONVENTIONAL_PREFIXES) + r")[\(:)]"
|
|
479
554
|
if re.search(r"^.*?</think>", message, flags=re.DOTALL | re.IGNORECASE):
|
|
480
555
|
prefix_match = re.search(conventional_prefixes_pattern, message, flags=re.IGNORECASE)
|
|
481
556
|
think_match = re.search(r"</think>", message, flags=re.IGNORECASE)
|
|
482
557
|
|
|
483
558
|
if not prefix_match or (think_match and think_match.start() < prefix_match.start()):
|
|
484
|
-
# No prefix or </think> comes before prefix - remove everything up to and including it
|
|
485
559
|
message = re.sub(r"^.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE)
|
|
486
560
|
|
|
487
|
-
# Step 5: Remove orphaned closing </think> tags at the end (not part of inline mentions)
|
|
488
561
|
message = re.sub(r"</think>\s*$", "", message, flags=re.IGNORECASE)
|
|
489
562
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
563
|
+
return message
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _remove_code_blocks(message: str) -> str:
|
|
567
|
+
"""Remove markdown code blocks from the message.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
message: The message to clean
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Message with code blocks removed
|
|
574
|
+
"""
|
|
575
|
+
return re.sub(r"```[\w]*\n|```", "", message)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _extract_commit_from_reasoning(message: str) -> str:
|
|
579
|
+
"""Extract the actual commit message from reasoning/preamble text.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
message: The message potentially containing reasoning
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Extracted commit message
|
|
586
|
+
"""
|
|
587
|
+
for indicator in CommitMessageConstants.COMMIT_INDICATORS:
|
|
506
588
|
if indicator.lower() in message.lower():
|
|
507
|
-
# Extract everything after the indicator
|
|
508
589
|
message = message.split(indicator, 1)[1].strip()
|
|
509
590
|
break
|
|
510
591
|
|
|
511
|
-
# If message starts with any kind of explanation text, try to locate a conventional prefix
|
|
512
592
|
lines = message.split("\n")
|
|
513
593
|
for i, line in enumerate(lines):
|
|
514
|
-
if any(
|
|
515
|
-
line.strip().startswith(prefix)
|
|
516
|
-
for prefix in ["feat:", "fix:", "docs:", "style:", "refactor:", "perf:", "test:", "build:", "ci:", "chore:"]
|
|
517
|
-
):
|
|
594
|
+
if any(line.strip().startswith(f"{prefix}:") for prefix in CommitMessageConstants.CONVENTIONAL_PREFIXES):
|
|
518
595
|
message = "\n".join(lines[i:])
|
|
519
596
|
break
|
|
520
597
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
"</instructions>",
|
|
535
|
-
"<format>",
|
|
536
|
-
"</format>",
|
|
537
|
-
"<conventions>",
|
|
538
|
-
"</conventions>",
|
|
539
|
-
]:
|
|
598
|
+
return message
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _remove_xml_tags(message: str) -> str:
|
|
602
|
+
"""Remove XML tags that might have leaked into the message.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
message: The message to clean
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Message with XML tags removed
|
|
609
|
+
"""
|
|
610
|
+
for tag in CommitMessageConstants.XML_TAGS_TO_REMOVE:
|
|
540
611
|
message = message.replace(tag, "")
|
|
612
|
+
return message
|
|
613
|
+
|
|
541
614
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
"build",
|
|
552
|
-
"ci",
|
|
553
|
-
"chore",
|
|
554
|
-
]
|
|
555
|
-
|
|
556
|
-
# Look for double prefix pattern like "chore: feat(scope):" and fix it
|
|
557
|
-
# This regex looks for a conventional prefix followed by another conventional prefix with a scope
|
|
615
|
+
def _fix_double_prefix(message: str) -> str:
|
|
616
|
+
"""Fix double type prefix issues like 'chore: feat(scope):' to 'feat(scope):'.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
message: The message to fix
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Message with double prefix corrected
|
|
623
|
+
"""
|
|
558
624
|
double_prefix_pattern = re.compile(
|
|
559
|
-
r"^("
|
|
625
|
+
r"^("
|
|
626
|
+
+ r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
|
|
627
|
+
+ r"):\s*("
|
|
628
|
+
+ r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
|
|
629
|
+
+ r")\(([^)]+)\):"
|
|
560
630
|
)
|
|
561
631
|
match = double_prefix_pattern.match(message)
|
|
562
632
|
|
|
563
633
|
if match:
|
|
564
|
-
# Extract the second type and scope, which is what we want to keep
|
|
565
634
|
second_type = match.group(2)
|
|
566
635
|
scope = match.group(3)
|
|
567
636
|
description = message[match.end() :].strip()
|
|
568
637
|
message = f"{second_type}({scope}): {description}"
|
|
569
638
|
|
|
570
|
-
|
|
639
|
+
return message
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _ensure_conventional_prefix(message: str) -> str:
|
|
643
|
+
"""Ensure the message starts with a conventional commit prefix.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
message: The message to check
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Message with conventional prefix ensured
|
|
650
|
+
"""
|
|
571
651
|
if not any(
|
|
572
652
|
message.strip().startswith(prefix + ":") or message.strip().startswith(prefix + "(")
|
|
573
|
-
for prefix in
|
|
653
|
+
for prefix in CommitMessageConstants.CONVENTIONAL_PREFIXES
|
|
574
654
|
):
|
|
575
655
|
message = f"chore: {message.strip()}"
|
|
656
|
+
return message
|
|
657
|
+
|
|
576
658
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
659
|
+
def _normalize_whitespace(message: str) -> str:
|
|
660
|
+
"""Normalize whitespace, ensuring no more than one blank line between paragraphs.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
message: The message to normalize
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
Message with normalized whitespace
|
|
667
|
+
"""
|
|
668
|
+
return re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", message).strip()
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# ============================================================================
|
|
672
|
+
# Message Cleaning
|
|
673
|
+
# ============================================================================
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def clean_commit_message(message: str) -> str:
|
|
677
|
+
"""Clean up a commit message generated by an AI model.
|
|
580
678
|
|
|
679
|
+
This function:
|
|
680
|
+
1. Removes any preamble or reasoning text
|
|
681
|
+
2. Removes code block markers and formatting
|
|
682
|
+
3. Removes XML tags that might have leaked into the response
|
|
683
|
+
4. Fixes double type prefix issues (e.g., "chore: feat(scope):")
|
|
684
|
+
5. Normalizes whitespace
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
message: Raw commit message from AI
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Cleaned commit message ready for use
|
|
691
|
+
"""
|
|
692
|
+
message = message.strip()
|
|
693
|
+
message = _remove_think_tags(message)
|
|
694
|
+
message = _remove_code_blocks(message)
|
|
695
|
+
message = _extract_commit_from_reasoning(message)
|
|
696
|
+
message = _remove_xml_tags(message)
|
|
697
|
+
message = _fix_double_prefix(message)
|
|
698
|
+
message = _normalize_whitespace(message)
|
|
581
699
|
return message
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.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
|
|
@@ -108,6 +108,13 @@ gac
|
|
|
108
108
|
- **Standard** (default): Summary with bullet points explaining implementation details
|
|
109
109
|
- **Verbose** (-v flag): Comprehensive explanations including motivation, technical approach, and impact analysis
|
|
110
110
|
|
|
111
|
+
### 🌍 **Multilingual Support**
|
|
112
|
+
|
|
113
|
+
- **25+ languages**: Generate commit messages in English, Chinese, Japanese, Korean, Spanish, French, German, and 20+ more languages
|
|
114
|
+
- **Flexible translation**: Choose to keep conventional commit prefixes in English for tool compatibility, or fully translate them
|
|
115
|
+
- **Multiple workflows**: Set a default language with `gac language`, or use `-l <language>` flag for one-time overrides
|
|
116
|
+
- **Native script support**: Full support for non-Latin scripts including CJK, Cyrillic, Arabic, and more
|
|
117
|
+
|
|
111
118
|
### 💻 **Developer Experience**
|
|
112
119
|
|
|
113
120
|
- **Interactive feedback**: Regenerate messages with specific requests like `r "make it shorter"` or `r "focus on the bug fix"`
|
|
@@ -197,11 +204,16 @@ ANTHROPIC_API_KEY=your_key_here
|
|
|
197
204
|
|
|
198
205
|
See `.gac.env.example` for all available options.
|
|
199
206
|
|
|
207
|
+
**Want commit messages in another language?** Run `gac language` to select from 25+ languages including Español, Français, 日本語, and more.
|
|
208
|
+
|
|
209
|
+
**Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
|
|
210
|
+
|
|
200
211
|
---
|
|
201
212
|
|
|
202
213
|
## Getting Help
|
|
203
214
|
|
|
204
215
|
- **Full documentation**: [USAGE.md](USAGE.md) - Complete CLI reference
|
|
216
|
+
- **Custom prompts**: [CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
|
|
205
217
|
- **Troubleshooting**: [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
|
206
218
|
- **Contributing**: [CONTRIBUTING.md](docs/CONTRIBUTING.md) - Development setup and guidelines
|
|
207
219
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
|
|
2
|
-
gac/__version__.py,sha256=
|
|
2
|
+
gac/__version__.py,sha256=1LLX3m2CsmQ_0vaoNYdP9xP6_EScz7OC8NL_kyIIrls,67
|
|
3
3
|
gac/ai.py,sha256=fg642la4yMecOwfZHQ7Ixl6z-5_qj9Q1SxwVMnPDCcY,4244
|
|
4
4
|
gac/ai_utils.py,sha256=EDkw0nnwnV5Ba2CLEo2HC15-L5BZtGJATin5Az0ZHkg,7426
|
|
5
|
-
gac/cli.py,sha256=
|
|
6
|
-
gac/config.py,sha256=
|
|
5
|
+
gac/cli.py,sha256=TV1IWwcRmEfbqXLlfGmTms5NCEqaJXUGIdFxmOg0tC0,5546
|
|
6
|
+
gac/config.py,sha256=O9n09-sFOqlkf47vieEP7fI5I7uhu1cXn9PUZ5yiYkw,1974
|
|
7
7
|
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
8
|
-
gac/constants.py,sha256
|
|
8
|
+
gac/constants.py,sha256=-VYqL1M99RAmEwQHTe6lMYVkPHSSYQJUvzYvBp8Bmck,8494
|
|
9
9
|
gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
|
|
10
10
|
gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
|
|
11
11
|
gac/git.py,sha256=g6tvph50zV-wrTWrxARYXEpl0NeI8-ffFwHoqhp3fSE,8033
|
|
12
|
-
gac/init_cli.py,sha256=
|
|
13
|
-
gac/
|
|
14
|
-
gac/
|
|
15
|
-
gac/
|
|
12
|
+
gac/init_cli.py,sha256=wyyPNjO47IcwFrO-jrqJ5rs1SsKdDJnhuRO83heh1tw,9700
|
|
13
|
+
gac/language_cli.py,sha256=xmmIonIhOR83VtRFj5Dy8JDtpa5qCNbNxSEZt-1T0a8,4040
|
|
14
|
+
gac/main.py,sha256=bQKtSo052U52l9VVZjw0-tEal21vcocSQtpCfpiRtNA,15569
|
|
15
|
+
gac/preprocess.py,sha256=hk2p2X4-xVDvuy-T1VMzMa9k5fTUbhlWDyw89DCf81Q,15379
|
|
16
|
+
gac/prompt.py,sha256=BdrOucRNnGncKlWhrO0q-o5vo5oCbXWWUFx6WUEy6Zw,28681
|
|
16
17
|
gac/security.py,sha256=15Yp6YR8QC4eECJi1BUCkMteh_veZXUbLL6W8qGcDm4,9920
|
|
17
18
|
gac/utils.py,sha256=nV42-brIHW_fBg7x855GM8nRrqEBbRzTSweg-GTyGE8,3971
|
|
18
19
|
gac/providers/__init__.py,sha256=3WTzh3ngAdvR40eezpMMFD7Zibb-LxexDYUcSm4axQI,1305
|
|
@@ -34,8 +35,8 @@ gac/providers/streamlake.py,sha256=KAA2ZnpuEI5imzvdWVWUhEBHSP0BMnprKXte6CbwBWY,2
|
|
|
34
35
|
gac/providers/synthetic.py,sha256=sRMIJTS9LpcXd9A7qp_ZjZxdqtTKRn9fl1W4YwJZP4c,1855
|
|
35
36
|
gac/providers/together.py,sha256=1bUIVHfYzcEDw4hQPE8qV6hjc2JNHPv_khVgpk2IJxI,1667
|
|
36
37
|
gac/providers/zai.py,sha256=kywhhrCfPBu0rElZyb-iENxQxxpVGykvePuL4xrXlaU,2739
|
|
37
|
-
gac-1.
|
|
38
|
-
gac-1.
|
|
39
|
-
gac-1.
|
|
40
|
-
gac-1.
|
|
41
|
-
gac-1.
|
|
38
|
+
gac-1.15.0.dist-info/METADATA,sha256=Z936z3djd55gxBEHe_W9kEeVLDB8-AtutmpQRo8D50k,8825
|
|
39
|
+
gac-1.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
40
|
+
gac-1.15.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
41
|
+
gac-1.15.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
|
|
42
|
+
gac-1.15.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|