gac 0.15.1__py3-none-any.whl → 0.15.2__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/__init__.py +15 -0
- gac/__version__.py +3 -0
- gac/ai.py +166 -0
- gac/cli.py +130 -0
- gac/config.py +32 -0
- gac/config_cli.py +62 -0
- gac/constants.py +149 -0
- gac/diff_cli.py +177 -0
- gac/errors.py +217 -0
- gac/git.py +158 -0
- gac/init_cli.py +45 -0
- gac/main.py +254 -0
- gac/preprocess.py +506 -0
- gac/prompt.py +355 -0
- gac/utils.py +133 -0
- {gac-0.15.1.dist-info → gac-0.15.2.dist-info}/METADATA +1 -1
- gac-0.15.2.dist-info/RECORD +20 -0
- gac-0.15.1.dist-info/RECORD +0 -5
- {gac-0.15.1.dist-info → gac-0.15.2.dist-info}/WHEEL +0 -0
- {gac-0.15.1.dist-info → gac-0.15.2.dist-info}/entry_points.txt +0 -0
- {gac-0.15.1.dist-info → gac-0.15.2.dist-info}/licenses/LICENSE +0 -0
gac/prompt.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# flake8: noqa: E501
|
|
2
|
+
"""Prompt creation for gac.
|
|
3
|
+
|
|
4
|
+
This module handles the creation of prompts for AI models, including template loading,
|
|
5
|
+
formatting, and integration with diff preprocessing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Default template to use when no template file is found
|
|
14
|
+
DEFAULT_TEMPLATE = """<role>
|
|
15
|
+
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
|
+
</role>
|
|
17
|
+
|
|
18
|
+
<format>
|
|
19
|
+
<one_liner>
|
|
20
|
+
Create a single-line commit message (50-72 characters if possible).
|
|
21
|
+
Your message should be clear, concise, and descriptive of the core change.
|
|
22
|
+
Use present tense ("Add feature" not "Added feature").
|
|
23
|
+
</one_liner><multi_line>
|
|
24
|
+
Create a commit message with:
|
|
25
|
+
- First line: A concise summary (50-72 characters) that could stand alone
|
|
26
|
+
- Blank line after the summary
|
|
27
|
+
- Detailed body with multiple bullet points explaining the key changes
|
|
28
|
+
- Focus on WHY changes were made, not just WHAT was changed
|
|
29
|
+
- Order points from most important to least important
|
|
30
|
+
</multi_line>
|
|
31
|
+
</format>
|
|
32
|
+
|
|
33
|
+
<conventions_no_scope>
|
|
34
|
+
You MUST start your commit message with the most appropriate conventional commit prefix:
|
|
35
|
+
- feat: A new feature or functionality addition
|
|
36
|
+
- fix: A bug fix or error correction
|
|
37
|
+
- docs: Documentation changes only
|
|
38
|
+
- style: Changes to code style/formatting without logic changes
|
|
39
|
+
- refactor: Code restructuring without behavior changes
|
|
40
|
+
- perf: Performance improvements
|
|
41
|
+
- test: Adding/modifying tests
|
|
42
|
+
- build: Changes to build system/dependencies
|
|
43
|
+
- ci: Changes to CI configuration
|
|
44
|
+
- chore: Miscellaneous changes not affecting src/test files
|
|
45
|
+
|
|
46
|
+
Select ONE prefix that best matches the primary purpose of the changes.
|
|
47
|
+
If multiple prefixes apply, choose the one that represents the most significant change.
|
|
48
|
+
If you cannot confidently determine a type, use 'chore'.
|
|
49
|
+
|
|
50
|
+
Do NOT include a scope in your commit prefix.
|
|
51
|
+
</conventions_no_scope>
|
|
52
|
+
|
|
53
|
+
<conventions_scope_provided>
|
|
54
|
+
You MUST write a conventional commit message with EXACTLY ONE type and the REQUIRED scope '{scope}'.
|
|
55
|
+
|
|
56
|
+
FORMAT: type({scope}): description
|
|
57
|
+
|
|
58
|
+
Select ONE type from this list that best matches the primary purpose of the changes:
|
|
59
|
+
- feat: A new feature or functionality addition
|
|
60
|
+
- fix: A bug fix or error correction
|
|
61
|
+
- docs: Documentation changes only
|
|
62
|
+
- style: Changes to code style/formatting without logic changes
|
|
63
|
+
- refactor: Code restructuring without behavior changes
|
|
64
|
+
- perf: Performance improvements
|
|
65
|
+
- test: Adding/modifying tests
|
|
66
|
+
- build: Changes to build system/dependencies
|
|
67
|
+
- ci: Changes to CI configuration
|
|
68
|
+
- chore: Miscellaneous changes not affecting src/test files
|
|
69
|
+
|
|
70
|
+
CORRECT EXAMPLES (these formats are correct):
|
|
71
|
+
✅ feat({scope}): add new feature
|
|
72
|
+
✅ fix({scope}): resolve bug
|
|
73
|
+
✅ refactor({scope}): improve code structure
|
|
74
|
+
✅ chore({scope}): update dependencies
|
|
75
|
+
|
|
76
|
+
INCORRECT EXAMPLES (these formats are wrong and must NOT be used):
|
|
77
|
+
❌ chore: feat({scope}): description
|
|
78
|
+
❌ fix: refactor({scope}): description
|
|
79
|
+
❌ feat: feat({scope}): description
|
|
80
|
+
❌ chore: chore({scope}): description
|
|
81
|
+
|
|
82
|
+
You MUST NOT prefix the type({scope}) with another type. Use EXACTLY ONE type, which MUST include the scope in parentheses.
|
|
83
|
+
</conventions_scope_provided>
|
|
84
|
+
|
|
85
|
+
<conventions_scope_inferred>
|
|
86
|
+
You MUST write a conventional commit message with EXACTLY ONE type and an inferred scope.
|
|
87
|
+
|
|
88
|
+
FORMAT: type(scope): description
|
|
89
|
+
|
|
90
|
+
Select ONE type from this list that best matches the primary purpose of the changes:
|
|
91
|
+
- feat: A new feature or functionality addition
|
|
92
|
+
- fix: A bug fix or error correction
|
|
93
|
+
- docs: Documentation changes only
|
|
94
|
+
- style: Changes to code style/formatting without logic changes
|
|
95
|
+
- refactor: Code restructuring without behavior changes
|
|
96
|
+
- perf: Performance improvements
|
|
97
|
+
- test: Adding/modifying tests
|
|
98
|
+
- build: Changes to build system/dependencies
|
|
99
|
+
- ci: Changes to CI configuration
|
|
100
|
+
- chore: Miscellaneous changes not affecting src/test files
|
|
101
|
+
|
|
102
|
+
You MUST infer an appropriate scope from the changes. A good scope is concise (usually one word) and indicates the component or area that was changed.
|
|
103
|
+
Examples of good scopes: api, auth, ui, core, docs, build, prompt, config
|
|
104
|
+
|
|
105
|
+
CORRECT EXAMPLES (these formats are correct):
|
|
106
|
+
✅ feat(auth): add login functionality
|
|
107
|
+
✅ fix(api): resolve null response issue
|
|
108
|
+
✅ refactor(core): improve data processing
|
|
109
|
+
✅ docs(readme): update installation instructions
|
|
110
|
+
|
|
111
|
+
INCORRECT EXAMPLES (these formats are wrong and must NOT be used):
|
|
112
|
+
❌ chore: feat(component): description
|
|
113
|
+
❌ fix: refactor(component): description
|
|
114
|
+
❌ feat: feat(component): description
|
|
115
|
+
❌ chore: chore(component): description
|
|
116
|
+
|
|
117
|
+
You MUST NOT prefix the type(scope) with another type. Use EXACTLY ONE type, which MUST include the scope in parentheses.
|
|
118
|
+
</conventions_scope_inferred>
|
|
119
|
+
|
|
120
|
+
<hint>
|
|
121
|
+
Additional context provided by the user: <hint_text></hint_text>
|
|
122
|
+
</hint>
|
|
123
|
+
|
|
124
|
+
<git_status>
|
|
125
|
+
<status></status>
|
|
126
|
+
</git_status>
|
|
127
|
+
|
|
128
|
+
<git_diff_stat>
|
|
129
|
+
<diff_stat></diff_stat>
|
|
130
|
+
</git_diff_stat>
|
|
131
|
+
|
|
132
|
+
<git_diff>
|
|
133
|
+
<diff></diff>
|
|
134
|
+
</git_diff>
|
|
135
|
+
|
|
136
|
+
<instructions>
|
|
137
|
+
IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
|
|
138
|
+
DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
|
|
139
|
+
DO NOT use markdown formatting, headers, or code blocks.
|
|
140
|
+
The entire response will be passed directly to 'git commit -m'.
|
|
141
|
+
</instructions>"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def load_prompt_template() -> str:
|
|
145
|
+
"""Load the prompt template from the embedded default template.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Template content as string
|
|
149
|
+
"""
|
|
150
|
+
logger.debug("Using default template")
|
|
151
|
+
return DEFAULT_TEMPLATE
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_prompt(
|
|
155
|
+
status: str,
|
|
156
|
+
processed_diff: str,
|
|
157
|
+
diff_stat: str = "",
|
|
158
|
+
one_liner: bool = False,
|
|
159
|
+
hint: str = "",
|
|
160
|
+
scope: str | None = None,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Build a prompt for the AI model using the provided template and git information.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
status: Git status output
|
|
166
|
+
processed_diff: Git diff output, already preprocessed and ready to use
|
|
167
|
+
diff_stat: Git diff stat output showing file changes summary
|
|
168
|
+
one_liner: Whether to request a one-line commit message
|
|
169
|
+
hint: Optional hint to guide the AI
|
|
170
|
+
scope: Optional scope parameter. None = no scope, "infer" = infer scope, any other string = use as scope
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Formatted prompt string ready to be sent to an AI model
|
|
174
|
+
"""
|
|
175
|
+
template = load_prompt_template()
|
|
176
|
+
|
|
177
|
+
# Select the appropriate conventions section based on scope parameter
|
|
178
|
+
try:
|
|
179
|
+
logger.debug(f"Processing scope parameter: {scope}")
|
|
180
|
+
if scope is None:
|
|
181
|
+
# No scope - use the plain conventions section
|
|
182
|
+
logger.debug("Using no-scope conventions")
|
|
183
|
+
template = re.sub(
|
|
184
|
+
r"<conventions_scope_provided>.*?</conventions_scope_provided>\n", "", template, flags=re.DOTALL
|
|
185
|
+
)
|
|
186
|
+
template = re.sub(
|
|
187
|
+
r"<conventions_scope_inferred>.*?</conventions_scope_inferred>\n", "", template, flags=re.DOTALL
|
|
188
|
+
)
|
|
189
|
+
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
190
|
+
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
191
|
+
elif scope == "infer" or scope == "":
|
|
192
|
+
# User wants to infer a scope from changes (either with "infer" or empty string)
|
|
193
|
+
logger.debug(f"Using inferred-scope conventions (scope={scope})")
|
|
194
|
+
template = re.sub(
|
|
195
|
+
r"<conventions_scope_provided>.*?</conventions_scope_provided>\n", "", template, flags=re.DOTALL
|
|
196
|
+
)
|
|
197
|
+
template = re.sub(r"<conventions_no_scope>.*?</conventions_no_scope>\n", "", template, flags=re.DOTALL)
|
|
198
|
+
template = template.replace("<conventions_scope_inferred>", "<conventions>")
|
|
199
|
+
template = template.replace("</conventions_scope_inferred>", "</conventions>")
|
|
200
|
+
else:
|
|
201
|
+
# User provided a specific scope
|
|
202
|
+
logger.debug(f"Using provided-scope conventions with scope '{scope}'")
|
|
203
|
+
template = re.sub(
|
|
204
|
+
r"<conventions_scope_inferred>.*?</conventions_scope_inferred>\n", "", template, flags=re.DOTALL
|
|
205
|
+
)
|
|
206
|
+
template = re.sub(r"<conventions_no_scope>.*?</conventions_no_scope>\n", "", template, flags=re.DOTALL)
|
|
207
|
+
template = template.replace("<conventions_scope_provided>", "<conventions>")
|
|
208
|
+
template = template.replace("</conventions_scope_provided>", "</conventions>")
|
|
209
|
+
template = template.replace("{scope}", scope)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(f"Error processing scope parameter: {e}")
|
|
212
|
+
# Fallback to no scope if there's an error
|
|
213
|
+
template = re.sub(
|
|
214
|
+
r"<conventions_scope_provided>.*?</conventions_scope_provided>\n", "", template, flags=re.DOTALL
|
|
215
|
+
)
|
|
216
|
+
template = re.sub(
|
|
217
|
+
r"<conventions_scope_inferred>.*?</conventions_scope_inferred>\n", "", template, flags=re.DOTALL
|
|
218
|
+
)
|
|
219
|
+
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
220
|
+
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
221
|
+
|
|
222
|
+
template = template.replace("<status></status>", status)
|
|
223
|
+
template = template.replace("<diff_stat></diff_stat>", diff_stat)
|
|
224
|
+
template = template.replace("<diff></diff>", processed_diff)
|
|
225
|
+
|
|
226
|
+
# Add hint if present
|
|
227
|
+
if hint:
|
|
228
|
+
template = template.replace("<hint_text></hint_text>", hint)
|
|
229
|
+
logger.debug(f"Added hint ({len(hint)} characters)")
|
|
230
|
+
else:
|
|
231
|
+
template = re.sub(r"<hint>.*?</hint>", "", template, flags=re.DOTALL)
|
|
232
|
+
logger.debug("No hint provided")
|
|
233
|
+
|
|
234
|
+
# Process format options (one-liner vs multi-line)
|
|
235
|
+
if one_liner:
|
|
236
|
+
template = re.sub(r"<multi_line>.*?</multi_line>", "", template, flags=re.DOTALL)
|
|
237
|
+
else:
|
|
238
|
+
template = re.sub(r"<one_liner>.*?</one_liner>", "", template, flags=re.DOTALL)
|
|
239
|
+
|
|
240
|
+
# Clean up extra whitespace, collapsing blank lines that may contain spaces
|
|
241
|
+
template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", template)
|
|
242
|
+
|
|
243
|
+
return template.strip()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def clean_commit_message(message: str) -> str:
|
|
247
|
+
"""Clean up a commit message generated by an AI model.
|
|
248
|
+
|
|
249
|
+
This function:
|
|
250
|
+
1. Removes any preamble or reasoning text
|
|
251
|
+
2. Removes code block markers and formatting
|
|
252
|
+
3. Removes XML tags that might have leaked into the response
|
|
253
|
+
4. Ensures the message starts with a conventional commit prefix
|
|
254
|
+
5. Fixes double type prefix issues (e.g., "chore: feat(scope):")
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
message: Raw commit message from AI
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Cleaned commit message ready for use
|
|
261
|
+
"""
|
|
262
|
+
message = message.strip()
|
|
263
|
+
|
|
264
|
+
# Remove any markdown code blocks
|
|
265
|
+
message = re.sub(r"```[\w]*\n|```", "", message)
|
|
266
|
+
|
|
267
|
+
# Extract the actual commit message if it follows our reasoning pattern
|
|
268
|
+
# Look for different indicators of where the actual commit message starts
|
|
269
|
+
commit_indicators = [
|
|
270
|
+
"# Your commit message:",
|
|
271
|
+
"Your commit message:",
|
|
272
|
+
"The commit message is:",
|
|
273
|
+
"Here's the commit message:",
|
|
274
|
+
"Commit message:",
|
|
275
|
+
"Final commit message:",
|
|
276
|
+
"# Commit Message",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
for indicator in commit_indicators:
|
|
280
|
+
if indicator.lower() in message.lower():
|
|
281
|
+
# Extract everything after the indicator
|
|
282
|
+
message = message.split(indicator, 1)[1].strip()
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
# If message starts with any kind of explanation text, try to locate a conventional prefix
|
|
286
|
+
lines = message.split("\n")
|
|
287
|
+
for i, line in enumerate(lines):
|
|
288
|
+
if any(
|
|
289
|
+
line.strip().startswith(prefix)
|
|
290
|
+
for prefix in ["feat:", "fix:", "docs:", "style:", "refactor:", "perf:", "test:", "build:", "ci:", "chore:"]
|
|
291
|
+
):
|
|
292
|
+
message = "\n".join(lines[i:])
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
# Remove any XML tags that might have leaked into the response
|
|
296
|
+
for tag in [
|
|
297
|
+
"<git-status>",
|
|
298
|
+
"</git-status>",
|
|
299
|
+
"<git_status>",
|
|
300
|
+
"</git_status>",
|
|
301
|
+
"<git-diff>",
|
|
302
|
+
"</git-diff>",
|
|
303
|
+
"<git_diff>",
|
|
304
|
+
"</git_diff>",
|
|
305
|
+
"<repository_context>",
|
|
306
|
+
"</repository_context>",
|
|
307
|
+
"<instructions>",
|
|
308
|
+
"</instructions>",
|
|
309
|
+
"<format>",
|
|
310
|
+
"</format>",
|
|
311
|
+
"<conventions>",
|
|
312
|
+
"</conventions>",
|
|
313
|
+
]:
|
|
314
|
+
message = message.replace(tag, "")
|
|
315
|
+
|
|
316
|
+
# Fix double type prefix issues (e.g., "chore: feat(scope):") to just "feat(scope):")
|
|
317
|
+
conventional_prefixes = [
|
|
318
|
+
"feat",
|
|
319
|
+
"fix",
|
|
320
|
+
"docs",
|
|
321
|
+
"style",
|
|
322
|
+
"refactor",
|
|
323
|
+
"perf",
|
|
324
|
+
"test",
|
|
325
|
+
"build",
|
|
326
|
+
"ci",
|
|
327
|
+
"chore",
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
# Look for double prefix pattern like "chore: feat(scope):" and fix it
|
|
331
|
+
# This regex looks for a conventional prefix followed by another conventional prefix with a scope
|
|
332
|
+
double_prefix_pattern = re.compile(
|
|
333
|
+
r"^(" + r"|\s*".join(conventional_prefixes) + r"):\s*(" + r"|\s*".join(conventional_prefixes) + r")\(([^)]+)\):"
|
|
334
|
+
)
|
|
335
|
+
match = double_prefix_pattern.match(message)
|
|
336
|
+
|
|
337
|
+
if match:
|
|
338
|
+
# Extract the second type and scope, which is what we want to keep
|
|
339
|
+
second_type = match.group(2)
|
|
340
|
+
scope = match.group(3)
|
|
341
|
+
description = message[match.end() :].strip()
|
|
342
|
+
message = f"{second_type}({scope}): {description}"
|
|
343
|
+
|
|
344
|
+
# Ensure message starts with a conventional commit prefix
|
|
345
|
+
if not any(
|
|
346
|
+
message.strip().startswith(prefix + ":") or message.strip().startswith(prefix + "(")
|
|
347
|
+
for prefix in conventional_prefixes
|
|
348
|
+
):
|
|
349
|
+
message = f"chore: {message.strip()}"
|
|
350
|
+
|
|
351
|
+
# Final cleanup: trim extra whitespace and ensure no more than one blank line
|
|
352
|
+
# Handle blank lines that may include spaces or tabs
|
|
353
|
+
message = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", message).strip()
|
|
354
|
+
|
|
355
|
+
return message
|
gac/utils.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Utility functions for gac."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.theme import Theme
|
|
8
|
+
|
|
9
|
+
from gac.constants import Logging
|
|
10
|
+
from gac.errors import GacError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(
|
|
14
|
+
log_level: int | str = Logging.DEFAULT_LEVEL,
|
|
15
|
+
quiet: bool = False,
|
|
16
|
+
force: bool = False,
|
|
17
|
+
suppress_noisy: bool = False,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Configure logging for the application.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
log_level: Log level to use (DEBUG, INFO, WARNING, ERROR)
|
|
23
|
+
quiet: If True, suppress all output except errors
|
|
24
|
+
force: If True, force reconfiguration of logging
|
|
25
|
+
suppress_noisy: If True, suppress noisy third-party loggers
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(log_level, str):
|
|
28
|
+
log_level = getattr(logging, log_level.upper(), logging.WARNING)
|
|
29
|
+
|
|
30
|
+
if quiet:
|
|
31
|
+
log_level = logging.ERROR
|
|
32
|
+
|
|
33
|
+
kwargs = {"force": force} if force else {}
|
|
34
|
+
|
|
35
|
+
logging.basicConfig(
|
|
36
|
+
level=log_level,
|
|
37
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
38
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
39
|
+
**kwargs,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if suppress_noisy:
|
|
43
|
+
for noisy_logger in ["requests", "urllib3"]:
|
|
44
|
+
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
|
|
45
|
+
|
|
46
|
+
logger.info(f"Logging initialized with level: {logging.getLevelName(log_level)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
theme = Theme(
|
|
50
|
+
{
|
|
51
|
+
"success": "green bold",
|
|
52
|
+
"info": "blue",
|
|
53
|
+
"warning": "yellow",
|
|
54
|
+
"error": "red bold",
|
|
55
|
+
"header": "magenta",
|
|
56
|
+
"notification": "bright_cyan bold",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
console = Console(theme=theme)
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_message(message: str, level: str = "info") -> None:
|
|
64
|
+
"""Print a styled message with the specified level."""
|
|
65
|
+
console.print(message, style=level)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_subprocess(
|
|
69
|
+
command: list[str],
|
|
70
|
+
silent: bool = False,
|
|
71
|
+
timeout: int = 60,
|
|
72
|
+
check: bool = True,
|
|
73
|
+
strip_output: bool = True,
|
|
74
|
+
raise_on_error: bool = True,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Run a subprocess command safely and return the output.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
command: List of command arguments
|
|
80
|
+
silent: If True, suppress debug logging
|
|
81
|
+
timeout: Command timeout in seconds
|
|
82
|
+
check: Whether to check return code (for compatibility)
|
|
83
|
+
strip_output: Whether to strip whitespace from output
|
|
84
|
+
raise_on_error: Whether to raise an exception on error
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Command output as string
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
GacError: If the command times out
|
|
91
|
+
subprocess.CalledProcessError: If the command fails and raise_on_error is True
|
|
92
|
+
"""
|
|
93
|
+
if not silent:
|
|
94
|
+
logger.debug(f"Running command: {' '.join(command)}")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
command,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
check=False,
|
|
102
|
+
timeout=timeout,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if result.returncode != 0 and (check or raise_on_error):
|
|
106
|
+
if not silent:
|
|
107
|
+
logger.debug(f"Command stderr: {result.stderr}")
|
|
108
|
+
|
|
109
|
+
error = subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
|
|
110
|
+
raise error
|
|
111
|
+
|
|
112
|
+
output = result.stdout
|
|
113
|
+
if strip_output:
|
|
114
|
+
output = output.strip()
|
|
115
|
+
|
|
116
|
+
return output
|
|
117
|
+
except subprocess.TimeoutExpired as e:
|
|
118
|
+
logger.error(f"Command timed out after {timeout} seconds: {' '.join(command)}")
|
|
119
|
+
raise GacError(f"Command timed out: {' '.join(command)}") from e
|
|
120
|
+
except subprocess.CalledProcessError as e:
|
|
121
|
+
if not silent:
|
|
122
|
+
logger.error(f"Command failed: {e.stderr.strip() if hasattr(e, 'stderr') else str(e)}")
|
|
123
|
+
if raise_on_error:
|
|
124
|
+
raise
|
|
125
|
+
return ""
|
|
126
|
+
except Exception as e:
|
|
127
|
+
if not silent:
|
|
128
|
+
logger.debug(f"Command error: {e}")
|
|
129
|
+
if raise_on_error:
|
|
130
|
+
# Convert generic exceptions to CalledProcessError for consistency
|
|
131
|
+
error = subprocess.CalledProcessError(1, command, "", str(e))
|
|
132
|
+
raise error from e
|
|
133
|
+
return ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.2
|
|
4
4
|
Summary: AI-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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
|
|
2
|
+
gac/__version__.py,sha256=_F5sPfbH3qFakn-iGVTon_XI7JINqZymKVSAHN_lSp8,67
|
|
3
|
+
gac/ai.py,sha256=Ijpgcp1L_SJy37Q3p3leSFMXST6RNWruUEsYlkoz-XM,5549
|
|
4
|
+
gac/cli.py,sha256=UCGaKpGrm8B603V04yMYGkfv9S5-CksSy7zzeqwp13s,4280
|
|
5
|
+
gac/config.py,sha256=9-llgaRMzL2qtH5nmoRGtgpUw7IJg143jWDcppmYcSA,1128
|
|
6
|
+
gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
|
|
7
|
+
gac/constants.py,sha256=gcygNGl4I48vYtuhpdCFKnYRjhxcRWMQYCerxPq0wCY,4731
|
|
8
|
+
gac/diff_cli.py,sha256=GVegRNB1a4D-wUOtVlb6Q_6sXbj4kD2Yt-H4r2sRHxc,5659
|
|
9
|
+
gac/errors.py,sha256=3vIRMQ2QF3sP9_rPfXAFuu5ZSjIVX4FxM-FAuiR8N-8,7416
|
|
10
|
+
gac/git.py,sha256=ZdEvq5ssJw-EncQi_PnAevF9WZoVz7or4AwSJHbMFMs,5181
|
|
11
|
+
gac/init_cli.py,sha256=s6nB4k__6Da7Ljq4MB9-rlrcTxB6Nt82OMDKk0pZP-M,1778
|
|
12
|
+
gac/main.py,sha256=yll8TzauryYLkQHiVuu3Bfh0nb3tzbPrLmtvQo2FkPM,9492
|
|
13
|
+
gac/preprocess.py,sha256=UwcijCKjfuqGeeMRR1yd3FWLdX-uHjb8UIwxjxpbEpo,15339
|
|
14
|
+
gac/prompt.py,sha256=mgKcSuhs0VKXznkcEbPXGia_Mgz66NtYMlJPLVLH6jg,13779
|
|
15
|
+
gac/utils.py,sha256=koJNQ4FXUTitnKQ7hC_9sPudLi_BSOpcryYsz5n_z-Q,3973
|
|
16
|
+
gac-0.15.2.dist-info/METADATA,sha256=_7behv0mEe0E5ef4Q-mlKigvEO30uIMhbvhrKUkUzwo,7326
|
|
17
|
+
gac-0.15.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
gac-0.15.2.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
19
|
+
gac-0.15.2.dist-info/licenses/LICENSE,sha256=s11puNmYfzwoSwG96nhOJe268Y1QFckr8-Hmzo3_eJE,1087
|
|
20
|
+
gac-0.15.2.dist-info/RECORD,,
|
gac-0.15.1.dist-info/RECORD
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
gac-0.15.1.dist-info/METADATA,sha256=PKZN0bM652cM0YE48l0lDvNehkNxia2hhibIXzpZoWk,7326
|
|
2
|
-
gac-0.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
3
|
-
gac-0.15.1.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
|
|
4
|
-
gac-0.15.1.dist-info/licenses/LICENSE,sha256=s11puNmYfzwoSwG96nhOJe268Y1QFckr8-Hmzo3_eJE,1087
|
|
5
|
-
gac-0.15.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|