git-commit-message 0.7.0__py3-none-any.whl → 0.8.1__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 +87 -20
- git_commit_message/_gemini.py +6 -1
- git_commit_message/_git.py +168 -13
- git_commit_message/_gpt.py +7 -1
- git_commit_message/_llm.py +23 -5
- git_commit_message/_ollama.py +122 -0
- {git_commit_message-0.7.0.dist-info → git_commit_message-0.8.1.dist-info}/METADATA +109 -45
- git_commit_message-0.8.1.dist-info/RECORD +13 -0
- git_commit_message-0.7.0.dist-info/RECORD +0 -12
- {git_commit_message-0.7.0.dist-info → git_commit_message-0.8.1.dist-info}/WHEEL +0 -0
- {git_commit_message-0.7.0.dist-info → git_commit_message-0.8.1.dist-info}/entry_points.txt +0 -0
- {git_commit_message-0.7.0.dist-info → git_commit_message-0.8.1.dist-info}/top_level.txt +0 -0
git_commit_message/_cli.py
CHANGED
|
@@ -17,7 +17,9 @@ from ._git import (
|
|
|
17
17
|
commit_with_message,
|
|
18
18
|
get_repo_root,
|
|
19
19
|
get_staged_diff,
|
|
20
|
+
has_head_commit,
|
|
20
21
|
has_staged_changes,
|
|
22
|
+
resolve_amend_base_ref,
|
|
21
23
|
)
|
|
22
24
|
from ._llm import (
|
|
23
25
|
CommitMessageResult,
|
|
@@ -27,6 +29,40 @@ from ._llm import (
|
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
class CliArgs(Namespace):
|
|
33
|
+
__slots__ = (
|
|
34
|
+
"description",
|
|
35
|
+
"commit",
|
|
36
|
+
"amend",
|
|
37
|
+
"edit",
|
|
38
|
+
"provider",
|
|
39
|
+
"model",
|
|
40
|
+
"language",
|
|
41
|
+
"debug",
|
|
42
|
+
"one_line",
|
|
43
|
+
"max_length",
|
|
44
|
+
"chunk_tokens",
|
|
45
|
+
"host",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
/,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.description: str | None = None
|
|
53
|
+
self.commit: bool = False
|
|
54
|
+
self.amend: bool = False
|
|
55
|
+
self.edit: bool = False
|
|
56
|
+
self.provider: str | None = None
|
|
57
|
+
self.model: str | None = None
|
|
58
|
+
self.language: str | None = None
|
|
59
|
+
self.debug: bool = False
|
|
60
|
+
self.one_line: bool = False
|
|
61
|
+
self.max_length: int | None = None
|
|
62
|
+
self.chunk_tokens: int | None = None
|
|
63
|
+
self.host: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
30
66
|
def _env_chunk_tokens_default() -> int | None:
|
|
31
67
|
"""Return chunk token default from env if valid, else None."""
|
|
32
68
|
|
|
@@ -67,6 +103,16 @@ def _build_parser() -> ArgumentParser:
|
|
|
67
103
|
help="Commit immediately with the generated message.",
|
|
68
104
|
)
|
|
69
105
|
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--amend",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help=(
|
|
110
|
+
"Generate a message suitable for amending the previous commit. "
|
|
111
|
+
"When set, the diff is computed from the amended commit's parent to the staged index. "
|
|
112
|
+
"Use with '--commit' to run the amend, or omit '--commit' to print the message only."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
70
116
|
parser.add_argument(
|
|
71
117
|
"--edit",
|
|
72
118
|
action="store_true",
|
|
@@ -87,7 +133,7 @@ def _build_parser() -> ArgumentParser:
|
|
|
87
133
|
"--model",
|
|
88
134
|
default=None,
|
|
89
135
|
help=(
|
|
90
|
-
"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)."
|
|
136
|
+
"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)."
|
|
91
137
|
),
|
|
92
138
|
)
|
|
93
139
|
|
|
@@ -134,11 +180,21 @@ def _build_parser() -> ArgumentParser:
|
|
|
134
180
|
),
|
|
135
181
|
)
|
|
136
182
|
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"--host",
|
|
185
|
+
dest="host",
|
|
186
|
+
default=None,
|
|
187
|
+
help=(
|
|
188
|
+
"Host URL for API providers like Ollama (default: http://localhost:11434). "
|
|
189
|
+
"You may also set OLLAMA_HOST for Ollama."
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
137
193
|
return parser
|
|
138
194
|
|
|
139
195
|
|
|
140
196
|
def _run(
|
|
141
|
-
args:
|
|
197
|
+
args: CliArgs,
|
|
142
198
|
/,
|
|
143
199
|
) -> int:
|
|
144
200
|
"""Main execution logic.
|
|
@@ -156,11 +212,19 @@ def _run(
|
|
|
156
212
|
|
|
157
213
|
repo_root: Path = get_repo_root()
|
|
158
214
|
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
215
|
+
if args.amend:
|
|
216
|
+
if not has_head_commit(repo_root):
|
|
217
|
+
print("Cannot amend: the repository has no commits yet.", file=stderr)
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
base_ref = resolve_amend_base_ref(repo_root)
|
|
221
|
+
diff_text: str = get_staged_diff(repo_root, base_ref=base_ref)
|
|
222
|
+
else:
|
|
223
|
+
if not has_staged_changes(repo_root):
|
|
224
|
+
print("No staged changes. Run 'git add' and try again.", file=stderr)
|
|
225
|
+
return 2
|
|
162
226
|
|
|
163
|
-
|
|
227
|
+
diff_text = get_staged_diff(repo_root)
|
|
164
228
|
|
|
165
229
|
hint: str | None = args.description if isinstance(args.description, str) else None
|
|
166
230
|
|
|
@@ -177,11 +241,12 @@ def _run(
|
|
|
177
241
|
diff_text,
|
|
178
242
|
hint,
|
|
179
243
|
args.model,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
args.one_line,
|
|
245
|
+
args.max_length,
|
|
246
|
+
args.language,
|
|
183
247
|
chunk_tokens,
|
|
184
|
-
|
|
248
|
+
args.provider,
|
|
249
|
+
args.host,
|
|
185
250
|
)
|
|
186
251
|
message = result.message
|
|
187
252
|
else:
|
|
@@ -189,11 +254,12 @@ def _run(
|
|
|
189
254
|
diff_text,
|
|
190
255
|
hint,
|
|
191
256
|
args.model,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
257
|
+
args.one_line,
|
|
258
|
+
args.max_length,
|
|
259
|
+
args.language,
|
|
195
260
|
chunk_tokens,
|
|
196
|
-
|
|
261
|
+
args.provider,
|
|
262
|
+
args.host,
|
|
197
263
|
)
|
|
198
264
|
except UnsupportedProviderError as exc:
|
|
199
265
|
print(str(exc), file=stderr)
|
|
@@ -203,7 +269,7 @@ def _run(
|
|
|
203
269
|
return 3
|
|
204
270
|
|
|
205
271
|
# Option: force single-line message
|
|
206
|
-
if
|
|
272
|
+
if args.one_line:
|
|
207
273
|
# Use the first non-empty line only
|
|
208
274
|
for line in (ln.strip() for ln in message.splitlines()):
|
|
209
275
|
if line:
|
|
@@ -218,7 +284,7 @@ def _run(
|
|
|
218
284
|
print(f"==== {result.provider} Usage ====")
|
|
219
285
|
print(f"provider: {result.provider}")
|
|
220
286
|
print(f"model: {result.model}")
|
|
221
|
-
print(f"response_id: {
|
|
287
|
+
print(f"response_id: {result.response_id or '(n/a)'}")
|
|
222
288
|
if result.total_tokens is not None:
|
|
223
289
|
print(
|
|
224
290
|
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
@@ -240,7 +306,7 @@ def _run(
|
|
|
240
306
|
print(f"==== {result.provider} Usage ====")
|
|
241
307
|
print(f"provider: {result.provider}")
|
|
242
308
|
print(f"model: {result.model}")
|
|
243
|
-
print(f"response_id: {
|
|
309
|
+
print(f"response_id: {result.response_id or '(n/a)'}")
|
|
244
310
|
if result.total_tokens is not None:
|
|
245
311
|
print(
|
|
246
312
|
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
@@ -255,9 +321,9 @@ def _run(
|
|
|
255
321
|
print(message)
|
|
256
322
|
|
|
257
323
|
if args.edit:
|
|
258
|
-
rc: int = commit_with_message(message, True, repo_root)
|
|
324
|
+
rc: int = commit_with_message(message, True, repo_root, amend=args.amend)
|
|
259
325
|
else:
|
|
260
|
-
rc = commit_with_message(message, False, repo_root)
|
|
326
|
+
rc = commit_with_message(message, False, repo_root, amend=args.amend)
|
|
261
327
|
|
|
262
328
|
return rc
|
|
263
329
|
|
|
@@ -269,7 +335,8 @@ def main() -> None:
|
|
|
269
335
|
"""
|
|
270
336
|
|
|
271
337
|
parser: Final[ArgumentParser] = _build_parser()
|
|
272
|
-
args
|
|
338
|
+
args = CliArgs()
|
|
339
|
+
parser.parse_args(namespace=args)
|
|
273
340
|
|
|
274
341
|
if args.edit and not args.commit:
|
|
275
342
|
print("'--edit' must be used together with '--commit'.", file=stderr)
|
git_commit_message/_gemini.py
CHANGED
|
@@ -7,6 +7,7 @@ Provider-agnostic orchestration/prompt logic lives in `_llm.py`.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from os import environ
|
|
10
|
+
from typing import ClassVar
|
|
10
11
|
|
|
11
12
|
from google import genai
|
|
12
13
|
from google.genai import types
|
|
@@ -15,7 +16,11 @@ from ._llm import LLMTextResult, LLMUsage
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class GoogleGenAIProvider:
|
|
18
|
-
|
|
19
|
+
__slots__ = (
|
|
20
|
+
"_client",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
name: ClassVar[str] = "google"
|
|
19
24
|
|
|
20
25
|
def __init__(
|
|
21
26
|
self,
|
git_commit_message/_git.py
CHANGED
|
@@ -10,6 +10,61 @@ from pathlib import Path
|
|
|
10
10
|
from subprocess import CalledProcessError, check_call, check_output, run
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _get_empty_tree_hash(
|
|
14
|
+
cwd: Path,
|
|
15
|
+
/,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Return the empty tree hash for this repository.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
cwd
|
|
22
|
+
Repository directory in which to run Git.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
Do not hard-code the SHA, because repositories may use different
|
|
27
|
+
hash algorithms (e.g. SHA-1 vs SHA-256). We ask Git to compute the
|
|
28
|
+
empty tree object ID for the current repo.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
str
|
|
33
|
+
The empty tree object ID for the current repository.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
completed = run(
|
|
38
|
+
[
|
|
39
|
+
"git",
|
|
40
|
+
"hash-object",
|
|
41
|
+
"-t",
|
|
42
|
+
"tree",
|
|
43
|
+
"--stdin",
|
|
44
|
+
],
|
|
45
|
+
cwd=str(cwd),
|
|
46
|
+
check=True,
|
|
47
|
+
input=b"",
|
|
48
|
+
capture_output=True,
|
|
49
|
+
)
|
|
50
|
+
except CalledProcessError as exc:
|
|
51
|
+
stderr_text = (exc.stderr or b"").decode(errors="replace").strip()
|
|
52
|
+
suffix = f"\nGit stderr: {stderr_text}" if stderr_text else ""
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
f"Failed to compute empty tree hash (git exited with {exc.returncode}).{suffix}"
|
|
55
|
+
) from exc
|
|
56
|
+
except OSError as exc:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"Failed to run git to compute empty tree hash: {exc}"
|
|
59
|
+
) from exc
|
|
60
|
+
oid = completed.stdout.decode().strip()
|
|
61
|
+
if not oid:
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"Failed to compute empty tree hash: git returned an empty object ID."
|
|
64
|
+
)
|
|
65
|
+
return oid
|
|
66
|
+
|
|
67
|
+
|
|
13
68
|
def get_repo_root(
|
|
14
69
|
cwd: Path | None = None,
|
|
15
70
|
/,
|
|
@@ -60,23 +115,114 @@ def has_staged_changes(
|
|
|
60
115
|
return True
|
|
61
116
|
|
|
62
117
|
|
|
63
|
-
def
|
|
118
|
+
def has_head_commit(
|
|
119
|
+
cwd: Path,
|
|
120
|
+
/,
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""Return True if the repository has at least one commit (HEAD exists).
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
cwd
|
|
127
|
+
Repository directory in which to run Git.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
bool
|
|
132
|
+
True if ``HEAD`` exists in the repository, False otherwise.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
completed = run(
|
|
136
|
+
["git", "rev-parse", "--verify", "HEAD"],
|
|
137
|
+
cwd=str(cwd),
|
|
138
|
+
check=False,
|
|
139
|
+
capture_output=True,
|
|
140
|
+
)
|
|
141
|
+
return completed.returncode == 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_amend_base_ref(
|
|
64
145
|
cwd: Path,
|
|
65
146
|
/,
|
|
66
147
|
) -> str:
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
"""Resolve the base ref for an amend diff.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
cwd
|
|
153
|
+
Repository directory in which to run Git.
|
|
154
|
+
|
|
155
|
+
Notes
|
|
156
|
+
-----
|
|
157
|
+
The amended commit keeps the same parent as the current HEAD commit.
|
|
158
|
+
|
|
159
|
+
- If HEAD has a parent, base is ``HEAD^``.
|
|
160
|
+
- If HEAD is a root commit (no parent), base is the empty tree.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
str
|
|
165
|
+
The base reference for the amend diff: either ``HEAD^`` (when the
|
|
166
|
+
current ``HEAD`` commit has a parent) or the empty tree object ID
|
|
167
|
+
(when ``HEAD`` is a root commit).
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
completed = run(
|
|
171
|
+
["git", "rev-parse", "--verify", "HEAD^"],
|
|
78
172
|
cwd=str(cwd),
|
|
173
|
+
check=False,
|
|
174
|
+
capture_output=True,
|
|
79
175
|
)
|
|
176
|
+
if completed.returncode == 0:
|
|
177
|
+
return "HEAD^"
|
|
178
|
+
return _get_empty_tree_hash(cwd)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_staged_diff(
|
|
182
|
+
cwd: Path,
|
|
183
|
+
/,
|
|
184
|
+
*,
|
|
185
|
+
base_ref: str | None = None,
|
|
186
|
+
) -> str:
|
|
187
|
+
"""Return the staged changes as diff text.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
cwd
|
|
192
|
+
Git working directory.
|
|
193
|
+
base_ref
|
|
194
|
+
Optional Git reference or tree object ID (e.g., branch name, tag,
|
|
195
|
+
commit hash, or the empty tree hash) to diff against. When provided,
|
|
196
|
+
the diff shows changes from ``base_ref`` to the staged index, instead
|
|
197
|
+
of changes from ``HEAD`` to the staged index.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
str
|
|
202
|
+
Unified diff text for the staged changes.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
cmd: list[str] = [
|
|
206
|
+
"git",
|
|
207
|
+
"diff",
|
|
208
|
+
"--cached",
|
|
209
|
+
"--patch",
|
|
210
|
+
"--minimal",
|
|
211
|
+
"--no-color",
|
|
212
|
+
]
|
|
213
|
+
if base_ref:
|
|
214
|
+
cmd.append(base_ref)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
out: bytes = check_output(cmd, cwd=str(cwd))
|
|
218
|
+
except CalledProcessError as exc:
|
|
219
|
+
message = "Failed to retrieve staged diff from Git."
|
|
220
|
+
if base_ref:
|
|
221
|
+
message += (
|
|
222
|
+
" Ensure that the provided base_ref exists and is a valid Git reference."
|
|
223
|
+
)
|
|
224
|
+
raise RuntimeError(message) from exc
|
|
225
|
+
|
|
80
226
|
return out.decode()
|
|
81
227
|
|
|
82
228
|
|
|
@@ -85,6 +231,8 @@ def commit_with_message(
|
|
|
85
231
|
edit: bool,
|
|
86
232
|
cwd: Path,
|
|
87
233
|
/,
|
|
234
|
+
*,
|
|
235
|
+
amend: bool = False,
|
|
88
236
|
) -> int:
|
|
89
237
|
"""Create a commit with the given message.
|
|
90
238
|
|
|
@@ -96,6 +244,9 @@ def commit_with_message(
|
|
|
96
244
|
If True, use the `--edit` flag to open an editor for amendments.
|
|
97
245
|
cwd
|
|
98
246
|
Git working directory.
|
|
247
|
+
amend
|
|
248
|
+
If True, pass ``--amend`` to Git to amend the current ``HEAD`` commit
|
|
249
|
+
instead of creating a new commit.
|
|
99
250
|
|
|
100
251
|
Returns
|
|
101
252
|
-------
|
|
@@ -103,7 +254,11 @@ def commit_with_message(
|
|
|
103
254
|
The subprocess exit code.
|
|
104
255
|
"""
|
|
105
256
|
|
|
106
|
-
cmd: list[str] = ["git", "commit"
|
|
257
|
+
cmd: list[str] = ["git", "commit"]
|
|
258
|
+
if amend:
|
|
259
|
+
cmd.append("--amend")
|
|
260
|
+
|
|
261
|
+
cmd.extend(["-m", message])
|
|
107
262
|
if edit:
|
|
108
263
|
cmd.append("--edit")
|
|
109
264
|
|
git_commit_message/_gpt.py
CHANGED
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
from openai import OpenAI
|
|
10
10
|
from openai.types.responses import Response
|
|
11
11
|
from os import environ
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
12
14
|
from tiktoken import Encoding, encoding_for_model, get_encoding
|
|
13
15
|
from ._llm import LLMTextResult, LLMUsage
|
|
14
16
|
|
|
@@ -24,7 +26,11 @@ def _encoding_for_model(
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class OpenAIResponsesProvider:
|
|
27
|
-
|
|
29
|
+
__slots__ = (
|
|
30
|
+
"_client",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
name: ClassVar[str] = "openai"
|
|
28
34
|
|
|
29
35
|
def __init__(
|
|
30
36
|
self,
|
git_commit_message/_llm.py
CHANGED
|
@@ -12,16 +12,19 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from babel import Locale
|
|
14
14
|
from os import environ
|
|
15
|
-
from typing import Final, Protocol
|
|
15
|
+
from typing import ClassVar, Final, Protocol
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
_DEFAULT_PROVIDER: Final[str] = "openai"
|
|
19
19
|
_DEFAULT_MODEL_OPENAI: Final[str] = "gpt-5-mini"
|
|
20
20
|
_DEFAULT_MODEL_GOOGLE: Final[str] = "gemini-2.5-flash"
|
|
21
|
+
_DEFAULT_MODEL_OLLAMA: Final[str] = "gpt-oss:20b"
|
|
21
22
|
_DEFAULT_LANGUAGE: Final[str] = "en-GB"
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class UnsupportedProviderError(RuntimeError):
|
|
26
|
+
__slots__ = ()
|
|
27
|
+
|
|
25
28
|
pass
|
|
26
29
|
|
|
27
30
|
|
|
@@ -66,7 +69,9 @@ class LLMTextResult:
|
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
class CommitMessageProvider(Protocol):
|
|
69
|
-
|
|
72
|
+
__slots__ = ()
|
|
73
|
+
|
|
74
|
+
name: ClassVar[str]
|
|
70
75
|
|
|
71
76
|
def generate_text(
|
|
72
77
|
self,
|
|
@@ -147,6 +152,9 @@ def _resolve_model(
|
|
|
147
152
|
if provider_name == "google":
|
|
148
153
|
default_model = _DEFAULT_MODEL_GOOGLE
|
|
149
154
|
provider_model = None
|
|
155
|
+
elif provider_name == "ollama":
|
|
156
|
+
default_model = _DEFAULT_MODEL_OLLAMA
|
|
157
|
+
provider_model = environ.get("OLLAMA_MODEL")
|
|
150
158
|
else:
|
|
151
159
|
default_model = _DEFAULT_MODEL_OPENAI
|
|
152
160
|
provider_model = environ.get("OPENAI_MODEL")
|
|
@@ -164,6 +172,8 @@ def _resolve_language(
|
|
|
164
172
|
def get_provider(
|
|
165
173
|
provider: str | None,
|
|
166
174
|
/,
|
|
175
|
+
*,
|
|
176
|
+
host: str | None = None,
|
|
167
177
|
) -> CommitMessageProvider:
|
|
168
178
|
name = _resolve_provider(provider)
|
|
169
179
|
|
|
@@ -179,8 +189,14 @@ def get_provider(
|
|
|
179
189
|
|
|
180
190
|
return GoogleGenAIProvider()
|
|
181
191
|
|
|
192
|
+
if name == "ollama":
|
|
193
|
+
# Local import to avoid import cycles: providers may import shared types from this module.
|
|
194
|
+
from ._ollama import OllamaProvider
|
|
195
|
+
|
|
196
|
+
return OllamaProvider(host=host)
|
|
197
|
+
|
|
182
198
|
raise UnsupportedProviderError(
|
|
183
|
-
f"Unsupported provider: {name}. Supported providers: openai, google"
|
|
199
|
+
f"Unsupported provider: {name}. Supported providers: openai, google, ollama"
|
|
184
200
|
)
|
|
185
201
|
|
|
186
202
|
|
|
@@ -459,13 +475,14 @@ def generate_commit_message(
|
|
|
459
475
|
language: str | None = None,
|
|
460
476
|
chunk_tokens: int | None = 0,
|
|
461
477
|
provider: str | None = None,
|
|
478
|
+
host: str | None = None,
|
|
462
479
|
/,
|
|
463
480
|
) -> str:
|
|
464
481
|
chosen_provider = _resolve_provider(provider)
|
|
465
482
|
chosen_model = _resolve_model(model, chosen_provider)
|
|
466
483
|
chosen_language = _resolve_language(language)
|
|
467
484
|
|
|
468
|
-
llm = get_provider(chosen_provider)
|
|
485
|
+
llm = get_provider(chosen_provider, host=host)
|
|
469
486
|
|
|
470
487
|
normalized_chunk_tokens = 0 if chunk_tokens is None else chunk_tokens
|
|
471
488
|
|
|
@@ -513,13 +530,14 @@ def generate_commit_message_with_info(
|
|
|
513
530
|
language: str | None = None,
|
|
514
531
|
chunk_tokens: int | None = 0,
|
|
515
532
|
provider: str | None = None,
|
|
533
|
+
host: str | None = None,
|
|
516
534
|
/,
|
|
517
535
|
) -> CommitMessageResult:
|
|
518
536
|
chosen_provider = _resolve_provider(provider)
|
|
519
537
|
chosen_model = _resolve_model(model, chosen_provider)
|
|
520
538
|
chosen_language = _resolve_language(language)
|
|
521
539
|
|
|
522
|
-
llm = get_provider(chosen_provider)
|
|
540
|
+
llm = get_provider(chosen_provider, host=host)
|
|
523
541
|
|
|
524
542
|
normalized_chunk_tokens = 0 if chunk_tokens is None else chunk_tokens
|
|
525
543
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Ollama provider implementation.
|
|
2
|
+
|
|
3
|
+
Mirrors the Gemini provider structure: a single provider class that exposes
|
|
4
|
+
`generate_text` and `count_tokens`. Provider-agnostic orchestration lives in
|
|
5
|
+
`_llm.py`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from os import environ
|
|
11
|
+
from typing import ClassVar, Final
|
|
12
|
+
|
|
13
|
+
from ollama import Client, ResponseError
|
|
14
|
+
from tiktoken import Encoding, get_encoding
|
|
15
|
+
|
|
16
|
+
from ._llm import LLMTextResult, LLMUsage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_DEFAULT_OLLAMA_HOST: Final[str] = "http://localhost:11434"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_ollama_host(
|
|
23
|
+
host: str | None,
|
|
24
|
+
/,
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Resolve the Ollama host URL from arg, env, or default."""
|
|
27
|
+
|
|
28
|
+
return host or environ.get("OLLAMA_HOST") or _DEFAULT_OLLAMA_HOST
|
|
29
|
+
|
|
30
|
+
|
|
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
|
+
class OllamaProvider:
|
|
41
|
+
"""Ollama provider implementation for the LLM protocol."""
|
|
42
|
+
|
|
43
|
+
__slots__ = (
|
|
44
|
+
"_host",
|
|
45
|
+
"_client",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
name: ClassVar[str] = "ollama"
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
/,
|
|
53
|
+
*,
|
|
54
|
+
host: str | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._host = _resolve_ollama_host(host)
|
|
57
|
+
self._client = Client(host=self._host)
|
|
58
|
+
|
|
59
|
+
def generate_text(
|
|
60
|
+
self,
|
|
61
|
+
/,
|
|
62
|
+
*,
|
|
63
|
+
model: str,
|
|
64
|
+
instructions: str,
|
|
65
|
+
user_text: str,
|
|
66
|
+
) -> LLMTextResult:
|
|
67
|
+
"""Generate text using Ollama (non-streaming)."""
|
|
68
|
+
|
|
69
|
+
messages = [
|
|
70
|
+
{"role": "system", "content": instructions},
|
|
71
|
+
{"role": "user", "content": user_text},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
response = self._client.chat(model=model, messages=messages)
|
|
76
|
+
except ResponseError as exc:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"Ollama API error: {exc}"
|
|
79
|
+
) from exc
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"Failed to connect to Ollama at {self._host}. Make sure Ollama is running: {exc}"
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
response_text = response.message.content or ""
|
|
86
|
+
|
|
87
|
+
# Extract token usage if available
|
|
88
|
+
prompt_tokens: int | None = None
|
|
89
|
+
completion_tokens: int | None = None
|
|
90
|
+
total_tokens: int | None = None
|
|
91
|
+
|
|
92
|
+
if hasattr(response, "prompt_eval_count") and response.prompt_eval_count:
|
|
93
|
+
prompt_tokens = response.prompt_eval_count
|
|
94
|
+
if hasattr(response, "eval_count") and response.eval_count:
|
|
95
|
+
completion_tokens = response.eval_count
|
|
96
|
+
if prompt_tokens is not None and completion_tokens is not None:
|
|
97
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
98
|
+
|
|
99
|
+
return LLMTextResult(
|
|
100
|
+
text=response_text.strip(),
|
|
101
|
+
response_id=None,
|
|
102
|
+
usage=LLMUsage(
|
|
103
|
+
prompt_tokens=prompt_tokens,
|
|
104
|
+
completion_tokens=completion_tokens,
|
|
105
|
+
total_tokens=total_tokens,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def count_tokens(
|
|
110
|
+
self,
|
|
111
|
+
/,
|
|
112
|
+
*,
|
|
113
|
+
model: str,
|
|
114
|
+
text: str,
|
|
115
|
+
) -> int:
|
|
116
|
+
"""Approximate token count using tiktoken; fallback to whitespace split."""
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
encoding = _get_encoding()
|
|
120
|
+
return len(encoding.encode(text))
|
|
121
|
+
except Exception:
|
|
122
|
+
return len(text.split())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-commit-message
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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.
|
|
@@ -45,16 +45,22 @@ Requires-Python: >=3.13
|
|
|
45
45
|
Description-Content-Type: text/markdown
|
|
46
46
|
Requires-Dist: babel>=2.17.0
|
|
47
47
|
Requires-Dist: google-genai>=1.56.0
|
|
48
|
+
Requires-Dist: ollama>=0.4.0
|
|
48
49
|
Requires-Dist: openai>=2.6.1
|
|
49
50
|
Requires-Dist: tiktoken>=0.12.0
|
|
50
51
|
|
|
51
52
|
# git-commit-message
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Generate a commit message from your staged changes using OpenAI, Google Gemini, or Ollama.
|
|
54
55
|
|
|
55
56
|
[](https://asciinema.org/a/jk0phFqNnc5vaCiIZEYBwZOyN)
|
|
56
57
|
|
|
57
|
-
##
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Python 3.13+
|
|
61
|
+
- A Git repo with staged changes (`git add ...`) (or use `--amend` even if nothing is staged)
|
|
62
|
+
|
|
63
|
+
## Install
|
|
58
64
|
|
|
59
65
|
Install the latest released version from PyPI:
|
|
60
66
|
|
|
@@ -78,19 +84,43 @@ Quick check:
|
|
|
78
84
|
git-commit-message --help
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
## Setup
|
|
88
|
+
|
|
89
|
+
### OpenAI
|
|
82
90
|
|
|
83
91
|
```sh
|
|
84
92
|
export OPENAI_API_KEY="sk-..."
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
### Google Gemini
|
|
88
96
|
|
|
89
97
|
```sh
|
|
90
98
|
export GOOGLE_API_KEY="..."
|
|
91
99
|
```
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
### Ollama (local models)
|
|
102
|
+
|
|
103
|
+
1. Install Ollama: https://ollama.ai
|
|
104
|
+
2. Start the server:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
ollama serve
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
3. Pull a model:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
ollama pull mistral
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Optional: set defaults:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
export GIT_COMMIT_MESSAGE_PROVIDER=ollama
|
|
120
|
+
export OLLAMA_MODEL=mistral
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Note (fish):
|
|
94
124
|
|
|
95
125
|
```fish
|
|
96
126
|
set -x OPENAI_API_KEY "sk-..."
|
|
@@ -104,96 +134,130 @@ python -m pip install -e .
|
|
|
104
134
|
|
|
105
135
|
## Usage
|
|
106
136
|
|
|
107
|
-
|
|
137
|
+
Generate and print a commit message:
|
|
108
138
|
|
|
109
139
|
```sh
|
|
110
140
|
git add -A
|
|
111
141
|
git-commit-message "optional extra context about the change"
|
|
112
142
|
```
|
|
113
143
|
|
|
114
|
-
|
|
144
|
+
Generate a single-line subject only:
|
|
115
145
|
|
|
116
146
|
```sh
|
|
117
147
|
git-commit-message --one-line "optional context"
|
|
118
148
|
```
|
|
119
149
|
|
|
120
|
-
|
|
150
|
+
Select provider:
|
|
121
151
|
|
|
122
152
|
```sh
|
|
123
|
-
|
|
153
|
+
# OpenAI (default)
|
|
154
|
+
git-commit-message --provider openai
|
|
155
|
+
|
|
156
|
+
# Google Gemini (via google-genai)
|
|
157
|
+
git-commit-message --provider google
|
|
158
|
+
|
|
159
|
+
# Ollama
|
|
160
|
+
git-commit-message --provider ollama
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Commit immediately (optionally open editor):
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
git-commit-message --commit "refactor parser for speed"
|
|
167
|
+
git-commit-message --commit --edit "refactor parser for speed"
|
|
124
168
|
```
|
|
125
169
|
|
|
126
|
-
|
|
170
|
+
Amend the previous commit:
|
|
127
171
|
|
|
128
172
|
```sh
|
|
129
|
-
|
|
173
|
+
# print only (useful for pasting into a GUI editor)
|
|
174
|
+
git-commit-message --amend "optional context"
|
|
175
|
+
|
|
176
|
+
# amend immediately
|
|
177
|
+
git-commit-message --commit --amend "optional context"
|
|
178
|
+
|
|
179
|
+
# amend immediately, but open editor for final tweaks
|
|
180
|
+
git-commit-message --commit --amend --edit "optional context"
|
|
130
181
|
```
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
Limit subject length:
|
|
133
184
|
|
|
134
185
|
```sh
|
|
135
|
-
git-commit-message --one-line --max-length 50
|
|
186
|
+
git-commit-message --one-line --max-length 50
|
|
136
187
|
```
|
|
137
188
|
|
|
138
|
-
|
|
189
|
+
Chunk/summarise long diffs by token budget:
|
|
139
190
|
|
|
140
191
|
```sh
|
|
141
192
|
# force a single summary pass over the whole diff (default)
|
|
142
|
-
git-commit-message --chunk-tokens 0
|
|
193
|
+
git-commit-message --chunk-tokens 0
|
|
143
194
|
|
|
144
195
|
# chunk the diff into ~4000-token pieces before summarising
|
|
145
|
-
git-commit-message --chunk-tokens 4000
|
|
196
|
+
git-commit-message --chunk-tokens 4000
|
|
146
197
|
|
|
147
198
|
# disable summarisation and use the legacy one-shot prompt
|
|
148
|
-
git-commit-message --chunk-tokens -1
|
|
199
|
+
git-commit-message --chunk-tokens -1
|
|
149
200
|
```
|
|
150
201
|
|
|
151
|
-
|
|
202
|
+
Select output language/locale (IETF language tag):
|
|
152
203
|
|
|
153
204
|
```sh
|
|
154
|
-
git-commit-message --
|
|
205
|
+
git-commit-message --language en-US
|
|
206
|
+
git-commit-message --language ko-KR
|
|
207
|
+
git-commit-message --language ja-JP
|
|
155
208
|
```
|
|
156
209
|
|
|
157
|
-
|
|
210
|
+
Print debug info:
|
|
158
211
|
|
|
159
212
|
```sh
|
|
160
|
-
git-commit-message --debug
|
|
213
|
+
git-commit-message --debug
|
|
161
214
|
```
|
|
162
215
|
|
|
163
|
-
|
|
216
|
+
Configure Ollama host (if running on a different machine):
|
|
164
217
|
|
|
165
218
|
```sh
|
|
166
|
-
|
|
167
|
-
|
|
219
|
+
git-commit-message --provider ollama --host http://192.168.1.100:11434
|
|
220
|
+
```
|
|
168
221
|
|
|
169
|
-
|
|
170
|
-
git-commit-message --language ko-KR
|
|
222
|
+
## Options
|
|
171
223
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
224
|
+
- `--provider {openai,google,ollama}`: provider to use (default: `openai`)
|
|
225
|
+
- `--model MODEL`: model override (provider-specific)
|
|
226
|
+
- `--language TAG`: output language/locale (default: `en-GB`)
|
|
227
|
+
- `--one-line`: output subject only
|
|
228
|
+
- `--max-length N`: max subject length (default: 72)
|
|
229
|
+
- `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
|
|
230
|
+
- `--debug`: print request/response details
|
|
231
|
+
- `--commit`: run `git commit -m <message>`
|
|
232
|
+
- `--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
|
+
- `--edit`: with `--commit`, open editor for final message
|
|
234
|
+
- `--host URL`: host URL for providers like Ollama (default: `http://localhost:11434`)
|
|
235
|
+
|
|
236
|
+
## Environment variables
|
|
175
237
|
|
|
176
|
-
|
|
238
|
+
Required:
|
|
177
239
|
|
|
178
|
-
-
|
|
179
|
-
-
|
|
240
|
+
- `OPENAI_API_KEY`: when provider is `openai`
|
|
241
|
+
- `GOOGLE_API_KEY`: when provider is `google`
|
|
180
242
|
|
|
181
|
-
|
|
243
|
+
Optional:
|
|
182
244
|
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
185
|
-
- `
|
|
186
|
-
- `
|
|
187
|
-
- `
|
|
188
|
-
- `GIT_COMMIT_MESSAGE_LANGUAGE`:
|
|
189
|
-
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`:
|
|
245
|
+
- `GIT_COMMIT_MESSAGE_PROVIDER`: default provider (`openai` by default). `--provider` overrides this.
|
|
246
|
+
- `GIT_COMMIT_MESSAGE_MODEL`: model override for any provider. `--model` overrides this.
|
|
247
|
+
- `OPENAI_MODEL`: OpenAI-only model override (used if `--model`/`GIT_COMMIT_MESSAGE_MODEL` are not set)
|
|
248
|
+
- `OLLAMA_MODEL`: Ollama-only model override (used if `--model`/`GIT_COMMIT_MESSAGE_MODEL` are not set)
|
|
249
|
+
- `OLLAMA_HOST`: Ollama server URL (default: `http://localhost:11434`)
|
|
250
|
+
- `GIT_COMMIT_MESSAGE_LANGUAGE`: default language/locale (default: `en-GB`)
|
|
251
|
+
- `GIT_COMMIT_MESSAGE_CHUNK_TOKENS`: default chunk token budget (default: `0`)
|
|
190
252
|
|
|
191
|
-
|
|
253
|
+
Default models (if not overridden):
|
|
192
254
|
|
|
193
|
-
-
|
|
255
|
+
- OpenAI: `gpt-5-mini`
|
|
256
|
+
- Google: `gemini-2.5-flash`
|
|
257
|
+
- Ollama: `gpt-oss:20b`
|
|
194
258
|
|
|
195
|
-
## AI
|
|
259
|
+
## AI-generated code notice
|
|
196
260
|
|
|
197
261
|
Parts of this project were created with assistance from AI tools (e.g. large language models).
|
|
198
|
-
All AI
|
|
262
|
+
All AI-assisted contributions were reviewed and adapted by maintainers before inclusion.
|
|
199
263
|
If you need provenance for specific changes, please refer to the Git history and commit messages.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
git_commit_message/__init__.py,sha256=bmUVTlV1SYJAnoSaIKcpDCPkJ5JW2BANfFGvKt_A22w,190
|
|
2
|
+
git_commit_message/__main__.py,sha256=n5lvkLiCZ1Q4dwhEwonWntcKTeTaJL9qOJzdiLf0Gfk,99
|
|
3
|
+
git_commit_message/_cli.py,sha256=vF3HCFOUNtfYf1mUgSfUm_SocFpXfdOmTEDWHeuyS6Y,9726
|
|
4
|
+
git_commit_message/_gemini.py,sha256=QBKWnFCgc62KqC8agPhfO6eIizNj3V0oPTj6rqbTKiY,3487
|
|
5
|
+
git_commit_message/_git.py,sha256=qj8LuIA-qu4zQfe5CRzBPGNWVeicP9JZibVvCaXLoaY,6354
|
|
6
|
+
git_commit_message/_gpt.py,sha256=RSw3JyjuzfQ3sRHJu224CosfEHhjdUOGmRjU079G7io,2414
|
|
7
|
+
git_commit_message/_llm.py,sha256=_d0pKfs8BArx6RlJ-09DRMOj8WzYOmYQVJVTVxNli98,18279
|
|
8
|
+
git_commit_message/_ollama.py,sha256=-HI73J_n4kU7WGl3oBweRHw23JRWq7QH9tsEy-6dxlA,3308
|
|
9
|
+
git_commit_message-0.8.1.dist-info/METADATA,sha256=b1ONHLiGa723SrVBszsYPe6wa1VtUWVY75jFfL-73HU,7615
|
|
10
|
+
git_commit_message-0.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
git_commit_message-0.8.1.dist-info/entry_points.txt,sha256=e2cRvoyZnmP7yVItmFKwZofYG86WWKhm8KbzZSo2mf0,63
|
|
12
|
+
git_commit_message-0.8.1.dist-info/top_level.txt,sha256=qeP45y7y44R4KrPEihvMdwdM8tXYDY_3nCvCD3I9EcI,19
|
|
13
|
+
git_commit_message-0.8.1.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
git_commit_message/__init__.py,sha256=bmUVTlV1SYJAnoSaIKcpDCPkJ5JW2BANfFGvKt_A22w,190
|
|
2
|
-
git_commit_message/__main__.py,sha256=n5lvkLiCZ1Q4dwhEwonWntcKTeTaJL9qOJzdiLf0Gfk,99
|
|
3
|
-
git_commit_message/_cli.py,sha256=62lKSJmeW5JoEc-6-Xq2uZn_KK_o6zvU4V3oEYRcdUY,7982
|
|
4
|
-
git_commit_message/_gemini.py,sha256=AwevAnGB8LPekwVGroS2lwDVPszfGw0Xa_aPP9DP2yY,3400
|
|
5
|
-
git_commit_message/_git.py,sha256=foQIG6e4QLv00JAhQgMUQ1cw7WExxU5SFezfgXJ10XA,2424
|
|
6
|
-
git_commit_message/_gpt.py,sha256=in6hf78pQKt3PWEgw8-kD9X33yicY-Bg7COVcabCur8,2326
|
|
7
|
-
git_commit_message/_llm.py,sha256=ZUl1mRxuz37UIWXPXLBJJgppVS-a3HrnxdYTVvkXYm8,17699
|
|
8
|
-
git_commit_message-0.7.0.dist-info/METADATA,sha256=Z1Xni2T94vfpow9g_fHwrscaZETMtjNtnVklSAFYL7Y,6106
|
|
9
|
-
git_commit_message-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
git_commit_message-0.7.0.dist-info/entry_points.txt,sha256=e2cRvoyZnmP7yVItmFKwZofYG86WWKhm8KbzZSo2mf0,63
|
|
11
|
-
git_commit_message-0.7.0.dist-info/top_level.txt,sha256=qeP45y7y44R4KrPEihvMdwdM8tXYDY_3nCvCD3I9EcI,19
|
|
12
|
-
git_commit_message-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|