git-commit-msg-ai 1.2.0__tar.gz → 1.4.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.2.0 → git_commit_msg_ai-1.4.0}/PKG-INFO +6 -4
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/README.md +2 -2
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai/ai_client.py +19 -9
- git_commit_msg_ai-1.4.0/git_commit_msg_ai/cli.py +35 -0
- git_commit_msg_ai-1.4.0/git_commit_msg_ai/editor.py +43 -0
- git_commit_msg_ai-1.4.0/git_commit_msg_ai/exceptions.py +14 -0
- git_commit_msg_ai-1.4.0/git_commit_msg_ai/git_ops.py +28 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/PKG-INFO +6 -4
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/SOURCES.txt +7 -1
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/requires.txt +2 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/pyproject.toml +15 -5
- git_commit_msg_ai-1.4.0/tests/test_ai_client.py +74 -0
- git_commit_msg_ai-1.4.0/tests/test_cli.py +139 -0
- git_commit_msg_ai-1.4.0/tests/test_editor.py +130 -0
- git_commit_msg_ai-1.4.0/tests/test_exceptions.py +25 -0
- git_commit_msg_ai-1.4.0/tests/test_git_ops.py +70 -0
- git_commit_msg_ai-1.2.0/git_commit_msg_ai/cli.py +0 -22
- git_commit_msg_ai-1.2.0/git_commit_msg_ai/editor.py +0 -19
- git_commit_msg_ai-1.2.0/git_commit_msg_ai/git_ops.py +0 -12
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai/__init__.py +0 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/dependency_links.txt +0 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/entry_points.txt +0 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/top_level.txt +0 -0
- {git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/setup.cfg +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-msg-ai
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: anthropic
|
|
9
9
|
Provides-Extra: dev
|
|
10
10
|
Requires-Dist: mypy; extra == "dev"
|
|
11
11
|
Requires-Dist: ruff; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
12
14
|
|
|
13
15
|
# git-commit-msg-ai
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
16
18
|
|
|
17
19
|
## Prerequisites
|
|
18
20
|
|
|
19
|
-
- Python 3.
|
|
21
|
+
- Python 3.10+
|
|
20
22
|
- An Anthropic API key set as an environment variable:
|
|
21
23
|
|
|
22
24
|
```sh
|
|
@@ -52,7 +54,7 @@ The tool will:
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
- **a** — commits immediately with the generated message
|
|
55
|
-
- **e** — opens the message in your `$EDITOR
|
|
57
|
+
- **e** — opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
|
|
56
58
|
- **r** — exits without committing
|
|
57
59
|
|
|
58
60
|
## Commit message format
|
|
@@ -4,7 +4,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
- Python 3.
|
|
7
|
+
- Python 3.10+
|
|
8
8
|
- An Anthropic API key set as an environment variable:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
@@ -40,7 +40,7 @@ The tool will:
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
- **a** — commits immediately with the generated message
|
|
43
|
-
- **e** — opens the message in your `$EDITOR
|
|
43
|
+
- **e** — opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
|
|
44
44
|
- **r** — exits without committing
|
|
45
45
|
|
|
46
46
|
## Commit message format
|
|
@@ -3,6 +3,8 @@ from typing import Final, cast
|
|
|
3
3
|
|
|
4
4
|
import anthropic
|
|
5
5
|
|
|
6
|
+
from git_commit_msg_ai.exceptions import AIError
|
|
7
|
+
|
|
6
8
|
SYSTEM_PROMPT: Final[str] = textwrap.dedent("""\
|
|
7
9
|
You are a Git commit message generator. Output only the commit message, nothing else. Follow the Conventional Commits specification:
|
|
8
10
|
- First line: <type>(<optional scope>)<!>: <short subject> (50 chars max); append ! before the colon if the commit introduces a breaking change
|
|
@@ -16,17 +18,25 @@ MAX_TOKENS: Final[int] = 1024
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def generate_commit_message(diff: str) -> str:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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}.')
|
|
27
38
|
|
|
28
39
|
anthropic_api_response_message = anthropic_api_response.content[0]
|
|
29
40
|
commit_message = cast(anthropic.types.TextBlock, anthropic_api_response_message).text.strip()
|
|
30
41
|
|
|
31
42
|
return commit_message
|
|
32
|
-
|
|
@@ -0,0 +1,35 @@
|
|
|
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()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from git_commit_msg_ai.exceptions import EditorError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_default_editor() -> str:
|
|
10
|
+
current_platform = platform.system()
|
|
11
|
+
is_windows = current_platform == 'Windows'
|
|
12
|
+
|
|
13
|
+
return 'notepad' if is_windows else 'vi'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def open_in_editor(initial_text: str) -> str:
|
|
17
|
+
try:
|
|
18
|
+
with tempfile.NamedTemporaryFile(suffix='.txt', mode='w', delete=False) as temp_file:
|
|
19
|
+
temp_file.write(initial_text)
|
|
20
|
+
temp_file_path = temp_file.name
|
|
21
|
+
except OSError:
|
|
22
|
+
raise EditorError('Could not create a temporary file.')
|
|
23
|
+
|
|
24
|
+
platform_default_editor = get_default_editor()
|
|
25
|
+
editor_command = os.environ.get('EDITOR', platform_default_editor)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run([editor_command, temp_file_path], check=True)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
raise EditorError(f'Editor "{editor_command}" was not found. Set the EDITOR environment variable to a valid editor.')
|
|
32
|
+
except subprocess.CalledProcessError:
|
|
33
|
+
raise EditorError(f'Editor "{editor_command}" exited with an error.')
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(temp_file_path) as temp_file:
|
|
37
|
+
edited_text = temp_file.read().strip()
|
|
38
|
+
except OSError:
|
|
39
|
+
raise EditorError('Could not read the edited commit message from the temporary file.')
|
|
40
|
+
finally:
|
|
41
|
+
os.unlink(temp_file_path)
|
|
42
|
+
|
|
43
|
+
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."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from git_commit_msg_ai.exceptions import GitError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_staged_diff() -> str:
|
|
7
|
+
try:
|
|
8
|
+
staged_diff = subprocess.check_output(['git', 'diff', '--cached']).decode('utf-8')
|
|
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
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def commit(message: str) -> None:
|
|
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,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-msg-ai
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: AI-powered git commit message generator following Conventional Commits
|
|
5
5
|
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: anthropic
|
|
9
9
|
Provides-Extra: dev
|
|
10
10
|
Requires-Dist: mypy; extra == "dev"
|
|
11
11
|
Requires-Dist: ruff; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
12
14
|
|
|
13
15
|
# git-commit-msg-ai
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ AI-powered git commit message generator that follows the [Conventional Commits](
|
|
|
16
18
|
|
|
17
19
|
## Prerequisites
|
|
18
20
|
|
|
19
|
-
- Python 3.
|
|
21
|
+
- Python 3.10+
|
|
20
22
|
- An Anthropic API key set as an environment variable:
|
|
21
23
|
|
|
22
24
|
```sh
|
|
@@ -52,7 +54,7 @@ The tool will:
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
- **a** — commits immediately with the generated message
|
|
55
|
-
- **e** — opens the message in your `$EDITOR
|
|
57
|
+
- **e** — opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
|
|
56
58
|
- **r** — exits without committing
|
|
57
59
|
|
|
58
60
|
## Commit message format
|
|
@@ -4,10 +4,16 @@ git_commit_msg_ai/__init__.py
|
|
|
4
4
|
git_commit_msg_ai/ai_client.py
|
|
5
5
|
git_commit_msg_ai/cli.py
|
|
6
6
|
git_commit_msg_ai/editor.py
|
|
7
|
+
git_commit_msg_ai/exceptions.py
|
|
7
8
|
git_commit_msg_ai/git_ops.py
|
|
8
9
|
git_commit_msg_ai.egg-info/PKG-INFO
|
|
9
10
|
git_commit_msg_ai.egg-info/SOURCES.txt
|
|
10
11
|
git_commit_msg_ai.egg-info/dependency_links.txt
|
|
11
12
|
git_commit_msg_ai.egg-info/entry_points.txt
|
|
12
13
|
git_commit_msg_ai.egg-info/requires.txt
|
|
13
|
-
git_commit_msg_ai.egg-info/top_level.txt
|
|
14
|
+
git_commit_msg_ai.egg-info/top_level.txt
|
|
15
|
+
tests/test_ai_client.py
|
|
16
|
+
tests/test_cli.py
|
|
17
|
+
tests/test_editor.py
|
|
18
|
+
tests/test_exceptions.py
|
|
19
|
+
tests/test_git_ops.py
|
|
@@ -4,30 +4,40 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "git-commit-msg-ai"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "AI-powered git commit message generator following Conventional Commits"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
12
|
dependencies = ["anthropic"]
|
|
13
13
|
|
|
14
14
|
[project.scripts]
|
|
15
15
|
git-commit-msg-ai = "git_commit_msg_ai.cli:main"
|
|
16
16
|
|
|
17
17
|
[project.optional-dependencies]
|
|
18
|
-
dev = ["mypy", "ruff"]
|
|
18
|
+
dev = ["mypy", "ruff", "pytest", "pytest-cov"]
|
|
19
19
|
|
|
20
20
|
[tool.ruff]
|
|
21
|
-
target-version = "
|
|
21
|
+
target-version = "py310"
|
|
22
22
|
line-length = 320
|
|
23
23
|
|
|
24
24
|
[tool.ruff.lint]
|
|
25
25
|
select = ["E", "F", "I"]
|
|
26
26
|
|
|
27
27
|
[tool.mypy]
|
|
28
|
-
python_version = "3.
|
|
28
|
+
python_version = "3.10"
|
|
29
29
|
strict = true
|
|
30
30
|
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
addopts = "--cov --cov-report=term-missing"
|
|
34
|
+
|
|
35
|
+
[tool.coverage.run]
|
|
36
|
+
source = ["git_commit_msg_ai"]
|
|
37
|
+
|
|
38
|
+
[tool.coverage.report]
|
|
39
|
+
show_missing = true
|
|
40
|
+
|
|
31
41
|
[tool.setuptools.packages.find]
|
|
32
42
|
where = ["."]
|
|
33
43
|
include = ["git_commit_msg_ai*"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import anthropic
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from git_commit_msg_ai.ai_client import generate_commit_message
|
|
7
|
+
from git_commit_msg_ai.exceptions import AIError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_api_response(text: str) -> MagicMock:
|
|
11
|
+
mock_text_block = MagicMock(spec=anthropic.types.TextBlock)
|
|
12
|
+
mock_text_block.text = text
|
|
13
|
+
|
|
14
|
+
mock_response = MagicMock(spec=anthropic.types.Message)
|
|
15
|
+
mock_response.content = [mock_text_block]
|
|
16
|
+
|
|
17
|
+
return mock_response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_status_error(exception_class: type[anthropic.APIStatusError], status_code: int) -> anthropic.APIStatusError:
|
|
21
|
+
mock_response = MagicMock()
|
|
22
|
+
mock_response.status_code = status_code
|
|
23
|
+
|
|
24
|
+
return exception_class('error', response=mock_response, body={})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestGenerateCommitMessage:
|
|
28
|
+
def test_returns_stripped_commit_message(self) -> None:
|
|
29
|
+
with patch('git_commit_msg_ai.ai_client.anthropic.Anthropic') as mock_anthropic_class:
|
|
30
|
+
mock_client = MagicMock()
|
|
31
|
+
mock_client.messages.create.return_value = _make_api_response(' feat: add feature ')
|
|
32
|
+
mock_anthropic_class.return_value = mock_client
|
|
33
|
+
|
|
34
|
+
result = generate_commit_message('diff content')
|
|
35
|
+
|
|
36
|
+
assert result == 'feat: add feature'
|
|
37
|
+
|
|
38
|
+
def test_raises_ai_error_on_authentication_error(self) -> None:
|
|
39
|
+
with patch('git_commit_msg_ai.ai_client.anthropic.Anthropic') as mock_anthropic_class:
|
|
40
|
+
mock_client = MagicMock()
|
|
41
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.AuthenticationError, 401)
|
|
42
|
+
mock_anthropic_class.return_value = mock_client
|
|
43
|
+
|
|
44
|
+
with pytest.raises(AIError, match='ANTHROPIC_API_KEY'):
|
|
45
|
+
generate_commit_message('diff content')
|
|
46
|
+
|
|
47
|
+
def test_raises_ai_error_on_rate_limit_error(self) -> None:
|
|
48
|
+
with patch('git_commit_msg_ai.ai_client.anthropic.Anthropic') as mock_anthropic_class:
|
|
49
|
+
mock_client = MagicMock()
|
|
50
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.RateLimitError, 429)
|
|
51
|
+
mock_anthropic_class.return_value = mock_client
|
|
52
|
+
|
|
53
|
+
with pytest.raises(AIError, match='rate limit'):
|
|
54
|
+
generate_commit_message('diff content')
|
|
55
|
+
|
|
56
|
+
def test_raises_ai_error_on_api_connection_error(self) -> None:
|
|
57
|
+
mock_request = MagicMock()
|
|
58
|
+
|
|
59
|
+
with patch('git_commit_msg_ai.ai_client.anthropic.Anthropic') as mock_anthropic_class:
|
|
60
|
+
mock_client = MagicMock()
|
|
61
|
+
mock_client.messages.create.side_effect = anthropic.APIConnectionError(request=mock_request)
|
|
62
|
+
mock_anthropic_class.return_value = mock_client
|
|
63
|
+
|
|
64
|
+
with pytest.raises(AIError, match='network'):
|
|
65
|
+
generate_commit_message('diff content')
|
|
66
|
+
|
|
67
|
+
def test_raises_ai_error_on_api_status_error_with_status_code(self) -> None:
|
|
68
|
+
with patch('git_commit_msg_ai.ai_client.anthropic.Anthropic') as mock_anthropic_class:
|
|
69
|
+
mock_client = MagicMock()
|
|
70
|
+
mock_client.messages.create.side_effect = _make_status_error(anthropic.APIStatusError, 500)
|
|
71
|
+
mock_anthropic_class.return_value = mock_client
|
|
72
|
+
|
|
73
|
+
with pytest.raises(AIError, match='500'):
|
|
74
|
+
generate_commit_message('diff content')
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from git_commit_msg_ai.cli import main
|
|
6
|
+
from git_commit_msg_ai.exceptions import AIError, GitError
|
|
7
|
+
|
|
8
|
+
COMMIT_MESSAGE = 'feat: add feature'
|
|
9
|
+
EDITED_COMMIT_MESSAGE = 'edited commit message'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestMain:
|
|
13
|
+
def _run_with_input(self, user_input: str, commit_message: str = COMMIT_MESSAGE) -> tuple[MagicMock, MagicMock, MagicMock]:
|
|
14
|
+
mock_git_ops = MagicMock()
|
|
15
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
16
|
+
|
|
17
|
+
mock_ai_client = MagicMock()
|
|
18
|
+
mock_ai_client.generate_commit_message.return_value = commit_message
|
|
19
|
+
|
|
20
|
+
mock_editor = MagicMock()
|
|
21
|
+
mock_editor.open_in_editor.return_value = EDITED_COMMIT_MESSAGE
|
|
22
|
+
|
|
23
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
24
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
25
|
+
with patch('git_commit_msg_ai.cli.editor', mock_editor):
|
|
26
|
+
with patch('builtins.input', return_value=user_input):
|
|
27
|
+
main()
|
|
28
|
+
|
|
29
|
+
return mock_git_ops, mock_ai_client, mock_editor
|
|
30
|
+
|
|
31
|
+
def test_accept_calls_commit_with_generated_message(self) -> None:
|
|
32
|
+
mock_git_ops, _, _ = self._run_with_input('a', COMMIT_MESSAGE)
|
|
33
|
+
|
|
34
|
+
mock_git_ops.commit.assert_called_once_with(COMMIT_MESSAGE)
|
|
35
|
+
|
|
36
|
+
def test_edit_opens_editor_then_commits_edited_message(self) -> None:
|
|
37
|
+
mock_git_ops, _, mock_editor = self._run_with_input('e', COMMIT_MESSAGE)
|
|
38
|
+
|
|
39
|
+
mock_editor.open_in_editor.assert_called_once_with(COMMIT_MESSAGE)
|
|
40
|
+
mock_git_ops.commit.assert_called_once_with(EDITED_COMMIT_MESSAGE)
|
|
41
|
+
|
|
42
|
+
def test_reject_does_not_commit(self) -> None:
|
|
43
|
+
mock_git_ops, _, _ = self._run_with_input('r')
|
|
44
|
+
|
|
45
|
+
mock_git_ops.commit.assert_not_called()
|
|
46
|
+
|
|
47
|
+
def test_invalid_input_exits_with_code_1(self) -> None:
|
|
48
|
+
mock_git_ops = MagicMock()
|
|
49
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
50
|
+
|
|
51
|
+
mock_ai_client = MagicMock()
|
|
52
|
+
mock_ai_client.generate_commit_message.return_value = 'msg'
|
|
53
|
+
|
|
54
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
55
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
56
|
+
with patch('git_commit_msg_ai.cli.editor'):
|
|
57
|
+
with patch('builtins.input', return_value='x'):
|
|
58
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
59
|
+
main()
|
|
60
|
+
|
|
61
|
+
assert exc_info.value.code == 1
|
|
62
|
+
|
|
63
|
+
def test_git_error_from_get_staged_diff_exits_with_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
64
|
+
mock_git_ops = MagicMock()
|
|
65
|
+
mock_git_ops.get_staged_diff.side_effect = GitError('staged error')
|
|
66
|
+
|
|
67
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
68
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
69
|
+
main()
|
|
70
|
+
|
|
71
|
+
assert exc_info.value.code == 1
|
|
72
|
+
assert 'staged error' in capsys.readouterr().out
|
|
73
|
+
|
|
74
|
+
def test_ai_error_from_generate_commit_message_exits_with_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
75
|
+
mock_git_ops = MagicMock()
|
|
76
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
77
|
+
|
|
78
|
+
mock_ai_client = MagicMock()
|
|
79
|
+
mock_ai_client.generate_commit_message.side_effect = AIError('ai error')
|
|
80
|
+
|
|
81
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
82
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
83
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
84
|
+
main()
|
|
85
|
+
|
|
86
|
+
assert exc_info.value.code == 1
|
|
87
|
+
assert 'ai error' in capsys.readouterr().out
|
|
88
|
+
|
|
89
|
+
def test_git_error_from_commit_exits_with_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
90
|
+
mock_git_ops = MagicMock()
|
|
91
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
92
|
+
mock_git_ops.commit.side_effect = GitError('commit error')
|
|
93
|
+
|
|
94
|
+
mock_ai_client = MagicMock()
|
|
95
|
+
mock_ai_client.generate_commit_message.return_value = 'msg'
|
|
96
|
+
|
|
97
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
98
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
99
|
+
with patch('git_commit_msg_ai.cli.editor'):
|
|
100
|
+
with patch('builtins.input', return_value='a'):
|
|
101
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
102
|
+
main()
|
|
103
|
+
|
|
104
|
+
assert exc_info.value.code == 1
|
|
105
|
+
assert 'commit error' in capsys.readouterr().out
|
|
106
|
+
|
|
107
|
+
def test_keyboard_interrupt_exits_with_aborted_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
108
|
+
mock_git_ops = MagicMock()
|
|
109
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
110
|
+
|
|
111
|
+
mock_ai_client = MagicMock()
|
|
112
|
+
mock_ai_client.generate_commit_message.return_value = 'msg'
|
|
113
|
+
|
|
114
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
115
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
116
|
+
with patch('git_commit_msg_ai.cli.editor'):
|
|
117
|
+
with patch('builtins.input', side_effect=KeyboardInterrupt):
|
|
118
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
119
|
+
main()
|
|
120
|
+
|
|
121
|
+
assert exc_info.value.code == 1
|
|
122
|
+
assert 'Aborted.' in capsys.readouterr().out
|
|
123
|
+
|
|
124
|
+
def test_eof_error_exits_with_aborted_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
125
|
+
mock_git_ops = MagicMock()
|
|
126
|
+
mock_git_ops.get_staged_diff.return_value = 'diff content'
|
|
127
|
+
|
|
128
|
+
mock_ai_client = MagicMock()
|
|
129
|
+
mock_ai_client.generate_commit_message.return_value = 'msg'
|
|
130
|
+
|
|
131
|
+
with patch('git_commit_msg_ai.cli.git_ops', mock_git_ops):
|
|
132
|
+
with patch('git_commit_msg_ai.cli.ai_client', mock_ai_client):
|
|
133
|
+
with patch('git_commit_msg_ai.cli.editor'):
|
|
134
|
+
with patch('builtins.input', side_effect=EOFError):
|
|
135
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
136
|
+
main()
|
|
137
|
+
|
|
138
|
+
assert exc_info.value.code == 1
|
|
139
|
+
assert 'Aborted.' in capsys.readouterr().out
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
from contextlib import ExitStack
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from git_commit_msg_ai.editor import get_default_editor, open_in_editor
|
|
10
|
+
from git_commit_msg_ai.exceptions import EditorError
|
|
11
|
+
|
|
12
|
+
TEMP_FILE_PATH = os.path.join(tempfile.gettempdir(), 'tmpfile.txt')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _make_named_temporary_file_mock() -> MagicMock:
|
|
16
|
+
mock_temp_file = MagicMock()
|
|
17
|
+
mock_temp_file.name = TEMP_FILE_PATH
|
|
18
|
+
mock_temp_file.__enter__ = MagicMock(return_value=mock_temp_file)
|
|
19
|
+
mock_temp_file.__exit__ = MagicMock(return_value=False)
|
|
20
|
+
|
|
21
|
+
return mock_temp_file
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_open_mock(text: str) -> MagicMock:
|
|
25
|
+
mock_file = MagicMock()
|
|
26
|
+
mock_file.read.return_value = text
|
|
27
|
+
mock_file.__enter__ = MagicMock(return_value=mock_file)
|
|
28
|
+
mock_file.__exit__ = MagicMock(return_value=False)
|
|
29
|
+
|
|
30
|
+
return mock_file
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestOpenInEditor:
|
|
34
|
+
def test_returns_stripped_edited_text(self) -> None:
|
|
35
|
+
with ExitStack() as stack:
|
|
36
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
37
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run'))
|
|
38
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.open', return_value=_make_open_mock(' edited text ')))
|
|
39
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
40
|
+
|
|
41
|
+
result = open_in_editor('initial text')
|
|
42
|
+
|
|
43
|
+
assert result == 'edited text'
|
|
44
|
+
|
|
45
|
+
def test_uses_editor_env_var(self) -> None:
|
|
46
|
+
with ExitStack() as stack:
|
|
47
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
48
|
+
mock_run = stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run'))
|
|
49
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.open', return_value=_make_open_mock('text')))
|
|
50
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
51
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.environ.get', return_value='vim'))
|
|
52
|
+
|
|
53
|
+
open_in_editor('initial text')
|
|
54
|
+
|
|
55
|
+
mock_run.assert_called_once_with(['vim', TEMP_FILE_PATH], check=True)
|
|
56
|
+
|
|
57
|
+
def test_falls_back_to_platform_default_when_editor_not_set(self) -> None:
|
|
58
|
+
sentinel_editor = 'test-sentinel-editor'
|
|
59
|
+
|
|
60
|
+
with ExitStack() as stack:
|
|
61
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
62
|
+
mock_run = stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run'))
|
|
63
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.open', return_value=_make_open_mock('text')))
|
|
64
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
65
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.get_default_editor', return_value=sentinel_editor))
|
|
66
|
+
stack.enter_context(patch.dict('os.environ', {}, clear=True))
|
|
67
|
+
|
|
68
|
+
open_in_editor('initial text')
|
|
69
|
+
|
|
70
|
+
mock_run.assert_called_once_with([sentinel_editor, TEMP_FILE_PATH], check=True)
|
|
71
|
+
|
|
72
|
+
def test_raises_editor_error_on_temp_file_creation_failure(self) -> None:
|
|
73
|
+
with ExitStack() as stack:
|
|
74
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', side_effect=OSError))
|
|
75
|
+
mock_unlink = stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
76
|
+
|
|
77
|
+
with pytest.raises(EditorError, match='temporary file'):
|
|
78
|
+
open_in_editor('initial text')
|
|
79
|
+
|
|
80
|
+
mock_unlink.assert_not_called()
|
|
81
|
+
|
|
82
|
+
def test_raises_editor_error_when_editor_not_found(self) -> None:
|
|
83
|
+
with ExitStack() as stack:
|
|
84
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
85
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run', side_effect=FileNotFoundError))
|
|
86
|
+
mock_unlink = stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
87
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.environ.get', return_value='vim'))
|
|
88
|
+
|
|
89
|
+
with pytest.raises(EditorError, match='was not found'):
|
|
90
|
+
open_in_editor('initial text')
|
|
91
|
+
|
|
92
|
+
mock_unlink.assert_called_once_with(TEMP_FILE_PATH)
|
|
93
|
+
|
|
94
|
+
def test_raises_editor_error_when_editor_exits_with_error(self) -> None:
|
|
95
|
+
with ExitStack() as stack:
|
|
96
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
97
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run', side_effect=subprocess.CalledProcessError(1, 'vim')))
|
|
98
|
+
mock_unlink = stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
99
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.os.environ.get', return_value='vim'))
|
|
100
|
+
|
|
101
|
+
with pytest.raises(EditorError, match='exited with an error'):
|
|
102
|
+
open_in_editor('initial text')
|
|
103
|
+
|
|
104
|
+
mock_unlink.assert_called_once_with(TEMP_FILE_PATH)
|
|
105
|
+
|
|
106
|
+
def test_raises_editor_error_when_file_read_fails(self) -> None:
|
|
107
|
+
with ExitStack() as stack:
|
|
108
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.tempfile.NamedTemporaryFile', return_value=_make_named_temporary_file_mock()))
|
|
109
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.subprocess.run'))
|
|
110
|
+
stack.enter_context(patch('git_commit_msg_ai.editor.open', side_effect=OSError))
|
|
111
|
+
mock_unlink = stack.enter_context(patch('git_commit_msg_ai.editor.os.unlink'))
|
|
112
|
+
|
|
113
|
+
with pytest.raises(EditorError, match='Could not read'):
|
|
114
|
+
open_in_editor('initial text')
|
|
115
|
+
|
|
116
|
+
mock_unlink.assert_called_once_with(TEMP_FILE_PATH)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestGetDefaultEditor:
|
|
120
|
+
def test_returns_notepad_on_windows(self) -> None:
|
|
121
|
+
with patch('git_commit_msg_ai.editor.platform.system', return_value='Windows'):
|
|
122
|
+
result = get_default_editor()
|
|
123
|
+
|
|
124
|
+
assert result == 'notepad'
|
|
125
|
+
|
|
126
|
+
def test_returns_vi_on_non_windows(self) -> None:
|
|
127
|
+
with patch('git_commit_msg_ai.editor.platform.system', return_value='Linux'):
|
|
128
|
+
result = get_default_editor()
|
|
129
|
+
|
|
130
|
+
assert result == 'vi'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from git_commit_msg_ai.exceptions import AIError, EditorError, GitCommitAIError, GitError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_git_error_is_subclass_of_base() -> None:
|
|
5
|
+
assert issubclass(GitError, GitCommitAIError)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_ai_error_is_subclass_of_base() -> None:
|
|
9
|
+
assert issubclass(AIError, GitCommitAIError)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_editor_error_is_subclass_of_base() -> None:
|
|
13
|
+
assert issubclass(EditorError, GitCommitAIError)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_git_error_carries_message() -> None:
|
|
17
|
+
assert str(GitError('git failed')) == 'git failed'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_ai_error_carries_message() -> None:
|
|
21
|
+
assert str(AIError('api failed')) == 'api failed'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_editor_error_carries_message() -> None:
|
|
25
|
+
assert str(EditorError('editor failed')) == 'editor failed'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from git_commit_msg_ai.exceptions import GitError
|
|
7
|
+
from git_commit_msg_ai.git_ops import commit, get_staged_diff
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestGetStagedDiff:
|
|
11
|
+
def test_returns_decoded_diff(self) -> None:
|
|
12
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.check_output') as mock_check_output:
|
|
13
|
+
mock_check_output.return_value = b'diff content'
|
|
14
|
+
|
|
15
|
+
result = get_staged_diff()
|
|
16
|
+
|
|
17
|
+
assert result == 'diff content'
|
|
18
|
+
|
|
19
|
+
def test_raises_git_error_when_git_not_found(self) -> None:
|
|
20
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.check_output') as mock_check_output:
|
|
21
|
+
mock_check_output.side_effect = FileNotFoundError
|
|
22
|
+
|
|
23
|
+
with pytest.raises(GitError, match='not installed'):
|
|
24
|
+
get_staged_diff()
|
|
25
|
+
|
|
26
|
+
def test_raises_git_error_on_called_process_error(self) -> None:
|
|
27
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.check_output') as mock_check_output:
|
|
28
|
+
mock_check_output.side_effect = subprocess.CalledProcessError(1, 'git')
|
|
29
|
+
|
|
30
|
+
with pytest.raises(GitError, match='git repository'):
|
|
31
|
+
get_staged_diff()
|
|
32
|
+
|
|
33
|
+
def test_raises_git_error_on_unicode_decode_error(self) -> None:
|
|
34
|
+
mock_bytes = MagicMock()
|
|
35
|
+
mock_bytes.decode.side_effect = UnicodeDecodeError('utf-8', b'', 0, 1, 'reason')
|
|
36
|
+
|
|
37
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.check_output') as mock_check_output:
|
|
38
|
+
mock_check_output.return_value = mock_bytes
|
|
39
|
+
|
|
40
|
+
with pytest.raises(GitError, match='UTF-8'):
|
|
41
|
+
get_staged_diff()
|
|
42
|
+
|
|
43
|
+
def test_raises_git_error_when_diff_is_empty(self) -> None:
|
|
44
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.check_output') as mock_check_output:
|
|
45
|
+
mock_check_output.return_value = b''
|
|
46
|
+
|
|
47
|
+
with pytest.raises(GitError, match='No staged changes'):
|
|
48
|
+
get_staged_diff()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestCommit:
|
|
52
|
+
def test_calls_subprocess_run_with_correct_args(self) -> None:
|
|
53
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.run') as mock_run:
|
|
54
|
+
commit('my message')
|
|
55
|
+
|
|
56
|
+
mock_run.assert_called_once_with(['git', 'commit', '-m', 'my message'], check=True)
|
|
57
|
+
|
|
58
|
+
def test_raises_git_error_when_git_not_found(self) -> None:
|
|
59
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.run') as mock_run:
|
|
60
|
+
mock_run.side_effect = FileNotFoundError
|
|
61
|
+
|
|
62
|
+
with pytest.raises(GitError, match='not installed'):
|
|
63
|
+
commit('my message')
|
|
64
|
+
|
|
65
|
+
def test_raises_git_error_on_called_process_error(self) -> None:
|
|
66
|
+
with patch('git_commit_msg_ai.git_ops.subprocess.run') as mock_run:
|
|
67
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, 'git')
|
|
68
|
+
|
|
69
|
+
with pytest.raises(GitError, match='git commit failed'):
|
|
70
|
+
commit('my message')
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
commit_message = ai_client.generate_commit_message(diff)
|
|
7
|
-
print(commit_message)
|
|
8
|
-
|
|
9
|
-
print()
|
|
10
|
-
user_selection = input('[a]ccept / [e]dit / [r]eject: ').strip().lower()
|
|
11
|
-
|
|
12
|
-
if user_selection == 'a':
|
|
13
|
-
git_ops.commit(commit_message)
|
|
14
|
-
elif user_selection == 'e':
|
|
15
|
-
updated_commit_message = editor.open_in_editor(commit_message)
|
|
16
|
-
git_ops.commit(updated_commit_message)
|
|
17
|
-
elif user_selection == 'r':
|
|
18
|
-
print('User rejected the generated commit message. No commit made.')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if __name__ == '__main__':
|
|
22
|
-
main()
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
|
-
import tempfile
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def open_in_editor(initial_text: str) -> str:
|
|
7
|
-
with tempfile.NamedTemporaryFile(suffix='.txt', mode='w', delete=False) as temp_file:
|
|
8
|
-
temp_file.write(initial_text)
|
|
9
|
-
temp_file_path = temp_file.name
|
|
10
|
-
|
|
11
|
-
editor_command = os.environ.get('EDITOR', 'notepad')
|
|
12
|
-
subprocess.run([editor_command, temp_file_path])
|
|
13
|
-
|
|
14
|
-
with open(temp_file_path) as temp_file:
|
|
15
|
-
edited_text = temp_file.read().strip()
|
|
16
|
-
|
|
17
|
-
os.unlink(temp_file_path)
|
|
18
|
-
|
|
19
|
-
return edited_text
|
|
File without changes
|
{git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{git_commit_msg_ai-1.2.0 → git_commit_msg_ai-1.4.0}/git_commit_msg_ai.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|