git-commit-message 0.4.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/__init__.py +10 -0
- git_commit_message/__main__.py +7 -0
- git_commit_message/_cli.py +222 -0
- git_commit_message/_git.py +114 -0
- git_commit_message/_gpt.py +266 -0
- git_commit_message-0.4.0.dist-info/METADATA +74 -0
- git_commit_message-0.4.0.dist-info/RECORD +10 -0
- git_commit_message-0.4.0.dist-info/WHEEL +5 -0
- git_commit_message-0.4.0.dist-info/entry_points.txt +2 -0
- git_commit_message-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Command-line interface entry point.
|
|
4
|
+
|
|
5
|
+
Collect staged changes from the repository and call an OpenAI GPT model
|
|
6
|
+
to generate a commit message, or create a commit straight away.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from argparse import ArgumentParser, Namespace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from ._git import commit_with_message, get_repo_root, get_staged_diff, has_staged_changes
|
|
15
|
+
from ._gpt import (
|
|
16
|
+
generate_commit_message,
|
|
17
|
+
generate_commit_message_with_info,
|
|
18
|
+
CommitMessageResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_parser() -> ArgumentParser:
|
|
23
|
+
"""Create the CLI argument parser.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
ArgumentParser
|
|
28
|
+
A configured argument parser.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
parser: ArgumentParser = ArgumentParser(
|
|
32
|
+
prog="git-commit-message",
|
|
33
|
+
description=(
|
|
34
|
+
"Generate a commit message with OpenAI GPT based on the staged changes."
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"description",
|
|
40
|
+
nargs="?",
|
|
41
|
+
help="Optional auxiliary description of the changes.",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--commit",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Commit immediately with the generated message.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--edit",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Open an editor to amend the message before committing. Use with '--commit'.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--model",
|
|
58
|
+
default=None,
|
|
59
|
+
help=(
|
|
60
|
+
"OpenAI model name to use. If unspecified, uses the environment variables (GIT_COMMIT_MESSAGE_MODEL, OPENAI_MODEL) or 'gpt-5-mini'."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--language",
|
|
66
|
+
dest="language",
|
|
67
|
+
default=None,
|
|
68
|
+
help=(
|
|
69
|
+
"Target language/locale IETF tag for the output (default: en-GB). "
|
|
70
|
+
"You may also set GIT_COMMIT_MESSAGE_LANGUAGE or OPENAI_LANGUAGE."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--debug",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Print the request/response and token usage.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--one-line",
|
|
82
|
+
dest="one_line",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Use only a single-line subject.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--max-length",
|
|
89
|
+
dest="max_length",
|
|
90
|
+
type=int,
|
|
91
|
+
default=None,
|
|
92
|
+
help="Maximum subject (first line) length (default: 72).",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run(
|
|
99
|
+
*,
|
|
100
|
+
args: Namespace,
|
|
101
|
+
) -> int:
|
|
102
|
+
"""Main execution logic.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
args
|
|
107
|
+
Parsed CLI arguments.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
int
|
|
112
|
+
Process exit code. 0 indicates success; any other value indicates failure.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
repo_root: Path = get_repo_root()
|
|
116
|
+
|
|
117
|
+
if not has_staged_changes(cwd=repo_root):
|
|
118
|
+
print("No staged changes. Run 'git add' and try again.", file=sys.stderr)
|
|
119
|
+
return 2
|
|
120
|
+
|
|
121
|
+
diff_text: str = get_staged_diff(cwd=repo_root)
|
|
122
|
+
|
|
123
|
+
hint: str | None = args.description if isinstance(args.description, str) else None
|
|
124
|
+
|
|
125
|
+
result: CommitMessageResult | None = None
|
|
126
|
+
try:
|
|
127
|
+
if args.debug:
|
|
128
|
+
result = generate_commit_message_with_info(
|
|
129
|
+
diff=diff_text,
|
|
130
|
+
hint=hint,
|
|
131
|
+
model=args.model,
|
|
132
|
+
single_line=getattr(args, "one_line", False),
|
|
133
|
+
subject_max=getattr(args, "max_length", None),
|
|
134
|
+
language=getattr(args, "language", None),
|
|
135
|
+
)
|
|
136
|
+
message = result.message
|
|
137
|
+
else:
|
|
138
|
+
message = generate_commit_message(
|
|
139
|
+
diff=diff_text,
|
|
140
|
+
hint=hint,
|
|
141
|
+
model=args.model,
|
|
142
|
+
single_line=getattr(args, "one_line", False),
|
|
143
|
+
subject_max=getattr(args, "max_length", None),
|
|
144
|
+
language=getattr(args, "language", None),
|
|
145
|
+
)
|
|
146
|
+
except Exception as exc: # noqa: BLE001 - to preserve standard output messaging
|
|
147
|
+
print(f"Failed to generate commit message: {exc}", file=sys.stderr)
|
|
148
|
+
return 3
|
|
149
|
+
|
|
150
|
+
# Option: force single-line message
|
|
151
|
+
if getattr(args, "one_line", False):
|
|
152
|
+
# Use the first non-empty line only
|
|
153
|
+
for line in (ln.strip() for ln in message.splitlines()):
|
|
154
|
+
if line:
|
|
155
|
+
message = line
|
|
156
|
+
break
|
|
157
|
+
else:
|
|
158
|
+
message = ""
|
|
159
|
+
|
|
160
|
+
if not args.commit:
|
|
161
|
+
if args.debug and result is not None:
|
|
162
|
+
# Print debug information
|
|
163
|
+
print("==== OpenAI Usage ====")
|
|
164
|
+
print(f"model: {result.model}")
|
|
165
|
+
print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
|
|
166
|
+
if result.total_tokens is not None:
|
|
167
|
+
print(
|
|
168
|
+
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
print("tokens: (provider did not return usage)")
|
|
172
|
+
print("\n==== Prompt ====")
|
|
173
|
+
print(result.prompt)
|
|
174
|
+
print("\n==== Response ====")
|
|
175
|
+
print(result.response_text)
|
|
176
|
+
print("\n==== Commit Message ====")
|
|
177
|
+
print(message)
|
|
178
|
+
else:
|
|
179
|
+
print(message)
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
if args.debug and result is not None:
|
|
183
|
+
# Also print debug info before commit
|
|
184
|
+
print("==== OpenAI Usage ====")
|
|
185
|
+
print(f"model: {result.model}")
|
|
186
|
+
print(f"response_id: {getattr(result, 'response_id', '(n/a)')}")
|
|
187
|
+
if result.total_tokens is not None:
|
|
188
|
+
print(
|
|
189
|
+
f"tokens: prompt={result.prompt_tokens} completion={result.completion_tokens} total={result.total_tokens}"
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
print("tokens: (provider did not return usage)")
|
|
193
|
+
print("\n==== Prompt ====")
|
|
194
|
+
print(result.prompt)
|
|
195
|
+
print("\n==== Response ====")
|
|
196
|
+
print(result.response_text)
|
|
197
|
+
print("\n==== Commit Message ====")
|
|
198
|
+
print(message)
|
|
199
|
+
|
|
200
|
+
if args.edit:
|
|
201
|
+
rc: int = commit_with_message(message=message, edit=True, cwd=repo_root)
|
|
202
|
+
else:
|
|
203
|
+
rc = commit_with_message(message=message, edit=False, cwd=repo_root)
|
|
204
|
+
|
|
205
|
+
return rc
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
"""Script entry point.
|
|
210
|
+
|
|
211
|
+
Parse command-line arguments, delegate to the execution logic, and exit with its code.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
parser: Final[ArgumentParser] = _build_parser()
|
|
215
|
+
args: Namespace = parser.parse_args()
|
|
216
|
+
|
|
217
|
+
if args.edit and not args.commit:
|
|
218
|
+
print("'--edit' must be used together with '--commit'.", file=sys.stderr)
|
|
219
|
+
sys.exit(2)
|
|
220
|
+
|
|
221
|
+
code: int = _run(args=args)
|
|
222
|
+
sys.exit(code)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Git-related helper functions.
|
|
4
|
+
|
|
5
|
+
Provides repository root discovery, extraction of staged changes, and
|
|
6
|
+
creating commits from a message.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_repo_root(
|
|
14
|
+
*,
|
|
15
|
+
cwd: Path | None = None,
|
|
16
|
+
) -> Path:
|
|
17
|
+
"""Find the repository root from the current working directory.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
cwd
|
|
22
|
+
Starting directory for the search. Defaults to the current working directory.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Path
|
|
27
|
+
The repository root path.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
start: Path = cwd or Path.cwd()
|
|
31
|
+
try:
|
|
32
|
+
out: bytes = subprocess.check_output(
|
|
33
|
+
[
|
|
34
|
+
"git",
|
|
35
|
+
"rev-parse",
|
|
36
|
+
"--show-toplevel",
|
|
37
|
+
],
|
|
38
|
+
cwd=str(start),
|
|
39
|
+
)
|
|
40
|
+
except subprocess.CalledProcessError as exc: # noqa: TRY003
|
|
41
|
+
raise RuntimeError("Not a Git repository.") from exc
|
|
42
|
+
|
|
43
|
+
root = Path(out.decode().strip())
|
|
44
|
+
return root
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def has_staged_changes(
|
|
48
|
+
*,
|
|
49
|
+
cwd: Path,
|
|
50
|
+
) -> bool:
|
|
51
|
+
"""Check whether there are staged changes."""
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
subprocess.check_call(
|
|
55
|
+
["git", "diff", "--cached", "--quiet", "--exit-code"],
|
|
56
|
+
cwd=str(cwd),
|
|
57
|
+
)
|
|
58
|
+
return False
|
|
59
|
+
except subprocess.CalledProcessError:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_staged_diff(
|
|
64
|
+
*,
|
|
65
|
+
cwd: Path,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Return the staged changes as diff text."""
|
|
68
|
+
|
|
69
|
+
out: bytes = subprocess.check_output(
|
|
70
|
+
[
|
|
71
|
+
"git",
|
|
72
|
+
"diff",
|
|
73
|
+
"--cached",
|
|
74
|
+
"--patch",
|
|
75
|
+
"--minimal",
|
|
76
|
+
"--no-color",
|
|
77
|
+
],
|
|
78
|
+
cwd=str(cwd),
|
|
79
|
+
)
|
|
80
|
+
return out.decode()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def commit_with_message(
|
|
84
|
+
*,
|
|
85
|
+
message: str,
|
|
86
|
+
edit: bool,
|
|
87
|
+
cwd: Path,
|
|
88
|
+
) -> int:
|
|
89
|
+
"""Create a commit with the given message.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
message
|
|
94
|
+
Commit message.
|
|
95
|
+
edit
|
|
96
|
+
If True, use the `--edit` flag to open an editor for amendments.
|
|
97
|
+
cwd
|
|
98
|
+
Git working directory.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
int
|
|
103
|
+
The subprocess exit code.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
cmd: list[str] = ["git", "commit", "-m", message]
|
|
107
|
+
if edit:
|
|
108
|
+
cmd.append("--edit")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
completed = subprocess.run(cmd, cwd=str(cwd), check=False)
|
|
112
|
+
return int(completed.returncode)
|
|
113
|
+
except OSError as exc: # e.g., editor launch failure, etc.
|
|
114
|
+
raise RuntimeError(f"Failed to run 'git commit': {exc}") from exc
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Generate Git commit messages by calling an OpenAI GPT model."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Final, Any, cast
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DEFAULT_MODEL: Final[str] = "gpt-5-mini"
|
|
11
|
+
_DEFAULT_LANGUAGE: Final[str] = "en-GB"
|
|
12
|
+
|
|
13
|
+
def _build_system_prompt(
|
|
14
|
+
*,
|
|
15
|
+
single_line: bool,
|
|
16
|
+
subject_max: int | None,
|
|
17
|
+
language: str,
|
|
18
|
+
) -> str:
|
|
19
|
+
max_len = subject_max or 72
|
|
20
|
+
if single_line:
|
|
21
|
+
return (
|
|
22
|
+
f"You are an expert Git commit message generator. "
|
|
23
|
+
f"Always use '{language}' spelling and style. "
|
|
24
|
+
f"Return a single-line imperative subject only (<= {max_len} chars). "
|
|
25
|
+
f"Do not include a body, bullet points, or any rationale. Do not include any line breaks. "
|
|
26
|
+
f"Consider the user-provided auxiliary context if present. "
|
|
27
|
+
f"Return only the commit message text (no code fences or prefixes like 'Commit message:')."
|
|
28
|
+
)
|
|
29
|
+
return (
|
|
30
|
+
f"You are an expert Git commit message generator. "
|
|
31
|
+
f"Always use '{language}' spelling and style. "
|
|
32
|
+
f"The subject line is mandatory: you MUST start the output with the subject as the very first non-empty line, "
|
|
33
|
+
f"in imperative mood, and keep it <= {max_len} chars. Insert exactly one blank line after the subject. "
|
|
34
|
+
f"Never start with bullets, headings, labels, or any other text. Then include a body in this format.\n\n"
|
|
35
|
+
f"Example format (do not include the --- lines in the output):\n\n"
|
|
36
|
+
f"---\n\n"
|
|
37
|
+
f"<Subject line>\n\n"
|
|
38
|
+
f"- <detail 1>\n"
|
|
39
|
+
f"- <detail 2>\n"
|
|
40
|
+
f"- <detail N>\n\n"
|
|
41
|
+
f"<Rationale label translated into the target language>: <1-2 concise sentences explaining the intent and why>\n\n"
|
|
42
|
+
f"---\n\n"
|
|
43
|
+
f"Guidelines:\n"
|
|
44
|
+
f"- The first non-empty line MUST be the subject line; include exactly one blank line after it.\n"
|
|
45
|
+
f"- Never place bullets, headings, or labels before the subject line.\n"
|
|
46
|
+
f"- Use '-' bullets; keep each bullet short (<= 1 line).\n"
|
|
47
|
+
f"- Prefer imperative mood verbs (Add, Fix, Update, Remove, Refactor, Document, etc.).\n"
|
|
48
|
+
f"- Focus on what changed and why; avoid copying diff hunks verbatim.\n"
|
|
49
|
+
f"- The only allowed label is the equivalent of 'Rationale:' translated into the target language; do not add other headings or prefaces.\n"
|
|
50
|
+
f"- Do not include the '---' delimiter lines, code fences, or any surrounding labels like 'Commit message:'.\n"
|
|
51
|
+
f"- Do not copy or reuse any example text verbatim; produce original content based on the provided diff and context.\n"
|
|
52
|
+
f"- If few details are necessary, include at least one bullet summarising the key change.\n"
|
|
53
|
+
f"- If you cannot provide any body content, still output the subject line; the subject line must never be omitted.\n"
|
|
54
|
+
f"- Consider the user-provided auxiliary context if present.\n"
|
|
55
|
+
f"Return only the commit message text in the above format (no code fences or extra labels)."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _system_message(
|
|
60
|
+
*,
|
|
61
|
+
single_line: bool,
|
|
62
|
+
subject_max: int | None,
|
|
63
|
+
language: str,
|
|
64
|
+
) -> dict[str, str]:
|
|
65
|
+
"""Create the system message dictionary."""
|
|
66
|
+
return {"role": "system", "content": _build_system_prompt(single_line=single_line, subject_max=subject_max, language=language)}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CommitMessageResult:
|
|
70
|
+
"""Hold the generated commit message and debugging information.
|
|
71
|
+
|
|
72
|
+
Notes
|
|
73
|
+
-----
|
|
74
|
+
Treat all fields as read-only by convention.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
__slots__ = (
|
|
78
|
+
"message",
|
|
79
|
+
"model",
|
|
80
|
+
"prompt",
|
|
81
|
+
"response_text",
|
|
82
|
+
"response_id",
|
|
83
|
+
"prompt_tokens",
|
|
84
|
+
"completion_tokens",
|
|
85
|
+
"total_tokens",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
/,
|
|
91
|
+
*,
|
|
92
|
+
message: str,
|
|
93
|
+
model: str,
|
|
94
|
+
prompt: str,
|
|
95
|
+
response_text: str,
|
|
96
|
+
response_id: str | None,
|
|
97
|
+
prompt_tokens: int | None,
|
|
98
|
+
completion_tokens: int | None,
|
|
99
|
+
total_tokens: int | None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.message = message
|
|
102
|
+
self.model = model
|
|
103
|
+
self.prompt = prompt
|
|
104
|
+
self.response_text = response_text
|
|
105
|
+
self.response_id = response_id
|
|
106
|
+
self.prompt_tokens = prompt_tokens
|
|
107
|
+
self.completion_tokens = completion_tokens
|
|
108
|
+
self.total_tokens = total_tokens
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_model(
|
|
112
|
+
*,
|
|
113
|
+
model: str | None,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Resolve the model name."""
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
model
|
|
119
|
+
or os.environ.get("GIT_COMMIT_MESSAGE_MODEL")
|
|
120
|
+
or os.environ.get("OPENAI_MODEL")
|
|
121
|
+
or _DEFAULT_MODEL
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resolve_language(
|
|
126
|
+
*,
|
|
127
|
+
language: str | None,
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Resolve the target language/locale tag used for output style."""
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
language
|
|
133
|
+
or os.environ.get("GIT_COMMIT_MESSAGE_LANGUAGE")
|
|
134
|
+
or os.environ.get("OPENAI_LANGUAGE")
|
|
135
|
+
or _DEFAULT_LANGUAGE
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _build_user_messages(
|
|
140
|
+
*,
|
|
141
|
+
diff: str,
|
|
142
|
+
hint: str | None,
|
|
143
|
+
) -> tuple[str, list[dict[str, str]]]:
|
|
144
|
+
"""Compose user messages, separating auxiliary context and diff.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
tuple[str, list[dict[str, str]]]
|
|
149
|
+
The first element is the combined string for debugging output; the
|
|
150
|
+
second is the list of user messages to send to the Chat Completions API.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
hint_content: str | None = (
|
|
154
|
+
f"# Auxiliary context (user-provided)\n{hint}" if hint else None
|
|
155
|
+
)
|
|
156
|
+
diff_content: str = f"# Changes (diff)\n{diff}"
|
|
157
|
+
|
|
158
|
+
messages: list[dict[str, str]] = []
|
|
159
|
+
if hint_content:
|
|
160
|
+
messages.append({"role": "user", "content": hint_content})
|
|
161
|
+
messages.append({"role": "user", "content": diff_content})
|
|
162
|
+
|
|
163
|
+
combined_prompt: str = "\n\n".join(
|
|
164
|
+
[part for part in (hint_content, diff_content) if part is not None]
|
|
165
|
+
)
|
|
166
|
+
return combined_prompt, messages
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def generate_commit_message(
|
|
170
|
+
*,
|
|
171
|
+
diff: str,
|
|
172
|
+
hint: str | None,
|
|
173
|
+
model: str | None,
|
|
174
|
+
single_line: bool = False,
|
|
175
|
+
subject_max: int | None = None,
|
|
176
|
+
language: str | None = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Generate a commit message using an OpenAI GPT model."""
|
|
179
|
+
|
|
180
|
+
chosen_model: str = _resolve_model(model=model)
|
|
181
|
+
chosen_language: str = _resolve_language(language=language)
|
|
182
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
183
|
+
if not api_key:
|
|
184
|
+
raise RuntimeError("The OPENAI_API_KEY environment variable is required.")
|
|
185
|
+
|
|
186
|
+
client = OpenAI(api_key=api_key)
|
|
187
|
+
|
|
188
|
+
_combined_prompt, user_messages = _build_user_messages(diff=diff, hint=hint)
|
|
189
|
+
|
|
190
|
+
# Use Chat Completions API to generate a single response (send hint and diff as separate user messages)
|
|
191
|
+
all_messages: list[dict[str, str]] = [
|
|
192
|
+
_system_message(single_line=single_line, subject_max=subject_max, language=chosen_language),
|
|
193
|
+
*user_messages,
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
resp = client.chat.completions.create(
|
|
197
|
+
model=chosen_model,
|
|
198
|
+
messages=cast(Any, all_messages),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
text: str = (resp.choices[0].message.content or "").strip()
|
|
202
|
+
if not text:
|
|
203
|
+
raise RuntimeError("An empty commit message was generated.")
|
|
204
|
+
return text
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def generate_commit_message_with_info(
|
|
208
|
+
*,
|
|
209
|
+
diff: str,
|
|
210
|
+
hint: str | None,
|
|
211
|
+
model: str | None,
|
|
212
|
+
single_line: bool = False,
|
|
213
|
+
subject_max: int | None = None,
|
|
214
|
+
language: str | None = None,
|
|
215
|
+
) -> CommitMessageResult:
|
|
216
|
+
"""Return the OpenAI GPT call result together with debugging information.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
CommitMessageResult
|
|
221
|
+
The generated message, token usage, and prompt/response text.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
chosen_model: str = _resolve_model(model=model)
|
|
225
|
+
chosen_language: str = _resolve_language(language=language)
|
|
226
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
227
|
+
if not api_key:
|
|
228
|
+
raise RuntimeError("The OPENAI_API_KEY environment variable is required.")
|
|
229
|
+
|
|
230
|
+
client = OpenAI(api_key=api_key)
|
|
231
|
+
combined_prompt, user_messages = _build_user_messages(diff=diff, hint=hint)
|
|
232
|
+
|
|
233
|
+
all_messages = [
|
|
234
|
+
_system_message(single_line=single_line, subject_max=subject_max, language=chosen_language),
|
|
235
|
+
*user_messages,
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
resp = client.chat.completions.create(
|
|
239
|
+
model=chosen_model,
|
|
240
|
+
messages=cast(Any, all_messages),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
response_text: str = (resp.choices[0].message.content or "").strip()
|
|
244
|
+
if not response_text:
|
|
245
|
+
raise RuntimeError("An empty commit message was generated.")
|
|
246
|
+
|
|
247
|
+
response_id: str | None = getattr(resp, "id", None)
|
|
248
|
+
usage = getattr(resp, "usage", None)
|
|
249
|
+
prompt_tokens: int | None = None
|
|
250
|
+
completion_tokens: int | None = None
|
|
251
|
+
total_tokens: int | None = None
|
|
252
|
+
if usage is not None:
|
|
253
|
+
prompt_tokens = getattr(usage, "prompt_tokens", None)
|
|
254
|
+
completion_tokens = getattr(usage, "completion_tokens", None)
|
|
255
|
+
total_tokens = getattr(usage, "total_tokens", None)
|
|
256
|
+
|
|
257
|
+
return CommitMessageResult(
|
|
258
|
+
message=response_text,
|
|
259
|
+
model=chosen_model,
|
|
260
|
+
prompt=combined_prompt,
|
|
261
|
+
response_text=response_text,
|
|
262
|
+
response_id=response_id,
|
|
263
|
+
prompt_tokens=prompt_tokens,
|
|
264
|
+
completion_tokens=completion_tokens,
|
|
265
|
+
total_tokens=total_tokens,
|
|
266
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-commit-message
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Generate Git commit messages from staged changes using OpenAI GPT
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: openai>=2.6.1
|
|
8
|
+
|
|
9
|
+
# git-commit-message
|
|
10
|
+
|
|
11
|
+
Staged changes -> GPT commit message generator.
|
|
12
|
+
|
|
13
|
+
## Install (editable)
|
|
14
|
+
|
|
15
|
+
```fish
|
|
16
|
+
python -m pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
- Print commit message only:
|
|
22
|
+
|
|
23
|
+
```fish
|
|
24
|
+
git add -A
|
|
25
|
+
git-commit-message "optional extra context about the change"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Force single-line subject only:
|
|
29
|
+
|
|
30
|
+
```fish
|
|
31
|
+
git-commit-message --one-line "optional context"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Limit subject length (default 72):
|
|
35
|
+
|
|
36
|
+
```fish
|
|
37
|
+
git-commit-message --one-line --max-length 50 "optional context"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Commit immediately with editor:
|
|
41
|
+
|
|
42
|
+
```fish
|
|
43
|
+
git-commit-message --commit --edit "refactor parser for speed"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Select output language/locale (default: en-GB):
|
|
47
|
+
|
|
48
|
+
```fish
|
|
49
|
+
# American English
|
|
50
|
+
git-commit-message --language en-US "optional context"
|
|
51
|
+
|
|
52
|
+
# Korean
|
|
53
|
+
git-commit-message --language ko-KR
|
|
54
|
+
|
|
55
|
+
# Japanese
|
|
56
|
+
git-commit-message --language ja-JP
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Notes:
|
|
60
|
+
|
|
61
|
+
- The model is instructed to write using the selected language/locale.
|
|
62
|
+
- In multi-line mode, the only allowed label ("Rationale:") is also translated into the target language.
|
|
63
|
+
|
|
64
|
+
Environment:
|
|
65
|
+
|
|
66
|
+
- `OPENAI_API_KEY`: required
|
|
67
|
+
- `GIT_COMMIT_MESSAGE_MODEL` or `OPENAI_MODEL`: optional (default: `gpt-5-mini`)
|
|
68
|
+
- `GIT_COMMIT_MESSAGE_LANGUAGE` or `OPENAI_LANGUAGE`: optional (default: `en-GB`)
|
|
69
|
+
|
|
70
|
+
## AI‑generated code notice
|
|
71
|
+
|
|
72
|
+
Parts of this project were created with assistance from AI tools (e.g. large language models).
|
|
73
|
+
All AI‑assisted contributions were reviewed and adapted by maintainers before inclusion.
|
|
74
|
+
If you need provenance for specific changes, please refer to the Git history and commit messages.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
git_commit_message/__init__.py,sha256=cJvTj8-8_I1VYz3EO7Eq1LJZ6RS-WhNSAhjVVliVLtU,196
|
|
2
|
+
git_commit_message/__main__.py,sha256=n5lvkLiCZ1Q4dwhEwonWntcKTeTaJL9qOJzdiLf0Gfk,99
|
|
3
|
+
git_commit_message/_cli.py,sha256=N1ZF3hPTnIO-K6vh-VgT9JnqTOeWP-Xp2xZJ6Rz1Nig,6510
|
|
4
|
+
git_commit_message/_git.py,sha256=-FNXmFtlsbuArgCvEcCqpXSB7CjwdLdhoXgdZk1qtcE,2435
|
|
5
|
+
git_commit_message/_gpt.py,sha256=etsE_IcxR-atZKPzJqOl30wlOxtcRGTK6tTLm28L-U8,9040
|
|
6
|
+
git_commit_message-0.4.0.dist-info/METADATA,sha256=VHuXMRfyiGJNqsRSjgJTZ4onUh7CKXPQIqHJWiyMpSw,1753
|
|
7
|
+
git_commit_message-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
git_commit_message-0.4.0.dist-info/entry_points.txt,sha256=e2cRvoyZnmP7yVItmFKwZofYG86WWKhm8KbzZSo2mf0,63
|
|
9
|
+
git_commit_message-0.4.0.dist-info/top_level.txt,sha256=qeP45y7y44R4KrPEihvMdwdM8tXYDY_3nCvCD3I9EcI,19
|
|
10
|
+
git_commit_message-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
git_commit_message
|