gac 0.17.2__py3-none-any.whl → 3.6.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.
- gac/__version__.py +1 -1
- gac/ai.py +69 -123
- gac/ai_utils.py +227 -0
- gac/auth_cli.py +69 -0
- gac/cli.py +87 -19
- gac/config.py +13 -7
- gac/config_cli.py +26 -5
- gac/constants.py +176 -5
- gac/errors.py +14 -0
- gac/git.py +207 -11
- gac/init_cli.py +52 -29
- gac/language_cli.py +378 -0
- gac/main.py +922 -189
- gac/model_cli.py +374 -0
- gac/oauth/__init__.py +1 -0
- gac/oauth/claude_code.py +397 -0
- gac/preprocess.py +5 -5
- gac/prompt.py +656 -219
- gac/providers/__init__.py +88 -0
- gac/providers/anthropic.py +51 -0
- gac/providers/azure_openai.py +97 -0
- gac/providers/cerebras.py +38 -0
- gac/providers/chutes.py +71 -0
- gac/providers/claude_code.py +102 -0
- gac/providers/custom_anthropic.py +133 -0
- gac/providers/custom_openai.py +98 -0
- gac/providers/deepseek.py +38 -0
- gac/providers/fireworks.py +38 -0
- gac/providers/gemini.py +87 -0
- gac/providers/groq.py +63 -0
- gac/providers/kimi_coding.py +63 -0
- gac/providers/lmstudio.py +59 -0
- gac/providers/minimax.py +38 -0
- gac/providers/mistral.py +38 -0
- gac/providers/moonshot.py +38 -0
- gac/providers/ollama.py +50 -0
- gac/providers/openai.py +38 -0
- gac/providers/openrouter.py +58 -0
- gac/providers/replicate.py +98 -0
- gac/providers/streamlake.py +51 -0
- gac/providers/synthetic.py +42 -0
- gac/providers/together.py +38 -0
- gac/providers/zai.py +59 -0
- gac/security.py +293 -0
- gac/utils.py +243 -4
- gac/workflow_utils.py +222 -0
- gac-3.6.0.dist-info/METADATA +281 -0
- gac-3.6.0.dist-info/RECORD +53 -0
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/WHEEL +1 -1
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/licenses/LICENSE +1 -1
- gac-0.17.2.dist-info/METADATA +0 -221
- gac-0.17.2.dist-info/RECORD +0 -20
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/entry_points.txt +0 -0
gac/cli.py
CHANGED
|
@@ -9,26 +9,36 @@ import logging
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
|
+
from rich.console import Console
|
|
12
13
|
|
|
13
14
|
from gac import __version__
|
|
15
|
+
from gac.auth_cli import auth as auth_cli
|
|
14
16
|
from gac.config import load_config
|
|
15
17
|
from gac.config_cli import config as config_cli
|
|
16
|
-
from gac.constants import Logging
|
|
18
|
+
from gac.constants import Languages, Logging
|
|
17
19
|
from gac.diff_cli import diff as diff_cli
|
|
18
20
|
from gac.errors import handle_error
|
|
19
21
|
from gac.init_cli import init as init_cli
|
|
22
|
+
from gac.language_cli import language as language_cli
|
|
20
23
|
from gac.main import main
|
|
24
|
+
from gac.model_cli import model as model_cli
|
|
21
25
|
from gac.utils import setup_logging
|
|
22
26
|
|
|
23
27
|
config = load_config()
|
|
24
28
|
logger = logging.getLogger(__name__)
|
|
29
|
+
console = Console()
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
@click.group(invoke_without_command=True, context_settings={"ignore_unknown_options": True})
|
|
28
33
|
# Git workflow options
|
|
29
34
|
@click.option("--add-all", "-a", is_flag=True, help="Stage all changes before committing")
|
|
35
|
+
@click.option("--group", "-g", is_flag=True, help="Group changes into multiple logical commits")
|
|
36
|
+
@click.option(
|
|
37
|
+
"--interactive", "-i", is_flag=True, help="Ask interactive questions to gather more context for the commit message"
|
|
38
|
+
)
|
|
30
39
|
@click.option("--push", "-p", is_flag=True, help="Push changes to remote after committing")
|
|
31
40
|
@click.option("--dry-run", is_flag=True, help="Dry run the commit workflow")
|
|
41
|
+
@click.option("--message-only", is_flag=True, help="Output only the generated commit message without committing")
|
|
32
42
|
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
33
43
|
# Commit message options
|
|
34
44
|
@click.option("--one-liner", "-o", is_flag=True, help="Generate a single-line commit message")
|
|
@@ -36,17 +46,24 @@ logger = logging.getLogger(__name__)
|
|
|
36
46
|
@click.option(
|
|
37
47
|
"--scope",
|
|
38
48
|
"-s",
|
|
39
|
-
is_flag=
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
help="Add a scope to the commit message. If used without a value, the LLM will determine an appropriate scope.",
|
|
49
|
+
is_flag=True,
|
|
50
|
+
default=False,
|
|
51
|
+
help="Infer an appropriate scope for the commit message",
|
|
43
52
|
)
|
|
44
53
|
@click.option("--hint", "-h", default="", help="Additional context to include in the prompt")
|
|
45
54
|
# Model options
|
|
46
55
|
@click.option("--model", "-m", help="Override the default model (format: 'provider:model_name')")
|
|
56
|
+
@click.option(
|
|
57
|
+
"--language", "-l", help="Override the language for commit messages (e.g., 'Spanish', 'es', 'zh-CN', 'ja')"
|
|
58
|
+
)
|
|
47
59
|
# Output options
|
|
48
60
|
@click.option("--quiet", "-q", is_flag=True, help="Suppress non-error output")
|
|
49
|
-
@click.option(
|
|
61
|
+
@click.option(
|
|
62
|
+
"--verbose",
|
|
63
|
+
"-v",
|
|
64
|
+
is_flag=True,
|
|
65
|
+
help="Generate detailed commit messages with motivation, architecture, and impact sections",
|
|
66
|
+
)
|
|
50
67
|
@click.option(
|
|
51
68
|
"--log-level",
|
|
52
69
|
default=config["log_level"],
|
|
@@ -54,26 +71,39 @@ logger = logging.getLogger(__name__)
|
|
|
54
71
|
help=f"Set log level (default: {config['log_level']})",
|
|
55
72
|
)
|
|
56
73
|
# Advanced options
|
|
57
|
-
@click.option("--no-verify", is_flag=True, help="Skip pre-commit hooks when committing")
|
|
74
|
+
@click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
|
|
75
|
+
@click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
|
|
76
|
+
@click.option(
|
|
77
|
+
"--hook-timeout",
|
|
78
|
+
type=int,
|
|
79
|
+
default=0,
|
|
80
|
+
help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
|
|
81
|
+
)
|
|
58
82
|
# Other options
|
|
59
83
|
@click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
|
|
60
84
|
@click.pass_context
|
|
61
85
|
def cli(
|
|
62
86
|
ctx: click.Context,
|
|
63
87
|
add_all: bool = False,
|
|
64
|
-
|
|
88
|
+
group: bool = False,
|
|
89
|
+
interactive: bool = False,
|
|
90
|
+
log_level: str = str(config["log_level"]),
|
|
65
91
|
one_liner: bool = False,
|
|
66
92
|
push: bool = False,
|
|
67
93
|
show_prompt: bool = False,
|
|
68
|
-
scope:
|
|
94
|
+
scope: bool = False,
|
|
69
95
|
quiet: bool = False,
|
|
70
96
|
yes: bool = False,
|
|
71
97
|
hint: str = "",
|
|
72
|
-
model: str = None,
|
|
98
|
+
model: str | None = None,
|
|
99
|
+
language: str | None = None,
|
|
73
100
|
version: bool = False,
|
|
74
101
|
dry_run: bool = False,
|
|
102
|
+
message_only: bool = False,
|
|
75
103
|
verbose: bool = False,
|
|
76
104
|
no_verify: bool = False,
|
|
105
|
+
skip_secret_scan: bool = False,
|
|
106
|
+
hook_timeout: int = 0,
|
|
77
107
|
) -> None:
|
|
78
108
|
"""Git Auto Commit - Generate commit messages with AI."""
|
|
79
109
|
if ctx.invoked_subcommand is None:
|
|
@@ -81,56 +111,94 @@ def cli(
|
|
|
81
111
|
print(f"Git Auto Commit (gac) version: {__version__}")
|
|
82
112
|
sys.exit(0)
|
|
83
113
|
effective_log_level = log_level
|
|
84
|
-
if verbose and log_level not in ("DEBUG", "INFO"):
|
|
85
|
-
effective_log_level = "INFO"
|
|
86
114
|
if quiet:
|
|
87
115
|
effective_log_level = "ERROR"
|
|
88
116
|
setup_logging(effective_log_level)
|
|
89
117
|
logger.info("Starting gac")
|
|
90
118
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
# Validate incompatible flag combinations
|
|
120
|
+
if message_only and group:
|
|
121
|
+
console.print("[red]Error: --message-only and --group options are mutually exclusive[/red]")
|
|
122
|
+
console.print("[yellow]--message-only is for generating a single commit message for external use[/yellow]")
|
|
123
|
+
console.print("[yellow]--group is for organizing multiple commits within the current workflow[/yellow]")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
127
|
+
infer_scope = bool(scope or config.get("always_include_scope", False))
|
|
128
|
+
|
|
129
|
+
# Determine if verbose mode should be enabled based on -v flag or verbose config setting
|
|
130
|
+
use_verbose = bool(verbose or config.get("verbose", False))
|
|
131
|
+
|
|
132
|
+
# Resolve language code to full name if provided
|
|
133
|
+
resolved_language = Languages.resolve_code(language) if language else None
|
|
95
134
|
|
|
96
135
|
try:
|
|
97
136
|
main(
|
|
98
137
|
stage_all=add_all,
|
|
138
|
+
group=group,
|
|
139
|
+
interactive=interactive,
|
|
99
140
|
model=model,
|
|
100
141
|
hint=hint,
|
|
101
142
|
one_liner=one_liner,
|
|
102
143
|
show_prompt=show_prompt,
|
|
103
|
-
|
|
144
|
+
infer_scope=bool(infer_scope),
|
|
104
145
|
require_confirmation=not yes,
|
|
105
146
|
push=push,
|
|
106
147
|
quiet=quiet,
|
|
107
148
|
dry_run=dry_run,
|
|
149
|
+
message_only=message_only,
|
|
150
|
+
verbose=use_verbose,
|
|
108
151
|
no_verify=no_verify,
|
|
152
|
+
skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
|
|
153
|
+
language=resolved_language,
|
|
154
|
+
hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
|
|
109
155
|
)
|
|
110
156
|
except Exception as e:
|
|
111
157
|
handle_error(e, exit_program=True)
|
|
112
158
|
else:
|
|
159
|
+
# Determine if we should infer scope based on -s flag or always_include_scope setting
|
|
160
|
+
infer_scope = bool(scope or config.get("always_include_scope", False))
|
|
161
|
+
|
|
113
162
|
ctx.obj = {
|
|
114
163
|
"add_all": add_all,
|
|
164
|
+
"group": group,
|
|
165
|
+
"interactive": interactive,
|
|
115
166
|
"log_level": log_level,
|
|
116
167
|
"one_liner": one_liner,
|
|
117
168
|
"push": push,
|
|
118
169
|
"show_prompt": show_prompt,
|
|
119
|
-
"scope":
|
|
170
|
+
"scope": infer_scope,
|
|
120
171
|
"quiet": quiet,
|
|
121
172
|
"yes": yes,
|
|
122
173
|
"hint": hint,
|
|
123
174
|
"model": model,
|
|
175
|
+
"language": language,
|
|
124
176
|
"version": version,
|
|
125
177
|
"dry_run": dry_run,
|
|
178
|
+
"message_only": message_only,
|
|
126
179
|
"verbose": verbose,
|
|
127
180
|
"no_verify": no_verify,
|
|
181
|
+
"skip_secret_scan": skip_secret_scan,
|
|
182
|
+
"hook_timeout": hook_timeout,
|
|
128
183
|
}
|
|
129
184
|
|
|
130
185
|
|
|
186
|
+
cli.add_command(auth_cli)
|
|
131
187
|
cli.add_command(config_cli)
|
|
132
|
-
cli.add_command(init_cli)
|
|
133
188
|
cli.add_command(diff_cli)
|
|
189
|
+
cli.add_command(init_cli)
|
|
190
|
+
cli.add_command(language_cli)
|
|
191
|
+
cli.add_command(model_cli)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@click.command(context_settings=language_cli.context_settings)
|
|
195
|
+
@click.pass_context
|
|
196
|
+
def lang(ctx):
|
|
197
|
+
"""Set the language for commit messages interactively. (Alias for 'language')"""
|
|
198
|
+
ctx.forward(language_cli)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
cli.add_command(lang) # Add the lang alias
|
|
134
202
|
|
|
135
203
|
if __name__ == "__main__":
|
|
136
204
|
cli()
|
gac/config.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Configuration loading for gac.
|
|
2
2
|
|
|
3
|
-
Handles environment variable and .env file precedence for application settings.
|
|
3
|
+
Handles environment variable and .gac.env file precedence for application settings.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
@@ -11,20 +11,17 @@ from dotenv import load_dotenv
|
|
|
11
11
|
from gac.constants import EnvDefaults, Logging
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def load_config() -> dict[str, str | int | float | bool]:
|
|
15
|
-
"""Load configuration from $HOME/.gac.env, then ./.gac.env
|
|
14
|
+
def load_config() -> dict[str, str | int | float | bool | None]:
|
|
15
|
+
"""Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
|
|
16
16
|
user_config = Path.home() / ".gac.env"
|
|
17
17
|
if user_config.exists():
|
|
18
18
|
load_dotenv(user_config)
|
|
19
19
|
|
|
20
|
-
# Check for
|
|
20
|
+
# Check for .gac.env in project directory
|
|
21
21
|
project_gac_env = Path(".gac.env")
|
|
22
|
-
project_env = Path(".env")
|
|
23
22
|
|
|
24
23
|
if project_gac_env.exists():
|
|
25
24
|
load_dotenv(project_gac_env, override=True)
|
|
26
|
-
elif project_env.exists():
|
|
27
|
-
load_dotenv(project_env, override=True)
|
|
28
25
|
|
|
29
26
|
config = {
|
|
30
27
|
"model": os.getenv("GAC_MODEL"),
|
|
@@ -35,6 +32,15 @@ def load_config() -> dict[str, str | int | float | bool]:
|
|
|
35
32
|
"warning_limit_tokens": int(os.getenv("GAC_WARNING_LIMIT_TOKENS", EnvDefaults.WARNING_LIMIT_TOKENS)),
|
|
36
33
|
"always_include_scope": os.getenv("GAC_ALWAYS_INCLUDE_SCOPE", str(EnvDefaults.ALWAYS_INCLUDE_SCOPE)).lower()
|
|
37
34
|
in ("true", "1", "yes", "on"),
|
|
35
|
+
"skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
|
|
36
|
+
in ("true", "1", "yes", "on"),
|
|
37
|
+
"no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
|
|
38
|
+
"verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
|
|
39
|
+
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
40
|
+
"language": os.getenv("GAC_LANGUAGE"),
|
|
41
|
+
"translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
|
|
42
|
+
"rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
|
|
43
|
+
"hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
return config
|
gac/config_cli.py
CHANGED
|
@@ -18,13 +18,34 @@ def config():
|
|
|
18
18
|
@config.command()
|
|
19
19
|
def show() -> None:
|
|
20
20
|
"""Show all current config values."""
|
|
21
|
-
|
|
21
|
+
project_env_path = Path(".gac.env")
|
|
22
|
+
user_exists = GAC_ENV_PATH.exists()
|
|
23
|
+
project_exists = project_env_path.exists()
|
|
24
|
+
|
|
25
|
+
if not user_exists and not project_exists:
|
|
22
26
|
click.echo("No $HOME/.gac.env found.")
|
|
27
|
+
click.echo("No project-level .gac.env found.")
|
|
23
28
|
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
if user_exists:
|
|
31
|
+
click.echo(f"User config ({GAC_ENV_PATH}):")
|
|
32
|
+
with open(GAC_ENV_PATH, encoding="utf-8") as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
click.echo(line.rstrip())
|
|
35
|
+
else:
|
|
36
|
+
click.echo("No $HOME/.gac.env found.")
|
|
37
|
+
|
|
38
|
+
if project_exists:
|
|
39
|
+
if user_exists:
|
|
40
|
+
click.echo("")
|
|
41
|
+
click.echo("Project config (./.gac.env):")
|
|
42
|
+
with open(project_env_path, encoding="utf-8") as f:
|
|
43
|
+
for line in f:
|
|
44
|
+
click.echo(line.rstrip())
|
|
45
|
+
click.echo("")
|
|
46
|
+
click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
|
|
47
|
+
else:
|
|
48
|
+
click.echo("No project-level .gac.env found.")
|
|
28
49
|
|
|
29
50
|
|
|
30
51
|
@config.command()
|
gac/constants.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from re import Pattern
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class FileStatus(Enum):
|
|
@@ -21,9 +20,13 @@ class EnvDefaults:
|
|
|
21
20
|
|
|
22
21
|
MAX_RETRIES: int = 3
|
|
23
22
|
TEMPERATURE: float = 1
|
|
24
|
-
MAX_OUTPUT_TOKENS: int =
|
|
25
|
-
WARNING_LIMIT_TOKENS: int =
|
|
23
|
+
MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
|
|
24
|
+
WARNING_LIMIT_TOKENS: int = 32768
|
|
26
25
|
ALWAYS_INCLUDE_SCOPE: bool = False
|
|
26
|
+
SKIP_SECRET_SCAN: bool = False
|
|
27
|
+
VERBOSE: bool = False
|
|
28
|
+
NO_TIKTOKEN: bool = False
|
|
29
|
+
HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class Logging:
|
|
@@ -39,13 +42,14 @@ class Utility:
|
|
|
39
42
|
DEFAULT_ENCODING: str = "cl100k_base" # llm encoding
|
|
40
43
|
DEFAULT_DIFF_TOKEN_LIMIT: int = 15000 # Maximum tokens for diff processing
|
|
41
44
|
MAX_WORKERS: int = os.cpu_count() or 4 # Maximum number of parallel workers
|
|
45
|
+
MAX_DISPLAYED_SECRET_LENGTH: int = 50 # Maximum length for displaying secrets
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
class FilePatterns:
|
|
45
49
|
"""Patterns for identifying special file types."""
|
|
46
50
|
|
|
47
51
|
# Regex patterns to detect binary file changes in git diffs (e.g., images or other non-text files)
|
|
48
|
-
BINARY: list[
|
|
52
|
+
BINARY: list[str] = [
|
|
49
53
|
r"Binary files .* differ",
|
|
50
54
|
r"GIT binary patch",
|
|
51
55
|
]
|
|
@@ -126,7 +130,7 @@ class CodePatternImportance:
|
|
|
126
130
|
|
|
127
131
|
# Regex patterns to detect code structure changes in git diffs (e.g., class, function, import)
|
|
128
132
|
# Note: The patterns are prefixed with "+" to match only added and modified lines
|
|
129
|
-
PATTERNS: dict[
|
|
133
|
+
PATTERNS: dict[str, float] = {
|
|
130
134
|
# Structure changes
|
|
131
135
|
r"\+\s*(class|interface|enum)\s+\w+": 1.8, # Class/interface/enum definitions
|
|
132
136
|
r"\+\s*(def|function|func)\s+\w+\s*\(": 1.5, # Function definitions
|
|
@@ -148,3 +152,170 @@ class CodePatternImportance:
|
|
|
148
152
|
r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
|
|
149
153
|
r"\+\s*(assert|expect)": 1.0, # Assertions
|
|
150
154
|
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Languages:
|
|
158
|
+
"""Language code mappings and utilities."""
|
|
159
|
+
|
|
160
|
+
# Language code to full name mapping
|
|
161
|
+
# Supports ISO 639-1 codes and common variants
|
|
162
|
+
CODE_MAP: dict[str, str] = {
|
|
163
|
+
# English
|
|
164
|
+
"en": "English",
|
|
165
|
+
# Chinese
|
|
166
|
+
"zh": "Simplified Chinese",
|
|
167
|
+
"zh-cn": "Simplified Chinese",
|
|
168
|
+
"zh-hans": "Simplified Chinese",
|
|
169
|
+
"zh-tw": "Traditional Chinese",
|
|
170
|
+
"zh-hant": "Traditional Chinese",
|
|
171
|
+
# Japanese
|
|
172
|
+
"ja": "Japanese",
|
|
173
|
+
# Korean
|
|
174
|
+
"ko": "Korean",
|
|
175
|
+
# Spanish
|
|
176
|
+
"es": "Spanish",
|
|
177
|
+
# Portuguese
|
|
178
|
+
"pt": "Portuguese",
|
|
179
|
+
# French
|
|
180
|
+
"fr": "French",
|
|
181
|
+
# German
|
|
182
|
+
"de": "German",
|
|
183
|
+
# Russian
|
|
184
|
+
"ru": "Russian",
|
|
185
|
+
# Hindi
|
|
186
|
+
"hi": "Hindi",
|
|
187
|
+
# Italian
|
|
188
|
+
"it": "Italian",
|
|
189
|
+
# Polish
|
|
190
|
+
"pl": "Polish",
|
|
191
|
+
# Turkish
|
|
192
|
+
"tr": "Turkish",
|
|
193
|
+
# Dutch
|
|
194
|
+
"nl": "Dutch",
|
|
195
|
+
# Vietnamese
|
|
196
|
+
"vi": "Vietnamese",
|
|
197
|
+
# Thai
|
|
198
|
+
"th": "Thai",
|
|
199
|
+
# Indonesian
|
|
200
|
+
"id": "Indonesian",
|
|
201
|
+
# Swedish
|
|
202
|
+
"sv": "Swedish",
|
|
203
|
+
# Arabic
|
|
204
|
+
"ar": "Arabic",
|
|
205
|
+
# Hebrew
|
|
206
|
+
"he": "Hebrew",
|
|
207
|
+
# Greek
|
|
208
|
+
"el": "Greek",
|
|
209
|
+
# Danish
|
|
210
|
+
"da": "Danish",
|
|
211
|
+
# Norwegian
|
|
212
|
+
"no": "Norwegian",
|
|
213
|
+
"nb": "Norwegian",
|
|
214
|
+
"nn": "Norwegian",
|
|
215
|
+
# Finnish
|
|
216
|
+
"fi": "Finnish",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# List of languages with display names and English names for CLI selection
|
|
220
|
+
# Format: (display_name, english_name)
|
|
221
|
+
LANGUAGES: list[tuple[str, str]] = [
|
|
222
|
+
("English", "English"),
|
|
223
|
+
("简体中文", "Simplified Chinese"),
|
|
224
|
+
("繁體中文", "Traditional Chinese"),
|
|
225
|
+
("日本語", "Japanese"),
|
|
226
|
+
("한국어", "Korean"),
|
|
227
|
+
("Español", "Spanish"),
|
|
228
|
+
("Português", "Portuguese"),
|
|
229
|
+
("Français", "French"),
|
|
230
|
+
("Deutsch", "German"),
|
|
231
|
+
("Русский", "Russian"),
|
|
232
|
+
("हिन्दी", "Hindi"),
|
|
233
|
+
("Italiano", "Italian"),
|
|
234
|
+
("Polski", "Polish"),
|
|
235
|
+
("Türkçe", "Turkish"),
|
|
236
|
+
("Nederlands", "Dutch"),
|
|
237
|
+
("Tiếng Việt", "Vietnamese"),
|
|
238
|
+
("ไทย", "Thai"),
|
|
239
|
+
("Bahasa Indonesia", "Indonesian"),
|
|
240
|
+
("Svenska", "Swedish"),
|
|
241
|
+
("العربية", "Arabic"),
|
|
242
|
+
("עברית", "Hebrew"),
|
|
243
|
+
("Ελληνικά", "Greek"),
|
|
244
|
+
("Dansk", "Danish"),
|
|
245
|
+
("Norsk", "Norwegian"),
|
|
246
|
+
("Suomi", "Finnish"),
|
|
247
|
+
("Custom", "Custom"),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def resolve_code(language: str) -> str:
|
|
252
|
+
"""Resolve a language code to its full name.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Full language name (e.g., 'Spanish', 'Simplified Chinese')
|
|
259
|
+
|
|
260
|
+
If the input is already a full language name, it's returned as-is.
|
|
261
|
+
If it's a recognized code, it's converted to the full name.
|
|
262
|
+
Otherwise, the input is returned unchanged (for custom languages).
|
|
263
|
+
"""
|
|
264
|
+
# Normalize the code to lowercase for lookup
|
|
265
|
+
code_lower = language.lower().strip()
|
|
266
|
+
|
|
267
|
+
# Check if it's a recognized code
|
|
268
|
+
if code_lower in Languages.CODE_MAP:
|
|
269
|
+
return Languages.CODE_MAP[code_lower]
|
|
270
|
+
|
|
271
|
+
# Return as-is (could be a full name or custom language)
|
|
272
|
+
return language
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class CommitMessageConstants:
|
|
276
|
+
"""Constants for commit message generation and cleaning."""
|
|
277
|
+
|
|
278
|
+
# Conventional commit type prefixes
|
|
279
|
+
CONVENTIONAL_PREFIXES: list[str] = [
|
|
280
|
+
"feat",
|
|
281
|
+
"fix",
|
|
282
|
+
"docs",
|
|
283
|
+
"style",
|
|
284
|
+
"refactor",
|
|
285
|
+
"perf",
|
|
286
|
+
"test",
|
|
287
|
+
"build",
|
|
288
|
+
"ci",
|
|
289
|
+
"chore",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
# XML tags that may leak from prompt templates into AI responses
|
|
293
|
+
XML_TAGS_TO_REMOVE: list[str] = [
|
|
294
|
+
"<git-status>",
|
|
295
|
+
"</git-status>",
|
|
296
|
+
"<git_status>",
|
|
297
|
+
"</git_status>",
|
|
298
|
+
"<git-diff>",
|
|
299
|
+
"</git-diff>",
|
|
300
|
+
"<git_diff>",
|
|
301
|
+
"</git_diff>",
|
|
302
|
+
"<repository_context>",
|
|
303
|
+
"</repository_context>",
|
|
304
|
+
"<instructions>",
|
|
305
|
+
"</instructions>",
|
|
306
|
+
"<format>",
|
|
307
|
+
"</format>",
|
|
308
|
+
"<conventions>",
|
|
309
|
+
"</conventions>",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
# Indicators that mark the start of the actual commit message in AI responses
|
|
313
|
+
COMMIT_INDICATORS: list[str] = [
|
|
314
|
+
"# Your commit message:",
|
|
315
|
+
"Your commit message:",
|
|
316
|
+
"The commit message is:",
|
|
317
|
+
"Here's the commit message:",
|
|
318
|
+
"Commit message:",
|
|
319
|
+
"Final commit message:",
|
|
320
|
+
"# Commit Message",
|
|
321
|
+
]
|
gac/errors.py
CHANGED
|
@@ -95,6 +95,11 @@ class AIError(GacError):
|
|
|
95
95
|
"""Create a model error."""
|
|
96
96
|
return cls(message, error_type="model")
|
|
97
97
|
|
|
98
|
+
@classmethod
|
|
99
|
+
def unknown_error(cls, message: str) -> "AIError":
|
|
100
|
+
"""Create an unknown error."""
|
|
101
|
+
return cls(message, error_type="unknown")
|
|
102
|
+
|
|
98
103
|
|
|
99
104
|
class FormattingError(GacError):
|
|
100
105
|
"""Error related to code formatting."""
|
|
@@ -102,6 +107,12 @@ class FormattingError(GacError):
|
|
|
102
107
|
exit_code = 5
|
|
103
108
|
|
|
104
109
|
|
|
110
|
+
class SecurityError(GacError):
|
|
111
|
+
"""Error related to security issues (e.g., detected secrets)."""
|
|
112
|
+
|
|
113
|
+
exit_code = 6
|
|
114
|
+
|
|
115
|
+
|
|
105
116
|
# Simplified error hierarchy - we use a single AIError class with error codes
|
|
106
117
|
# instead of multiple subclasses for better maintainability
|
|
107
118
|
|
|
@@ -130,6 +141,8 @@ def handle_error(error: Exception, exit_program: bool = False, quiet: bool = Fal
|
|
|
130
141
|
logger.error("Git operation failed. Please check your repository status.")
|
|
131
142
|
elif isinstance(error, AIError):
|
|
132
143
|
logger.error("AI operation failed. Please check your configuration and API keys.")
|
|
144
|
+
elif isinstance(error, SecurityError):
|
|
145
|
+
logger.error("Security scan detected potential secrets in staged changes.")
|
|
133
146
|
else:
|
|
134
147
|
logger.error("An unexpected error occurred.")
|
|
135
148
|
|
|
@@ -170,6 +183,7 @@ def format_error_for_user(error: Exception) -> str:
|
|
|
170
183
|
ConfigError: "Please check your configuration settings.",
|
|
171
184
|
GitError: "Please ensure Git is installed and you're in a valid Git repository.",
|
|
172
185
|
FormattingError: "Please check that required formatters are installed.",
|
|
186
|
+
SecurityError: "Please remove or secure any detected secrets before committing.",
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
# Generic remediation for unexpected errors
|