gac 0.15.0__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/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()