git-commit-msg-ai 1.5.0__tar.gz → 1.6.0__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.
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/PKG-INFO +161 -137
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/README.md +24 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai/ai_client.py +12 -12
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai/cli.py +12 -12
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai/editor.py +15 -15
- git_commit_msg_ai-1.6.0/git_commit_msg_ai/git_ops.py +52 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/PKG-INFO +161 -137
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/SOURCES.txt +1 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/pyproject.toml +2 -2
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/setup.cfg +4 -4
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/tests/test_ai_client.py +39 -39
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/tests/test_cli.py +64 -64
- git_commit_msg_ai-1.6.0/tests/test_editor.py +245 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/tests/test_exceptions.py +3 -3
- git_commit_msg_ai-1.6.0/tests/test_generate_release_notes.py +193 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/tests/test_git_ops.py +48 -48
- git_commit_msg_ai-1.5.0/git_commit_msg_ai/git_ops.py +0 -52
- git_commit_msg_ai-1.5.0/tests/test_editor.py +0 -245
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai/__init__.py +0 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai/exceptions.py +0 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/dependency_links.txt +0 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/entry_points.txt +0 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/requires.txt +0 -0
- {git_commit_msg_ai-1.5.0 → git_commit_msg_ai-1.6.0}/git_commit_msg_ai.egg-info/top_level.txt +0 -0
|
@@ -1,137 +1,161 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: git-commit-msg-ai
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: anthropic
|
|
9
|
-
Provides-Extra: dev
|
|
10
|
-
Requires-Dist: mypy; extra == "dev"
|
|
11
|
-
Requires-Dist: ruff; extra == "dev"
|
|
12
|
-
Requires-Dist: pytest; extra == "dev"
|
|
13
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
-
Requires-Dist: build; extra == "dev"
|
|
15
|
-
Requires-Dist: twine; extra == "dev"
|
|
16
|
-
|
|
17
|
-
# git-commit-msg-ai
|
|
18
|
-
|
|
19
|
-
AI-powered git commit message generator that follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
|
20
|
-
|
|
21
|
-
## Prerequisites
|
|
22
|
-
|
|
23
|
-
- Python 3.10+
|
|
24
|
-
- An Anthropic API key set as an environment variable:
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
export ANTHROPIC_API_KEY=sk-ant-... # macOS/Linux
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```powershell
|
|
31
|
-
[System.Environment]::SetEnvironmentVariable('ANTHROPIC_API_KEY', 'sk-ant-...', 'User') # Windows
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Development Setup
|
|
35
|
-
|
|
36
|
-
Clone the repository, create a virtual environment, and install the project in editable mode with all dev dependencies:
|
|
37
|
-
|
|
38
|
-
```sh
|
|
39
|
-
python -m venv .venv
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Activate the virtual environment:
|
|
43
|
-
|
|
44
|
-
```sh
|
|
45
|
-
# macOS/Linux
|
|
46
|
-
source .venv/bin/activate
|
|
47
|
-
|
|
48
|
-
# Windows PowerShell
|
|
49
|
-
.venv\Scripts\Activate.ps1
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Install the project and dev dependencies:
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
pip install -e ".[dev]"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
After activation the `git-commit-msg-ai` entry-point is on your PATH. You can also run the dev toolchain:
|
|
59
|
-
|
|
60
|
-
```sh
|
|
61
|
-
pytest # run tests with coverage
|
|
62
|
-
ruff check . # lint
|
|
63
|
-
mypy . # type-check
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Installation
|
|
67
|
-
|
|
68
|
-
```sh
|
|
69
|
-
pip install git-commit-msg-ai
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Usage
|
|
73
|
-
|
|
74
|
-
Stage your changes, then run the tool from inside any git repository:
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
git add <files>
|
|
78
|
-
git-commit-msg-ai
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The tool will:
|
|
82
|
-
1. Read your staged diff
|
|
83
|
-
2. Generate a commit message using Claude AI
|
|
84
|
-
3. Print the message and prompt you to choose:
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
[a]ccept / [e]dit / [r]eject:
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
- **a** - commits immediately with the generated message
|
|
91
|
-
- **e** - opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
|
|
92
|
-
- **r** - exits without committing
|
|
93
|
-
|
|
94
|
-
## Commit message format
|
|
95
|
-
|
|
96
|
-
Generated messages follow the Conventional Commits specification:
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
<type>(<optional scope>): <short subject>
|
|
100
|
-
|
|
101
|
-
- Bullet explaining why this change was made
|
|
102
|
-
- Another reason if applicable
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
For breaking changes, the subject line gets a `!` and a footer is added:
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
feat(api)!: remove deprecated endpoint
|
|
109
|
-
|
|
110
|
-
- Endpoint was unused and blocking the new auth rollout
|
|
111
|
-
|
|
112
|
-
BREAKING CHANGE: /v1/legacy is no longer available
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
|
116
|
-
|
|
117
|
-
##
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-commit-msg-ai
|
|
3
|
+
Version: 1.6.0
|
|
4
|
+
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: anthropic
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: mypy; extra == "dev"
|
|
11
|
+
Requires-Dist: ruff; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
+
Requires-Dist: build; extra == "dev"
|
|
15
|
+
Requires-Dist: twine; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# git-commit-msg-ai
|
|
18
|
+
|
|
19
|
+
AI-powered git commit message generator that follows the [Conventional Commits](https://www.conventionalcommits.org/) specification.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Python 3.10+
|
|
24
|
+
- An Anthropic API key set as an environment variable:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
export ANTHROPIC_API_KEY=sk-ant-... # macOS/Linux
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
[System.Environment]::SetEnvironmentVariable('ANTHROPIC_API_KEY', 'sk-ant-...', 'User') # Windows
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Development Setup
|
|
35
|
+
|
|
36
|
+
Clone the repository, create a virtual environment, and install the project in editable mode with all dev dependencies:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
python -m venv .venv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Activate the virtual environment:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# macOS/Linux
|
|
46
|
+
source .venv/bin/activate
|
|
47
|
+
|
|
48
|
+
# Windows PowerShell
|
|
49
|
+
.venv\Scripts\Activate.ps1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Install the project and dev dependencies:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After activation the `git-commit-msg-ai` entry-point is on your PATH. You can also run the dev toolchain:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pytest # run tests with coverage
|
|
62
|
+
ruff check . # lint
|
|
63
|
+
mypy . # type-check
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
pip install git-commit-msg-ai
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Stage your changes, then run the tool from inside any git repository:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
git add <files>
|
|
78
|
+
git-commit-msg-ai
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The tool will:
|
|
82
|
+
1. Read your staged diff
|
|
83
|
+
2. Generate a commit message using Claude AI
|
|
84
|
+
3. Print the message and prompt you to choose:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
[a]ccept / [e]dit / [r]eject:
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **a** - commits immediately with the generated message
|
|
91
|
+
- **e** - opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
|
|
92
|
+
- **r** - exits without committing
|
|
93
|
+
|
|
94
|
+
## Commit message format
|
|
95
|
+
|
|
96
|
+
Generated messages follow the Conventional Commits specification:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
<type>(<optional scope>): <short subject>
|
|
100
|
+
|
|
101
|
+
- Bullet explaining why this change was made
|
|
102
|
+
- Another reason if applicable
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For breaking changes, the subject line gets a `!` and a footer is added:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
feat(api)!: remove deprecated endpoint
|
|
109
|
+
|
|
110
|
+
- Endpoint was unused and blocking the new auth rollout
|
|
111
|
+
|
|
112
|
+
BREAKING CHANGE: /v1/legacy is no longer available
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
|
116
|
+
|
|
117
|
+
## CI/CD
|
|
118
|
+
|
|
119
|
+
Every push to `main` and every pull request runs the full CI pipeline (lint, type-check, tests, build).
|
|
120
|
+
|
|
121
|
+
Pushing a version tag (e.g. `v1.5.2`) triggers the CD pipeline:
|
|
122
|
+
|
|
123
|
+
1. Tests are re-run as a gate
|
|
124
|
+
2. The tag version is verified to match the version in `pyproject.toml`
|
|
125
|
+
3. The package is built and published to [PyPI](https://pypi.org/project/git-commit-msg-ai/)
|
|
126
|
+
4. A GitHub Release is created with release notes generated by Claude Haiku from the commit log
|
|
127
|
+
|
|
128
|
+
**Releasing a new version:**
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
# 1. Bump the version in pyproject.toml
|
|
132
|
+
# 2. Commit and push
|
|
133
|
+
git add pyproject.toml
|
|
134
|
+
git commit -m "chore: bump version to 1.5.2"
|
|
135
|
+
git push origin main
|
|
136
|
+
# 3. Tag and push — this triggers the CD pipeline
|
|
137
|
+
git tag v1.5.2
|
|
138
|
+
git push origin v1.5.2
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Debugging
|
|
142
|
+
|
|
143
|
+
By default the tool produces no diagnostic output. To enable logging, set the `GIT_COMMIT_AI_LOG_LEVEL` environment variable before running the command. Logs are written to **stderr** and do not interfere with the generated commit message on **stdout**.
|
|
144
|
+
|
|
145
|
+
Valid values: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
# macOS/Linux - show all internal diagnostic messages
|
|
149
|
+
GIT_COMMIT_AI_LOG_LEVEL=DEBUG git-commit-msg-ai
|
|
150
|
+
|
|
151
|
+
# Windows PowerShell
|
|
152
|
+
$env:GIT_COMMIT_AI_LOG_LEVEL = 'DEBUG'
|
|
153
|
+
git-commit-msg-ai
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Level | What you see |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `DEBUG` | git commands run, API model/token params, temp file paths, char counts |
|
|
159
|
+
| `INFO` | commit message generated, commit created |
|
|
160
|
+
| `WARNING` | no staged changes found |
|
|
161
|
+
| `ERROR` | git not found, API failures, editor errors |
|
|
@@ -98,6 +98,30 @@ BREAKING CHANGE: /v1/legacy is no longer available
|
|
|
98
98
|
|
|
99
99
|
Supported types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
|
100
100
|
|
|
101
|
+
## CI/CD
|
|
102
|
+
|
|
103
|
+
Every push to `main` and every pull request runs the full CI pipeline (lint, type-check, tests, build).
|
|
104
|
+
|
|
105
|
+
Pushing a version tag (e.g. `v1.5.2`) triggers the CD pipeline:
|
|
106
|
+
|
|
107
|
+
1. Tests are re-run as a gate
|
|
108
|
+
2. The tag version is verified to match the version in `pyproject.toml`
|
|
109
|
+
3. The package is built and published to [PyPI](https://pypi.org/project/git-commit-msg-ai/)
|
|
110
|
+
4. A GitHub Release is created with release notes generated by Claude Haiku from the commit log
|
|
111
|
+
|
|
112
|
+
**Releasing a new version:**
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
# 1. Bump the version in pyproject.toml
|
|
116
|
+
# 2. Commit and push
|
|
117
|
+
git add pyproject.toml
|
|
118
|
+
git commit -m "chore: bump version to 1.5.2"
|
|
119
|
+
git push origin main
|
|
120
|
+
# 3. Tag and push — this triggers the CD pipeline
|
|
121
|
+
git tag v1.5.2
|
|
122
|
+
git push origin v1.5.2
|
|
123
|
+
```
|
|
124
|
+
|
|
101
125
|
## Debugging
|
|
102
126
|
|
|
103
127
|
By default the tool produces no diagnostic output. To enable logging, set the `GIT_COMMIT_AI_LOG_LEVEL` environment variable before running the command. Logs are written to **stderr** and do not interfere with the generated commit message on **stdout**.
|
|
@@ -16,7 +16,7 @@ SYSTEM_PROMPT: Final[str] = textwrap.dedent("""\
|
|
|
16
16
|
- If there is a breaking change, add a blank line after the body followed by "BREAKING CHANGE: <description of what breaks and why>
|
|
17
17
|
Types: feat, fix, docs, style, refactor, test, chore""")
|
|
18
18
|
|
|
19
|
-
MODEL: Final[str] =
|
|
19
|
+
MODEL: Final[str] = "claude-haiku-4-5-20251001"
|
|
20
20
|
MAX_TOKENS: Final[int] = 1024
|
|
21
21
|
|
|
22
22
|
|
|
@@ -24,29 +24,29 @@ def generate_commit_message(diff: str) -> str:
|
|
|
24
24
|
try:
|
|
25
25
|
anthropic_client = anthropic.Anthropic()
|
|
26
26
|
|
|
27
|
-
logger.debug(
|
|
27
|
+
logger.debug("Calling Anthropic API: model=%s max_tokens=%d", MODEL, MAX_TOKENS)
|
|
28
28
|
anthropic_api_response = anthropic_client.messages.create(
|
|
29
29
|
model=MODEL,
|
|
30
30
|
max_tokens=MAX_TOKENS,
|
|
31
31
|
system=SYSTEM_PROMPT,
|
|
32
|
-
messages=[{
|
|
32
|
+
messages=[{"role": "user", "content": diff}],
|
|
33
33
|
)
|
|
34
34
|
except anthropic.AuthenticationError:
|
|
35
|
-
logger.error(
|
|
36
|
-
raise AIError(
|
|
35
|
+
logger.error("Anthropic authentication error")
|
|
36
|
+
raise AIError("Anthropic API key is missing or invalid. Set the ANTHROPIC_API_KEY environment variable.")
|
|
37
37
|
except anthropic.RateLimitError:
|
|
38
|
-
logger.error(
|
|
39
|
-
raise AIError(
|
|
38
|
+
logger.error("Anthropic rate limit error")
|
|
39
|
+
raise AIError("Anthropic API rate limit reached. Wait a moment and try again.")
|
|
40
40
|
except anthropic.APIConnectionError:
|
|
41
|
-
logger.error(
|
|
42
|
-
raise AIError(
|
|
41
|
+
logger.error("Anthropic API connection error")
|
|
42
|
+
raise AIError("Could not reach the Anthropic API. Check your network connection.")
|
|
43
43
|
except anthropic.APIStatusError as error:
|
|
44
|
-
logger.error(
|
|
45
|
-
raise AIError(f
|
|
44
|
+
logger.error("Anthropic API status error: %s", error.status_code)
|
|
45
|
+
raise AIError(f"Anthropic API returned an error: {error.status_code}.")
|
|
46
46
|
|
|
47
47
|
anthropic_api_response_message = anthropic_api_response.content[0]
|
|
48
48
|
commit_message = cast(anthropic.types.TextBlock, anthropic_api_response_message).text.strip()
|
|
49
49
|
|
|
50
|
-
logger.info(
|
|
50
|
+
logger.info("Commit message generated: %d chars", len(commit_message))
|
|
51
51
|
|
|
52
52
|
return commit_message
|
|
@@ -6,20 +6,20 @@ from typing import Final
|
|
|
6
6
|
from git_commit_msg_ai import ai_client, editor, git_ops
|
|
7
7
|
from git_commit_msg_ai.exceptions import GitCommitAIError
|
|
8
8
|
|
|
9
|
-
LOG_LEVEL_ENV_VAR: Final[str] =
|
|
9
|
+
LOG_LEVEL_ENV_VAR: Final[str] = "GIT_COMMIT_AI_LOG_LEVEL"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def main() -> None:
|
|
13
|
-
log_level_name = os.environ.get(LOG_LEVEL_ENV_VAR,
|
|
13
|
+
log_level_name = os.environ.get(LOG_LEVEL_ENV_VAR, "WARNING").upper()
|
|
14
14
|
numeric_log_level = getattr(logging, log_level_name, None)
|
|
15
15
|
|
|
16
16
|
if not isinstance(numeric_log_level, int):
|
|
17
17
|
numeric_log_level = logging.WARNING
|
|
18
18
|
|
|
19
|
-
logging.basicConfig(level=numeric_log_level, format=
|
|
19
|
+
logging.basicConfig(level=numeric_log_level, format="%(levelname)s:%(name)s:%(message)s", stream=sys.stderr)
|
|
20
20
|
|
|
21
21
|
if not isinstance(getattr(logging, log_level_name, None), int):
|
|
22
|
-
logging.warning(
|
|
22
|
+
logging.warning("Invalid %s value %r, defaulting to WARNING", LOG_LEVEL_ENV_VAR, log_level_name)
|
|
23
23
|
|
|
24
24
|
try:
|
|
25
25
|
diff = git_ops.get_staged_diff()
|
|
@@ -27,25 +27,25 @@ def main() -> None:
|
|
|
27
27
|
print(commit_message)
|
|
28
28
|
|
|
29
29
|
print()
|
|
30
|
-
user_selection = input(
|
|
30
|
+
user_selection = input("[a]ccept / [e]dit / [r]eject: ").strip().lower()
|
|
31
31
|
|
|
32
|
-
if user_selection ==
|
|
32
|
+
if user_selection == "a":
|
|
33
33
|
git_ops.commit(commit_message)
|
|
34
|
-
elif user_selection ==
|
|
34
|
+
elif user_selection == "e":
|
|
35
35
|
updated_commit_message = editor.open_in_editor(commit_message)
|
|
36
36
|
git_ops.commit(updated_commit_message)
|
|
37
|
-
elif user_selection ==
|
|
38
|
-
print(
|
|
37
|
+
elif user_selection == "r":
|
|
38
|
+
print("User rejected the generated commit message. No commit made.")
|
|
39
39
|
else:
|
|
40
|
-
print(
|
|
40
|
+
print("Invalid selection.")
|
|
41
41
|
sys.exit(1)
|
|
42
42
|
except GitCommitAIError as error:
|
|
43
43
|
print(str(error))
|
|
44
44
|
sys.exit(1)
|
|
45
45
|
except (KeyboardInterrupt, EOFError):
|
|
46
|
-
print(
|
|
46
|
+
print("Aborted.")
|
|
47
47
|
sys.exit(1)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
if __name__ ==
|
|
50
|
+
if __name__ == "__main__": # pragma: no cover
|
|
51
51
|
main()
|
|
@@ -11,49 +11,49 @@ logger = logging.getLogger(__name__)
|
|
|
11
11
|
|
|
12
12
|
def get_default_editor() -> str:
|
|
13
13
|
current_platform = platform.system()
|
|
14
|
-
is_windows = current_platform ==
|
|
14
|
+
is_windows = current_platform == "Windows"
|
|
15
15
|
|
|
16
|
-
return
|
|
16
|
+
return "notepad" if is_windows else "vi"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def open_in_editor(initial_text: str) -> str:
|
|
20
20
|
try:
|
|
21
|
-
with tempfile.NamedTemporaryFile(suffix=
|
|
21
|
+
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w", delete=False) as temp_file:
|
|
22
22
|
temp_file.write(initial_text)
|
|
23
23
|
temp_file_path = temp_file.name
|
|
24
24
|
except OSError:
|
|
25
|
-
logger.error(
|
|
26
|
-
raise EditorError(
|
|
25
|
+
logger.error("Could not create temporary file")
|
|
26
|
+
raise EditorError("Could not create a temporary file.")
|
|
27
27
|
|
|
28
|
-
logger.debug(
|
|
28
|
+
logger.debug("Temporary file created: %s", temp_file_path)
|
|
29
29
|
|
|
30
30
|
platform_default_editor = get_default_editor()
|
|
31
|
-
editor_command = os.environ.get(
|
|
31
|
+
editor_command = os.environ.get("EDITOR", platform_default_editor)
|
|
32
32
|
|
|
33
|
-
logger.debug(
|
|
33
|
+
logger.debug("Selected editor: %s", editor_command)
|
|
34
34
|
|
|
35
35
|
try:
|
|
36
36
|
try:
|
|
37
|
-
logger.debug(
|
|
37
|
+
logger.debug("Opening editor: %s %s", editor_command, temp_file_path)
|
|
38
38
|
subprocess.run([editor_command, temp_file_path], check=True)
|
|
39
39
|
except FileNotFoundError:
|
|
40
|
-
logger.error(
|
|
40
|
+
logger.error("Editor not found: %s", editor_command)
|
|
41
41
|
raise EditorError(f'Editor "{editor_command}" was not found. Set the EDITOR environment variable to a valid editor.')
|
|
42
42
|
except subprocess.CalledProcessError:
|
|
43
|
-
logger.error(
|
|
43
|
+
logger.error("Editor exited with error: %s", editor_command)
|
|
44
44
|
raise EditorError(f'Editor "{editor_command}" exited with an error.')
|
|
45
45
|
|
|
46
|
-
logger.debug(
|
|
46
|
+
logger.debug("Editor closed, reading edited content")
|
|
47
47
|
|
|
48
48
|
try:
|
|
49
49
|
with open(temp_file_path) as temp_file:
|
|
50
50
|
edited_text = temp_file.read().strip()
|
|
51
51
|
except OSError:
|
|
52
|
-
logger.error(
|
|
53
|
-
raise EditorError(
|
|
52
|
+
logger.error("Could not read temporary file: %s", temp_file_path)
|
|
53
|
+
raise EditorError("Could not read the edited commit message from the temporary file.")
|
|
54
54
|
finally:
|
|
55
55
|
os.unlink(temp_file_path)
|
|
56
56
|
|
|
57
|
-
logger.debug(
|
|
57
|
+
logger.debug("Edited text read: %d chars", len(edited_text))
|
|
58
58
|
|
|
59
59
|
return edited_text
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
from git_commit_msg_ai.exceptions import GitError
|
|
6
|
+
|
|
7
|
+
GIT_COMMAND: Final[str] = "git"
|
|
8
|
+
DIFF_SUBCOMMAND: Final[str] = "diff"
|
|
9
|
+
CACHED_FLAG: Final[str] = "--cached"
|
|
10
|
+
COMMIT_SUBCOMMAND: Final[str] = "commit"
|
|
11
|
+
MESSAGE_FLAG: Final[str] = "-m"
|
|
12
|
+
UTF8_ENCODING: Final[str] = "utf-8"
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_staged_diff() -> str:
|
|
18
|
+
try:
|
|
19
|
+
logger.debug("Running: %s %s %s", GIT_COMMAND, DIFF_SUBCOMMAND, CACHED_FLAG)
|
|
20
|
+
raw_diff_bytes = subprocess.check_output([GIT_COMMAND, DIFF_SUBCOMMAND, CACHED_FLAG])
|
|
21
|
+
staged_diff = raw_diff_bytes.decode(UTF8_ENCODING)
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
logger.error("git not found on PATH")
|
|
24
|
+
raise GitError("git is not installed or not on PATH.")
|
|
25
|
+
except subprocess.CalledProcessError:
|
|
26
|
+
logger.error("git diff --cached exited non-zero")
|
|
27
|
+
raise GitError("Failed to get staged diff. Are you inside a git repository?")
|
|
28
|
+
except UnicodeDecodeError:
|
|
29
|
+
logger.error("Staged diff could not be decoded as UTF-8")
|
|
30
|
+
raise GitError("Staged diff contains bytes that could not be decoded as UTF-8.")
|
|
31
|
+
|
|
32
|
+
logger.debug("Staged diff received: %d chars", len(staged_diff))
|
|
33
|
+
|
|
34
|
+
if not staged_diff:
|
|
35
|
+
logger.warning("No staged changes found")
|
|
36
|
+
raise GitError("No staged changes found. Stage files with git add before running.")
|
|
37
|
+
|
|
38
|
+
return staged_diff
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def commit(message: str) -> None:
|
|
42
|
+
try:
|
|
43
|
+
logger.debug("Running: %s %s %s <message>", GIT_COMMAND, COMMIT_SUBCOMMAND, MESSAGE_FLAG)
|
|
44
|
+
subprocess.run([GIT_COMMAND, COMMIT_SUBCOMMAND, MESSAGE_FLAG, message], check=True)
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
logger.error("git not found on PATH")
|
|
47
|
+
raise GitError("git is not installed or not on PATH.")
|
|
48
|
+
except subprocess.CalledProcessError:
|
|
49
|
+
logger.error("git commit exited non-zero")
|
|
50
|
+
raise GitError("git commit failed.")
|
|
51
|
+
|
|
52
|
+
logger.info("Commit created successfully")
|