git-commit-msg-ai 2.1.2__tar.gz → 2.3.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.
Files changed (30) hide show
  1. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/PKG-INFO +14 -7
  2. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/README.md +13 -6
  3. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai/ai_client.py +28 -4
  4. git_commit_msg_ai-2.3.0/git_commit_msg_ai/cli.py +77 -0
  5. git_commit_msg_ai-2.3.0/git_commit_msg_ai/config.py +142 -0
  6. git_commit_msg_ai-2.3.0/git_commit_msg_ai/git_ops.py +62 -0
  7. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/PKG-INFO +14 -7
  8. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/pyproject.toml +1 -1
  9. git_commit_msg_ai-2.3.0/tests/test_ai_client.py +237 -0
  10. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/tests/test_cli.py +146 -83
  11. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/tests/test_config.py +193 -0
  12. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/tests/test_generate_release_notes.py +16 -4
  13. git_commit_msg_ai-2.3.0/tests/test_git_ops.py +183 -0
  14. git_commit_msg_ai-2.1.2/git_commit_msg_ai/cli.py +0 -58
  15. git_commit_msg_ai-2.1.2/git_commit_msg_ai/config.py +0 -80
  16. git_commit_msg_ai-2.1.2/git_commit_msg_ai/git_ops.py +0 -41
  17. git_commit_msg_ai-2.1.2/tests/test_ai_client.py +0 -115
  18. git_commit_msg_ai-2.1.2/tests/test_git_ops.py +0 -82
  19. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/LICENSE +0 -0
  20. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai/__init__.py +0 -0
  21. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai/editor.py +0 -0
  22. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai/exceptions.py +0 -0
  23. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/SOURCES.txt +0 -0
  24. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/dependency_links.txt +0 -0
  25. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/entry_points.txt +0 -0
  26. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/requires.txt +0 -0
  27. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/git_commit_msg_ai.egg-info/top_level.txt +0 -0
  28. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/setup.cfg +0 -0
  29. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/tests/test_editor.py +0 -0
  30. {git_commit_msg_ai-2.1.2 → git_commit_msg_ai-2.3.0}/tests/test_exceptions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-msg-ai
3
- Version: 2.1.2
3
+ Version: 2.3.0
4
4
  Summary: AI-powered git commit message generator following Conventional Commits
5
5
  License: MIT License
6
6
 
@@ -118,12 +118,15 @@ The tool will:
118
118
  3. Print the message and prompt you to choose:
119
119
 
120
120
  ```
121
- [a]ccept / [e]dit / [r]eject:
121
+ [a]ccept / [e]dit / [r]eject / [f]eedback:
122
122
  ```
123
123
 
124
124
  - **a** - commits immediately with the generated message
125
125
  - **e** - opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
126
126
  - **r** - exits without committing
127
+ - **f** - prompts you for natural-language feedback, regenerates the message incorporating that feedback, and loops back so you can keep refining until satisfied
128
+
129
+ The tool automatically reads the current branch name and recent commit history and includes them in the request to the AI. This helps the AI match the commit style already established in the project. No configuration is required to enable this; see [Configuration](#configuration) to control how many recent commits are included.
127
130
 
128
131
  ## Commit message format
129
132
 
@@ -156,11 +159,11 @@ The set of optional commit types the AI may use can be customised through a conf
156
159
 
157
160
  ### Config file locations and precedence
158
161
 
159
- The tool checks the following locations in order, using the first one that defines a `types` list:
162
+ The tool checks the following locations in order, using the first one that defines a given setting:
160
163
 
161
164
  1. **Project-level** — `pyproject.toml` walked up from the current working directory, under `[tool.git-commit-msg-ai]`
162
165
  2. **User-level** — `~/.git-commit-msg-ai.toml` in your home directory
163
- 3. **Built-in defaults** — used when neither config file is present or neither defines `types`
166
+ 3. **Built-in defaults** — used when neither config file is present or neither defines the setting
164
167
 
165
168
  ### Config file formats
166
169
 
@@ -169,16 +172,20 @@ Project-level (`pyproject.toml`):
169
172
  ```toml
170
173
  [tool.git-commit-msg-ai]
171
174
  types = ["build", "ci", "docs", "perf", "refactor", "style", "test"]
175
+ context_commits = 10 # include last 10 commits for context (default: 5)
172
176
  ```
173
177
 
174
178
  User-level (`~/.git-commit-msg-ai.toml`):
175
179
 
176
180
  ```toml
177
181
  types = ["build", "ci", "docs", "chore"]
182
+ context_commits = 0 # disable commit context
178
183
  ```
179
184
 
180
185
  Setting `types = []` restricts the AI to only the three mandatory types (`feat`, `fix`, `revert`).
181
186
 
187
+ `context_commits` controls how many recent commit messages are sent to the AI as context. The default is `5`. Set it to `0` to disable context entirely.
188
+
182
189
  ### Default optional types
183
190
 
184
191
  | Type | Purpose |
@@ -208,11 +215,11 @@ Pushing a version tag (e.g. `v1.5.2`) triggers the CD pipeline:
208
215
  # 1. Bump the version in pyproject.toml
209
216
  # 2. Commit and push
210
217
  git add pyproject.toml
211
- git commit -m "chore: bump version to 2.2.0"
218
+ git commit -m "chore: bump version to 2.3.0"
212
219
  git push origin main
213
220
  # 3. Tag and push - this triggers the CD pipeline
214
- git tag v2.2.0
215
- git push origin v2.2.0
221
+ git tag v2.3.0
222
+ git push origin v2.3.0
216
223
  ```
217
224
 
218
225
  ## Debugging
@@ -75,12 +75,15 @@ The tool will:
75
75
  3. Print the message and prompt you to choose:
76
76
 
77
77
  ```
78
- [a]ccept / [e]dit / [r]eject:
78
+ [a]ccept / [e]dit / [r]eject / [f]eedback:
79
79
  ```
80
80
 
81
81
  - **a** - commits immediately with the generated message
82
82
  - **e** - opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
83
83
  - **r** - exits without committing
84
+ - **f** - prompts you for natural-language feedback, regenerates the message incorporating that feedback, and loops back so you can keep refining until satisfied
85
+
86
+ The tool automatically reads the current branch name and recent commit history and includes them in the request to the AI. This helps the AI match the commit style already established in the project. No configuration is required to enable this; see [Configuration](#configuration) to control how many recent commits are included.
84
87
 
85
88
  ## Commit message format
86
89
 
@@ -113,11 +116,11 @@ The set of optional commit types the AI may use can be customised through a conf
113
116
 
114
117
  ### Config file locations and precedence
115
118
 
116
- The tool checks the following locations in order, using the first one that defines a `types` list:
119
+ The tool checks the following locations in order, using the first one that defines a given setting:
117
120
 
118
121
  1. **Project-level** — `pyproject.toml` walked up from the current working directory, under `[tool.git-commit-msg-ai]`
119
122
  2. **User-level** — `~/.git-commit-msg-ai.toml` in your home directory
120
- 3. **Built-in defaults** — used when neither config file is present or neither defines `types`
123
+ 3. **Built-in defaults** — used when neither config file is present or neither defines the setting
121
124
 
122
125
  ### Config file formats
123
126
 
@@ -126,16 +129,20 @@ Project-level (`pyproject.toml`):
126
129
  ```toml
127
130
  [tool.git-commit-msg-ai]
128
131
  types = ["build", "ci", "docs", "perf", "refactor", "style", "test"]
132
+ context_commits = 10 # include last 10 commits for context (default: 5)
129
133
  ```
130
134
 
131
135
  User-level (`~/.git-commit-msg-ai.toml`):
132
136
 
133
137
  ```toml
134
138
  types = ["build", "ci", "docs", "chore"]
139
+ context_commits = 0 # disable commit context
135
140
  ```
136
141
 
137
142
  Setting `types = []` restricts the AI to only the three mandatory types (`feat`, `fix`, `revert`).
138
143
 
144
+ `context_commits` controls how many recent commit messages are sent to the AI as context. The default is `5`. Set it to `0` to disable context entirely.
145
+
139
146
  ### Default optional types
140
147
 
141
148
  | Type | Purpose |
@@ -165,11 +172,11 @@ Pushing a version tag (e.g. `v1.5.2`) triggers the CD pipeline:
165
172
  # 1. Bump the version in pyproject.toml
166
173
  # 2. Commit and push
167
174
  git add pyproject.toml
168
- git commit -m "chore: bump version to 2.2.0"
175
+ git commit -m "chore: bump version to 2.3.0"
169
176
  git push origin main
170
177
  # 3. Tag and push - this triggers the CD pipeline
171
- git tag v2.2.0
172
- git push origin v2.2.0
178
+ git tag v2.3.0
179
+ git push origin v2.3.0
173
180
  ```
174
181
 
175
182
  ## Debugging
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
  import textwrap
3
- from typing import Final
3
+ from typing import Final, cast
4
4
 
5
5
  import anthropic
6
- from anthropic.types import CacheControlEphemeralParam, TextBlockParam
6
+ from anthropic.types import CacheControlEphemeralParam, MessageParam, TextBlockParam
7
7
 
8
8
  from git_commit_msg_ai.exceptions import AIError
9
9
 
@@ -27,16 +27,40 @@ def _build_system_prompt(types: tuple[str, ...]) -> list[TextBlockParam]:
27
27
  return [TextBlockParam(type="text", text=text, cache_control=CacheControlEphemeralParam(type="ephemeral"))]
28
28
 
29
29
 
30
- def generate_commit_message(diff: str, types: tuple[str, ...]) -> str:
30
+ def _build_user_message(diff: str, recent_commits: list[str] | None, branch_name: str | None) -> str:
31
+ parts: list[str] = []
32
+ if branch_name:
33
+ parts.append(f"Branch: {branch_name}")
34
+ if recent_commits:
35
+ commit_lines = ["Recent commits (most recent first):"] + [f"- {commit}" for commit in recent_commits]
36
+ parts.append("\n".join(commit_lines))
37
+ if parts:
38
+ parts.append("\n".join(["Staged diff:", diff]))
39
+ return "\n\n".join(parts)
40
+ return diff
41
+
42
+
43
+ def generate_commit_message(
44
+ diff: str,
45
+ types: tuple[str, ...],
46
+ *,
47
+ recent_commits: list[str] | None = None,
48
+ branch_name: str | None = None,
49
+ conversation_history: list[dict[str, str]] | None = None,
50
+ ) -> str:
31
51
  try:
32
52
  anthropic_client = anthropic.Anthropic()
33
53
 
54
+ user_message = _build_user_message(diff, recent_commits, branch_name)
55
+ messages: list[MessageParam] = [{"role": "user", "content": user_message}]
56
+ if conversation_history:
57
+ messages.extend(cast(list[MessageParam], conversation_history))
34
58
  logger.debug(f"Calling Anthropic API: model={MODEL} max_tokens={MAX_TOKENS}")
35
59
  anthropic_api_response = anthropic_client.messages.create(
36
60
  model=MODEL,
37
61
  max_tokens=MAX_TOKENS,
38
62
  system=_build_system_prompt(types),
39
- messages=[{"role": "user", "content": diff}],
63
+ messages=messages,
40
64
  )
41
65
  except anthropic.AuthenticationError:
42
66
  raise AIError("Anthropic API key is missing or invalid. Set the ANTHROPIC_API_KEY environment variable.")
@@ -0,0 +1,77 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from typing import Final
5
+
6
+ from git_commit_msg_ai import ai_client, config, editor, git_ops
7
+ from git_commit_msg_ai.exceptions import GitCommitAIError
8
+
9
+ LOG_LEVEL_ENVIRONMENT_VARIABLE: Final[str] = "GIT_COMMIT_AI_LOG_LEVEL"
10
+ DEFAULT_LOG_LEVEL: Final[str] = "WARNING"
11
+ LOG_FORMAT: Final[str] = "%(levelname)s:%(name)s:%(message)s"
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def main() -> None:
17
+ log_level_name = os.environ.get(LOG_LEVEL_ENVIRONMENT_VARIABLE, DEFAULT_LOG_LEVEL).upper()
18
+ numeric_log_level = getattr(logging, log_level_name, None)
19
+ valid_log_level = isinstance(numeric_log_level, int)
20
+ effective_log_level = numeric_log_level if valid_log_level else logging.WARNING
21
+
22
+ if not valid_log_level:
23
+ logging.warning(f"Invalid {LOG_LEVEL_ENVIRONMENT_VARIABLE} value {log_level_name!r}, defaulting to {DEFAULT_LOG_LEVEL}")
24
+
25
+ logging.basicConfig(level=effective_log_level, format=LOG_FORMAT, stream=sys.stderr)
26
+
27
+ app_config = config.load_config()
28
+
29
+ try:
30
+ diff = git_ops.get_staged_diff()
31
+ recent_commits = git_ops.get_recent_commit_messages(app_config.context_commits)
32
+ branch_name = git_ops.get_branch_name()
33
+ commit_message = ai_client.generate_commit_message(diff, app_config.types, recent_commits=recent_commits, branch_name=branch_name)
34
+ print(commit_message)
35
+
36
+ conversation_history: list[dict[str, str]] = [{"role": "assistant", "content": commit_message}]
37
+
38
+ while True:
39
+ print()
40
+ user_selection = input("[a]ccept / [e]dit / [r]eject / [f]eedback: ").strip().lower()
41
+
42
+ if user_selection == "a":
43
+ git_ops.commit(commit_message)
44
+ break
45
+ elif user_selection == "e":
46
+ updated_commit_message = editor.open_in_editor(commit_message)
47
+ git_ops.commit(updated_commit_message)
48
+ break
49
+ elif user_selection == "r":
50
+ print("User rejected the generated commit message. No commit made.")
51
+ break
52
+ elif user_selection == "f":
53
+ feedback = input("Feedback: ").strip()
54
+ conversation_history.append({"role": "user", "content": feedback})
55
+ commit_message = ai_client.generate_commit_message(
56
+ diff,
57
+ app_config.types,
58
+ recent_commits=recent_commits,
59
+ branch_name=branch_name,
60
+ conversation_history=list(conversation_history),
61
+ )
62
+ print(commit_message)
63
+ conversation_history.append({"role": "assistant", "content": commit_message})
64
+ else:
65
+ print("Invalid selection.")
66
+ sys.exit(1)
67
+ except GitCommitAIError as error:
68
+ logger.error(str(error))
69
+ print(f"Error: {error}", file=sys.stderr)
70
+ sys.exit(1)
71
+ except KeyboardInterrupt, EOFError:
72
+ print("Aborted.")
73
+ sys.exit(1)
74
+
75
+
76
+ if __name__ == "__main__": # pragma: no cover
77
+ main()
@@ -0,0 +1,142 @@
1
+ import logging
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Final, cast
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ MANDATORY_TYPES: Final[tuple[str, ...]] = ("feat", "fix", "revert")
10
+ DEFAULT_OPTIONAL_TYPES: Final[tuple[str, ...]] = ("build", "ci", "docs", "perf", "refactor", "style", "test")
11
+ CONTEXT_COMMITS_DEFAULT: Final[int] = 5
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class AppConfig:
16
+ types: tuple[str, ...]
17
+ context_commits: int
18
+
19
+
20
+ def _find_pyproject_toml() -> Path | None:
21
+ for directory in [Path.cwd(), *Path.cwd().parents]:
22
+ candidate = directory / "pyproject.toml"
23
+ if candidate.is_file():
24
+ return candidate
25
+
26
+ return None
27
+
28
+
29
+ def _parse_types_list(types_value: object, source: str) -> tuple[str, ...] | None:
30
+ if types_value is None:
31
+ return None
32
+ if not isinstance(types_value, list):
33
+ logger.warning(f"'types' in {source} must be a TOML array, skipping")
34
+ return None
35
+
36
+ entries = cast(list[object], types_value)
37
+ return tuple(entry.strip().lower() for entry in entries if isinstance(entry, str) and entry.strip())
38
+
39
+
40
+ def _parse_context_commits(value: object, source: str) -> int | None:
41
+ if value is None:
42
+ return None
43
+ if isinstance(value, bool) or not isinstance(value, int):
44
+ logger.warning(f"'context_commits' in {source} must be an integer, skipping")
45
+ return None
46
+ return max(0, value)
47
+
48
+
49
+ def _read_pkg_section_from_pyproject_toml(path: Path) -> dict[str, object] | None:
50
+ try:
51
+ with open(path, "rb") as toml_file:
52
+ data = tomllib.load(toml_file)
53
+ except tomllib.TOMLDecodeError:
54
+ logger.warning(f"Could not parse {path}, trying next config source")
55
+ return None
56
+
57
+ tool_section = data.get("tool")
58
+ if not isinstance(tool_section, dict):
59
+ return None
60
+
61
+ pkg_section = cast(dict[str, object], tool_section).get("git-commit-msg-ai")
62
+ return cast(dict[str, object], pkg_section) if isinstance(pkg_section, dict) else None
63
+
64
+
65
+ def _read_global_config_data() -> dict[str, Any] | None:
66
+ path = Path.home() / ".git-commit-msg-ai.toml"
67
+ if not path.is_file():
68
+ return None
69
+
70
+ try:
71
+ with open(path, "rb") as toml_file:
72
+ return tomllib.load(toml_file)
73
+ except tomllib.TOMLDecodeError:
74
+ logger.warning(f"Could not parse {path}, using defaults")
75
+ return None
76
+
77
+
78
+ def _load_types_from_pyproject_toml(path: Path) -> tuple[str, ...] | None:
79
+ section = _read_pkg_section_from_pyproject_toml(path)
80
+ if section is None:
81
+ return None
82
+ return _parse_types_list(section.get("types"), f"[tool.git-commit-msg-ai] in {path}")
83
+
84
+
85
+ def _load_types_from_global_config() -> tuple[str, ...] | None:
86
+ data = _read_global_config_data()
87
+ if data is None:
88
+ return None
89
+ return _parse_types_list(data.get("types"), str(Path.home() / ".git-commit-msg-ai.toml"))
90
+
91
+
92
+ def _load_context_commits_from_pyproject_toml(path: Path) -> int | None:
93
+ section = _read_pkg_section_from_pyproject_toml(path)
94
+ if section is None:
95
+ return None
96
+ return _parse_context_commits(section.get("context_commits"), f"[tool.git-commit-msg-ai] in {path}")
97
+
98
+
99
+ def _load_context_commits_from_global_config() -> int | None:
100
+ data = _read_global_config_data()
101
+ if data is None:
102
+ return None
103
+ return _parse_context_commits(data.get("context_commits"), str(Path.home() / ".git-commit-msg-ai.toml"))
104
+
105
+
106
+ def load_optional_types() -> tuple[str, ...]:
107
+ pyproject_path = _find_pyproject_toml()
108
+ if pyproject_path is not None:
109
+ result = _load_types_from_pyproject_toml(pyproject_path)
110
+ if result is not None:
111
+ return result
112
+
113
+ result = _load_types_from_global_config()
114
+ if result is not None:
115
+ return result
116
+
117
+ return DEFAULT_OPTIONAL_TYPES
118
+
119
+
120
+ def load_context_commits() -> int:
121
+ pyproject_path = _find_pyproject_toml()
122
+ if pyproject_path is not None:
123
+ result = _load_context_commits_from_pyproject_toml(pyproject_path)
124
+ if result is not None:
125
+ return result
126
+
127
+ result = _load_context_commits_from_global_config()
128
+ if result is not None:
129
+ return result
130
+
131
+ return CONTEXT_COMMITS_DEFAULT
132
+
133
+
134
+ def load_config() -> AppConfig:
135
+ return AppConfig(
136
+ types=get_all_types(load_optional_types()),
137
+ context_commits=load_context_commits(),
138
+ )
139
+
140
+
141
+ def get_all_types(optional_types: tuple[str, ...]) -> tuple[str, ...]:
142
+ return tuple(dict.fromkeys(MANDATORY_TYPES + optional_types))
@@ -0,0 +1,62 @@
1
+ import logging
2
+ import subprocess
3
+
4
+ from git_commit_msg_ai.exceptions import GitError
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def get_staged_diff() -> str:
10
+ try:
11
+ logger.debug("Running: git diff --cached")
12
+ raw_diff_bytes = subprocess.check_output(["git", "diff", "--cached"])
13
+ staged_diff = raw_diff_bytes.decode("utf-8")
14
+ except FileNotFoundError:
15
+ raise GitError("git is not installed or not on PATH.")
16
+ except subprocess.CalledProcessError:
17
+ raise GitError("Failed to get staged diff. Are you inside a git repository?")
18
+ except UnicodeDecodeError:
19
+ raise GitError("Staged diff contains bytes that could not be decoded as UTF-8.")
20
+
21
+ logger.debug(f"Staged diff received: {len(staged_diff)} chars")
22
+
23
+ if not staged_diff:
24
+ raise GitError("No staged changes found. Stage files with git add before running.")
25
+
26
+ return staged_diff
27
+
28
+
29
+ def get_recent_commit_messages(n: int) -> list[str]:
30
+ if n <= 0:
31
+ return []
32
+ try:
33
+ logger.debug(f"Running: git log --format=%s -n {n}")
34
+ raw_git_log_bytes = subprocess.check_output(["git", "log", "--format=%s", f"-n{n}"])
35
+ commit_message_lines = raw_git_log_bytes.decode("utf-8").splitlines()
36
+ return [commit_message for commit_message in commit_message_lines if commit_message.strip()]
37
+ except Exception:
38
+ logger.debug("Could not retrieve recent commit messages; proceeding without context")
39
+ return []
40
+
41
+
42
+ def get_branch_name() -> str | None:
43
+ try:
44
+ logger.debug("Running: git rev-parse --abbrev-ref HEAD")
45
+ raw_branch_name_bytes = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
46
+ branch_name = raw_branch_name_bytes.decode("utf-8").strip()
47
+ return branch_name if branch_name != "HEAD" else None
48
+ except Exception:
49
+ logger.debug("Could not retrieve branch name; proceeding without context")
50
+ return None
51
+
52
+
53
+ def commit(message: str) -> None:
54
+ try:
55
+ logger.debug("Running: git commit -m <message>")
56
+ subprocess.run(["git", "commit", "-m", message], check=True)
57
+ except FileNotFoundError:
58
+ raise GitError("git is not installed or not on PATH.")
59
+ except subprocess.CalledProcessError:
60
+ raise GitError("git commit failed.")
61
+
62
+ logger.info("Commit created successfully")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-msg-ai
3
- Version: 2.1.2
3
+ Version: 2.3.0
4
4
  Summary: AI-powered git commit message generator following Conventional Commits
5
5
  License: MIT License
6
6
 
@@ -118,12 +118,15 @@ The tool will:
118
118
  3. Print the message and prompt you to choose:
119
119
 
120
120
  ```
121
- [a]ccept / [e]dit / [r]eject:
121
+ [a]ccept / [e]dit / [r]eject / [f]eedback:
122
122
  ```
123
123
 
124
124
  - **a** - commits immediately with the generated message
125
125
  - **e** - opens the message in your `$EDITOR` (defaults to `notepad` on Windows, `vi` on Linux/macOS), lets you modify it, then commits
126
126
  - **r** - exits without committing
127
+ - **f** - prompts you for natural-language feedback, regenerates the message incorporating that feedback, and loops back so you can keep refining until satisfied
128
+
129
+ The tool automatically reads the current branch name and recent commit history and includes them in the request to the AI. This helps the AI match the commit style already established in the project. No configuration is required to enable this; see [Configuration](#configuration) to control how many recent commits are included.
127
130
 
128
131
  ## Commit message format
129
132
 
@@ -156,11 +159,11 @@ The set of optional commit types the AI may use can be customised through a conf
156
159
 
157
160
  ### Config file locations and precedence
158
161
 
159
- The tool checks the following locations in order, using the first one that defines a `types` list:
162
+ The tool checks the following locations in order, using the first one that defines a given setting:
160
163
 
161
164
  1. **Project-level** — `pyproject.toml` walked up from the current working directory, under `[tool.git-commit-msg-ai]`
162
165
  2. **User-level** — `~/.git-commit-msg-ai.toml` in your home directory
163
- 3. **Built-in defaults** — used when neither config file is present or neither defines `types`
166
+ 3. **Built-in defaults** — used when neither config file is present or neither defines the setting
164
167
 
165
168
  ### Config file formats
166
169
 
@@ -169,16 +172,20 @@ Project-level (`pyproject.toml`):
169
172
  ```toml
170
173
  [tool.git-commit-msg-ai]
171
174
  types = ["build", "ci", "docs", "perf", "refactor", "style", "test"]
175
+ context_commits = 10 # include last 10 commits for context (default: 5)
172
176
  ```
173
177
 
174
178
  User-level (`~/.git-commit-msg-ai.toml`):
175
179
 
176
180
  ```toml
177
181
  types = ["build", "ci", "docs", "chore"]
182
+ context_commits = 0 # disable commit context
178
183
  ```
179
184
 
180
185
  Setting `types = []` restricts the AI to only the three mandatory types (`feat`, `fix`, `revert`).
181
186
 
187
+ `context_commits` controls how many recent commit messages are sent to the AI as context. The default is `5`. Set it to `0` to disable context entirely.
188
+
182
189
  ### Default optional types
183
190
 
184
191
  | Type | Purpose |
@@ -208,11 +215,11 @@ Pushing a version tag (e.g. `v1.5.2`) triggers the CD pipeline:
208
215
  # 1. Bump the version in pyproject.toml
209
216
  # 2. Commit and push
210
217
  git add pyproject.toml
211
- git commit -m "chore: bump version to 2.2.0"
218
+ git commit -m "chore: bump version to 2.3.0"
212
219
  git push origin main
213
220
  # 3. Tag and push - this triggers the CD pipeline
214
- git tag v2.2.0
215
- git push origin v2.2.0
221
+ git tag v2.3.0
222
+ git push origin v2.3.0
216
223
  ```
217
224
 
218
225
  ## Debugging
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-commit-msg-ai"
7
- version = "2.1.2"
7
+ version = "2.3.0"
8
8
  description = "AI-powered git commit message generator following Conventional Commits"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}