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.
@@ -1,28 +1,42 @@
1
- from typing import cast
1
+ import textwrap
2
+ from typing import Final, cast
3
+
2
4
  import anthropic
3
5
 
4
- SYSTEM_PROMPT = (
5
- 'You are a Git commit message generator. Output only the commit message, nothing else. '
6
- 'Follow the Conventional Commits specification:\n'
7
- '- First line: <type>(<optional scope>)<!>: <short subject> (50 chars max); '
8
- 'append ! before the colon if the commit introduces a breaking change\n'
9
- '- Blank line\n'
10
- '- Bullet-point body explaining WHY the changes were made, not what\n'
11
- '- If there is a breaking change, add a blank line after the body followed by '
12
- '"BREAKING CHANGE: <description of what breaks and why>\n'
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
- client = anthropic.Anthropic()
22
- response = client.messages.create(
23
- model=MODEL,
24
- max_tokens=MAX_TOKENS,
25
- system=SYSTEM_PROMPT,
26
- messages=[{'role': 'user', 'content': diff}],
27
- )
28
- return cast(anthropic.types.TextBlock, response.content[0]).text.strip()
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
- from git_commit_msg_ai import ai_client, editor, git_ops
2
-
3
-
4
- def main() -> None:
5
- diff = git_ops.get_staged_diff()
6
- msg = ai_client.generate_commit_message(diff)
7
- print(msg)
8
-
9
- choice = input('\n[a]ccept / [e]dit / [r]eject: ').strip().lower()
10
-
11
- if choice == 'a':
12
- git_ops.commit(msg)
13
- elif choice == 'e':
14
- msg = editor.open_in_editor(msg)
15
- git_ops.commit(msg)
16
- elif choice == 'r':
17
- print('User rejected the generated commit message. No commit made.')
18
-
19
-
20
- if __name__ == '__main__':
21
- main()
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()
@@ -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
- with tempfile.NamedTemporaryFile(suffix='.txt', mode='w', delete=False) as f:
8
- f.write(initial_text)
9
- tmp = f.name
10
- subprocess.run([os.environ.get('EDITOR', 'notepad'), tmp])
11
- with open(tmp) as f:
12
- result = f.read().strip()
13
- os.unlink(tmp)
14
- return result
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."""
@@ -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
- return subprocess.check_output(['git', 'diff', '--cached']).decode()
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
- subprocess.run(['git', 'commit', '-m', message])
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.1.0
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,,