gac 0.15.1__py3-none-any.whl → 0.15.3__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.3.dist-info}/METADATA +4 -4
- gac-0.15.3.dist-info/RECORD +20 -0
- gac-0.15.1.dist-info/RECORD +0 -5
- {gac-0.15.1.dist-info → gac-0.15.3.dist-info}/WHEEL +0 -0
- {gac-0.15.1.dist-info → gac-0.15.3.dist-info}/entry_points.txt +0 -0
- {gac-0.15.1.dist-info → gac-0.15.3.dist-info}/licenses/LICENSE +0 -0
gac/main.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Business logic for gac: orchestrates the commit workflow, including git state, formatting,
|
|
3
|
+
prompt building, AI generation, and commit/push operations. This module contains no CLI wiring.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from gac.ai import count_tokens, generate_commit_message
|
|
14
|
+
from gac.config import load_config
|
|
15
|
+
from gac.constants import EnvDefaults, Utility
|
|
16
|
+
from gac.errors import AIError, GitError, handle_error
|
|
17
|
+
from gac.git import (
|
|
18
|
+
get_staged_files,
|
|
19
|
+
push_changes,
|
|
20
|
+
run_git_command,
|
|
21
|
+
run_pre_commit_hooks,
|
|
22
|
+
)
|
|
23
|
+
from gac.preprocess import preprocess_diff
|
|
24
|
+
from gac.prompt import build_prompt, clean_commit_message
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
config = load_config()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(
|
|
32
|
+
stage_all: bool = False,
|
|
33
|
+
model: str | None = None,
|
|
34
|
+
hint: str = "",
|
|
35
|
+
one_liner: bool = False,
|
|
36
|
+
show_prompt: bool = False,
|
|
37
|
+
scope: str | None = None,
|
|
38
|
+
require_confirmation: bool = True,
|
|
39
|
+
push: bool = False,
|
|
40
|
+
quiet: bool = False,
|
|
41
|
+
dry_run: bool = False,
|
|
42
|
+
no_verify: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Main application logic for gac."""
|
|
45
|
+
try:
|
|
46
|
+
git_dir = run_git_command(["rev-parse", "--show-toplevel"])
|
|
47
|
+
if not git_dir:
|
|
48
|
+
raise GitError("Not in a git repository")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(f"Error checking git repository: {e}")
|
|
51
|
+
handle_error(GitError("Not in a git repository"), exit_program=True)
|
|
52
|
+
|
|
53
|
+
if model is None:
|
|
54
|
+
model = config["model"]
|
|
55
|
+
if model is None:
|
|
56
|
+
handle_error(
|
|
57
|
+
AIError.model_error(
|
|
58
|
+
"No model specified. Please set the GAC_MODEL environment variable or use --model."
|
|
59
|
+
),
|
|
60
|
+
exit_program=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
temperature = config["temperature"]
|
|
64
|
+
max_output_tokens = config["max_output_tokens"]
|
|
65
|
+
max_retries = config["max_retries"]
|
|
66
|
+
|
|
67
|
+
if stage_all and (not dry_run):
|
|
68
|
+
logger.info("Staging all changes")
|
|
69
|
+
run_git_command(["add", "--all"])
|
|
70
|
+
|
|
71
|
+
# Check for staged files
|
|
72
|
+
staged_files = get_staged_files(existing_only=False)
|
|
73
|
+
if not staged_files:
|
|
74
|
+
console = Console()
|
|
75
|
+
console.print(
|
|
76
|
+
"[yellow]No staged changes found. Stage your changes with git add first or use --add-all.[/yellow]"
|
|
77
|
+
)
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
# Run pre-commit hooks before doing expensive operations
|
|
81
|
+
if not no_verify and not dry_run:
|
|
82
|
+
if not run_pre_commit_hooks():
|
|
83
|
+
console = Console()
|
|
84
|
+
console.print("[red]Pre-commit hooks failed. Please fix the issues and try again.[/red]")
|
|
85
|
+
console.print("[yellow]You can use --no-verify to skip pre-commit hooks.[/yellow]")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
status = run_git_command(["status"])
|
|
89
|
+
diff = run_git_command(["diff", "--staged"])
|
|
90
|
+
diff_stat = " " + run_git_command(["diff", "--stat", "--cached"])
|
|
91
|
+
|
|
92
|
+
# Preprocess the diff before passing to build_prompt
|
|
93
|
+
logger.debug(f"Preprocessing diff ({len(diff)} characters)")
|
|
94
|
+
model_id = model or config["model"]
|
|
95
|
+
processed_diff = preprocess_diff(diff, token_limit=Utility.DEFAULT_DIFF_TOKEN_LIMIT, model=model_id)
|
|
96
|
+
logger.debug(f"Processed diff ({len(processed_diff)} characters)")
|
|
97
|
+
|
|
98
|
+
prompt = build_prompt(
|
|
99
|
+
status=status,
|
|
100
|
+
processed_diff=processed_diff,
|
|
101
|
+
diff_stat=diff_stat,
|
|
102
|
+
one_liner=one_liner,
|
|
103
|
+
hint=hint,
|
|
104
|
+
scope=scope,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if show_prompt:
|
|
108
|
+
console = Console()
|
|
109
|
+
console.print(
|
|
110
|
+
Panel(
|
|
111
|
+
prompt,
|
|
112
|
+
title="Prompt for LLM",
|
|
113
|
+
border_style="bright_blue",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
prompt_tokens = count_tokens(prompt, model)
|
|
119
|
+
|
|
120
|
+
warning_limit = config.get("warning_limit_tokens", EnvDefaults.WARNING_LIMIT_TOKENS)
|
|
121
|
+
if warning_limit and prompt_tokens > warning_limit:
|
|
122
|
+
console = Console()
|
|
123
|
+
console.print(
|
|
124
|
+
f"[yellow]⚠️ Warning: Prompt contains {prompt_tokens} tokens, which exceeds the warning limit of "
|
|
125
|
+
f"{warning_limit} tokens.[/yellow]"
|
|
126
|
+
)
|
|
127
|
+
if require_confirmation:
|
|
128
|
+
proceed = click.confirm("Do you want to continue anyway?", default=True)
|
|
129
|
+
if not proceed:
|
|
130
|
+
console.print("[yellow]Aborted due to token limit.[/yellow]")
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
commit_message = generate_commit_message(
|
|
134
|
+
model=model,
|
|
135
|
+
prompt=prompt,
|
|
136
|
+
temperature=temperature,
|
|
137
|
+
max_tokens=max_output_tokens,
|
|
138
|
+
max_retries=max_retries,
|
|
139
|
+
quiet=quiet,
|
|
140
|
+
)
|
|
141
|
+
commit_message = clean_commit_message(commit_message)
|
|
142
|
+
|
|
143
|
+
logger.info("Generated commit message:")
|
|
144
|
+
logger.info(commit_message)
|
|
145
|
+
|
|
146
|
+
console = Console()
|
|
147
|
+
|
|
148
|
+
# Reroll loop
|
|
149
|
+
while True:
|
|
150
|
+
console.print("[bold green]Generated commit message:[/bold green]")
|
|
151
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
152
|
+
|
|
153
|
+
if not quiet:
|
|
154
|
+
completion_tokens = count_tokens(commit_message, model)
|
|
155
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
156
|
+
console.print(
|
|
157
|
+
f"[dim]Token usage: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} "
|
|
158
|
+
"total[/dim]"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if require_confirmation:
|
|
162
|
+
# Custom prompt that accepts y/n/r
|
|
163
|
+
while True:
|
|
164
|
+
response = (
|
|
165
|
+
click.prompt("Proceed with commit above? [y/n/r]", type=str, show_default=False).lower().strip()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if response in ["y", "yes"]:
|
|
169
|
+
break # Exit both loops and proceed with commit
|
|
170
|
+
elif response in ["n", "no"]:
|
|
171
|
+
console.print("[yellow]Prompt not accepted. Exiting...[/yellow]")
|
|
172
|
+
sys.exit(0)
|
|
173
|
+
elif response in ["r", "reroll"]:
|
|
174
|
+
console.print("[cyan]Regenerating commit message...[/cyan]\n")
|
|
175
|
+
# Generate new message
|
|
176
|
+
commit_message = generate_commit_message(
|
|
177
|
+
model=model,
|
|
178
|
+
prompt=prompt,
|
|
179
|
+
temperature=temperature,
|
|
180
|
+
max_tokens=max_output_tokens,
|
|
181
|
+
max_retries=max_retries,
|
|
182
|
+
quiet=quiet,
|
|
183
|
+
)
|
|
184
|
+
commit_message = clean_commit_message(commit_message)
|
|
185
|
+
break # Exit inner loop, continue outer loop
|
|
186
|
+
else:
|
|
187
|
+
console.print("[red]Invalid response. Please enter y (yes), n (no), or r (reroll).[/red]")
|
|
188
|
+
|
|
189
|
+
# If we got here with 'y', break the outer loop
|
|
190
|
+
if response in ["y", "yes"]:
|
|
191
|
+
break
|
|
192
|
+
else:
|
|
193
|
+
# No confirmation required, exit loop
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if dry_run:
|
|
197
|
+
console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
|
|
198
|
+
console.print("Would commit with message:")
|
|
199
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
200
|
+
staged_files = get_staged_files(existing_only=False)
|
|
201
|
+
console.print(f"Would commit {len(staged_files)} files")
|
|
202
|
+
logger.info(f"Would commit {len(staged_files)} files")
|
|
203
|
+
else:
|
|
204
|
+
commit_args = ["commit", "-m", commit_message]
|
|
205
|
+
if no_verify:
|
|
206
|
+
commit_args.append("--no-verify")
|
|
207
|
+
run_git_command(commit_args)
|
|
208
|
+
logger.info("Commit created successfully")
|
|
209
|
+
console.print("[green]Commit created successfully[/green]")
|
|
210
|
+
except AIError as e:
|
|
211
|
+
logger.error(str(e))
|
|
212
|
+
console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
if push:
|
|
216
|
+
try:
|
|
217
|
+
if dry_run:
|
|
218
|
+
staged_files = get_staged_files(existing_only=False)
|
|
219
|
+
|
|
220
|
+
logger.info("Dry run: Would push changes")
|
|
221
|
+
logger.info("Would push with message:")
|
|
222
|
+
logger.info(commit_message)
|
|
223
|
+
logger.info(f"Would push {len(staged_files)} files")
|
|
224
|
+
|
|
225
|
+
console.print("[yellow]Dry run: Would push changes[/yellow]")
|
|
226
|
+
console.print("Would push with message:")
|
|
227
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
228
|
+
console.print(f"Would push {len(staged_files)} files")
|
|
229
|
+
sys.exit(0)
|
|
230
|
+
|
|
231
|
+
if push_changes():
|
|
232
|
+
logger.info("Changes pushed successfully")
|
|
233
|
+
console.print("[green]Changes pushed successfully[/green]")
|
|
234
|
+
else:
|
|
235
|
+
handle_error(
|
|
236
|
+
GitError("Failed to push changes. Check your remote configuration."),
|
|
237
|
+
exit_program=True,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
handle_error(
|
|
241
|
+
GitError(f"Error pushing changes: {e}"),
|
|
242
|
+
exit_program=True,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if not quiet:
|
|
246
|
+
logger.info("Successfully committed changes with message:")
|
|
247
|
+
logger.info(commit_message)
|
|
248
|
+
if push:
|
|
249
|
+
logger.info("Changes pushed to remote.")
|
|
250
|
+
sys.exit(0)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
main()
|