git-commit-message 0.8.2__tar.gz → 0.9.1__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_message-0.8.2 → git_commit_message-0.9.1}/PKG-INFO +34 -5
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/README.md +31 -2
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/pyproject.toml +3 -3
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_cli.py +120 -8
- git_commit_message-0.9.1/src/git_commit_message/_config.py +71 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_git.py +80 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_gpt.py +29 -13
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_llamacpp.py +14 -18
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_llm.py +112 -66
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_ollama.py +4 -17
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/PKG-INFO +34 -5
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/SOURCES.txt +1 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/requires.txt +0 -1
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/UNLICENSE +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/setup.cfg +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/__init__.py +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/__main__.py +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message/_gemini.py +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/dependency_links.txt +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/entry_points.txt +0 -0
- {git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-message
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Generate Git commit messages from staged changes using LLM
|
|
5
5
|
Maintainer-email: Mina Her <minacle@live.com>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -31,7 +31,7 @@ License: This is free and unencumbered software released into the public domain.
|
|
|
31
31
|
Project-URL: Homepage, https://github.com/minacle/git-commit-message
|
|
32
32
|
Project-URL: Repository, https://github.com/minacle/git-commit-message
|
|
33
33
|
Project-URL: Issues, https://github.com/minacle/git-commit-message/issues
|
|
34
|
-
Classifier: Development Status ::
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
35
|
Classifier: Environment :: Console
|
|
36
36
|
Classifier: Intended Audience :: Developers
|
|
37
37
|
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
@@ -40,6 +40,7 @@ Classifier: Programming Language :: Python
|
|
|
40
40
|
Classifier: Programming Language :: Python :: 3
|
|
41
41
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
42
|
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
43
44
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
44
45
|
Requires-Python: >=3.13
|
|
45
46
|
Description-Content-Type: text/markdown
|
|
@@ -47,7 +48,6 @@ Requires-Dist: babel>=2.17.0
|
|
|
47
48
|
Requires-Dist: google-genai>=1.56.0
|
|
48
49
|
Requires-Dist: ollama>=0.4.0
|
|
49
50
|
Requires-Dist: openai>=2.6.1
|
|
50
|
-
Requires-Dist: tiktoken>=0.12.0
|
|
51
51
|
|
|
52
52
|
# git-commit-message
|
|
53
53
|
|
|
@@ -167,6 +167,18 @@ git-commit-message --one-line "optional context"
|
|
|
167
167
|
git-commit-message --one-line --co-author 'John Doe <john.doe@example.com>'
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
Use Conventional Commits constraints for the subject/footer only (body format is preserved):
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
git-commit-message --conventional
|
|
174
|
+
|
|
175
|
+
# can be combined with one-line mode
|
|
176
|
+
git-commit-message --conventional --one-line
|
|
177
|
+
|
|
178
|
+
# co-author trailers are appended after any existing footers
|
|
179
|
+
git-commit-message --conventional --co-author copilot
|
|
180
|
+
```
|
|
181
|
+
|
|
170
182
|
Select provider:
|
|
171
183
|
|
|
172
184
|
```sh
|
|
@@ -223,10 +235,24 @@ git-commit-message --chunk-tokens 0
|
|
|
223
235
|
# chunk the diff into ~4000-token pieces before summarising
|
|
224
236
|
git-commit-message --chunk-tokens 4000
|
|
225
237
|
|
|
238
|
+
# note: for provider 'ollama', values >= 1 are not supported
|
|
239
|
+
# use 0 (single summary pass) or -1 (legacy one-shot)
|
|
240
|
+
git-commit-message --provider ollama --chunk-tokens 0
|
|
241
|
+
|
|
226
242
|
# disable summarisation and use the legacy one-shot prompt
|
|
227
243
|
git-commit-message --chunk-tokens -1
|
|
228
244
|
```
|
|
229
245
|
|
|
246
|
+
Adjust unified diff context lines:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
# use 5 context lines around each change hunk
|
|
250
|
+
git-commit-message --diff-context 5
|
|
251
|
+
|
|
252
|
+
# include only changed lines (no surrounding context)
|
|
253
|
+
git-commit-message --diff-context 0
|
|
254
|
+
```
|
|
255
|
+
|
|
230
256
|
Select output language/locale (IETF language tag):
|
|
231
257
|
|
|
232
258
|
```sh
|
|
@@ -258,9 +284,11 @@ git-commit-message --provider llamacpp --host http://192.168.1.100:8080
|
|
|
258
284
|
- `--provider {openai,google,ollama,llamacpp}`: provider to use (default: `openai`)
|
|
259
285
|
- `--model MODEL`: model override (provider-specific; ignored for llama.cpp)
|
|
260
286
|
- `--language TAG`: output language/locale (default: `en-GB`)
|
|
287
|
+
- `--conventional`: apply Conventional Commits constraints to the subject and footer behavior. The body format is unchanged and still includes the translated `Rationale:` line. Breaking changes are expressed with `!` in the subject line, and `BREAKING CHANGE` footer lines are not generated.
|
|
261
288
|
- `--one-line`: output subject only when no trailers are appended; with `--co-author`, output is a single-line subject plus `Co-authored-by:` trailer lines
|
|
262
289
|
- `--max-length N`: max subject length (default: 72)
|
|
263
|
-
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
|
|
290
|
+
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation). For `ollama`, values `>= 1` are not supported.
|
|
291
|
+
- `--diff-context N`: context lines in unified diff (`N >= 0`). If omitted, uses `GIT_COMMIT_MESSAGE_DIFF_CONTEXT` when set; otherwise uses Git default (usually `3`).
|
|
264
292
|
- `--debug`: print request/response details
|
|
265
293
|
- `--commit`: run `git commit -m <message>`
|
|
266
294
|
- `--amend`: generate a message suitable for amending the previous commit (diff is from the amended commit's parent to the staged index; if nothing is staged, this effectively becomes the diff introduced by `HEAD`)
|
|
@@ -284,7 +312,8 @@ Optional:
|
|
|
284
312
|
- `OLLAMA_HOST`: Ollama server URL (default: `http://localhost:11434`)
|
|
285
313
|
- `LLAMACPP_HOST`: llama.cpp server URL (default: `http://localhost:8080`)
|
|
286
314
|
- `GIT_COMMIT_MESSAGE_LANGUAGE`: default language/locale (default: `en-GB`)
|
|
287
|
-
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`)
|
|
315
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`; for `ollama`, values `>= 1` are not supported)
|
|
316
|
+
- `GIT_COMMIT_MESSAGE_DIFF_CONTEXT`: default unified diff context lines (`0` or greater). If unset, Git default is used (usually `3`).
|
|
288
317
|
|
|
289
318
|
Default models (if not overridden):
|
|
290
319
|
|
|
@@ -116,6 +116,18 @@ git-commit-message --one-line "optional context"
|
|
|
116
116
|
git-commit-message --one-line --co-author 'John Doe <john.doe@example.com>'
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
Use Conventional Commits constraints for the subject/footer only (body format is preserved):
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
git-commit-message --conventional
|
|
123
|
+
|
|
124
|
+
# can be combined with one-line mode
|
|
125
|
+
git-commit-message --conventional --one-line
|
|
126
|
+
|
|
127
|
+
# co-author trailers are appended after any existing footers
|
|
128
|
+
git-commit-message --conventional --co-author copilot
|
|
129
|
+
```
|
|
130
|
+
|
|
119
131
|
Select provider:
|
|
120
132
|
|
|
121
133
|
```sh
|
|
@@ -172,10 +184,24 @@ git-commit-message --chunk-tokens 0
|
|
|
172
184
|
# chunk the diff into ~4000-token pieces before summarising
|
|
173
185
|
git-commit-message --chunk-tokens 4000
|
|
174
186
|
|
|
187
|
+
# note: for provider 'ollama', values >= 1 are not supported
|
|
188
|
+
# use 0 (single summary pass) or -1 (legacy one-shot)
|
|
189
|
+
git-commit-message --provider ollama --chunk-tokens 0
|
|
190
|
+
|
|
175
191
|
# disable summarisation and use the legacy one-shot prompt
|
|
176
192
|
git-commit-message --chunk-tokens -1
|
|
177
193
|
```
|
|
178
194
|
|
|
195
|
+
Adjust unified diff context lines:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
# use 5 context lines around each change hunk
|
|
199
|
+
git-commit-message --diff-context 5
|
|
200
|
+
|
|
201
|
+
# include only changed lines (no surrounding context)
|
|
202
|
+
git-commit-message --diff-context 0
|
|
203
|
+
```
|
|
204
|
+
|
|
179
205
|
Select output language/locale (IETF language tag):
|
|
180
206
|
|
|
181
207
|
```sh
|
|
@@ -207,9 +233,11 @@ git-commit-message --provider llamacpp --host http://192.168.1.100:8080
|
|
|
207
233
|
- `--provider {openai,google,ollama,llamacpp}`: provider to use (default: `openai`)
|
|
208
234
|
- `--model MODEL`: model override (provider-specific; ignored for llama.cpp)
|
|
209
235
|
- `--language TAG`: output language/locale (default: `en-GB`)
|
|
236
|
+
- `--conventional`: apply Conventional Commits constraints to the subject and footer behavior. The body format is unchanged and still includes the translated `Rationale:` line. Breaking changes are expressed with `!` in the subject line, and `BREAKING CHANGE` footer lines are not generated.
|
|
210
237
|
- `--one-line`: output subject only when no trailers are appended; with `--co-author`, output is a single-line subject plus `Co-authored-by:` trailer lines
|
|
211
238
|
- `--max-length N`: max subject length (default: 72)
|
|
212
|
-
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
|
|
239
|
+
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation). For `ollama`, values `>= 1` are not supported.
|
|
240
|
+
- `--diff-context N`: context lines in unified diff (`N >= 0`). If omitted, uses `GIT_COMMIT_MESSAGE_DIFF_CONTEXT` when set; otherwise uses Git default (usually `3`).
|
|
213
241
|
- `--debug`: print request/response details
|
|
214
242
|
- `--commit`: run `git commit -m <message>`
|
|
215
243
|
- `--amend`: generate a message suitable for amending the previous commit (diff is from the amended commit's parent to the staged index; if nothing is staged, this effectively becomes the diff introduced by `HEAD`)
|
|
@@ -233,7 +261,8 @@ Optional:
|
|
|
233
261
|
- `OLLAMA_HOST`: Ollama server URL (default: `http://localhost:11434`)
|
|
234
262
|
- `LLAMACPP_HOST`: llama.cpp server URL (default: `http://localhost:8080`)
|
|
235
263
|
- `GIT_COMMIT_MESSAGE_LANGUAGE`: default language/locale (default: `en-GB`)
|
|
236
|
-
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`)
|
|
264
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`; for `ollama`, values `>= 1` are not supported)
|
|
265
|
+
- `GIT_COMMIT_MESSAGE_DIFF_CONTEXT`: default unified diff context lines (`0` or greater). If unset, Git default is used (usually `3`).
|
|
237
266
|
|
|
238
267
|
Default models (if not overridden):
|
|
239
268
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "git-commit-message"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Generate Git commit messages from staged changes using LLM"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -9,12 +9,11 @@ dependencies = [
|
|
|
9
9
|
"google-genai>=1.56.0",
|
|
10
10
|
"ollama>=0.4.0",
|
|
11
11
|
"openai>=2.6.1",
|
|
12
|
-
"tiktoken>=0.12.0",
|
|
13
12
|
]
|
|
14
13
|
maintainers = [{ name = "Mina Her", email = "minacle@live.com" }]
|
|
15
14
|
license = { file = "UNLICENSE" }
|
|
16
15
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
18
17
|
"Environment :: Console",
|
|
19
18
|
"Intended Audience :: Developers",
|
|
20
19
|
"License :: OSI Approved :: The Unlicense (Unlicense)",
|
|
@@ -23,6 +22,7 @@ classifiers = [
|
|
|
23
22
|
"Programming Language :: Python :: 3",
|
|
24
23
|
"Programming Language :: Python :: 3 :: Only",
|
|
25
24
|
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
26
|
"Topic :: Software Development :: Version Control :: Git",
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -17,12 +17,15 @@ from typing import Final
|
|
|
17
17
|
|
|
18
18
|
from ._git import (
|
|
19
19
|
commit_with_message,
|
|
20
|
+
get_current_branch,
|
|
21
|
+
get_git_log,
|
|
20
22
|
get_repo_root,
|
|
21
23
|
get_staged_diff,
|
|
22
24
|
has_head_commit,
|
|
23
25
|
has_staged_changes,
|
|
24
26
|
resolve_amend_base_ref,
|
|
25
27
|
)
|
|
28
|
+
from ._config import resolve_provider_name, validate_provider_chunk_tokens
|
|
26
29
|
from ._llm import (
|
|
27
30
|
CommitMessageResult,
|
|
28
31
|
UnsupportedProviderError,
|
|
@@ -37,6 +40,7 @@ class CliArgs(Namespace):
|
|
|
37
40
|
"commit",
|
|
38
41
|
"amend",
|
|
39
42
|
"edit",
|
|
43
|
+
"conventional",
|
|
40
44
|
"provider",
|
|
41
45
|
"model",
|
|
42
46
|
"language",
|
|
@@ -44,6 +48,10 @@ class CliArgs(Namespace):
|
|
|
44
48
|
"one_line",
|
|
45
49
|
"max_length",
|
|
46
50
|
"chunk_tokens",
|
|
51
|
+
"diff_context",
|
|
52
|
+
"no_branch",
|
|
53
|
+
"no_log",
|
|
54
|
+
"log_count",
|
|
47
55
|
"host",
|
|
48
56
|
"co_authors",
|
|
49
57
|
)
|
|
@@ -56,6 +64,7 @@ class CliArgs(Namespace):
|
|
|
56
64
|
self.commit: bool = False
|
|
57
65
|
self.amend: bool = False
|
|
58
66
|
self.edit: bool = False
|
|
67
|
+
self.conventional: bool = False
|
|
59
68
|
self.provider: str | None = None
|
|
60
69
|
self.model: str | None = None
|
|
61
70
|
self.language: str | None = None
|
|
@@ -63,6 +72,10 @@ class CliArgs(Namespace):
|
|
|
63
72
|
self.one_line: bool = False
|
|
64
73
|
self.max_length: int | None = None
|
|
65
74
|
self.chunk_tokens: int | None = None
|
|
75
|
+
self.diff_context: int | None = None
|
|
76
|
+
self.no_branch: bool = False
|
|
77
|
+
self.no_log: bool = False
|
|
78
|
+
self.log_count: int = 10
|
|
66
79
|
self.host: str | None = None
|
|
67
80
|
self.co_authors: list[str] | None = None
|
|
68
81
|
|
|
@@ -155,6 +168,21 @@ def _env_chunk_tokens_default() -> int | None:
|
|
|
155
168
|
return None
|
|
156
169
|
|
|
157
170
|
|
|
171
|
+
def _env_diff_context_default() -> int | None:
|
|
172
|
+
"""Return diff context default from env.
|
|
173
|
+
|
|
174
|
+
Raises
|
|
175
|
+
------
|
|
176
|
+
ValueError
|
|
177
|
+
If the configured value is not an integer.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
raw: str | None = environ.get("GIT_COMMIT_MESSAGE_DIFF_CONTEXT")
|
|
181
|
+
if raw is None:
|
|
182
|
+
return None
|
|
183
|
+
return int(raw)
|
|
184
|
+
|
|
185
|
+
|
|
158
186
|
def _build_parser() -> ArgumentParser:
|
|
159
187
|
"""Create the CLI argument parser.
|
|
160
188
|
|
|
@@ -199,6 +227,15 @@ def _build_parser() -> ArgumentParser:
|
|
|
199
227
|
help="Open an editor to amend the message before committing. Use with '--commit'.",
|
|
200
228
|
)
|
|
201
229
|
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--conventional",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help=(
|
|
234
|
+
"Use Conventional Commits constraints for the subject line and footer. "
|
|
235
|
+
"The existing body format remains unchanged, including the translated Rationale line."
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
|
|
202
239
|
parser.add_argument(
|
|
203
240
|
"--provider",
|
|
204
241
|
default=None,
|
|
@@ -258,10 +295,48 @@ def _build_parser() -> ArgumentParser:
|
|
|
258
295
|
help=(
|
|
259
296
|
"Target token budget per diff chunk. "
|
|
260
297
|
"0 forces a single chunk with summarisation; -1 disables summarisation (legacy one-shot). "
|
|
298
|
+
"For provider 'ollama', values >= 1 are not supported. "
|
|
261
299
|
"If omitted, uses GIT_COMMIT_MESSAGE_CHUNK_TOKENS when set (default: 0)."
|
|
262
300
|
),
|
|
263
301
|
)
|
|
264
302
|
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--diff-context",
|
|
305
|
+
dest="diff_context",
|
|
306
|
+
type=int,
|
|
307
|
+
default=None,
|
|
308
|
+
help=(
|
|
309
|
+
"Number of context lines in unified diff output. "
|
|
310
|
+
"If omitted, uses GIT_COMMIT_MESSAGE_DIFF_CONTEXT when set "
|
|
311
|
+
"(default: Git default, usually 3)."
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
parser.add_argument(
|
|
316
|
+
"--no-branch",
|
|
317
|
+
dest="no_branch",
|
|
318
|
+
action="store_true",
|
|
319
|
+
help="Do not include the current branch name in the LLM context.",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--no-log",
|
|
324
|
+
dest="no_log",
|
|
325
|
+
action="store_true",
|
|
326
|
+
help="Do not include recent Git log entries in the LLM context.",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
parser.add_argument(
|
|
330
|
+
"--log-count",
|
|
331
|
+
dest="log_count",
|
|
332
|
+
type=int,
|
|
333
|
+
default=10,
|
|
334
|
+
help=(
|
|
335
|
+
"Number of recent Git log entries to include in the LLM context "
|
|
336
|
+
"(default: 10). Ignored when --no-log is set."
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
|
|
265
340
|
parser.add_argument(
|
|
266
341
|
"--host",
|
|
267
342
|
dest="host",
|
|
@@ -308,6 +383,36 @@ def _run(
|
|
|
308
383
|
Process exit code. 0 indicates success; any other value indicates failure.
|
|
309
384
|
"""
|
|
310
385
|
|
|
386
|
+
chunk_tokens: int | None = args.chunk_tokens
|
|
387
|
+
if chunk_tokens is None:
|
|
388
|
+
chunk_tokens = _env_chunk_tokens_default()
|
|
389
|
+
if chunk_tokens is None:
|
|
390
|
+
chunk_tokens = 0
|
|
391
|
+
|
|
392
|
+
diff_context: int | None = args.diff_context
|
|
393
|
+
if diff_context is None:
|
|
394
|
+
try:
|
|
395
|
+
diff_context = _env_diff_context_default()
|
|
396
|
+
except ValueError:
|
|
397
|
+
print(
|
|
398
|
+
"GIT_COMMIT_MESSAGE_DIFF_CONTEXT must be an integer.",
|
|
399
|
+
file=stderr,
|
|
400
|
+
)
|
|
401
|
+
return 2
|
|
402
|
+
if diff_context is not None and diff_context < 0:
|
|
403
|
+
print("--diff-context must be greater than or equal to 0.", file=stderr)
|
|
404
|
+
return 2
|
|
405
|
+
|
|
406
|
+
if not args.no_log and args.log_count < 1:
|
|
407
|
+
print("--log-count must be greater than or equal to 1.", file=stderr)
|
|
408
|
+
return 2
|
|
409
|
+
|
|
410
|
+
provider_name: str = resolve_provider_name(args.provider)
|
|
411
|
+
provider_arg_error = validate_provider_chunk_tokens(provider_name, chunk_tokens)
|
|
412
|
+
if provider_arg_error is not None:
|
|
413
|
+
print(provider_arg_error, file=stderr)
|
|
414
|
+
return 2
|
|
415
|
+
|
|
311
416
|
repo_root: Path = get_repo_root()
|
|
312
417
|
|
|
313
418
|
if args.amend:
|
|
@@ -316,21 +421,22 @@ def _run(
|
|
|
316
421
|
return 2
|
|
317
422
|
|
|
318
423
|
base_ref = resolve_amend_base_ref(repo_root)
|
|
319
|
-
diff_text: str = get_staged_diff(
|
|
424
|
+
diff_text: str = get_staged_diff(
|
|
425
|
+
repo_root,
|
|
426
|
+
base_ref=base_ref,
|
|
427
|
+
context_lines=diff_context,
|
|
428
|
+
)
|
|
320
429
|
else:
|
|
321
430
|
if not has_staged_changes(repo_root):
|
|
322
431
|
print("No staged changes. Run 'git add' and try again.", file=stderr)
|
|
323
432
|
return 2
|
|
324
433
|
|
|
325
|
-
diff_text = get_staged_diff(repo_root)
|
|
434
|
+
diff_text = get_staged_diff(repo_root, context_lines=diff_context)
|
|
326
435
|
|
|
327
|
-
|
|
436
|
+
branch: str | None = None if args.no_branch else get_current_branch(repo_root)
|
|
437
|
+
log: str | None = None if args.no_log else get_git_log(repo_root, count=args.log_count)
|
|
328
438
|
|
|
329
|
-
|
|
330
|
-
if chunk_tokens is None:
|
|
331
|
-
chunk_tokens = _env_chunk_tokens_default()
|
|
332
|
-
if chunk_tokens is None:
|
|
333
|
-
chunk_tokens = 0
|
|
439
|
+
hint: str | None = args.description if isinstance(args.description, str) else None
|
|
334
440
|
|
|
335
441
|
normalized_co_authors: list[str] | None = None
|
|
336
442
|
if args.co_authors:
|
|
@@ -353,6 +459,9 @@ def _run(
|
|
|
353
459
|
chunk_tokens,
|
|
354
460
|
args.provider,
|
|
355
461
|
args.host,
|
|
462
|
+
args.conventional,
|
|
463
|
+
branch=branch,
|
|
464
|
+
log=log,
|
|
356
465
|
)
|
|
357
466
|
message = result.message
|
|
358
467
|
else:
|
|
@@ -366,6 +475,9 @@ def _run(
|
|
|
366
475
|
chunk_tokens,
|
|
367
476
|
args.provider,
|
|
368
477
|
args.host,
|
|
478
|
+
args.conventional,
|
|
479
|
+
branch=branch,
|
|
480
|
+
log=log,
|
|
369
481
|
)
|
|
370
482
|
except UnsupportedProviderError as exc:
|
|
371
483
|
print(str(exc), file=stderr)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Shared configuration resolvers for provider/model/language selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from os import environ
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_PROVIDER: Final[str] = "openai"
|
|
10
|
+
DEFAULT_MODEL_OPENAI: Final[str] = "gpt-5-mini"
|
|
11
|
+
DEFAULT_MODEL_GOOGLE: Final[str] = "gemini-2.5-flash"
|
|
12
|
+
DEFAULT_MODEL_OLLAMA: Final[str] = "gpt-oss:20b"
|
|
13
|
+
DEFAULT_MODEL_LLAMACPP: Final[str] = "default"
|
|
14
|
+
DEFAULT_LANGUAGE: Final[str] = "en-GB"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_provider_name(
|
|
18
|
+
provider: str | None,
|
|
19
|
+
/,
|
|
20
|
+
) -> str:
|
|
21
|
+
chosen = provider or environ.get("GIT_COMMIT_MESSAGE_PROVIDER") or DEFAULT_PROVIDER
|
|
22
|
+
return chosen.strip().lower()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_model_name(
|
|
26
|
+
model: str | None,
|
|
27
|
+
provider_name: str,
|
|
28
|
+
/,
|
|
29
|
+
) -> str:
|
|
30
|
+
if provider_name == "google":
|
|
31
|
+
default_model = DEFAULT_MODEL_GOOGLE
|
|
32
|
+
provider_model = None
|
|
33
|
+
elif provider_name == "ollama":
|
|
34
|
+
default_model = DEFAULT_MODEL_OLLAMA
|
|
35
|
+
provider_model = environ.get("OLLAMA_MODEL")
|
|
36
|
+
elif provider_name == "llamacpp":
|
|
37
|
+
default_model = DEFAULT_MODEL_LLAMACPP
|
|
38
|
+
provider_model = environ.get("LLAMACPP_MODEL")
|
|
39
|
+
else:
|
|
40
|
+
default_model = DEFAULT_MODEL_OPENAI
|
|
41
|
+
provider_model = environ.get("OPENAI_MODEL")
|
|
42
|
+
|
|
43
|
+
return model or environ.get("GIT_COMMIT_MESSAGE_MODEL") or provider_model or default_model
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_language_tag(
|
|
47
|
+
language: str | None,
|
|
48
|
+
/,
|
|
49
|
+
) -> str:
|
|
50
|
+
return language or environ.get("GIT_COMMIT_MESSAGE_LANGUAGE") or DEFAULT_LANGUAGE
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_provider_chunk_tokens(
|
|
54
|
+
provider_name: str,
|
|
55
|
+
chunk_tokens: int,
|
|
56
|
+
/,
|
|
57
|
+
) -> str | None:
|
|
58
|
+
if chunk_tokens < -1:
|
|
59
|
+
return (
|
|
60
|
+
"'--chunk-tokens' must be -1 or greater. "
|
|
61
|
+
"Use -1 to disable summarisation, or 0/positive values to enable summarisation."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if provider_name == "ollama" and chunk_tokens > 0:
|
|
65
|
+
return (
|
|
66
|
+
"'--chunk-tokens' with values >= 1 is not supported for provider 'ollama'. "
|
|
67
|
+
"Use '--chunk-tokens 0' (single summary pass) or '--chunk-tokens -1' "
|
|
68
|
+
"(disable summarisation)."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return None
|
|
@@ -183,6 +183,7 @@ def get_staged_diff(
|
|
|
183
183
|
/,
|
|
184
184
|
*,
|
|
185
185
|
base_ref: str | None = None,
|
|
186
|
+
context_lines: int | None = None,
|
|
186
187
|
) -> str:
|
|
187
188
|
"""Return the staged changes as diff text.
|
|
188
189
|
|
|
@@ -195,6 +196,9 @@ def get_staged_diff(
|
|
|
195
196
|
commit hash, or the empty tree hash) to diff against. When provided,
|
|
196
197
|
the diff shows changes from ``base_ref`` to the staged index, instead
|
|
197
198
|
of changes from ``HEAD`` to the staged index.
|
|
199
|
+
context_lines
|
|
200
|
+
Optional number of context lines for unified diff output. When ``None``,
|
|
201
|
+
Git's default context lines are used.
|
|
198
202
|
|
|
199
203
|
Returns
|
|
200
204
|
-------
|
|
@@ -210,6 +214,8 @@ def get_staged_diff(
|
|
|
210
214
|
"--minimal",
|
|
211
215
|
"--no-color",
|
|
212
216
|
]
|
|
217
|
+
if context_lines is not None:
|
|
218
|
+
cmd.append(f"-U{context_lines}")
|
|
213
219
|
if base_ref:
|
|
214
220
|
cmd.append(base_ref)
|
|
215
221
|
|
|
@@ -226,6 +232,80 @@ def get_staged_diff(
|
|
|
226
232
|
return out.decode()
|
|
227
233
|
|
|
228
234
|
|
|
235
|
+
def get_current_branch(
|
|
236
|
+
cwd: Path,
|
|
237
|
+
/,
|
|
238
|
+
) -> str | None:
|
|
239
|
+
"""Return the current branch name, or ``None`` if HEAD is detached.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
cwd
|
|
244
|
+
Repository directory in which to run Git.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
str | None
|
|
249
|
+
Branch name, or ``None`` when HEAD is detached or the command fails.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
completed = run(
|
|
253
|
+
["git", "branch", "--show-current"],
|
|
254
|
+
cwd=str(cwd),
|
|
255
|
+
check=False,
|
|
256
|
+
capture_output=True,
|
|
257
|
+
)
|
|
258
|
+
if completed.returncode != 0:
|
|
259
|
+
return None
|
|
260
|
+
name = completed.stdout.decode().strip()
|
|
261
|
+
return name or None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_git_log(
|
|
265
|
+
cwd: Path,
|
|
266
|
+
/,
|
|
267
|
+
*,
|
|
268
|
+
count: int = 10,
|
|
269
|
+
) -> str | None:
|
|
270
|
+
"""Return recent Git log entries as formatted text.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
cwd
|
|
275
|
+
Repository directory in which to run Git.
|
|
276
|
+
count
|
|
277
|
+
Maximum number of commits to include.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
str | None
|
|
282
|
+
Formatted log text, or ``None`` if the repository has no commits
|
|
283
|
+
or if ``git log`` fails.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
if count < 1:
|
|
287
|
+
raise ValueError(f"count must be >= 1, got {count}")
|
|
288
|
+
|
|
289
|
+
if not has_head_commit(cwd):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
out: bytes = check_output(
|
|
294
|
+
[
|
|
295
|
+
"git",
|
|
296
|
+
"log",
|
|
297
|
+
f"-{count}",
|
|
298
|
+
"--format=%h %s%n%n%b%n---%n",
|
|
299
|
+
],
|
|
300
|
+
cwd=str(cwd),
|
|
301
|
+
)
|
|
302
|
+
except CalledProcessError:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
text = out.decode().strip()
|
|
306
|
+
return text or None
|
|
307
|
+
|
|
308
|
+
|
|
229
309
|
def commit_with_message(
|
|
230
310
|
message: str,
|
|
231
311
|
edit: bool,
|
|
@@ -11,20 +11,9 @@ from openai.types.responses import Response
|
|
|
11
11
|
from os import environ
|
|
12
12
|
from typing import ClassVar
|
|
13
13
|
|
|
14
|
-
from tiktoken import Encoding, encoding_for_model, get_encoding
|
|
15
14
|
from ._llm import LLMTextResult, LLMUsage
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
def _encoding_for_model(
|
|
19
|
-
model: str,
|
|
20
|
-
/,
|
|
21
|
-
) -> Encoding:
|
|
22
|
-
try:
|
|
23
|
-
return encoding_for_model(model)
|
|
24
|
-
except Exception:
|
|
25
|
-
return get_encoding("cl100k_base")
|
|
26
|
-
|
|
27
|
-
|
|
28
17
|
class OpenAIResponsesProvider:
|
|
29
18
|
__slots__ = (
|
|
30
19
|
"_client",
|
|
@@ -50,8 +39,35 @@ class OpenAIResponsesProvider:
|
|
|
50
39
|
model: str,
|
|
51
40
|
text: str,
|
|
52
41
|
) -> int:
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
try:
|
|
43
|
+
resp = self._client.responses.input_tokens.count(
|
|
44
|
+
model=model,
|
|
45
|
+
input=[
|
|
46
|
+
{
|
|
47
|
+
"role": "user",
|
|
48
|
+
"content": [
|
|
49
|
+
{
|
|
50
|
+
"type": "input_text",
|
|
51
|
+
"text": text,
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"Token counting failed for the OpenAI provider. "
|
|
60
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
61
|
+
) from exc
|
|
62
|
+
|
|
63
|
+
prompt_tokens = getattr(resp, "input_tokens", None)
|
|
64
|
+
if not isinstance(prompt_tokens, int):
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Token counting returned an unexpected response from the OpenAI provider. "
|
|
67
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return prompt_tokens
|
|
55
71
|
|
|
56
72
|
def generate_text(
|
|
57
73
|
self,
|
|
@@ -12,7 +12,6 @@ from typing import ClassVar, Final
|
|
|
12
12
|
|
|
13
13
|
from openai import OpenAI
|
|
14
14
|
from openai.types.chat import ChatCompletionMessageParam
|
|
15
|
-
from tiktoken import Encoding, get_encoding
|
|
16
15
|
|
|
17
16
|
from ._llm import LLMTextResult, LLMUsage
|
|
18
17
|
|
|
@@ -29,15 +28,6 @@ def _resolve_llamacpp_host(
|
|
|
29
28
|
return host or environ.get("LLAMACPP_HOST") or _DEFAULT_LLAMACPP_HOST
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
def _get_encoding() -> Encoding:
|
|
33
|
-
"""Get a fallback encoding for token counting."""
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
return get_encoding("cl100k_base")
|
|
37
|
-
except Exception:
|
|
38
|
-
return get_encoding("gpt2")
|
|
39
|
-
|
|
40
|
-
|
|
41
31
|
class LlamaCppProvider:
|
|
42
32
|
"""llama.cpp provider implementation for the LLM protocol.
|
|
43
33
|
|
|
@@ -135,11 +125,17 @@ class LlamaCppProvider:
|
|
|
135
125
|
},
|
|
136
126
|
cast_to=dict,
|
|
137
127
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"Token counting failed for the llama.cpp provider. "
|
|
131
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
132
|
+
) from exc
|
|
133
|
+
|
|
134
|
+
total = response.get("total") if isinstance(response, dict) else None
|
|
135
|
+
if not isinstance(total, int):
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"Token counting returned an unexpected response from the llama.cpp provider. "
|
|
138
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return total
|
|
@@ -11,16 +11,14 @@ Provider-specific API calls live in provider modules (e.g. `_gpt.py`).
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
from babel import Locale
|
|
14
|
-
from
|
|
15
|
-
from typing import ClassVar, Final, Protocol
|
|
14
|
+
from typing import ClassVar, Protocol
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
_DEFAULT_LANGUAGE: Final[str] = "en-GB"
|
|
16
|
+
from ._config import (
|
|
17
|
+
resolve_language_tag,
|
|
18
|
+
resolve_model_name,
|
|
19
|
+
resolve_provider_name,
|
|
20
|
+
validate_provider_chunk_tokens,
|
|
21
|
+
)
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
class UnsupportedProviderError(RuntimeError):
|
|
@@ -137,49 +135,13 @@ class CommitMessageResult:
|
|
|
137
135
|
self.total_tokens = total_tokens
|
|
138
136
|
|
|
139
137
|
|
|
140
|
-
def _resolve_provider(
|
|
141
|
-
provider: str | None,
|
|
142
|
-
/,
|
|
143
|
-
) -> str:
|
|
144
|
-
chosen = provider or environ.get("GIT_COMMIT_MESSAGE_PROVIDER") or _DEFAULT_PROVIDER
|
|
145
|
-
return chosen.strip().lower()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _resolve_model(
|
|
149
|
-
model: str | None,
|
|
150
|
-
provider_name: str,
|
|
151
|
-
/,
|
|
152
|
-
) -> str:
|
|
153
|
-
if provider_name == "google":
|
|
154
|
-
default_model = _DEFAULT_MODEL_GOOGLE
|
|
155
|
-
provider_model = None
|
|
156
|
-
elif provider_name == "ollama":
|
|
157
|
-
default_model = _DEFAULT_MODEL_OLLAMA
|
|
158
|
-
provider_model = environ.get("OLLAMA_MODEL")
|
|
159
|
-
elif provider_name == "llamacpp":
|
|
160
|
-
default_model = _DEFAULT_MODEL_LLAMACPP
|
|
161
|
-
provider_model = environ.get("LLAMACPP_MODEL")
|
|
162
|
-
else:
|
|
163
|
-
default_model = _DEFAULT_MODEL_OPENAI
|
|
164
|
-
provider_model = environ.get("OPENAI_MODEL")
|
|
165
|
-
|
|
166
|
-
return model or environ.get("GIT_COMMIT_MESSAGE_MODEL") or provider_model or default_model
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _resolve_language(
|
|
170
|
-
language: str | None,
|
|
171
|
-
/,
|
|
172
|
-
) -> str:
|
|
173
|
-
return language or environ.get("GIT_COMMIT_MESSAGE_LANGUAGE") or _DEFAULT_LANGUAGE
|
|
174
|
-
|
|
175
|
-
|
|
176
138
|
def get_provider(
|
|
177
139
|
provider: str | None,
|
|
178
140
|
/,
|
|
179
141
|
*,
|
|
180
142
|
host: str | None = None,
|
|
181
143
|
) -> CommitMessageProvider:
|
|
182
|
-
name =
|
|
144
|
+
name = resolve_provider_name(provider)
|
|
183
145
|
|
|
184
146
|
if name == "openai":
|
|
185
147
|
# Local import to avoid import cycles: providers may import shared types from this module.
|
|
@@ -242,19 +204,54 @@ def _build_system_prompt(
|
|
|
242
204
|
single_line: bool,
|
|
243
205
|
subject_max: int | None,
|
|
244
206
|
language: str,
|
|
207
|
+
conventional: bool = False,
|
|
245
208
|
/,
|
|
246
209
|
) -> str:
|
|
247
210
|
display_language: str = _language_display(language)
|
|
248
211
|
max_len = subject_max or 72
|
|
249
212
|
if single_line:
|
|
213
|
+
conventional_rule: str
|
|
214
|
+
if conventional:
|
|
215
|
+
conventional_rule = (
|
|
216
|
+
"Use one of these Conventional Commits subject forms: '<type>: <description>', '<type>(<scope>): <description>', '<type>!: <description>', or '<type>(<scope>)!: <description>'. "
|
|
217
|
+
"When a scope is present, it MUST be parenthesized and directly attached to the type with no spaces. "
|
|
218
|
+
"Represent breaking changes with '!' before ':' in the subject; do not output a BREAKING CHANGE footer. "
|
|
219
|
+
"Do NOT translate the Conventional prefix token ('<type>', optional '(<scope>)', optional '!'); translate only the description into the target language. "
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
conventional_rule = (
|
|
223
|
+
"Do NOT use Conventional Commits title format. "
|
|
224
|
+
"Do not start with '<type>:' or '<type>(<scope>):' prefixes such as 'feat:', 'fix:', 'docs:', 'chore:', 'refactor:', 'test:', 'perf:', 'ci:', or 'build:'. "
|
|
225
|
+
)
|
|
250
226
|
return (
|
|
251
227
|
f"You are an expert Git commit message generator. "
|
|
252
228
|
f"Always use '{display_language}' spelling and style. "
|
|
229
|
+
f"{conventional_rule}"
|
|
253
230
|
f"Return a single-line imperative subject only (<= {max_len} chars). "
|
|
254
231
|
f"Do not include a body, bullet points, or any rationale. Do not include any line breaks. "
|
|
255
232
|
f"Consider the user-provided auxiliary context if present. "
|
|
256
233
|
f"Return only the commit message text (no code fences or prefixes like 'Commit message:')."
|
|
257
234
|
)
|
|
235
|
+
|
|
236
|
+
format_guidelines: str = ""
|
|
237
|
+
if conventional:
|
|
238
|
+
format_guidelines = (
|
|
239
|
+
"\n"
|
|
240
|
+
"- The subject line MUST use one of these forms: '<type>: <description>', '<type>(<scope>): <description>', '<type>!: <description>', or '<type>(<scope>)!: <description>'.\n"
|
|
241
|
+
"- If scope is used, it MUST be in parentheses and directly attached to type with no spaces, e.g. 'feat(parser):'.\n"
|
|
242
|
+
"- In Conventional mode, only the subject line and footer conventions are additionally constrained; keep the body structure unchanged.\n"
|
|
243
|
+
"- Keep the translated equivalent of 'Rationale:' as the final body line label; this section MUST be present.\n"
|
|
244
|
+
"- For breaking changes, use '!' immediately before ':' in the subject line.\n"
|
|
245
|
+
"- Do NOT generate any BREAKING CHANGE footer line.\n"
|
|
246
|
+
"- Do NOT translate the Conventional prefix token ('<type>', optional '(<scope>)', optional '!'). Translate only the description, bullet points, and rationale into the target language.\n"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
format_guidelines = (
|
|
250
|
+
"\n"
|
|
251
|
+
"- Do NOT use Conventional Commits subject prefixes.\n"
|
|
252
|
+
"- The subject MUST NOT start with '<type>:' or '<type>(<scope>):' patterns (for example: 'feat:', 'fix:', 'docs:', 'chore:', 'refactor:', 'test:', 'perf:', 'ci:', or 'build:').\n"
|
|
253
|
+
)
|
|
254
|
+
|
|
258
255
|
return (
|
|
259
256
|
f"You are an expert Git commit message generator. "
|
|
260
257
|
f"Always use '{display_language}' spelling and style. "
|
|
@@ -282,6 +279,7 @@ def _build_system_prompt(
|
|
|
282
279
|
f"- If few details are necessary, include at least one bullet summarising the key change.\n"
|
|
283
280
|
f"- If you cannot provide any body content, still output the subject line; the subject line must never be omitted.\n"
|
|
284
281
|
f"- Consider the user-provided auxiliary context if present.\n"
|
|
282
|
+
f"{format_guidelines}"
|
|
285
283
|
f"Return only the commit message text in the above format (no code fences or extra labels)."
|
|
286
284
|
)
|
|
287
285
|
|
|
@@ -300,12 +298,19 @@ def _build_combined_prompt(
|
|
|
300
298
|
hint: str | None,
|
|
301
299
|
content_label: str = "Changes (diff)",
|
|
302
300
|
/,
|
|
301
|
+
*,
|
|
302
|
+
branch: str | None = None,
|
|
303
|
+
log: str | None = None,
|
|
303
304
|
) -> str:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
parts: list[str] = []
|
|
306
|
+
if hint:
|
|
307
|
+
parts.append(f"# Auxiliary context (user-provided)\n{hint}")
|
|
308
|
+
if branch:
|
|
309
|
+
parts.append(f"# Current branch\n{branch}")
|
|
310
|
+
if log:
|
|
311
|
+
parts.append(f"# Recent commits\n{log}")
|
|
312
|
+
parts.append(f"# {content_label}\n{diff}")
|
|
313
|
+
return "\n\n".join(parts)
|
|
309
314
|
|
|
310
315
|
|
|
311
316
|
def _split_diff_into_hunks(
|
|
@@ -371,14 +376,17 @@ def _build_diff_chunks(
|
|
|
371
376
|
|
|
372
377
|
if current:
|
|
373
378
|
chunks.append("".join(current))
|
|
374
|
-
current = [hunk]
|
|
375
|
-
else:
|
|
376
379
|
single_tokens = provider.count_tokens(model=model, text=hunk)
|
|
377
380
|
if single_tokens > chunk_tokens:
|
|
378
381
|
raise ValueError(
|
|
379
382
|
"chunk_tokens is too small to fit a single diff hunk; increase the value or disable chunking"
|
|
380
383
|
)
|
|
381
384
|
current = [hunk]
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
raise ValueError(
|
|
388
|
+
"chunk_tokens is too small to fit a single diff hunk; increase the value or disable chunking"
|
|
389
|
+
)
|
|
382
390
|
|
|
383
391
|
if current:
|
|
384
392
|
chunks.append("".join(current))
|
|
@@ -420,14 +428,24 @@ def _generate_commit_from_summaries(
|
|
|
420
428
|
single_line: bool,
|
|
421
429
|
subject_max: int | None,
|
|
422
430
|
language: str,
|
|
431
|
+
conventional: bool = False,
|
|
423
432
|
/,
|
|
433
|
+
*,
|
|
434
|
+
branch: str | None = None,
|
|
435
|
+
log: str | None = None,
|
|
424
436
|
) -> LLMTextResult:
|
|
425
|
-
instructions = _build_system_prompt(single_line, subject_max, language)
|
|
437
|
+
instructions = _build_system_prompt(single_line, subject_max, language, conventional)
|
|
426
438
|
sections: list[str] = []
|
|
427
439
|
|
|
428
440
|
if hint:
|
|
429
441
|
sections.append(f"# Auxiliary context (user-provided)\n{hint}")
|
|
430
442
|
|
|
443
|
+
if branch:
|
|
444
|
+
sections.append(f"# Current branch\n{branch}")
|
|
445
|
+
|
|
446
|
+
if log:
|
|
447
|
+
sections.append(f"# Recent commits\n{log}")
|
|
448
|
+
|
|
431
449
|
if summaries:
|
|
432
450
|
numbered = [
|
|
433
451
|
f"Summary {idx + 1}:\n{summary}" for idx, summary in enumerate(summaries)
|
|
@@ -486,19 +504,29 @@ def generate_commit_message(
|
|
|
486
504
|
chunk_tokens: int | None = 0,
|
|
487
505
|
provider: str | None = None,
|
|
488
506
|
host: str | None = None,
|
|
507
|
+
conventional: bool = False,
|
|
489
508
|
/,
|
|
509
|
+
*,
|
|
510
|
+
branch: str | None = None,
|
|
511
|
+
log: str | None = None,
|
|
490
512
|
) -> str:
|
|
491
|
-
chosen_provider =
|
|
492
|
-
chosen_model =
|
|
493
|
-
chosen_language =
|
|
513
|
+
chosen_provider = resolve_provider_name(provider)
|
|
514
|
+
chosen_model = resolve_model_name(model, chosen_provider)
|
|
515
|
+
chosen_language = resolve_language_tag(language)
|
|
494
516
|
|
|
495
517
|
llm = get_provider(chosen_provider, host=host)
|
|
496
518
|
|
|
497
519
|
normalized_chunk_tokens = 0 if chunk_tokens is None else chunk_tokens
|
|
520
|
+
provider_arg_error = validate_provider_chunk_tokens(
|
|
521
|
+
chosen_provider,
|
|
522
|
+
normalized_chunk_tokens,
|
|
523
|
+
)
|
|
524
|
+
if provider_arg_error is not None:
|
|
525
|
+
raise ValueError(provider_arg_error)
|
|
498
526
|
|
|
499
527
|
if normalized_chunk_tokens != -1:
|
|
500
528
|
hunks = _split_diff_into_hunks(diff)
|
|
501
|
-
if normalized_chunk_tokens == 0
|
|
529
|
+
if normalized_chunk_tokens == 0:
|
|
502
530
|
chunks = ["".join(hunks) if hunks else diff]
|
|
503
531
|
else:
|
|
504
532
|
chunks = _build_diff_chunks(hunks, normalized_chunk_tokens, llm, chosen_model)
|
|
@@ -513,11 +541,14 @@ def generate_commit_message(
|
|
|
513
541
|
single_line,
|
|
514
542
|
subject_max,
|
|
515
543
|
chosen_language,
|
|
544
|
+
conventional,
|
|
545
|
+
branch=branch,
|
|
546
|
+
log=log,
|
|
516
547
|
)
|
|
517
548
|
text = (final.text or "").strip()
|
|
518
549
|
else:
|
|
519
|
-
instructions = _build_system_prompt(single_line, subject_max, chosen_language)
|
|
520
|
-
user_text = _build_combined_prompt(diff, hint)
|
|
550
|
+
instructions = _build_system_prompt(single_line, subject_max, chosen_language, conventional)
|
|
551
|
+
user_text = _build_combined_prompt(diff, hint, branch=branch, log=log)
|
|
521
552
|
final = llm.generate_text(
|
|
522
553
|
model=chosen_model,
|
|
523
554
|
instructions=instructions,
|
|
@@ -541,21 +572,31 @@ def generate_commit_message_with_info(
|
|
|
541
572
|
chunk_tokens: int | None = 0,
|
|
542
573
|
provider: str | None = None,
|
|
543
574
|
host: str | None = None,
|
|
575
|
+
conventional: bool = False,
|
|
544
576
|
/,
|
|
577
|
+
*,
|
|
578
|
+
branch: str | None = None,
|
|
579
|
+
log: str | None = None,
|
|
545
580
|
) -> CommitMessageResult:
|
|
546
|
-
chosen_provider =
|
|
547
|
-
chosen_model =
|
|
548
|
-
chosen_language =
|
|
581
|
+
chosen_provider = resolve_provider_name(provider)
|
|
582
|
+
chosen_model = resolve_model_name(model, chosen_provider)
|
|
583
|
+
chosen_language = resolve_language_tag(language)
|
|
549
584
|
|
|
550
585
|
llm = get_provider(chosen_provider, host=host)
|
|
551
586
|
|
|
552
587
|
normalized_chunk_tokens = 0 if chunk_tokens is None else chunk_tokens
|
|
588
|
+
provider_arg_error = validate_provider_chunk_tokens(
|
|
589
|
+
chosen_provider,
|
|
590
|
+
normalized_chunk_tokens,
|
|
591
|
+
)
|
|
592
|
+
if provider_arg_error is not None:
|
|
593
|
+
raise ValueError(provider_arg_error)
|
|
553
594
|
|
|
554
595
|
response_id: str | None = None
|
|
555
596
|
|
|
556
597
|
if normalized_chunk_tokens != -1:
|
|
557
598
|
hunks = _split_diff_into_hunks(diff)
|
|
558
|
-
if normalized_chunk_tokens == 0
|
|
599
|
+
if normalized_chunk_tokens == 0:
|
|
559
600
|
chunks = ["".join(hunks) if hunks else diff]
|
|
560
601
|
else:
|
|
561
602
|
chunks = _build_diff_chunks(hunks, normalized_chunk_tokens, llm, chosen_model)
|
|
@@ -570,12 +611,17 @@ def generate_commit_message_with_info(
|
|
|
570
611
|
single_line,
|
|
571
612
|
subject_max,
|
|
572
613
|
chosen_language,
|
|
614
|
+
conventional,
|
|
615
|
+
branch=branch,
|
|
616
|
+
log=log,
|
|
573
617
|
)
|
|
574
618
|
|
|
575
619
|
combined_prompt = _build_combined_prompt(
|
|
576
620
|
"\n".join(summary_texts),
|
|
577
621
|
hint,
|
|
578
622
|
"Combined summaries (English)",
|
|
623
|
+
branch=branch,
|
|
624
|
+
log=log,
|
|
579
625
|
)
|
|
580
626
|
|
|
581
627
|
prompt_tokens, completion_tokens, total_tokens = _sum_usage(
|
|
@@ -586,8 +632,8 @@ def generate_commit_message_with_info(
|
|
|
586
632
|
response_id = final_result.response_id
|
|
587
633
|
|
|
588
634
|
else:
|
|
589
|
-
instructions = _build_system_prompt(single_line, subject_max, chosen_language)
|
|
590
|
-
combined_prompt = _build_combined_prompt(diff, hint)
|
|
635
|
+
instructions = _build_system_prompt(single_line, subject_max, chosen_language, conventional)
|
|
636
|
+
combined_prompt = _build_combined_prompt(diff, hint, branch=branch, log=log)
|
|
591
637
|
|
|
592
638
|
final_result = llm.generate_text(
|
|
593
639
|
model=chosen_model,
|
|
@@ -11,7 +11,6 @@ from os import environ
|
|
|
11
11
|
from typing import ClassVar, Final
|
|
12
12
|
|
|
13
13
|
from ollama import Client, ResponseError
|
|
14
|
-
from tiktoken import Encoding, get_encoding
|
|
15
14
|
|
|
16
15
|
from ._llm import LLMTextResult, LLMUsage
|
|
17
16
|
|
|
@@ -28,15 +27,6 @@ def _resolve_ollama_host(
|
|
|
28
27
|
return host or environ.get("OLLAMA_HOST") or _DEFAULT_OLLAMA_HOST
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def _get_encoding() -> Encoding:
|
|
32
|
-
"""Get a fallback encoding for token counting."""
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
return get_encoding("cl100k_base")
|
|
36
|
-
except Exception:
|
|
37
|
-
return get_encoding("gpt2")
|
|
38
|
-
|
|
39
|
-
|
|
40
30
|
class OllamaProvider:
|
|
41
31
|
"""Ollama provider implementation for the LLM protocol."""
|
|
42
32
|
|
|
@@ -113,10 +103,7 @@ class OllamaProvider:
|
|
|
113
103
|
model: str,
|
|
114
104
|
text: str,
|
|
115
105
|
) -> int:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return len(encoding.encode(text))
|
|
121
|
-
except Exception:
|
|
122
|
-
return len(text.split())
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"Token counting is not supported for the Ollama provider. "
|
|
108
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
109
|
+
)
|
{git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-message
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Generate Git commit messages from staged changes using LLM
|
|
5
5
|
Maintainer-email: Mina Her <minacle@live.com>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -31,7 +31,7 @@ License: This is free and unencumbered software released into the public domain.
|
|
|
31
31
|
Project-URL: Homepage, https://github.com/minacle/git-commit-message
|
|
32
32
|
Project-URL: Repository, https://github.com/minacle/git-commit-message
|
|
33
33
|
Project-URL: Issues, https://github.com/minacle/git-commit-message/issues
|
|
34
|
-
Classifier: Development Status ::
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
35
|
Classifier: Environment :: Console
|
|
36
36
|
Classifier: Intended Audience :: Developers
|
|
37
37
|
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
@@ -40,6 +40,7 @@ Classifier: Programming Language :: Python
|
|
|
40
40
|
Classifier: Programming Language :: Python :: 3
|
|
41
41
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
42
|
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
43
44
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
44
45
|
Requires-Python: >=3.13
|
|
45
46
|
Description-Content-Type: text/markdown
|
|
@@ -47,7 +48,6 @@ Requires-Dist: babel>=2.17.0
|
|
|
47
48
|
Requires-Dist: google-genai>=1.56.0
|
|
48
49
|
Requires-Dist: ollama>=0.4.0
|
|
49
50
|
Requires-Dist: openai>=2.6.1
|
|
50
|
-
Requires-Dist: tiktoken>=0.12.0
|
|
51
51
|
|
|
52
52
|
# git-commit-message
|
|
53
53
|
|
|
@@ -167,6 +167,18 @@ git-commit-message --one-line "optional context"
|
|
|
167
167
|
git-commit-message --one-line --co-author 'John Doe <john.doe@example.com>'
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
Use Conventional Commits constraints for the subject/footer only (body format is preserved):
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
git-commit-message --conventional
|
|
174
|
+
|
|
175
|
+
# can be combined with one-line mode
|
|
176
|
+
git-commit-message --conventional --one-line
|
|
177
|
+
|
|
178
|
+
# co-author trailers are appended after any existing footers
|
|
179
|
+
git-commit-message --conventional --co-author copilot
|
|
180
|
+
```
|
|
181
|
+
|
|
170
182
|
Select provider:
|
|
171
183
|
|
|
172
184
|
```sh
|
|
@@ -223,10 +235,24 @@ git-commit-message --chunk-tokens 0
|
|
|
223
235
|
# chunk the diff into ~4000-token pieces before summarising
|
|
224
236
|
git-commit-message --chunk-tokens 4000
|
|
225
237
|
|
|
238
|
+
# note: for provider 'ollama', values >= 1 are not supported
|
|
239
|
+
# use 0 (single summary pass) or -1 (legacy one-shot)
|
|
240
|
+
git-commit-message --provider ollama --chunk-tokens 0
|
|
241
|
+
|
|
226
242
|
# disable summarisation and use the legacy one-shot prompt
|
|
227
243
|
git-commit-message --chunk-tokens -1
|
|
228
244
|
```
|
|
229
245
|
|
|
246
|
+
Adjust unified diff context lines:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
# use 5 context lines around each change hunk
|
|
250
|
+
git-commit-message --diff-context 5
|
|
251
|
+
|
|
252
|
+
# include only changed lines (no surrounding context)
|
|
253
|
+
git-commit-message --diff-context 0
|
|
254
|
+
```
|
|
255
|
+
|
|
230
256
|
Select output language/locale (IETF language tag):
|
|
231
257
|
|
|
232
258
|
```sh
|
|
@@ -258,9 +284,11 @@ git-commit-message --provider llamacpp --host http://192.168.1.100:8080
|
|
|
258
284
|
- `--provider {openai,google,ollama,llamacpp}`: provider to use (default: `openai`)
|
|
259
285
|
- `--model MODEL`: model override (provider-specific; ignored for llama.cpp)
|
|
260
286
|
- `--language TAG`: output language/locale (default: `en-GB`)
|
|
287
|
+
- `--conventional`: apply Conventional Commits constraints to the subject and footer behavior. The body format is unchanged and still includes the translated `Rationale:` line. Breaking changes are expressed with `!` in the subject line, and `BREAKING CHANGE` footer lines are not generated.
|
|
261
288
|
- `--one-line`: output subject only when no trailers are appended; with `--co-author`, output is a single-line subject plus `Co-authored-by:` trailer lines
|
|
262
289
|
- `--max-length N`: max subject length (default: 72)
|
|
263
|
-
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
|
|
290
|
+
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation). For `ollama`, values `>= 1` are not supported.
|
|
291
|
+
- `--diff-context N`: context lines in unified diff (`N >= 0`). If omitted, uses `GIT_COMMIT_MESSAGE_DIFF_CONTEXT` when set; otherwise uses Git default (usually `3`).
|
|
264
292
|
- `--debug`: print request/response details
|
|
265
293
|
- `--commit`: run `git commit -m <message>`
|
|
266
294
|
- `--amend`: generate a message suitable for amending the previous commit (diff is from the amended commit's parent to the staged index; if nothing is staged, this effectively becomes the diff introduced by `HEAD`)
|
|
@@ -284,7 +312,8 @@ Optional:
|
|
|
284
312
|
- `OLLAMA_HOST`: Ollama server URL (default: `http://localhost:11434`)
|
|
285
313
|
- `LLAMACPP_HOST`: llama.cpp server URL (default: `http://localhost:8080`)
|
|
286
314
|
- `GIT_COMMIT_MESSAGE_LANGUAGE`: default language/locale (default: `en-GB`)
|
|
287
|
-
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`)
|
|
315
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`; for `ollama`, values `>= 1` are not supported)
|
|
316
|
+
- `GIT_COMMIT_MESSAGE_DIFF_CONTEXT`: default unified diff context lines (`0` or greater). If unset, Git default is used (usually `3`).
|
|
288
317
|
|
|
289
318
|
Default models (if not overridden):
|
|
290
319
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{git_commit_message-0.8.2 → git_commit_message-0.9.1}/src/git_commit_message.egg-info/top_level.txt
RENAMED
|
File without changes
|