gac 2.3.0__tar.gz → 2.4.1__tar.gz
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-2.3.0 → gac-2.4.1}/PKG-INFO +32 -9
- {gac-2.3.0 → gac-2.4.1}/README.md +31 -8
- {gac-2.3.0 → gac-2.4.1}/src/gac/__version__.py +1 -1
- {gac-2.3.0 → gac-2.4.1}/src/gac/cli.py +3 -1
- {gac-2.3.0 → gac-2.4.1}/src/gac/config.py +1 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/git.py +65 -8
- {gac-2.3.0 → gac-2.4.1}/src/gac/init_cli.py +138 -19
- gac-2.4.1/src/gac/language_cli.py +250 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/utils.py +104 -3
- gac-2.3.0/src/gac/language_cli.py +0 -82
- {gac-2.3.0 → gac-2.4.1}/.gitignore +0 -0
- {gac-2.3.0 → gac-2.4.1}/LICENSE +0 -0
- {gac-2.3.0 → gac-2.4.1}/pyproject.toml +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/__init__.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/ai.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/ai_utils.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/config_cli.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/constants.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/diff_cli.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/errors.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/main.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/preprocess.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/prompt.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/__init__.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/anthropic.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/cerebras.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/chutes.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/custom_anthropic.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/custom_openai.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/deepseek.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/fireworks.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/gemini.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/groq.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/lmstudio.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/minimax.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/mistral.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/ollama.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/openai.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/openrouter.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/streamlake.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/synthetic.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/together.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/providers/zai.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/security.py +0 -0
- {gac-2.3.0 → gac-2.4.1}/src/gac/workflow_utils.py +0 -0
{gac-2.3.0 → gac-2.4.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gac
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: LLM-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
|
|
@@ -41,6 +41,9 @@ Requires-Dist: twine; extra == 'dev'
|
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
|
|
43
43
|
<!-- markdownlint-disable MD013 -->
|
|
44
|
+
<!-- markdownlint-disable MD033 MD036 -->
|
|
45
|
+
|
|
46
|
+
<div align="center">
|
|
44
47
|
|
|
45
48
|
# 🚀 Git Auto Commit (gac)
|
|
46
49
|
|
|
@@ -50,9 +53,11 @@ Description-Content-Type: text/markdown
|
|
|
50
53
|
[](https://app.codecov.io/gh/cellwebb/gac)
|
|
51
54
|
[](https://github.com/astral-sh/ruff)
|
|
52
55
|
[](https://mypy-lang.org/)
|
|
53
|
-
[](docs/CONTRIBUTING.md)
|
|
56
|
+
[](docs/en/CONTRIBUTING.md)
|
|
54
57
|
[](LICENSE)
|
|
55
58
|
|
|
59
|
+
**English** | [简体中文](README.zh-CN.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [हिन्दी](README.hi.md) | [Français](README.fr.md) | [Русский](README.ru.md) | [Español](README.es.md) | [Português](README.pt.md) | [Deutsch](README.de.md) | [Nederlands](README.nl.md)
|
|
60
|
+
|
|
56
61
|
**LLM-powered commit messages that understand your code!**
|
|
57
62
|
|
|
58
63
|
**Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
|
|
@@ -67,12 +72,19 @@ Intelligent, contextual messages that explain the **why** behind your changes:
|
|
|
67
72
|
|
|
68
73
|
---
|
|
69
74
|
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- markdownlint-enable MD033 MD036 -->
|
|
78
|
+
|
|
79
|
+
<!-- markdownlint-enable MD033 MD036 -->
|
|
80
|
+
|
|
70
81
|
## Quick Start
|
|
71
82
|
|
|
72
83
|
### Use gac without installing
|
|
73
84
|
|
|
74
85
|
```bash
|
|
75
|
-
uvx gac init
|
|
86
|
+
uvx gac init # Configure your provider, model, and language
|
|
87
|
+
uvx gac model # Re-run provider/model setup without language prompts
|
|
76
88
|
uvx gac # Generate and commit with LLM
|
|
77
89
|
```
|
|
78
90
|
|
|
@@ -83,6 +95,7 @@ That's it! Review the generated message and confirm with `y`.
|
|
|
83
95
|
```bash
|
|
84
96
|
uv tool install gac
|
|
85
97
|
gac init
|
|
98
|
+
gac model
|
|
86
99
|
gac
|
|
87
100
|
```
|
|
88
101
|
|
|
@@ -220,6 +233,8 @@ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
|
|
|
220
233
|
|
|
221
234
|
Run `gac init` to configure your provider interactively, or set environment variables:
|
|
222
235
|
|
|
236
|
+
Need to change providers or models later without touching language settings? Use `gac model` for a streamlined flow that skips the language prompts.
|
|
237
|
+
|
|
223
238
|
```bash
|
|
224
239
|
# Example configuration
|
|
225
240
|
GAC_MODEL=anthropic:your-model-name
|
|
@@ -231,16 +246,24 @@ See `.gac.env.example` for all available options.
|
|
|
231
246
|
|
|
232
247
|
**Want commit messages in another language?** Run `gac language` to select from 25+ languages including Español, Français, 日本語, and more.
|
|
233
248
|
|
|
234
|
-
**Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
|
|
249
|
+
**Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Project Analytics
|
|
254
|
+
|
|
255
|
+
📊 **[View live usage analytics and statistics →](https://clickpy.clickhouse.com/dashboard/gac)**
|
|
256
|
+
|
|
257
|
+
Track real-time installation metrics and package download statistics.
|
|
235
258
|
|
|
236
259
|
---
|
|
237
260
|
|
|
238
261
|
## Getting Help
|
|
239
262
|
|
|
240
|
-
- **Full documentation**: [USAGE.md](USAGE.md) - Complete CLI reference
|
|
241
|
-
- **Custom prompts**: [CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
|
|
242
|
-
- **Troubleshooting**: [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
|
243
|
-
- **Contributing**: [CONTRIBUTING.md](docs/CONTRIBUTING.md) - Development setup and guidelines
|
|
263
|
+
- **Full documentation**: [docs/USAGE.md](docs/en/USAGE.md) - Complete CLI reference
|
|
264
|
+
- **Custom prompts**: [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
|
|
265
|
+
- **Troubleshooting**: [docs/TROUBLESHOOTING.md](docs/en/TROUBLESHOOTING.md) - Common issues and solutions
|
|
266
|
+
- **Contributing**: [docs/CONTRIBUTING.md](docs/en/CONTRIBUTING.md) - Development setup and guidelines
|
|
244
267
|
|
|
245
268
|
---
|
|
246
269
|
|
|
@@ -250,7 +273,7 @@ See `.gac.env.example` for all available options.
|
|
|
250
273
|
|
|
251
274
|
Made with ❤️ for developers who want better commit messages
|
|
252
275
|
|
|
253
|
-
[⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](USAGE.md)
|
|
276
|
+
[⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](docs/en/USAGE.md)
|
|
254
277
|
|
|
255
278
|
</div>
|
|
256
279
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD013 -->
|
|
2
|
+
<!-- markdownlint-disable MD033 MD036 -->
|
|
3
|
+
|
|
4
|
+
<div align="center">
|
|
2
5
|
|
|
3
6
|
# 🚀 Git Auto Commit (gac)
|
|
4
7
|
|
|
@@ -8,9 +11,11 @@
|
|
|
8
11
|
[](https://app.codecov.io/gh/cellwebb/gac)
|
|
9
12
|
[](https://github.com/astral-sh/ruff)
|
|
10
13
|
[](https://mypy-lang.org/)
|
|
11
|
-
[](docs/CONTRIBUTING.md)
|
|
14
|
+
[](docs/en/CONTRIBUTING.md)
|
|
12
15
|
[](LICENSE)
|
|
13
16
|
|
|
17
|
+
**English** | [简体中文](README.zh-CN.md) | [繁體中文](README.zh-TW.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [हिन्दी](README.hi.md) | [Français](README.fr.md) | [Русский](README.ru.md) | [Español](README.es.md) | [Português](README.pt.md) | [Deutsch](README.de.md) | [Nederlands](README.nl.md)
|
|
18
|
+
|
|
14
19
|
**LLM-powered commit messages that understand your code!**
|
|
15
20
|
|
|
16
21
|
**Automate your commits!** Replace `git commit -m "..."` with `gac` for contextual, well-formatted commit messages generated by large language models!
|
|
@@ -25,12 +30,19 @@ Intelligent, contextual messages that explain the **why** behind your changes:
|
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- markdownlint-enable MD033 MD036 -->
|
|
36
|
+
|
|
37
|
+
<!-- markdownlint-enable MD033 MD036 -->
|
|
38
|
+
|
|
28
39
|
## Quick Start
|
|
29
40
|
|
|
30
41
|
### Use gac without installing
|
|
31
42
|
|
|
32
43
|
```bash
|
|
33
|
-
uvx gac init
|
|
44
|
+
uvx gac init # Configure your provider, model, and language
|
|
45
|
+
uvx gac model # Re-run provider/model setup without language prompts
|
|
34
46
|
uvx gac # Generate and commit with LLM
|
|
35
47
|
```
|
|
36
48
|
|
|
@@ -41,6 +53,7 @@ That's it! Review the generated message and confirm with `y`.
|
|
|
41
53
|
```bash
|
|
42
54
|
uv tool install gac
|
|
43
55
|
gac init
|
|
56
|
+
gac model
|
|
44
57
|
gac
|
|
45
58
|
```
|
|
46
59
|
|
|
@@ -178,6 +191,8 @@ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
|
|
|
178
191
|
|
|
179
192
|
Run `gac init` to configure your provider interactively, or set environment variables:
|
|
180
193
|
|
|
194
|
+
Need to change providers or models later without touching language settings? Use `gac model` for a streamlined flow that skips the language prompts.
|
|
195
|
+
|
|
181
196
|
```bash
|
|
182
197
|
# Example configuration
|
|
183
198
|
GAC_MODEL=anthropic:your-model-name
|
|
@@ -189,16 +204,24 @@ See `.gac.env.example` for all available options.
|
|
|
189
204
|
|
|
190
205
|
**Want commit messages in another language?** Run `gac language` to select from 25+ languages including Español, Français, 日本語, and more.
|
|
191
206
|
|
|
192
|
-
**Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
|
|
207
|
+
**Want to customize commit message style?** See [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) for guidance on writing custom system prompts.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Project Analytics
|
|
212
|
+
|
|
213
|
+
📊 **[View live usage analytics and statistics →](https://clickpy.clickhouse.com/dashboard/gac)**
|
|
214
|
+
|
|
215
|
+
Track real-time installation metrics and package download statistics.
|
|
193
216
|
|
|
194
217
|
---
|
|
195
218
|
|
|
196
219
|
## Getting Help
|
|
197
220
|
|
|
198
|
-
- **Full documentation**: [USAGE.md](USAGE.md) - Complete CLI reference
|
|
199
|
-
- **Custom prompts**: [CUSTOM_SYSTEM_PROMPTS.md](docs/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
|
|
200
|
-
- **Troubleshooting**: [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - Common issues and solutions
|
|
201
|
-
- **Contributing**: [CONTRIBUTING.md](docs/CONTRIBUTING.md) - Development setup and guidelines
|
|
221
|
+
- **Full documentation**: [docs/USAGE.md](docs/en/USAGE.md) - Complete CLI reference
|
|
222
|
+
- **Custom prompts**: [docs/CUSTOM_SYSTEM_PROMPTS.md](docs/en/CUSTOM_SYSTEM_PROMPTS.md) - Customize commit message style
|
|
223
|
+
- **Troubleshooting**: [docs/TROUBLESHOOTING.md](docs/en/TROUBLESHOOTING.md) - Common issues and solutions
|
|
224
|
+
- **Contributing**: [docs/CONTRIBUTING.md](docs/en/CONTRIBUTING.md) - Development setup and guidelines
|
|
202
225
|
|
|
203
226
|
---
|
|
204
227
|
|
|
@@ -208,7 +231,7 @@ See `.gac.env.example` for all available options.
|
|
|
208
231
|
|
|
209
232
|
Made with ❤️ for developers who want better commit messages
|
|
210
233
|
|
|
211
|
-
[⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](USAGE.md)
|
|
234
|
+
[⭐ Star us on GitHub](https://github.com/cellwebb/gac) • [🐛 Report issues](https://github.com/cellwebb/gac/issues) • [📖 Full docs](docs/en/USAGE.md)
|
|
212
235
|
|
|
213
236
|
</div>
|
|
214
237
|
|
|
@@ -17,6 +17,7 @@ from gac.constants import Languages, Logging
|
|
|
17
17
|
from gac.diff_cli import diff as diff_cli
|
|
18
18
|
from gac.errors import handle_error
|
|
19
19
|
from gac.init_cli import init as init_cli
|
|
20
|
+
from gac.init_cli import model as model_cli
|
|
20
21
|
from gac.language_cli import language as language_cli
|
|
21
22
|
from gac.main import main
|
|
22
23
|
from gac.utils import setup_logging
|
|
@@ -154,9 +155,10 @@ def cli(
|
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
cli.add_command(config_cli)
|
|
158
|
+
cli.add_command(diff_cli)
|
|
157
159
|
cli.add_command(init_cli)
|
|
158
160
|
cli.add_command(language_cli)
|
|
159
|
-
cli.add_command(
|
|
161
|
+
cli.add_command(model_cli)
|
|
160
162
|
|
|
161
163
|
|
|
162
164
|
@click.command(context_settings=language_cli.context_settings)
|
|
@@ -41,6 +41,7 @@ def load_config() -> dict[str, str | int | float | bool | None]:
|
|
|
41
41
|
"system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
|
|
42
42
|
"language": os.getenv("GAC_LANGUAGE"),
|
|
43
43
|
"translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
|
|
44
|
+
"rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return config
|
|
@@ -14,6 +14,63 @@ from gac.utils import run_subprocess
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def run_subprocess_with_encoding_fallback(
|
|
18
|
+
command: list[str], silent: bool = False, timeout: int = 60
|
|
19
|
+
) -> subprocess.CompletedProcess:
|
|
20
|
+
"""Run subprocess with encoding fallback, returning full CompletedProcess object.
|
|
21
|
+
|
|
22
|
+
This is used for cases where we need both stdout and stderr separately,
|
|
23
|
+
like pre-commit and lefthook hook execution.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
command: List of command arguments
|
|
27
|
+
silent: If True, suppress debug logging
|
|
28
|
+
timeout: Command timeout in seconds
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
CompletedProcess object with stdout, stderr, and returncode
|
|
32
|
+
"""
|
|
33
|
+
from gac.utils import get_safe_encodings
|
|
34
|
+
|
|
35
|
+
encodings = get_safe_encodings()
|
|
36
|
+
last_exception: Exception | None = None
|
|
37
|
+
|
|
38
|
+
for encoding in encodings:
|
|
39
|
+
try:
|
|
40
|
+
if not silent:
|
|
41
|
+
logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
|
|
42
|
+
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
command,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
check=False,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
encoding=encoding,
|
|
50
|
+
errors="replace",
|
|
51
|
+
)
|
|
52
|
+
return result
|
|
53
|
+
except UnicodeError as e:
|
|
54
|
+
last_exception = e
|
|
55
|
+
if not silent:
|
|
56
|
+
logger.debug(f"Failed to decode with {encoding}: {e}")
|
|
57
|
+
continue
|
|
58
|
+
except subprocess.TimeoutExpired:
|
|
59
|
+
raise
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if not silent:
|
|
62
|
+
logger.debug(f"Command error: {e}")
|
|
63
|
+
# Try next encoding for non-timeout errors
|
|
64
|
+
last_exception = e
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# If we get here, all encodings failed
|
|
68
|
+
if last_exception:
|
|
69
|
+
raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
|
|
70
|
+
else:
|
|
71
|
+
raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
|
|
72
|
+
|
|
73
|
+
|
|
17
74
|
def run_git_command(args: list[str], silent: bool = False, timeout: int = 30) -> str:
|
|
18
75
|
"""Run a git command and return the output."""
|
|
19
76
|
command = ["git"] + args
|
|
@@ -132,20 +189,20 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
|
|
|
132
189
|
|
|
133
190
|
def get_repo_root() -> str:
|
|
134
191
|
"""Get absolute path of repository root."""
|
|
135
|
-
result =
|
|
136
|
-
return result
|
|
192
|
+
result = run_git_command(["rev-parse", "--show-toplevel"])
|
|
193
|
+
return result
|
|
137
194
|
|
|
138
195
|
|
|
139
196
|
def get_current_branch() -> str:
|
|
140
197
|
"""Get name of current git branch."""
|
|
141
|
-
result =
|
|
142
|
-
return result
|
|
198
|
+
result = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
199
|
+
return result
|
|
143
200
|
|
|
144
201
|
|
|
145
202
|
def get_commit_hash() -> str:
|
|
146
203
|
"""Get SHA-1 hash of current commit."""
|
|
147
|
-
result =
|
|
148
|
-
return result
|
|
204
|
+
result = run_git_command(["rev-parse", "HEAD"])
|
|
205
|
+
return result
|
|
149
206
|
|
|
150
207
|
|
|
151
208
|
def run_pre_commit_hooks() -> bool:
|
|
@@ -170,7 +227,7 @@ def run_pre_commit_hooks() -> bool:
|
|
|
170
227
|
# Run pre-commit hooks on staged files
|
|
171
228
|
logger.info("Running pre-commit hooks...")
|
|
172
229
|
# Run pre-commit and capture both stdout and stderr
|
|
173
|
-
result =
|
|
230
|
+
result = run_subprocess_with_encoding_fallback(["pre-commit", "run"])
|
|
174
231
|
|
|
175
232
|
if result.returncode == 0:
|
|
176
233
|
# All hooks passed
|
|
@@ -220,7 +277,7 @@ def run_lefthook_hooks() -> bool:
|
|
|
220
277
|
# Run lefthook hooks on staged files
|
|
221
278
|
logger.info("Running Lefthook hooks...")
|
|
222
279
|
# Run lefthook and capture both stdout and stderr
|
|
223
|
-
result =
|
|
280
|
+
result = run_subprocess_with_encoding_fallback(["lefthook", "run", "pre-commit"])
|
|
224
281
|
|
|
225
282
|
if result.returncode == 0:
|
|
226
283
|
# All hooks passed
|
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
"""CLI for initializing gac configuration interactively."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
import questionary
|
|
7
|
-
from dotenv import dotenv_values, set_key
|
|
8
|
+
from dotenv import dotenv_values, load_dotenv, set_key
|
|
8
9
|
|
|
9
10
|
from gac.constants import Languages
|
|
10
11
|
|
|
11
12
|
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _should_show_rtl_warning_for_init() -> bool:
|
|
16
|
+
"""Check if RTL warning should be shown based on init's GAC_ENV_PATH.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if warning should be shown, False if user previously confirmed
|
|
20
|
+
"""
|
|
21
|
+
if GAC_ENV_PATH.exists():
|
|
22
|
+
load_dotenv(GAC_ENV_PATH)
|
|
23
|
+
rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
|
|
24
|
+
return not rtl_confirmed
|
|
25
|
+
return True # Show warning if no config exists
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _show_rtl_warning_for_init(language_name: str) -> bool:
|
|
29
|
+
"""Show RTL language warning for init command and save preference to GAC_ENV_PATH.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
language_name: Name of the RTL language
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if user wants to proceed, False if they cancel
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
terminal_width = 80 # Use default width
|
|
39
|
+
title = "⚠️ RTL Language Detected".center(terminal_width)
|
|
40
|
+
|
|
41
|
+
click.echo()
|
|
42
|
+
click.echo(click.style(title, fg="yellow", bold=True))
|
|
43
|
+
click.echo()
|
|
44
|
+
click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
|
|
45
|
+
click.echo("However, the commit messages will work fine and should be readable in Git clients")
|
|
46
|
+
click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
|
|
47
|
+
|
|
48
|
+
proceed = questionary.confirm("Do you want to proceed anyway?").ask()
|
|
49
|
+
if proceed:
|
|
50
|
+
# Remember that user has confirmed RTL acceptance
|
|
51
|
+
set_key(str(GAC_ENV_PATH), "GAC_RTL_CONFIRMED", "true")
|
|
52
|
+
click.echo("✓ RTL preference saved - you won't see this warning again")
|
|
53
|
+
return proceed if proceed is not None else False
|
|
54
|
+
|
|
55
|
+
|
|
14
56
|
def _prompt_required_text(prompt: str) -> str | None:
|
|
15
57
|
"""Prompt until a non-empty string is provided or the user cancels."""
|
|
16
58
|
while True:
|
|
@@ -23,20 +65,20 @@ def _prompt_required_text(prompt: str) -> str | None:
|
|
|
23
65
|
click.echo("A value is required. Please try again.")
|
|
24
66
|
|
|
25
67
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
click.echo("Welcome to gac initialization!\n")
|
|
30
|
-
|
|
31
|
-
# Load existing environment values
|
|
32
|
-
existing_env = {}
|
|
68
|
+
def _load_existing_env() -> dict[str, str]:
|
|
69
|
+
"""Ensure the env file exists and return its current values."""
|
|
70
|
+
existing_env: dict[str, str] = {}
|
|
33
71
|
if GAC_ENV_PATH.exists():
|
|
34
72
|
click.echo(f"$HOME/.gac.env already exists at {GAC_ENV_PATH}. Values will be updated.")
|
|
35
|
-
existing_env =
|
|
73
|
+
existing_env = {k: v for k, v in dotenv_values(str(GAC_ENV_PATH)).items() if v is not None}
|
|
36
74
|
else:
|
|
37
75
|
GAC_ENV_PATH.touch()
|
|
38
76
|
click.echo(f"Created $HOME/.gac.env at {GAC_ENV_PATH}.")
|
|
77
|
+
return existing_env
|
|
39
78
|
|
|
79
|
+
|
|
80
|
+
def _configure_model(existing_env: dict[str, str]) -> bool:
|
|
81
|
+
"""Run the provider/model/API key configuration flow."""
|
|
40
82
|
providers = [
|
|
41
83
|
("Anthropic", "claude-haiku-4-5"),
|
|
42
84
|
("Cerebras", "zai-glm-4.6"),
|
|
@@ -63,7 +105,7 @@ def init() -> None:
|
|
|
63
105
|
provider = questionary.select("Select your provider:", choices=provider_names).ask()
|
|
64
106
|
if not provider:
|
|
65
107
|
click.echo("Provider selection cancelled. Exiting.")
|
|
66
|
-
return
|
|
108
|
+
return False
|
|
67
109
|
provider_key = provider.lower().replace(".", "").replace(" ", "-").replace("(", "").replace(")", "")
|
|
68
110
|
|
|
69
111
|
is_ollama = provider_key == "ollama"
|
|
@@ -77,7 +119,7 @@ def init() -> None:
|
|
|
77
119
|
endpoint_id = _prompt_required_text("Enter the Streamlake inference endpoint ID (required):")
|
|
78
120
|
if endpoint_id is None:
|
|
79
121
|
click.echo("Streamlake configuration cancelled. Exiting.")
|
|
80
|
-
return
|
|
122
|
+
return False
|
|
81
123
|
model_to_save = endpoint_id
|
|
82
124
|
else:
|
|
83
125
|
model_suggestion = dict(providers)[provider]
|
|
@@ -88,7 +130,7 @@ def init() -> None:
|
|
|
88
130
|
model = questionary.text(model_prompt, default=model_suggestion).ask()
|
|
89
131
|
if model is None:
|
|
90
132
|
click.echo("Model entry cancelled. Exiting.")
|
|
91
|
-
return
|
|
133
|
+
return False
|
|
92
134
|
model_to_save = model.strip() if model.strip() else model_suggestion
|
|
93
135
|
|
|
94
136
|
set_key(str(GAC_ENV_PATH), "GAC_MODEL", f"{provider_key}:{model_to_save}")
|
|
@@ -98,7 +140,7 @@ def init() -> None:
|
|
|
98
140
|
base_url = _prompt_required_text("Enter the custom Anthropic-compatible base URL (required):")
|
|
99
141
|
if base_url is None:
|
|
100
142
|
click.echo("Custom Anthropic base URL entry cancelled. Exiting.")
|
|
101
|
-
return
|
|
143
|
+
return False
|
|
102
144
|
set_key(str(GAC_ENV_PATH), "CUSTOM_ANTHROPIC_BASE_URL", base_url)
|
|
103
145
|
click.echo(f"Set CUSTOM_ANTHROPIC_BASE_URL={base_url}")
|
|
104
146
|
|
|
@@ -112,7 +154,7 @@ def init() -> None:
|
|
|
112
154
|
base_url = _prompt_required_text("Enter the custom OpenAI-compatible base URL (required):")
|
|
113
155
|
if base_url is None:
|
|
114
156
|
click.echo("Custom OpenAI base URL entry cancelled. Exiting.")
|
|
115
|
-
return
|
|
157
|
+
return False
|
|
116
158
|
set_key(str(GAC_ENV_PATH), "CUSTOM_OPENAI_BASE_URL", base_url)
|
|
117
159
|
click.echo(f"Set CUSTOM_OPENAI_BASE_URL={base_url}")
|
|
118
160
|
elif is_ollama:
|
|
@@ -120,7 +162,7 @@ def init() -> None:
|
|
|
120
162
|
url = questionary.text(f"Enter the Ollama API URL (default: {url_default}):", default=url_default).ask()
|
|
121
163
|
if url is None:
|
|
122
164
|
click.echo("Ollama URL entry cancelled. Exiting.")
|
|
123
|
-
return
|
|
165
|
+
return False
|
|
124
166
|
url_to_save = url.strip() if url.strip() else url_default
|
|
125
167
|
set_key(str(GAC_ENV_PATH), "OLLAMA_API_URL", url_to_save)
|
|
126
168
|
click.echo(f"Set OLLAMA_API_URL={url_to_save}")
|
|
@@ -129,7 +171,7 @@ def init() -> None:
|
|
|
129
171
|
url = questionary.text(f"Enter the LM Studio API URL (default: {url_default}):", default=url_default).ask()
|
|
130
172
|
if url is None:
|
|
131
173
|
click.echo("LM Studio URL entry cancelled. Exiting.")
|
|
132
|
-
return
|
|
174
|
+
return False
|
|
133
175
|
url_to_save = url.strip() if url.strip() else url_default
|
|
134
176
|
set_key(str(GAC_ENV_PATH), "LMSTUDIO_API_URL", url_to_save)
|
|
135
177
|
click.echo(f"Set LMSTUDIO_API_URL={url_to_save}")
|
|
@@ -189,7 +231,13 @@ def init() -> None:
|
|
|
189
231
|
else:
|
|
190
232
|
click.echo("No API key entered. You can add one later by editing ~/.gac.env")
|
|
191
233
|
|
|
192
|
-
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _configure_language(existing_env: dict[str, str]) -> None:
|
|
238
|
+
"""Run the language configuration flow."""
|
|
239
|
+
from gac.language_cli import is_rtl_text
|
|
240
|
+
|
|
193
241
|
click.echo("\n")
|
|
194
242
|
existing_language = existing_env.get("GAC_LANGUAGE")
|
|
195
243
|
|
|
@@ -216,7 +264,11 @@ def init() -> None:
|
|
|
216
264
|
# Proceed with language selection
|
|
217
265
|
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
218
266
|
language_selection = questionary.select(
|
|
219
|
-
"Select a language for commit messages:",
|
|
267
|
+
"Select a language for commit messages:",
|
|
268
|
+
choices=display_names,
|
|
269
|
+
use_shortcuts=True,
|
|
270
|
+
use_arrow_keys=True,
|
|
271
|
+
use_jk_keys=False,
|
|
220
272
|
).ask()
|
|
221
273
|
|
|
222
274
|
if not language_selection:
|
|
@@ -237,10 +289,30 @@ def init() -> None:
|
|
|
237
289
|
language_value = None
|
|
238
290
|
else:
|
|
239
291
|
language_value = custom_language.strip()
|
|
292
|
+
|
|
293
|
+
# Check if the custom language appears to be RTL
|
|
294
|
+
if is_rtl_text(language_value):
|
|
295
|
+
if not _should_show_rtl_warning_for_init():
|
|
296
|
+
click.echo(
|
|
297
|
+
f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)"
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
301
|
+
click.echo("Language selection cancelled. Keeping existing language.")
|
|
302
|
+
language_value = None
|
|
240
303
|
else:
|
|
241
304
|
# Find the English name for the selected language
|
|
242
305
|
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
|
|
243
306
|
|
|
307
|
+
# Check if predefined language is RTL
|
|
308
|
+
if is_rtl_text(language_value):
|
|
309
|
+
if not _should_show_rtl_warning_for_init():
|
|
310
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
311
|
+
else:
|
|
312
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
313
|
+
click.echo("Language selection cancelled. Keeping existing language.")
|
|
314
|
+
language_value = None
|
|
315
|
+
|
|
244
316
|
if language_value:
|
|
245
317
|
# Ask about prefix translation
|
|
246
318
|
prefix_choice = questionary.select(
|
|
@@ -266,7 +338,11 @@ def init() -> None:
|
|
|
266
338
|
# No existing language - proceed with normal flow
|
|
267
339
|
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
268
340
|
language_selection = questionary.select(
|
|
269
|
-
"Select a language for commit messages:",
|
|
341
|
+
"Select a language for commit messages:",
|
|
342
|
+
choices=display_names,
|
|
343
|
+
use_shortcuts=True,
|
|
344
|
+
use_arrow_keys=True,
|
|
345
|
+
use_jk_keys=False,
|
|
270
346
|
).ask()
|
|
271
347
|
|
|
272
348
|
if not language_selection:
|
|
@@ -287,10 +363,28 @@ def init() -> None:
|
|
|
287
363
|
language_value = None
|
|
288
364
|
else:
|
|
289
365
|
language_value = custom_language.strip()
|
|
366
|
+
|
|
367
|
+
# Check if the custom language appears to be RTL
|
|
368
|
+
if is_rtl_text(language_value):
|
|
369
|
+
if not _should_show_rtl_warning_for_init():
|
|
370
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
371
|
+
else:
|
|
372
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
373
|
+
click.echo("Language selection cancelled. Using English (default).")
|
|
374
|
+
language_value = None
|
|
290
375
|
else:
|
|
291
376
|
# Find the English name for the selected language
|
|
292
377
|
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
|
|
293
378
|
|
|
379
|
+
# Check if predefined language is RTL
|
|
380
|
+
if is_rtl_text(language_value):
|
|
381
|
+
if not _should_show_rtl_warning_for_init():
|
|
382
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
383
|
+
else:
|
|
384
|
+
if not _show_rtl_warning_for_init(language_value):
|
|
385
|
+
click.echo("Language selection cancelled. Using English (default).")
|
|
386
|
+
language_value = None
|
|
387
|
+
|
|
294
388
|
if language_value:
|
|
295
389
|
# Ask about prefix translation
|
|
296
390
|
prefix_choice = questionary.select(
|
|
@@ -313,4 +407,29 @@ def init() -> None:
|
|
|
313
407
|
click.echo(f"Set GAC_LANGUAGE={language_value}")
|
|
314
408
|
click.echo(f"Set GAC_TRANSLATE_PREFIXES={'true' if translate_prefixes else 'false'}")
|
|
315
409
|
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@click.command()
|
|
414
|
+
def init() -> None:
|
|
415
|
+
"""Interactively set up $HOME/.gac.env for gac."""
|
|
416
|
+
click.echo("Welcome to gac initialization!\n")
|
|
417
|
+
|
|
418
|
+
existing_env = _load_existing_env()
|
|
419
|
+
if not _configure_model(existing_env):
|
|
420
|
+
return
|
|
421
|
+
_configure_language(existing_env)
|
|
422
|
+
|
|
316
423
|
click.echo(f"\ngac environment setup complete. You can edit {GAC_ENV_PATH} to update values later.")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@click.command()
|
|
427
|
+
def model() -> None:
|
|
428
|
+
"""Interactively update provider/model/API key without language prompts."""
|
|
429
|
+
click.echo("Welcome to gac model configuration!\n")
|
|
430
|
+
|
|
431
|
+
existing_env = _load_existing_env()
|
|
432
|
+
if not _configure_model(existing_env):
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
click.echo(f"\nModel configuration complete. You can edit {GAC_ENV_PATH} to update values later.")
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""CLI for selecting commit message language interactively."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import unicodedata
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import questionary
|
|
9
|
+
from dotenv import load_dotenv, set_key
|
|
10
|
+
|
|
11
|
+
from gac.constants import Languages
|
|
12
|
+
|
|
13
|
+
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def should_show_rtl_warning() -> bool:
|
|
17
|
+
"""Check if RTL warning should be shown based on saved preference.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if warning should be shown, False if user previously confirmed
|
|
21
|
+
"""
|
|
22
|
+
# Load the current config to check RTL confirmation
|
|
23
|
+
if GAC_ENV_PATH.exists():
|
|
24
|
+
load_dotenv(GAC_ENV_PATH)
|
|
25
|
+
rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
|
|
26
|
+
return not rtl_confirmed
|
|
27
|
+
return True # Show warning if no config exists
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_rtl_text(text: str) -> bool:
|
|
31
|
+
"""Detect if text contains RTL characters or is a known RTL language.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
text: Text to analyze
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if text contains RTL script characters or is RTL language
|
|
38
|
+
"""
|
|
39
|
+
# Known RTL language names (case insensitive)
|
|
40
|
+
rtl_languages = {
|
|
41
|
+
"arabic",
|
|
42
|
+
"ar",
|
|
43
|
+
"العربية",
|
|
44
|
+
"hebrew",
|
|
45
|
+
"he",
|
|
46
|
+
"עברית",
|
|
47
|
+
"persian",
|
|
48
|
+
"farsi",
|
|
49
|
+
"fa",
|
|
50
|
+
"urdu",
|
|
51
|
+
"ur",
|
|
52
|
+
"اردو",
|
|
53
|
+
"pashto",
|
|
54
|
+
"ps",
|
|
55
|
+
"kurdish",
|
|
56
|
+
"ku",
|
|
57
|
+
"کوردی",
|
|
58
|
+
"yiddish",
|
|
59
|
+
"yi",
|
|
60
|
+
"ייִדיש",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Check if it's a known RTL language name or code (case insensitive)
|
|
64
|
+
if text.lower().strip() in rtl_languages:
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
rtl_scripts = {"Arabic", "Hebrew", "Thaana", "Nko", "Syriac", "Mandeic", "Samaritan", "Mongolian", "Phags-Pa"}
|
|
68
|
+
|
|
69
|
+
for char in text:
|
|
70
|
+
if unicodedata.name(char, "").startswith(("ARABIC", "HEBREW")):
|
|
71
|
+
return True
|
|
72
|
+
script = unicodedata.name(char, "").split()[0] if unicodedata.name(char, "") else ""
|
|
73
|
+
if script in rtl_scripts:
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def center_text(text: str, width: int = 80) -> str:
|
|
79
|
+
"""Center text within specified width, handling display width properly.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
text: Text to center
|
|
83
|
+
width: Terminal width to center within (default 80)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Centered text with proper padding
|
|
87
|
+
"""
|
|
88
|
+
import unicodedata
|
|
89
|
+
|
|
90
|
+
def get_display_width(s: str) -> int:
|
|
91
|
+
"""Get the display width of a string, accounting for wide characters."""
|
|
92
|
+
width = 0
|
|
93
|
+
for char in s:
|
|
94
|
+
# East Asian characters are typically 2 columns wide
|
|
95
|
+
if unicodedata.east_asian_width(char) in ("W", "F"):
|
|
96
|
+
width += 2
|
|
97
|
+
else:
|
|
98
|
+
width += 1
|
|
99
|
+
return width
|
|
100
|
+
|
|
101
|
+
# Handle multi-line text
|
|
102
|
+
lines = text.split("\n")
|
|
103
|
+
centered_lines = []
|
|
104
|
+
|
|
105
|
+
for line in lines:
|
|
106
|
+
# Strip existing whitespace to avoid double padding
|
|
107
|
+
stripped_line = line.strip()
|
|
108
|
+
if stripped_line:
|
|
109
|
+
# Calculate padding using display width for accurate centering
|
|
110
|
+
display_width = get_display_width(stripped_line)
|
|
111
|
+
padding = max(0, (width - display_width) // 2)
|
|
112
|
+
centered_line = " " * padding + stripped_line
|
|
113
|
+
centered_lines.append(centered_line)
|
|
114
|
+
else:
|
|
115
|
+
centered_lines.append("")
|
|
116
|
+
|
|
117
|
+
return "\n".join(centered_lines)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_terminal_width() -> int:
|
|
121
|
+
"""Get the current terminal width.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Terminal width in characters, or default if can't be determined
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
import shutil
|
|
128
|
+
|
|
129
|
+
return shutil.get_terminal_size().columns
|
|
130
|
+
except (OSError, AttributeError):
|
|
131
|
+
return 80 # Fallback to 80 columns
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def show_rtl_warning(language_name: str) -> bool:
|
|
135
|
+
"""Show RTL language warning and ask for confirmation.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
language_name: Name of the RTL language
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if user wants to proceed, False if they cancel
|
|
142
|
+
"""
|
|
143
|
+
terminal_width = get_terminal_width()
|
|
144
|
+
|
|
145
|
+
# Center just the title
|
|
146
|
+
title = center_text("⚠️ RTL Language Detected", terminal_width)
|
|
147
|
+
|
|
148
|
+
click.echo()
|
|
149
|
+
click.echo(click.style(title, fg="yellow", bold=True))
|
|
150
|
+
click.echo()
|
|
151
|
+
click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
|
|
152
|
+
click.echo("However, the commit messages will work fine and should be readable in Git clients")
|
|
153
|
+
click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
|
|
154
|
+
|
|
155
|
+
proceed = questionary.confirm("Do you want to proceed anyway?").ask()
|
|
156
|
+
if proceed:
|
|
157
|
+
# Remember that user has confirmed RTL acceptance
|
|
158
|
+
set_key(str(GAC_ENV_PATH), "GAC_RTL_CONFIRMED", "true")
|
|
159
|
+
click.echo("✓ RTL preference saved - you won't see this warning again")
|
|
160
|
+
return proceed if proceed is not None else False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.command()
|
|
164
|
+
def language() -> None:
|
|
165
|
+
"""Set the language for commit messages interactively."""
|
|
166
|
+
click.echo("Select a language for your commit messages:\n")
|
|
167
|
+
|
|
168
|
+
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
169
|
+
selection = questionary.select(
|
|
170
|
+
"Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True, use_jk_keys=False
|
|
171
|
+
).ask()
|
|
172
|
+
|
|
173
|
+
if not selection:
|
|
174
|
+
click.echo("Language selection cancelled.")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Ensure .gac.env exists
|
|
178
|
+
if not GAC_ENV_PATH.exists():
|
|
179
|
+
GAC_ENV_PATH.touch()
|
|
180
|
+
click.echo(f"Created {GAC_ENV_PATH}")
|
|
181
|
+
|
|
182
|
+
# Handle English - set explicitly
|
|
183
|
+
if selection == "English":
|
|
184
|
+
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", "English")
|
|
185
|
+
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "false")
|
|
186
|
+
click.echo("✓ Set language to English")
|
|
187
|
+
click.echo(" GAC_LANGUAGE=English")
|
|
188
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
189
|
+
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Handle custom input
|
|
193
|
+
if selection == "Custom":
|
|
194
|
+
custom_language = questionary.text("Enter the language name (e.g., 'Spanish', 'Français', '日本語'):").ask()
|
|
195
|
+
if not custom_language or not custom_language.strip():
|
|
196
|
+
click.echo("No language entered. Cancelled.")
|
|
197
|
+
return
|
|
198
|
+
language_value = custom_language.strip()
|
|
199
|
+
|
|
200
|
+
# Check if the custom language appears to be RTL
|
|
201
|
+
if is_rtl_text(language_value):
|
|
202
|
+
if not should_show_rtl_warning():
|
|
203
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
204
|
+
else:
|
|
205
|
+
if not show_rtl_warning(language_value):
|
|
206
|
+
click.echo("Language selection cancelled.")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
else:
|
|
210
|
+
# Find the English name for the selected language
|
|
211
|
+
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == selection)
|
|
212
|
+
|
|
213
|
+
# Check if predefined language is RTL
|
|
214
|
+
if is_rtl_text(language_value):
|
|
215
|
+
if not should_show_rtl_warning():
|
|
216
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
217
|
+
else:
|
|
218
|
+
if not show_rtl_warning(language_value):
|
|
219
|
+
click.echo("Language selection cancelled.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Ask about prefix translation
|
|
223
|
+
click.echo() # Blank line for spacing
|
|
224
|
+
prefix_choice = questionary.select(
|
|
225
|
+
"How should conventional commit prefixes be handled?",
|
|
226
|
+
choices=[
|
|
227
|
+
"Keep prefixes in English (feat:, fix:, etc.)",
|
|
228
|
+
f"Translate prefixes into {language_value}",
|
|
229
|
+
],
|
|
230
|
+
).ask()
|
|
231
|
+
|
|
232
|
+
if not prefix_choice:
|
|
233
|
+
click.echo("Prefix translation selection cancelled.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
translate_prefixes = prefix_choice.startswith("Translate prefixes")
|
|
237
|
+
|
|
238
|
+
# Set the language and prefix translation preference in .gac.env
|
|
239
|
+
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", language_value)
|
|
240
|
+
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "true" if translate_prefixes else "false")
|
|
241
|
+
|
|
242
|
+
click.echo(f"\n✓ Set language to {selection}")
|
|
243
|
+
click.echo(f" GAC_LANGUAGE={language_value}")
|
|
244
|
+
if translate_prefixes:
|
|
245
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=true")
|
|
246
|
+
click.echo("\n Prefixes will be translated (e.g., 'corrección:' instead of 'fix:')")
|
|
247
|
+
else:
|
|
248
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
249
|
+
click.echo(f"\n Prefixes will remain in English (e.g., 'fix: <{language_value} description>')")
|
|
250
|
+
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Utility functions for gac."""
|
|
2
2
|
|
|
3
|
+
import locale
|
|
3
4
|
import logging
|
|
4
5
|
import subprocess
|
|
6
|
+
import sys
|
|
5
7
|
|
|
6
8
|
from rich.console import Console
|
|
7
9
|
from rich.theme import Theme
|
|
@@ -65,18 +67,47 @@ def print_message(message: str, level: str = "info") -> None:
|
|
|
65
67
|
console.print(message, style=level)
|
|
66
68
|
|
|
67
69
|
|
|
68
|
-
def
|
|
70
|
+
def get_safe_encodings() -> list[str]:
|
|
71
|
+
"""Get a list of safe encodings to try for subprocess calls, in order of preference.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of encoding strings to try, with UTF-8 first
|
|
75
|
+
"""
|
|
76
|
+
encodings = ["utf-8"]
|
|
77
|
+
|
|
78
|
+
# Add locale encoding as fallback
|
|
79
|
+
locale_encoding = locale.getpreferredencoding(False)
|
|
80
|
+
if locale_encoding and locale_encoding not in encodings:
|
|
81
|
+
encodings.append(locale_encoding)
|
|
82
|
+
|
|
83
|
+
# Windows-specific fallbacks
|
|
84
|
+
if sys.platform == "win32":
|
|
85
|
+
windows_encodings = ["cp65001", "cp936", "cp1252"] # UTF-8, GBK, Windows-1252
|
|
86
|
+
for enc in windows_encodings:
|
|
87
|
+
if enc not in encodings:
|
|
88
|
+
encodings.append(enc)
|
|
89
|
+
|
|
90
|
+
# Final fallback to system default
|
|
91
|
+
if "utf-8" not in encodings:
|
|
92
|
+
encodings.append("utf-8")
|
|
93
|
+
|
|
94
|
+
return encodings
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_subprocess_with_encoding(
|
|
69
98
|
command: list[str],
|
|
99
|
+
encoding: str,
|
|
70
100
|
silent: bool = False,
|
|
71
101
|
timeout: int = 60,
|
|
72
102
|
check: bool = True,
|
|
73
103
|
strip_output: bool = True,
|
|
74
104
|
raise_on_error: bool = True,
|
|
75
105
|
) -> str:
|
|
76
|
-
"""Run a
|
|
106
|
+
"""Run subprocess with a specific encoding, handling encoding errors gracefully.
|
|
77
107
|
|
|
78
108
|
Args:
|
|
79
109
|
command: List of command arguments
|
|
110
|
+
encoding: Specific encoding to use
|
|
80
111
|
silent: If True, suppress debug logging
|
|
81
112
|
timeout: Command timeout in seconds
|
|
82
113
|
check: Whether to check return code (for compatibility)
|
|
@@ -91,7 +122,7 @@ def run_subprocess(
|
|
|
91
122
|
subprocess.CalledProcessError: If the command fails and raise_on_error is True
|
|
92
123
|
"""
|
|
93
124
|
if not silent:
|
|
94
|
-
logger.debug(f"Running command: {' '.join(command)}")
|
|
125
|
+
logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
|
|
95
126
|
|
|
96
127
|
try:
|
|
97
128
|
result = subprocess.run(
|
|
@@ -100,6 +131,8 @@ def run_subprocess(
|
|
|
100
131
|
text=True,
|
|
101
132
|
check=False,
|
|
102
133
|
timeout=timeout,
|
|
134
|
+
encoding=encoding,
|
|
135
|
+
errors="replace", # Replace problematic characters instead of crashing
|
|
103
136
|
)
|
|
104
137
|
|
|
105
138
|
should_raise = result.returncode != 0 and (check or raise_on_error)
|
|
@@ -123,6 +156,11 @@ def run_subprocess(
|
|
|
123
156
|
if raise_on_error:
|
|
124
157
|
raise
|
|
125
158
|
return ""
|
|
159
|
+
except UnicodeError as e:
|
|
160
|
+
# This should be rare with errors="replace", but handle it just in case
|
|
161
|
+
if not silent:
|
|
162
|
+
logger.debug(f"Encoding error with {encoding}: {e}")
|
|
163
|
+
raise
|
|
126
164
|
except Exception as e:
|
|
127
165
|
if not silent:
|
|
128
166
|
logger.debug(f"Command error: {e}")
|
|
@@ -132,6 +170,69 @@ def run_subprocess(
|
|
|
132
170
|
return ""
|
|
133
171
|
|
|
134
172
|
|
|
173
|
+
def run_subprocess(
|
|
174
|
+
command: list[str],
|
|
175
|
+
silent: bool = False,
|
|
176
|
+
timeout: int = 60,
|
|
177
|
+
check: bool = True,
|
|
178
|
+
strip_output: bool = True,
|
|
179
|
+
raise_on_error: bool = True,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Run a subprocess command safely and return the output, trying multiple encodings.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
command: List of command arguments
|
|
185
|
+
silent: If True, suppress debug logging
|
|
186
|
+
timeout: Command timeout in seconds
|
|
187
|
+
check: Whether to check return code (for compatibility)
|
|
188
|
+
strip_output: Whether to strip whitespace from output
|
|
189
|
+
raise_on_error: Whether to raise an exception on error
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Command output as string
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
GacError: If the command times out
|
|
196
|
+
subprocess.CalledProcessError: If the command fails and raise_on_error is True
|
|
197
|
+
|
|
198
|
+
Note:
|
|
199
|
+
Tries multiple encodings in order: utf-8, locale encoding, platform-specific fallbacks
|
|
200
|
+
This prevents UnicodeDecodeError on systems with non-UTF-8 locales (e.g., Chinese Windows)
|
|
201
|
+
"""
|
|
202
|
+
encodings = get_safe_encodings()
|
|
203
|
+
last_exception = None
|
|
204
|
+
|
|
205
|
+
for encoding in encodings:
|
|
206
|
+
try:
|
|
207
|
+
return run_subprocess_with_encoding(
|
|
208
|
+
command=command,
|
|
209
|
+
encoding=encoding,
|
|
210
|
+
silent=silent,
|
|
211
|
+
timeout=timeout,
|
|
212
|
+
check=check,
|
|
213
|
+
strip_output=strip_output,
|
|
214
|
+
raise_on_error=raise_on_error,
|
|
215
|
+
)
|
|
216
|
+
except UnicodeError as e:
|
|
217
|
+
last_exception = e
|
|
218
|
+
if not silent:
|
|
219
|
+
logger.debug(f"Failed to decode with {encoding}: {e}")
|
|
220
|
+
continue
|
|
221
|
+
except (subprocess.CalledProcessError, GacError, subprocess.TimeoutExpired):
|
|
222
|
+
# These are not encoding-related errors, so don't retry with other encodings
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
# If we get here, all encodings failed with UnicodeError
|
|
226
|
+
if not silent:
|
|
227
|
+
logger.error(f"Failed to decode command output with any encoding: {encodings}")
|
|
228
|
+
|
|
229
|
+
# Raise the last UnicodeError we encountered
|
|
230
|
+
if last_exception:
|
|
231
|
+
raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
|
|
232
|
+
else:
|
|
233
|
+
raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
|
|
234
|
+
|
|
235
|
+
|
|
135
236
|
def edit_commit_message_inplace(message: str) -> str | None:
|
|
136
237
|
"""Edit commit message in-place using rich terminal editing.
|
|
137
238
|
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""CLI for selecting commit message language interactively."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
import questionary
|
|
7
|
-
from dotenv import set_key
|
|
8
|
-
|
|
9
|
-
from gac.constants import Languages
|
|
10
|
-
|
|
11
|
-
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.command()
|
|
15
|
-
def language() -> None:
|
|
16
|
-
"""Set the language for commit messages interactively."""
|
|
17
|
-
click.echo("Select a language for your commit messages:\n")
|
|
18
|
-
|
|
19
|
-
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
20
|
-
selection = questionary.select(
|
|
21
|
-
"Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
|
|
22
|
-
).ask()
|
|
23
|
-
|
|
24
|
-
if not selection:
|
|
25
|
-
click.echo("Language selection cancelled.")
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
# Ensure .gac.env exists
|
|
29
|
-
if not GAC_ENV_PATH.exists():
|
|
30
|
-
GAC_ENV_PATH.touch()
|
|
31
|
-
click.echo(f"Created {GAC_ENV_PATH}")
|
|
32
|
-
|
|
33
|
-
# Handle English - set explicitly
|
|
34
|
-
if selection == "English":
|
|
35
|
-
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", "English")
|
|
36
|
-
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "false")
|
|
37
|
-
click.echo("✓ Set language to English")
|
|
38
|
-
click.echo(" GAC_LANGUAGE=English")
|
|
39
|
-
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
40
|
-
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
# Handle custom input
|
|
44
|
-
if selection == "Custom":
|
|
45
|
-
custom_language = questionary.text("Enter the language name (e.g., 'Spanish', 'Français', '日本語'):").ask()
|
|
46
|
-
if not custom_language or not custom_language.strip():
|
|
47
|
-
click.echo("No language entered. Cancelled.")
|
|
48
|
-
return
|
|
49
|
-
language_value = custom_language.strip()
|
|
50
|
-
else:
|
|
51
|
-
# Find the English name for the selected language
|
|
52
|
-
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == selection)
|
|
53
|
-
|
|
54
|
-
# Ask about prefix translation
|
|
55
|
-
click.echo() # Blank line for spacing
|
|
56
|
-
prefix_choice = questionary.select(
|
|
57
|
-
"How should conventional commit prefixes be handled?",
|
|
58
|
-
choices=[
|
|
59
|
-
"Keep prefixes in English (feat:, fix:, etc.)",
|
|
60
|
-
f"Translate prefixes into {language_value}",
|
|
61
|
-
],
|
|
62
|
-
).ask()
|
|
63
|
-
|
|
64
|
-
if not prefix_choice:
|
|
65
|
-
click.echo("Prefix translation selection cancelled.")
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
translate_prefixes = prefix_choice.startswith("Translate prefixes")
|
|
69
|
-
|
|
70
|
-
# Set the language and prefix translation preference in .gac.env
|
|
71
|
-
set_key(str(GAC_ENV_PATH), "GAC_LANGUAGE", language_value)
|
|
72
|
-
set_key(str(GAC_ENV_PATH), "GAC_TRANSLATE_PREFIXES", "true" if translate_prefixes else "false")
|
|
73
|
-
|
|
74
|
-
click.echo(f"\n✓ Set language to {selection}")
|
|
75
|
-
click.echo(f" GAC_LANGUAGE={language_value}")
|
|
76
|
-
if translate_prefixes:
|
|
77
|
-
click.echo(" GAC_TRANSLATE_PREFIXES=true")
|
|
78
|
-
click.echo("\n Prefixes will be translated (e.g., 'corrección:' instead of 'fix:')")
|
|
79
|
-
else:
|
|
80
|
-
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
81
|
-
click.echo(f"\n Prefixes will remain in English (e.g., 'fix: <{language_value} description>')")
|
|
82
|
-
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|
|
File without changes
|
{gac-2.3.0 → gac-2.4.1}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|