git-commit-message 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_commit_message/_cli.py +81 -18
- git_commit_message/_gemini.py +127 -0
- git_commit_message/_gpt.py +47 -555
- git_commit_message/_llm.py +612 -0
- git_commit_message/_ollama.py +122 -0
- {git_commit_message-0.6.0.dist-info → git_commit_message-0.8.0.dist-info}/METADATA +115 -33
- git_commit_message-0.8.0.dist-info/RECORD +13 -0
- git_commit_message-0.6.0.dist-info/RECORD +0 -10
- {git_commit_message-0.6.0.dist-info → git_commit_message-0.8.0.dist-info}/WHEEL +0 -0
- {git_commit_message-0.6.0.dist-info → git_commit_message-0.8.0.dist-info}/entry_points.txt +0 -0
- {git_commit_message-0.6.0.dist-info → git_commit_message-0.8.0.dist-info}/top_level.txt +0 -0
git_commit_message/_cli.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Command-line interface entry point.
|
|
2
2
|
|
|
3
|
-
Collect staged changes from the repository and call an
|
|
3
|
+
Collect staged changes from the repository and call an LLM provider
|
|
4
4
|
to generate a commit message, or create a commit straight away.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -19,13 +19,46 @@ from ._git import (
|
|
|
19
19
|
get_staged_diff,
|
|
20
20
|
has_staged_changes,
|
|
21
21
|
)
|
|
22
|
-
from .
|
|
22
|
+
from ._llm import (
|
|
23
|
+
CommitMessageResult,
|
|
24
|
+
UnsupportedProviderError,
|
|
23
25
|
generate_commit_message,
|
|
24
26
|
generate_commit_message_with_info,
|
|
25
|
-
CommitMessageResult,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
class CliArgs(Namespace):
|
|
31
|
+
__slots__ = (
|
|
32
|
+
"description",
|
|
33
|
+
"commit",
|
|
34
|
+
"edit",
|
|
35
|
+
"provider",
|
|
36
|
+
"model",
|
|
37
|
+
"language",
|
|
38
|
+
"debug",
|
|
39
|
+
"one_line",
|
|
40
|
+
"max_length",
|
|
41
|
+
"chunk_tokens",
|
|
42
|
+
"host",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
/,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.description: str | None = None
|
|
50
|
+
self.commit: bool = False
|
|
51
|
+
self.edit: bool = False
|
|
52
|
+
self.provider: str | None = None
|
|
53
|
+
self.model: str | None = None
|
|
54
|
+
self.language: str | None = None
|
|
55
|
+
self.debug: bool = False
|
|
56
|
+
self.one_line: bool = False
|
|
57
|
+
self.max_length: int | None = None
|
|
58
|
+
self.chunk_tokens: int | None = None
|
|
59
|
+
self.host: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
29
62
|
def _env_chunk_tokens_default() -> int | None:
|
|
30
63
|
"""Return chunk token default from env if valid, else None."""
|
|
31
64
|
|
|
@@ -50,7 +83,7 @@ def _build_parser() -> ArgumentParser:
|
|
|
50
83
|
parser: ArgumentParser = ArgumentParser(
|
|
51
84
|
prog="git-commit-message",
|
|
52
85
|
description=(
|
|
53
|
-
"Generate a commit message
|
|
86
|
+
"Generate a commit message based on the staged changes."
|
|
54
87
|
),
|
|
55
88
|
)
|
|
56
89
|
|
|
@@ -72,11 +105,21 @@ def _build_parser() -> ArgumentParser:
|
|
|
72
105
|
help="Open an editor to amend the message before committing. Use with '--commit'.",
|
|
73
106
|
)
|
|
74
107
|
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--provider",
|
|
110
|
+
default=None,
|
|
111
|
+
help=(
|
|
112
|
+
"LLM provider to use (default: openai). "
|
|
113
|
+
"You may also set GIT_COMMIT_MESSAGE_PROVIDER. "
|
|
114
|
+
"The CLI flag overrides the environment variable."
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
75
118
|
parser.add_argument(
|
|
76
119
|
"--model",
|
|
77
120
|
default=None,
|
|
78
121
|
help=(
|
|
79
|
-
"
|
|
122
|
+
"Model name to use. If unspecified, uses GIT_COMMIT_MESSAGE_MODEL or a provider-specific default (openai: gpt-5-mini; google: gemini-2.5-flash; ollama: gpt-oss:20b)."
|
|
80
123
|
),
|
|
81
124
|
)
|
|
82
125
|
|
|
@@ -123,11 +166,21 @@ def _build_parser() -> ArgumentParser:
|
|
|
123
166
|
),
|
|
124
167
|
)
|
|
125
168
|
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--host",
|
|
171
|
+
dest="host",
|
|
172
|
+
default=None,
|
|
173
|
+
help=(
|
|
174
|
+
"Host URL for API providers like Ollama (default: http://localhost:11434). "
|
|
175
|
+
"You may also set OLLAMA_HOST for Ollama."
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
|
|
126
179
|
return parser
|
|
127
180
|
|
|
128
181
|
|
|
129
182
|
def _run(
|
|
130
|
-
args:
|
|
183
|
+
args: CliArgs,
|
|
131
184
|
/,
|
|
132
185
|
) -> int:
|
|
133
186
|
"""Main execution logic.
|
|
@@ -166,10 +219,12 @@ def _run(
|
|
|
166
219
|
diff_text,
|
|
167
220
|
hint,
|
|
168
221
|
args.model,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
args.one_line,
|
|
223
|
+
args.max_length,
|
|
224
|
+
args.language,
|
|
172
225
|
chunk_tokens,
|
|
226
|
+
args.provider,
|
|
227
|
+
args.host,
|
|
173
228
|
)
|
|
174
229
|
message = result.message
|
|
175
230
|
else:
|
|
@@ -177,17 +232,22 @@ def _run(
|
|
|
177
232
|
diff_text,
|
|
178
233
|
hint,
|
|
179
234
|
args.model,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
args.one_line,
|
|
236
|
+
args.max_length,
|
|
237
|
+
args.language,
|
|
183
238
|
chunk_tokens,
|
|
239
|
+
args.provider,
|
|
240
|
+
args.host,
|
|
184
241
|
)
|
|
242
|
+
except UnsupportedProviderError as exc:
|
|
243
|
+
print(str(exc), file=stderr)
|
|
244
|
+
return 3
|
|
185
245
|
except Exception as exc: # noqa: BLE001 - to preserve standard output messaging
|
|
186
246
|
print(f"Failed to generate commit message: {exc}", file=stderr)
|
|
187
247
|
return 3
|
|
188
248
|
|
|
189
249
|
# Option: force single-line message
|
|
190
|
-
if
|
|
250
|
+
if args.one_line:
|
|
191
251
|
# Use the first non-empty line only
|
|
192
252
|
for line in (ln.strip() for ln in message.splitlines()):
|
|
193
253
|
if line:
|
|
@@ -199,9 +259,10 @@ def _run(
|
|
|
199
259
|
if not args.commit:
|
|
200
260
|
if args.debug and result is not None:
|
|
201
261
|
# Print debug information
|
|
202
|
-
print("====
|
|
262
|
+
print(f"==== {result.provider} Usage ====")
|
|
263
|
+
print(f"provider: {result.provider}")
|
|
203
264
|
print(f"model: {result.model}")
|
|
204
|
-
print(f"response_id: {
|
|
265
|
+
print(f"response_id: {result.response_id or '(n/a)'}")
|
|
205
266
|
if result.total_tokens is not None:
|
|
206
267
|
print(
|
|
207
268
|
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
@@ -220,9 +281,10 @@ def _run(
|
|
|
220
281
|
|
|
221
282
|
if args.debug and result is not None:
|
|
222
283
|
# Also print debug info before commit
|
|
223
|
-
print("====
|
|
284
|
+
print(f"==== {result.provider} Usage ====")
|
|
285
|
+
print(f"provider: {result.provider}")
|
|
224
286
|
print(f"model: {result.model}")
|
|
225
|
-
print(f"response_id: {
|
|
287
|
+
print(f"response_id: {result.response_id or '(n/a)'}")
|
|
226
288
|
if result.total_tokens is not None:
|
|
227
289
|
print(
|
|
228
290
|
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
@@ -251,7 +313,8 @@ def main() -> None:
|
|
|
251
313
|
"""
|
|
252
314
|
|
|
253
315
|
parser: Final[ArgumentParser] = _build_parser()
|
|
254
|
-
args
|
|
316
|
+
args = CliArgs()
|
|
317
|
+
parser.parse_args(namespace=args)
|
|
255
318
|
|
|
256
319
|
if args.edit and not args.commit:
|
|
257
320
|
print("'--edit' must be used together with '--commit'.", file=stderr)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Google (Gemini) provider implementation.
|
|
2
|
+
|
|
3
|
+
This module contains only Google GenAI-specific API calls and token counting.
|
|
4
|
+
Provider-agnostic orchestration/prompt logic lives in `_llm.py`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from os import environ
|
|
10
|
+
from typing import ClassVar
|
|
11
|
+
|
|
12
|
+
from google import genai
|
|
13
|
+
from google.genai import types
|
|
14
|
+
|
|
15
|
+
from ._llm import LLMTextResult, LLMUsage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GoogleGenAIProvider:
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"_client",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
name: ClassVar[str] = "google"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
/,
|
|
28
|
+
*,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
key = api_key or environ.get("GOOGLE_API_KEY")
|
|
32
|
+
if not key:
|
|
33
|
+
raise RuntimeError("The GOOGLE_API_KEY environment variable is required.")
|
|
34
|
+
self._client = genai.Client(api_key=key)
|
|
35
|
+
|
|
36
|
+
def count_tokens(
|
|
37
|
+
self,
|
|
38
|
+
/,
|
|
39
|
+
*,
|
|
40
|
+
model: str,
|
|
41
|
+
text: str,
|
|
42
|
+
) -> int:
|
|
43
|
+
try:
|
|
44
|
+
resp = self._client.models.count_tokens(
|
|
45
|
+
model=model,
|
|
46
|
+
contents=text,
|
|
47
|
+
)
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"Token counting failed for the Google provider. "
|
|
51
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
52
|
+
) from exc
|
|
53
|
+
|
|
54
|
+
total = getattr(resp, "total_tokens", None)
|
|
55
|
+
if not isinstance(total, int):
|
|
56
|
+
raise RuntimeError(
|
|
57
|
+
"Token counting returned an unexpected response from the Google provider. "
|
|
58
|
+
"Try `--chunk-tokens 0` (default) or `--chunk-tokens -1` to disable summarisation."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return total
|
|
62
|
+
|
|
63
|
+
def generate_text(
|
|
64
|
+
self,
|
|
65
|
+
/,
|
|
66
|
+
*,
|
|
67
|
+
model: str,
|
|
68
|
+
instructions: str,
|
|
69
|
+
user_text: str,
|
|
70
|
+
) -> LLMTextResult:
|
|
71
|
+
config = types.GenerateContentConfig(
|
|
72
|
+
system_instruction=instructions,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
resp = self._client.models.generate_content(
|
|
76
|
+
model=model,
|
|
77
|
+
contents=user_text,
|
|
78
|
+
config=config,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
text = self._extract_text(resp)
|
|
82
|
+
if not text:
|
|
83
|
+
raise RuntimeError("An empty response text was generated by the provider.")
|
|
84
|
+
|
|
85
|
+
usage = self._extract_usage(resp)
|
|
86
|
+
|
|
87
|
+
return LLMTextResult(
|
|
88
|
+
text=text,
|
|
89
|
+
response_id=getattr(resp, "response_id", None),
|
|
90
|
+
usage=usage,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _extract_text(
|
|
95
|
+
resp: types.GenerateContentResponse,
|
|
96
|
+
/,
|
|
97
|
+
) -> str:
|
|
98
|
+
candidates = getattr(resp, "candidates", None)
|
|
99
|
+
if not candidates:
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
parts = getattr(candidates[0].content, "parts", None) if candidates[0].content else None
|
|
103
|
+
if not parts:
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
texts: list[str] = []
|
|
107
|
+
for part in parts:
|
|
108
|
+
t = getattr(part, "text", None)
|
|
109
|
+
if isinstance(t, str) and t.strip():
|
|
110
|
+
texts.append(t)
|
|
111
|
+
|
|
112
|
+
return "\n".join(texts).strip()
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _extract_usage(
|
|
116
|
+
resp: types.GenerateContentResponse,
|
|
117
|
+
/,
|
|
118
|
+
) -> LLMUsage | None:
|
|
119
|
+
metadata = getattr(resp, "usage_metadata", None)
|
|
120
|
+
if metadata is None:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
return LLMUsage(
|
|
124
|
+
prompt_tokens=getattr(metadata, "prompt_token_count", None),
|
|
125
|
+
completion_tokens=getattr(metadata, "candidates_token_count", None),
|
|
126
|
+
total_tokens=getattr(metadata, "total_token_count", None),
|
|
127
|
+
)
|