git-commit-msg-ai 1.1.0__py3-none-any.whl → 1.3.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.
- git_commit_msg_ai/ai_client.py +36 -22
- git_commit_msg_ai/cli.py +35 -21
- git_commit_msg_ai/editor.py +28 -8
- git_commit_msg_ai/exceptions.py +14 -0
- git_commit_msg_ai/git_ops.py +21 -2
- {git_commit_msg_ai-1.1.0.dist-info → git_commit_msg_ai-1.3.0.dist-info}/METADATA +6 -1
- git_commit_msg_ai-1.3.0.dist-info/RECORD +11 -0
- git_commit_msg_ai-1.1.0.dist-info/RECORD +0 -10
- {git_commit_msg_ai-1.1.0.dist-info → git_commit_msg_ai-1.3.0.dist-info}/WHEEL +0 -0
- {git_commit_msg_ai-1.1.0.dist-info → git_commit_msg_ai-1.3.0.dist-info}/entry_points.txt +0 -0
- {git_commit_msg_ai-1.1.0.dist-info → git_commit_msg_ai-1.3.0.dist-info}/top_level.txt +0 -0
git_commit_msg_ai/ai_client.py
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
import textwrap
|
|
2
|
+
from typing import Final, cast
|
|
3
|
+
|
|
2
4
|
import anthropic
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'Types: feat, fix, docs, style, refactor, test, chore'
|
|
14
|
-
)
|
|
6
|
+
from git_commit_msg_ai.exceptions import AIError
|
|
7
|
+
|
|
8
|
+
SYSTEM_PROMPT: Final[str] = textwrap.dedent("""\
|
|
9
|
+
You are a Git commit message generator. Output only the commit message, nothing else. Follow the Conventional Commits specification:
|
|
10
|
+
- First line: <type>(<optional scope>)<!>: <short subject> (50 chars max); append ! before the colon if the commit introduces a breaking change
|
|
11
|
+
- Blank line
|
|
12
|
+
- Bullet-point body explaining WHY the changes were made, not what
|
|
13
|
+
- If there is a breaking change, add a blank line after the body followed by "BREAKING CHANGE: <description of what breaks and why>
|
|
14
|
+
Types: feat, fix, docs, style, refactor, test, chore""")
|
|
15
15
|
|
|
16
|
-
MODEL = 'claude-haiku-4-5-20251001'
|
|
17
|
-
MAX_TOKENS = 1024
|
|
16
|
+
MODEL: Final[str] = 'claude-haiku-4-5-20251001'
|
|
17
|
+
MAX_TOKENS: Final[int] = 1024
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def generate_commit_message(diff: str) -> str:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
try:
|
|
22
|
+
anthropic_client = anthropic.Anthropic()
|
|
23
|
+
|
|
24
|
+
anthropic_api_response = anthropic_client.messages.create(
|
|
25
|
+
model=MODEL,
|
|
26
|
+
max_tokens=MAX_TOKENS,
|
|
27
|
+
system=SYSTEM_PROMPT,
|
|
28
|
+
messages=[{'role': 'user', 'content': diff}],
|
|
29
|
+
)
|
|
30
|
+
except anthropic.AuthenticationError:
|
|
31
|
+
raise AIError('Anthropic API key is missing or invalid. Set the ANTHROPIC_API_KEY environment variable.')
|
|
32
|
+
except anthropic.RateLimitError:
|
|
33
|
+
raise AIError('Anthropic API rate limit reached. Wait a moment and try again.')
|
|
34
|
+
except anthropic.APIConnectionError:
|
|
35
|
+
raise AIError('Could not reach the Anthropic API. Check your network connection.')
|
|
36
|
+
except anthropic.APIStatusError as error:
|
|
37
|
+
raise AIError(f'Anthropic API returned an error: {error.status_code}.')
|
|
38
|
+
|
|
39
|
+
anthropic_api_response_message = anthropic_api_response.content[0]
|
|
40
|
+
commit_message = cast(anthropic.types.TextBlock, anthropic_api_response_message).text.strip()
|
|
41
|
+
|
|
42
|
+
return commit_message
|
git_commit_msg_ai/cli.py
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from git_commit_msg_ai import ai_client, editor, git_ops
|
|
4
|
+
from git_commit_msg_ai.exceptions import GitCommitAIError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
try:
|
|
9
|
+
diff = git_ops.get_staged_diff()
|
|
10
|
+
commit_message = ai_client.generate_commit_message(diff)
|
|
11
|
+
print(commit_message)
|
|
12
|
+
|
|
13
|
+
print()
|
|
14
|
+
user_selection = input('[a]ccept / [e]dit / [r]eject: ').strip().lower()
|
|
15
|
+
|
|
16
|
+
if user_selection == 'a':
|
|
17
|
+
git_ops.commit(commit_message)
|
|
18
|
+
elif user_selection == 'e':
|
|
19
|
+
updated_commit_message = editor.open_in_editor(commit_message)
|
|
20
|
+
git_ops.commit(updated_commit_message)
|
|
21
|
+
elif user_selection == 'r':
|
|
22
|
+
print('User rejected the generated commit message. No commit made.')
|
|
23
|
+
else:
|
|
24
|
+
print('Invalid selection.')
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
except GitCommitAIError as error:
|
|
27
|
+
print(str(error))
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
except (KeyboardInterrupt, EOFError):
|
|
30
|
+
print('Aborted.')
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == '__main__': # pragma: no cover
|
|
35
|
+
main()
|
git_commit_msg_ai/editor.py
CHANGED
|
@@ -2,13 +2,33 @@ import os
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import tempfile
|
|
4
4
|
|
|
5
|
+
from git_commit_msg_ai.exceptions import EditorError
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def open_in_editor(initial_text: str) -> str:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
try:
|
|
10
|
+
with tempfile.NamedTemporaryFile(suffix='.txt', mode='w', delete=False) as temp_file:
|
|
11
|
+
temp_file.write(initial_text)
|
|
12
|
+
temp_file_path = temp_file.name
|
|
13
|
+
except OSError:
|
|
14
|
+
raise EditorError('Could not create a temporary file.')
|
|
15
|
+
|
|
16
|
+
editor_command = os.environ.get('EDITOR', 'notepad')
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
try:
|
|
20
|
+
subprocess.run([editor_command, temp_file_path], check=True)
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
raise EditorError(f'Editor "{editor_command}" was not found. Set the EDITOR environment variable to a valid editor.')
|
|
23
|
+
except subprocess.CalledProcessError:
|
|
24
|
+
raise EditorError(f'Editor "{editor_command}" exited with an error.')
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with open(temp_file_path) as temp_file:
|
|
28
|
+
edited_text = temp_file.read().strip()
|
|
29
|
+
except OSError:
|
|
30
|
+
raise EditorError('Could not read the edited commit message from the temporary file.')
|
|
31
|
+
finally:
|
|
32
|
+
os.unlink(temp_file_path)
|
|
33
|
+
|
|
34
|
+
return edited_text
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class GitCommitAIError(Exception):
|
|
2
|
+
"""Base exception for the application."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GitError(GitCommitAIError):
|
|
6
|
+
"""Raised when a git operation fails."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AIError(GitCommitAIError):
|
|
10
|
+
"""Raised when the Anthropic API call fails."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EditorError(GitCommitAIError):
|
|
14
|
+
"""Raised when opening or reading from the editor fails."""
|
git_commit_msg_ai/git_ops.py
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
|
|
3
|
+
from git_commit_msg_ai.exceptions import GitError
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def get_staged_diff() -> str:
|
|
5
|
-
|
|
7
|
+
try:
|
|
8
|
+
staged_diff = subprocess.check_output(['git', 'diff', '--cached']).decode()
|
|
9
|
+
except FileNotFoundError:
|
|
10
|
+
raise GitError('git is not installed or not on PATH.')
|
|
11
|
+
except subprocess.CalledProcessError:
|
|
12
|
+
raise GitError('Failed to get staged diff. Are you inside a git repository?')
|
|
13
|
+
except UnicodeDecodeError:
|
|
14
|
+
raise GitError('Staged diff contains bytes that could not be decoded as UTF-8.')
|
|
15
|
+
|
|
16
|
+
if not staged_diff:
|
|
17
|
+
raise GitError('No staged changes found. Stage files with git add before running.')
|
|
18
|
+
|
|
19
|
+
return staged_diff
|
|
6
20
|
|
|
7
21
|
|
|
8
22
|
def commit(message: str) -> None:
|
|
9
|
-
|
|
23
|
+
try:
|
|
24
|
+
subprocess.run(['git', 'commit', '-m', message], check=True)
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
raise GitError('git is not installed or not on PATH.')
|
|
27
|
+
except subprocess.CalledProcessError:
|
|
28
|
+
raise GitError('git commit failed.')
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-msg-ai
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
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"
|
|
9
14
|
|
|
10
15
|
# git-commit-msg-ai
|
|
11
16
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
git_commit_msg_ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
git_commit_msg_ai/ai_client.py,sha256=RnzgIx1ieuiiH6w2VijA6_F-cs6tIQR57W1x3avg8lE,1887
|
|
3
|
+
git_commit_msg_ai/cli.py,sha256=s94orvxin-38vqUYWAO-j5_JZTzxBXbZSVOHYZ5KO_k,1059
|
|
4
|
+
git_commit_msg_ai/editor.py,sha256=5X4cKFz2ry70BTtWQopSuDpGJGP3JwMcpa7vJpPbjyA,1180
|
|
5
|
+
git_commit_msg_ai/exceptions.py,sha256=7Hwluf3zHMjs4lpGktWS-Lwgo8y_4Xbb1WqzPQHkkUA,352
|
|
6
|
+
git_commit_msg_ai/git_ops.py,sha256=UJr3FUHPg8OTATy5Z5kbnXpy4Kh7fuORKR5HZV774NQ,970
|
|
7
|
+
git_commit_msg_ai-1.3.0.dist-info/METADATA,sha256=cIlbCpC92sAcvU1LLA4GsqgqbKLylX1l-2Pgl8BMBf8,2008
|
|
8
|
+
git_commit_msg_ai-1.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
git_commit_msg_ai-1.3.0.dist-info/entry_points.txt,sha256=KTu6wUhl0p3nf27k8L4vpSH_hpeRQpwzMPSmKv7K5Cs,65
|
|
10
|
+
git_commit_msg_ai-1.3.0.dist-info/top_level.txt,sha256=XYQC2BXvrcREGKEG7sm9nbwO7ifqcUSVgU7SW8BTURs,18
|
|
11
|
+
git_commit_msg_ai-1.3.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
git_commit_msg_ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
git_commit_msg_ai/ai_client.py,sha256=A6EukpLUldU5IZRCajIQwex06vpZSdioJTiR-1HqfU0,1067
|
|
3
|
-
git_commit_msg_ai/cli.py,sha256=t0MMBmGmclGOKVQZi8IqexZfinrHKpWBmqwli0mwDKo,559
|
|
4
|
-
git_commit_msg_ai/editor.py,sha256=P5I2HQIdsoZCc5PuUdE6CvpepenWYJTieVkONzPgLCs,384
|
|
5
|
-
git_commit_msg_ai/git_ops.py,sha256=s6i0JPjQAXfBkX41KPk4oXbRhLyK-HtIFzMF3dQouv0,212
|
|
6
|
-
git_commit_msg_ai-1.1.0.dist-info/METADATA,sha256=BenF9whnrOE4pLFVk33c71BD_C6S2qFjW0QhY5BoETw,1831
|
|
7
|
-
git_commit_msg_ai-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
-
git_commit_msg_ai-1.1.0.dist-info/entry_points.txt,sha256=KTu6wUhl0p3nf27k8L4vpSH_hpeRQpwzMPSmKv7K5Cs,65
|
|
9
|
-
git_commit_msg_ai-1.1.0.dist-info/top_level.txt,sha256=XYQC2BXvrcREGKEG7sm9nbwO7ifqcUSVgU7SW8BTURs,18
|
|
10
|
-
git_commit_msg_ai-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|