gac 3.8.1__py3-none-any.whl → 3.10.10__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.
- gac/__init__.py +4 -6
- gac/__version__.py +1 -1
- gac/ai_utils.py +18 -49
- gac/cli.py +14 -10
- gac/commit_executor.py +59 -0
- gac/config.py +28 -3
- gac/config_cli.py +19 -7
- gac/constants/__init__.py +34 -0
- gac/constants/commit.py +63 -0
- gac/constants/defaults.py +40 -0
- gac/constants/file_patterns.py +110 -0
- gac/constants/languages.py +119 -0
- gac/diff_cli.py +0 -22
- gac/errors.py +8 -2
- gac/git.py +6 -6
- gac/git_state_validator.py +193 -0
- gac/grouped_commit_workflow.py +458 -0
- gac/init_cli.py +2 -1
- gac/interactive_mode.py +179 -0
- gac/language_cli.py +0 -1
- gac/main.py +222 -959
- gac/model_cli.py +2 -1
- gac/model_identifier.py +70 -0
- gac/oauth/claude_code.py +2 -2
- gac/oauth/qwen_oauth.py +4 -0
- gac/oauth/token_store.py +2 -2
- gac/oauth_retry.py +161 -0
- gac/postprocess.py +155 -0
- gac/prompt.py +20 -490
- gac/prompt_builder.py +88 -0
- gac/providers/README.md +437 -0
- gac/providers/__init__.py +70 -81
- gac/providers/anthropic.py +12 -56
- gac/providers/azure_openai.py +48 -92
- gac/providers/base.py +329 -0
- gac/providers/cerebras.py +10 -43
- gac/providers/chutes.py +16 -72
- gac/providers/claude_code.py +64 -97
- gac/providers/custom_anthropic.py +51 -85
- gac/providers/custom_openai.py +29 -87
- gac/providers/deepseek.py +10 -43
- gac/providers/error_handler.py +139 -0
- gac/providers/fireworks.py +10 -43
- gac/providers/gemini.py +66 -73
- gac/providers/groq.py +10 -62
- gac/providers/kimi_coding.py +19 -59
- gac/providers/lmstudio.py +62 -52
- gac/providers/minimax.py +10 -43
- gac/providers/mistral.py +10 -43
- gac/providers/moonshot.py +10 -43
- gac/providers/ollama.py +54 -41
- gac/providers/openai.py +30 -46
- gac/providers/openrouter.py +15 -62
- gac/providers/protocol.py +71 -0
- gac/providers/qwen.py +55 -67
- gac/providers/registry.py +58 -0
- gac/providers/replicate.py +137 -91
- gac/providers/streamlake.py +26 -56
- gac/providers/synthetic.py +35 -47
- gac/providers/together.py +10 -43
- gac/providers/zai.py +21 -59
- gac/py.typed +0 -0
- gac/security.py +1 -1
- gac/templates/__init__.py +1 -0
- gac/templates/question_generation.txt +60 -0
- gac/templates/system_prompt.txt +224 -0
- gac/templates/user_prompt.txt +28 -0
- gac/utils.py +6 -5
- gac/workflow_context.py +162 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/METADATA +1 -1
- gac-3.10.10.dist-info/RECORD +79 -0
- gac/constants.py +0 -328
- gac-3.8.1.dist-info/RECORD +0 -56
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
- {gac-3.8.1.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/__init__.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"""Git Auto Commit (gac) - Generate commit messages using AI."""
|
|
2
2
|
|
|
3
|
+
from gac import init_cli
|
|
3
4
|
from gac.__version__ import __version__
|
|
4
5
|
from gac.ai import generate_commit_message
|
|
5
|
-
from gac.
|
|
6
|
-
from gac.prompt import build_prompt, clean_commit_message
|
|
6
|
+
from gac.prompt import build_prompt
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"__version__",
|
|
10
|
-
"generate_commit_message",
|
|
11
10
|
"build_prompt",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"push_changes",
|
|
11
|
+
"generate_commit_message",
|
|
12
|
+
"init_cli",
|
|
15
13
|
]
|
gac/__version__.py
CHANGED
gac/ai_utils.py
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
This module provides utility functions that support the AI provider implementations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
8
9
|
import time
|
|
10
|
+
from collections.abc import Callable
|
|
9
11
|
from functools import lru_cache
|
|
10
|
-
from typing import Any
|
|
12
|
+
from typing import Any, cast
|
|
11
13
|
|
|
12
14
|
import tiktoken
|
|
13
15
|
from rich.console import Console
|
|
@@ -15,6 +17,8 @@ from rich.status import Status
|
|
|
15
17
|
|
|
16
18
|
from gac.constants import EnvDefaults, Utility
|
|
17
19
|
from gac.errors import AIError
|
|
20
|
+
from gac.oauth import QwenOAuthProvider, refresh_token_if_expired
|
|
21
|
+
from gac.oauth.token_store import TokenStore
|
|
18
22
|
from gac.providers import SUPPORTED_PROVIDERS
|
|
19
23
|
|
|
20
24
|
logger = logging.getLogger(__name__)
|
|
@@ -40,7 +44,7 @@ def count_tokens(content: str | list[dict[str, str]] | dict[str, Any], model: st
|
|
|
40
44
|
try:
|
|
41
45
|
encoding = get_encoding(model)
|
|
42
46
|
return len(encoding.encode(text))
|
|
43
|
-
except
|
|
47
|
+
except (KeyError, UnicodeError, ValueError) as e:
|
|
44
48
|
logger.error(f"Error counting tokens: {e}")
|
|
45
49
|
# Fallback to rough estimation (4 chars per token on average)
|
|
46
50
|
return len(text) // 4
|
|
@@ -53,7 +57,7 @@ def extract_text_content(content: str | list[dict[str, str]] | dict[str, Any]) -
|
|
|
53
57
|
elif isinstance(content, list):
|
|
54
58
|
return "\n".join(msg["content"] for msg in content if isinstance(msg, dict) and "content" in msg)
|
|
55
59
|
elif isinstance(content, dict) and "content" in content:
|
|
56
|
-
return content["content"]
|
|
60
|
+
return cast(str, content["content"])
|
|
57
61
|
return ""
|
|
58
62
|
|
|
59
63
|
|
|
@@ -70,36 +74,13 @@ def get_encoding(model: str) -> tiktoken.Encoding:
|
|
|
70
74
|
except KeyError:
|
|
71
75
|
# Fall back to default encoding if model not found
|
|
72
76
|
return tiktoken.get_encoding(Utility.DEFAULT_ENCODING)
|
|
73
|
-
except
|
|
77
|
+
except (OSError, ConnectionError):
|
|
74
78
|
# If there are any network/SSL issues, fall back to default encoding
|
|
75
79
|
return tiktoken.get_encoding(Utility.DEFAULT_ENCODING)
|
|
76
80
|
|
|
77
81
|
|
|
78
|
-
def _classify_error(error_str: str) -> str:
|
|
79
|
-
"""Classify error types based on error message content."""
|
|
80
|
-
error_str = error_str.lower()
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
"api key" in error_str
|
|
84
|
-
or "unauthorized" in error_str
|
|
85
|
-
or "authentication" in error_str
|
|
86
|
-
or "invalid api key" in error_str
|
|
87
|
-
):
|
|
88
|
-
return "authentication"
|
|
89
|
-
elif "timeout" in error_str or "timed out" in error_str or "request timeout" in error_str:
|
|
90
|
-
return "timeout"
|
|
91
|
-
elif "rate limit" in error_str or "too many requests" in error_str or "rate limit exceeded" in error_str:
|
|
92
|
-
return "rate_limit"
|
|
93
|
-
elif "connect" in error_str or "network" in error_str or "network connection failed" in error_str:
|
|
94
|
-
return "connection"
|
|
95
|
-
elif "model" in error_str or "not found" in error_str or "model not found" in error_str:
|
|
96
|
-
return "model"
|
|
97
|
-
else:
|
|
98
|
-
return "unknown"
|
|
99
|
-
|
|
100
|
-
|
|
101
82
|
def generate_with_retries(
|
|
102
|
-
provider_funcs: dict,
|
|
83
|
+
provider_funcs: dict[str, Callable[..., str]],
|
|
103
84
|
model: str,
|
|
104
85
|
messages: list[dict[str, str]],
|
|
105
86
|
temperature: float,
|
|
@@ -126,9 +107,6 @@ def generate_with_retries(
|
|
|
126
107
|
|
|
127
108
|
# Load Claude Code token from TokenStore if needed
|
|
128
109
|
if provider == "claude-code":
|
|
129
|
-
from gac.oauth import refresh_token_if_expired
|
|
130
|
-
from gac.oauth.token_store import TokenStore
|
|
131
|
-
|
|
132
110
|
# Check token expiry and refresh if needed
|
|
133
111
|
if not refresh_token_if_expired(quiet=True):
|
|
134
112
|
raise AIError.authentication_error(
|
|
@@ -147,8 +125,6 @@ def generate_with_retries(
|
|
|
147
125
|
|
|
148
126
|
# Check Qwen OAuth token expiry and refresh if needed
|
|
149
127
|
if provider == "qwen":
|
|
150
|
-
from gac.oauth import QwenOAuthProvider, TokenStore
|
|
151
|
-
|
|
152
128
|
oauth_provider = QwenOAuthProvider(TokenStore())
|
|
153
129
|
token = oauth_provider.get_token()
|
|
154
130
|
if not token:
|
|
@@ -166,7 +142,7 @@ def generate_with_retries(
|
|
|
166
142
|
console.print("[green]✓ Authentication successful![/green]\n")
|
|
167
143
|
except AIError:
|
|
168
144
|
raise
|
|
169
|
-
except
|
|
145
|
+
except (ValueError, KeyError, json.JSONDecodeError, ConnectionError, OSError) as e:
|
|
170
146
|
raise AIError.authentication_error(
|
|
171
147
|
f"Qwen authentication failed: {e}. Run 'gac auth qwen login' to authenticate manually."
|
|
172
148
|
) from e
|
|
@@ -183,7 +159,7 @@ def generate_with_retries(
|
|
|
183
159
|
spinner = Status(f"Generating {message_type} with {provider} {model_name}...")
|
|
184
160
|
spinner.start()
|
|
185
161
|
|
|
186
|
-
last_exception = None
|
|
162
|
+
last_exception: Exception | None = None
|
|
187
163
|
last_error_type = "unknown"
|
|
188
164
|
|
|
189
165
|
for attempt in range(max_retries):
|
|
@@ -208,14 +184,14 @@ def generate_with_retries(
|
|
|
208
184
|
console.print(f"✓ Generated {message_type} with {provider} {model_name}")
|
|
209
185
|
|
|
210
186
|
if content is not None and content.strip():
|
|
211
|
-
return content.strip()
|
|
187
|
+
return content.strip()
|
|
212
188
|
else:
|
|
213
189
|
logger.warning(f"Empty or None content received from {provider} {model_name}: {repr(content)}")
|
|
214
190
|
raise AIError.model_error("Empty response from AI model")
|
|
215
191
|
|
|
216
|
-
except
|
|
192
|
+
except AIError as e:
|
|
217
193
|
last_exception = e
|
|
218
|
-
error_type =
|
|
194
|
+
error_type = e.error_type
|
|
219
195
|
last_error_type = error_type
|
|
220
196
|
|
|
221
197
|
# For authentication and model errors, don't retry
|
|
@@ -223,23 +199,16 @@ def generate_with_retries(
|
|
|
223
199
|
if spinner and not skip_success_message:
|
|
224
200
|
spinner.stop()
|
|
225
201
|
console.print(f"✗ Failed to generate {message_type} with {provider} {model_name}")
|
|
226
|
-
|
|
227
|
-
# Create the appropriate error type based on classification
|
|
228
|
-
if error_type == "authentication":
|
|
229
|
-
raise AIError.authentication_error(f"AI generation failed: {str(e)}") from e
|
|
230
|
-
elif error_type == "model":
|
|
231
|
-
raise AIError.model_error(f"AI generation failed: {str(e)}") from e
|
|
202
|
+
raise
|
|
232
203
|
|
|
233
204
|
if attempt < max_retries - 1:
|
|
234
205
|
# Exponential backoff
|
|
235
206
|
wait_time = 2**attempt
|
|
236
207
|
if not quiet and not skip_success_message:
|
|
237
208
|
if attempt == 0:
|
|
238
|
-
logger.warning(f"AI generation failed, retrying in {wait_time}s: {
|
|
209
|
+
logger.warning(f"AI generation failed, retrying in {wait_time}s: {e}")
|
|
239
210
|
else:
|
|
240
|
-
logger.warning(
|
|
241
|
-
f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {str(e)}"
|
|
242
|
-
)
|
|
211
|
+
logger.warning(f"AI generation failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}")
|
|
243
212
|
|
|
244
213
|
if spinner and not skip_success_message:
|
|
245
214
|
for i in range(wait_time, 0, -1):
|
|
@@ -250,7 +219,7 @@ def generate_with_retries(
|
|
|
250
219
|
else:
|
|
251
220
|
num_retries = max_retries
|
|
252
221
|
retry_word = "retry" if num_retries == 1 else "retries"
|
|
253
|
-
logger.error(f"AI generation failed after {num_retries} {retry_word}: {
|
|
222
|
+
logger.error(f"AI generation failed after {num_retries} {retry_word}: {e}")
|
|
254
223
|
|
|
255
224
|
if spinner and not skip_success_message:
|
|
256
225
|
spinner.stop()
|
gac/cli.py
CHANGED
|
@@ -8,13 +8,14 @@ Defines the Click-based command-line interface and delegates execution to the ma
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
|
+
from typing import Any
|
|
11
12
|
|
|
12
13
|
import click
|
|
13
14
|
from rich.console import Console
|
|
14
15
|
|
|
15
16
|
from gac import __version__
|
|
16
17
|
from gac.auth_cli import auth as auth_cli
|
|
17
|
-
from gac.config import load_config
|
|
18
|
+
from gac.config import GACConfig, load_config
|
|
18
19
|
from gac.config_cli import config as config_cli
|
|
19
20
|
from gac.constants import Languages, Logging
|
|
20
21
|
from gac.diff_cli import diff as diff_cli
|
|
@@ -24,8 +25,9 @@ from gac.language_cli import language as language_cli
|
|
|
24
25
|
from gac.main import main
|
|
25
26
|
from gac.model_cli import model as model_cli
|
|
26
27
|
from gac.utils import setup_logging
|
|
28
|
+
from gac.workflow_context import CLIOptions
|
|
27
29
|
|
|
28
|
-
config = load_config()
|
|
30
|
+
config: GACConfig = load_config()
|
|
29
31
|
logger = logging.getLogger(__name__)
|
|
30
32
|
console = Console()
|
|
31
33
|
|
|
@@ -124,7 +126,7 @@ def cli(
|
|
|
124
126
|
logger.info("Starting gac")
|
|
125
127
|
|
|
126
128
|
# Set SSL verification environment variable if flag is used or config is set
|
|
127
|
-
if no_verify_ssl or config
|
|
129
|
+
if no_verify_ssl or config["no_verify_ssl"]:
|
|
128
130
|
os.environ["GAC_NO_VERIFY_SSL"] = "true"
|
|
129
131
|
logger.info("SSL certificate verification disabled")
|
|
130
132
|
|
|
@@ -136,16 +138,16 @@ def cli(
|
|
|
136
138
|
sys.exit(1)
|
|
137
139
|
|
|
138
140
|
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
139
|
-
infer_scope = bool(scope or config
|
|
141
|
+
infer_scope = bool(scope or config["always_include_scope"])
|
|
140
142
|
|
|
141
143
|
# Determine if verbose mode should be enabled based on -v flag or verbose config setting
|
|
142
|
-
use_verbose = bool(verbose or config
|
|
144
|
+
use_verbose = bool(verbose or config["verbose"])
|
|
143
145
|
|
|
144
146
|
# Resolve language code to full name if provided
|
|
145
147
|
resolved_language = Languages.resolve_code(language) if language else None
|
|
146
148
|
|
|
147
149
|
try:
|
|
148
|
-
|
|
150
|
+
opts = CLIOptions(
|
|
149
151
|
stage_all=add_all,
|
|
150
152
|
group=group,
|
|
151
153
|
interactive=interactive,
|
|
@@ -161,15 +163,17 @@ def cli(
|
|
|
161
163
|
message_only=message_only,
|
|
162
164
|
verbose=use_verbose,
|
|
163
165
|
no_verify=no_verify,
|
|
164
|
-
skip_secret_scan=skip_secret_scan or
|
|
166
|
+
skip_secret_scan=skip_secret_scan or config["skip_secret_scan"],
|
|
165
167
|
language=resolved_language,
|
|
166
|
-
hook_timeout=hook_timeout if hook_timeout > 0 else
|
|
168
|
+
hook_timeout=hook_timeout if hook_timeout > 0 else config["hook_timeout"],
|
|
167
169
|
)
|
|
170
|
+
exit_code = main(opts)
|
|
171
|
+
sys.exit(exit_code)
|
|
168
172
|
except Exception as e:
|
|
169
173
|
handle_error(e, exit_program=True)
|
|
170
174
|
else:
|
|
171
175
|
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
172
|
-
infer_scope = bool(scope or config
|
|
176
|
+
infer_scope = bool(scope or config["always_include_scope"])
|
|
173
177
|
|
|
174
178
|
ctx.obj = {
|
|
175
179
|
"add_all": add_all,
|
|
@@ -206,7 +210,7 @@ cli.add_command(model_cli)
|
|
|
206
210
|
|
|
207
211
|
@click.command(context_settings=language_cli.context_settings)
|
|
208
212
|
@click.pass_context
|
|
209
|
-
def lang(ctx):
|
|
213
|
+
def lang(ctx: Any) -> None:
|
|
210
214
|
"""Set the language for commit messages interactively. (Alias for 'language')"""
|
|
211
215
|
ctx.forward(language_cli)
|
|
212
216
|
|
gac/commit_executor.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Commit execution logic for gac."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from gac.errors import GitError
|
|
9
|
+
from gac.git import get_staged_files, push_changes
|
|
10
|
+
from gac.workflow_utils import execute_commit
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommitExecutor:
|
|
17
|
+
"""Handles commit creation and related operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, dry_run: bool = False, quiet: bool = False, no_verify: bool = False, hook_timeout: int = 120):
|
|
20
|
+
self.dry_run = dry_run
|
|
21
|
+
self.quiet = quiet
|
|
22
|
+
self.no_verify = no_verify
|
|
23
|
+
self.hook_timeout = hook_timeout
|
|
24
|
+
|
|
25
|
+
def create_commit(self, commit_message: str) -> None:
|
|
26
|
+
"""Create a single commit with the given message."""
|
|
27
|
+
if self.dry_run:
|
|
28
|
+
console.print("[yellow]Dry run: Commit message generated but not applied[/yellow]")
|
|
29
|
+
console.print("Would commit with message:")
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
|
|
32
|
+
console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
|
|
33
|
+
staged_files = get_staged_files(existing_only=False)
|
|
34
|
+
console.print(f"Would commit {len(staged_files)} files")
|
|
35
|
+
logger.info(f"Would commit {len(staged_files)} files")
|
|
36
|
+
else:
|
|
37
|
+
execute_commit(commit_message, self.no_verify, self.hook_timeout)
|
|
38
|
+
|
|
39
|
+
def push_to_remote(self) -> None:
|
|
40
|
+
"""Push changes to remote repository.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
GitError: If push fails or remote is not configured.
|
|
44
|
+
"""
|
|
45
|
+
if self.dry_run:
|
|
46
|
+
staged_files = get_staged_files(existing_only=False)
|
|
47
|
+
logger.info("Dry run: Would push changes")
|
|
48
|
+
logger.info(f"Would push {len(staged_files)} files")
|
|
49
|
+
console.print("[yellow]Dry run: Would push changes[/yellow]")
|
|
50
|
+
console.print(f"Would push {len(staged_files)} files")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if push_changes():
|
|
54
|
+
logger.info("Changes pushed successfully")
|
|
55
|
+
if not self.quiet:
|
|
56
|
+
console.print("[green]Changes pushed successfully[/green]")
|
|
57
|
+
else:
|
|
58
|
+
console.print("[red]Failed to push changes. Check your remote configuration and network connection.[/red]")
|
|
59
|
+
raise GitError("Failed to push changes")
|
gac/config.py
CHANGED
|
@@ -5,6 +5,7 @@ Handles environment variable and .gac.env file precedence for application settin
|
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import TypedDict
|
|
8
9
|
|
|
9
10
|
from dotenv import load_dotenv
|
|
10
11
|
|
|
@@ -12,7 +13,31 @@ from gac.constants import EnvDefaults, Logging
|
|
|
12
13
|
from gac.errors import ConfigError
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
class GACConfig(TypedDict, total=False):
|
|
17
|
+
"""TypedDict for GAC configuration values.
|
|
18
|
+
|
|
19
|
+
Fields that can be None or omitted are marked with total=False.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model: str | None
|
|
23
|
+
temperature: float
|
|
24
|
+
max_output_tokens: int
|
|
25
|
+
max_retries: int
|
|
26
|
+
log_level: str
|
|
27
|
+
warning_limit_tokens: int
|
|
28
|
+
always_include_scope: bool
|
|
29
|
+
skip_secret_scan: bool
|
|
30
|
+
no_tiktoken: bool
|
|
31
|
+
no_verify_ssl: bool
|
|
32
|
+
verbose: bool
|
|
33
|
+
system_prompt_path: str | None
|
|
34
|
+
language: str | None
|
|
35
|
+
translate_prefixes: bool
|
|
36
|
+
rtl_confirmed: bool
|
|
37
|
+
hook_timeout: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_config(config: GACConfig) -> None:
|
|
16
41
|
"""Validate configuration values at load time.
|
|
17
42
|
|
|
18
43
|
Args:
|
|
@@ -62,7 +87,7 @@ def validate_config(config: dict[str, str | int | float | bool | None]) -> None:
|
|
|
62
87
|
raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
|
|
63
88
|
|
|
64
89
|
|
|
65
|
-
def load_config() ->
|
|
90
|
+
def load_config() -> GACConfig:
|
|
66
91
|
"""Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
|
|
67
92
|
user_config = Path.home() / ".gac.env"
|
|
68
93
|
if user_config.exists():
|
|
@@ -74,7 +99,7 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
74
99
|
if project_gac_env.exists():
|
|
75
100
|
load_dotenv(project_gac_env, override=True)
|
|
76
101
|
|
|
77
|
-
config = {
|
|
102
|
+
config: GACConfig = {
|
|
78
103
|
"model": os.getenv("GAC_MODEL"),
|
|
79
104
|
"temperature": float(os.getenv("GAC_TEMPERATURE", EnvDefaults.TEMPERATURE)),
|
|
80
105
|
"max_output_tokens": int(os.getenv("GAC_MAX_OUTPUT_TOKENS", EnvDefaults.MAX_OUTPUT_TOKENS)),
|
gac/config_cli.py
CHANGED
|
@@ -10,7 +10,7 @@ GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@click.group()
|
|
13
|
-
def config():
|
|
13
|
+
def config() -> None:
|
|
14
14
|
"""Manage gac configuration."""
|
|
15
15
|
pass
|
|
16
16
|
|
|
@@ -18,6 +18,8 @@ def config():
|
|
|
18
18
|
@config.command()
|
|
19
19
|
def show() -> None:
|
|
20
20
|
"""Show all current config values."""
|
|
21
|
+
from dotenv import dotenv_values
|
|
22
|
+
|
|
21
23
|
project_env_path = Path(".gac.env")
|
|
22
24
|
user_exists = GAC_ENV_PATH.exists()
|
|
23
25
|
project_exists = project_env_path.exists()
|
|
@@ -29,9 +31,14 @@ def show() -> None:
|
|
|
29
31
|
|
|
30
32
|
if user_exists:
|
|
31
33
|
click.echo(f"User config ({GAC_ENV_PATH}):")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
user_config = dotenv_values(str(GAC_ENV_PATH))
|
|
35
|
+
for key, value in sorted(user_config.items()):
|
|
36
|
+
if value is not None:
|
|
37
|
+
if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
|
|
38
|
+
display_value = "***hidden***"
|
|
39
|
+
else:
|
|
40
|
+
display_value = value
|
|
41
|
+
click.echo(f" {key}={display_value}")
|
|
35
42
|
else:
|
|
36
43
|
click.echo("No $HOME/.gac.env found.")
|
|
37
44
|
|
|
@@ -39,9 +46,14 @@ def show() -> None:
|
|
|
39
46
|
if user_exists:
|
|
40
47
|
click.echo("")
|
|
41
48
|
click.echo("Project config (./.gac.env):")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
project_config = dotenv_values(str(project_env_path))
|
|
50
|
+
for key, value in sorted(project_config.items()):
|
|
51
|
+
if value is not None:
|
|
52
|
+
if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
|
|
53
|
+
display_value = "***hidden***"
|
|
54
|
+
else:
|
|
55
|
+
display_value = value
|
|
56
|
+
click.echo(f" {key}={display_value}")
|
|
45
57
|
click.echo("")
|
|
46
58
|
click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
|
|
47
59
|
else:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Constants for the Git Auto Commit (gac) project.
|
|
2
|
+
|
|
3
|
+
This package provides all constants used throughout gac, organized into
|
|
4
|
+
logical modules:
|
|
5
|
+
|
|
6
|
+
- defaults: Environment defaults, provider defaults, logging, and utility constants
|
|
7
|
+
- file_patterns: File pattern matching and importance weighting
|
|
8
|
+
- languages: Language code mappings for internationalization
|
|
9
|
+
- commit: Git file status and commit message constants
|
|
10
|
+
|
|
11
|
+
All constants are re-exported from this package for backward compatibility.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from gac.constants.commit import CommitMessageConstants, FileStatus
|
|
15
|
+
from gac.constants.defaults import EnvDefaults, Logging, ProviderDefaults, Utility
|
|
16
|
+
from gac.constants.file_patterns import CodePatternImportance, FilePatterns, FileTypeImportance
|
|
17
|
+
from gac.constants.languages import Languages
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
# From defaults
|
|
21
|
+
"EnvDefaults",
|
|
22
|
+
"ProviderDefaults",
|
|
23
|
+
"Logging",
|
|
24
|
+
"Utility",
|
|
25
|
+
# From file_patterns
|
|
26
|
+
"FilePatterns",
|
|
27
|
+
"FileTypeImportance",
|
|
28
|
+
"CodePatternImportance",
|
|
29
|
+
# From languages
|
|
30
|
+
"Languages",
|
|
31
|
+
# From commit
|
|
32
|
+
"FileStatus",
|
|
33
|
+
"CommitMessageConstants",
|
|
34
|
+
]
|
gac/constants/commit.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Constants for git operations and commit message generation."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileStatus(Enum):
|
|
7
|
+
"""File status for Git operations."""
|
|
8
|
+
|
|
9
|
+
MODIFIED = "M"
|
|
10
|
+
ADDED = "A"
|
|
11
|
+
DELETED = "D"
|
|
12
|
+
RENAMED = "R"
|
|
13
|
+
COPIED = "C"
|
|
14
|
+
UNTRACKED = "?"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommitMessageConstants:
|
|
18
|
+
"""Constants for commit message generation and cleaning."""
|
|
19
|
+
|
|
20
|
+
# Conventional commit type prefixes
|
|
21
|
+
CONVENTIONAL_PREFIXES: list[str] = [
|
|
22
|
+
"feat",
|
|
23
|
+
"fix",
|
|
24
|
+
"docs",
|
|
25
|
+
"style",
|
|
26
|
+
"refactor",
|
|
27
|
+
"perf",
|
|
28
|
+
"test",
|
|
29
|
+
"build",
|
|
30
|
+
"ci",
|
|
31
|
+
"chore",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# XML tags that may leak from prompt templates into AI responses
|
|
35
|
+
XML_TAGS_TO_REMOVE: list[str] = [
|
|
36
|
+
"<git-status>",
|
|
37
|
+
"</git-status>",
|
|
38
|
+
"<git_status>",
|
|
39
|
+
"</git_status>",
|
|
40
|
+
"<git-diff>",
|
|
41
|
+
"</git-diff>",
|
|
42
|
+
"<git_diff>",
|
|
43
|
+
"</git_diff>",
|
|
44
|
+
"<repository_context>",
|
|
45
|
+
"</repository_context>",
|
|
46
|
+
"<instructions>",
|
|
47
|
+
"</instructions>",
|
|
48
|
+
"<format>",
|
|
49
|
+
"</format>",
|
|
50
|
+
"<conventions>",
|
|
51
|
+
"</conventions>",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Indicators that mark the start of the actual commit message in AI responses
|
|
55
|
+
COMMIT_INDICATORS: list[str] = [
|
|
56
|
+
"# Your commit message:",
|
|
57
|
+
"Your commit message:",
|
|
58
|
+
"The commit message is:",
|
|
59
|
+
"Here's the commit message:",
|
|
60
|
+
"Commit message:",
|
|
61
|
+
"Final commit message:",
|
|
62
|
+
"# Commit Message",
|
|
63
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Default values for environment variables and provider configurations."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EnvDefaults:
|
|
7
|
+
"""Default values for environment variables."""
|
|
8
|
+
|
|
9
|
+
MAX_RETRIES: int = 3
|
|
10
|
+
TEMPERATURE: float = 1
|
|
11
|
+
MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
|
|
12
|
+
WARNING_LIMIT_TOKENS: int = 32768
|
|
13
|
+
ALWAYS_INCLUDE_SCOPE: bool = False
|
|
14
|
+
SKIP_SECRET_SCAN: bool = False
|
|
15
|
+
VERBOSE: bool = False
|
|
16
|
+
NO_TIKTOKEN: bool = False
|
|
17
|
+
NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
|
|
18
|
+
HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProviderDefaults:
|
|
22
|
+
"""Default values for provider configurations."""
|
|
23
|
+
|
|
24
|
+
HTTP_TIMEOUT: int = 120 # seconds - timeout for HTTP requests to LLM providers
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Logging:
|
|
28
|
+
"""Logging configuration constants."""
|
|
29
|
+
|
|
30
|
+
DEFAULT_LEVEL: str = "WARNING"
|
|
31
|
+
LEVELS: list[str] = ["DEBUG", "INFO", "WARNING", "ERROR"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Utility:
|
|
35
|
+
"""General utility constants."""
|
|
36
|
+
|
|
37
|
+
DEFAULT_ENCODING: str = "cl100k_base" # llm encoding
|
|
38
|
+
DEFAULT_DIFF_TOKEN_LIMIT: int = 15000 # Maximum tokens for diff processing
|
|
39
|
+
MAX_WORKERS: int = os.cpu_count() or 4 # Maximum number of parallel workers
|
|
40
|
+
MAX_DISPLAYED_SECRET_LENGTH: int = 50 # Maximum length for displaying secrets
|