git-commit-message 0.8.0__tar.gz → 0.8.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.
Files changed (20) hide show
  1. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/PKG-INFO +16 -2
  2. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/README.md +15 -1
  3. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/pyproject.toml +1 -1
  4. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/_cli.py +28 -6
  5. git_commit_message-0.8.1/src/git_commit_message/_git.py +269 -0
  6. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message.egg-info/PKG-INFO +16 -2
  7. git_commit_message-0.8.0/src/git_commit_message/_git.py +0 -114
  8. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/UNLICENSE +0 -0
  9. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/setup.cfg +0 -0
  10. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/__init__.py +0 -0
  11. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/__main__.py +0 -0
  12. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/_gemini.py +0 -0
  13. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/_gpt.py +0 -0
  14. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/_llm.py +0 -0
  15. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message/_ollama.py +0 -0
  16. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message.egg-info/SOURCES.txt +0 -0
  17. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message.egg-info/dependency_links.txt +0 -0
  18. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message.egg-info/entry_points.txt +0 -0
  19. {git_commit_message-0.8.0 → git_commit_message-0.8.1}/src/git_commit_message.egg-info/requires.txt +0 -0
  20. {git_commit_message-0.8.0 → git_commit_message-0.8.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.8.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.
@@ -58,7 +58,7 @@ Generate a commit message from your staged changes using OpenAI, Google Gemini,
58
58
  ## Requirements
59
59
 
60
60
  - Python 3.13+
61
- - A Git repo with staged changes (`git add ...`)
61
+ - A Git repo with staged changes (`git add ...`) (or use `--amend` even if nothing is staged)
62
62
 
63
63
  ## Install
64
64
 
@@ -167,6 +167,19 @@ git-commit-message --commit "refactor parser for speed"
167
167
  git-commit-message --commit --edit "refactor parser for speed"
168
168
  ```
169
169
 
170
+ Amend the previous commit:
171
+
172
+ ```sh
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"
181
+ ```
182
+
170
183
  Limit subject length:
171
184
 
172
185
  ```sh
@@ -216,6 +229,7 @@ git-commit-message --provider ollama --host http://192.168.1.100:11434
216
229
  - `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
217
230
  - `--debug`: print request/response details
218
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`)
219
233
  - `--edit`: with `--commit`, open editor for final message
220
234
  - `--host URL`: host URL for providers like Ollama (default: `http://localhost:11434`)
221
235
 
@@ -7,7 +7,7 @@ Generate a commit message from your staged changes using OpenAI, Google Gemini,
7
7
  ## Requirements
8
8
 
9
9
  - Python 3.13+
10
- - A Git repo with staged changes (`git add ...`)
10
+ - A Git repo with staged changes (`git add ...`) (or use `--amend` even if nothing is staged)
11
11
 
12
12
  ## Install
13
13
 
@@ -116,6 +116,19 @@ git-commit-message --commit "refactor parser for speed"
116
116
  git-commit-message --commit --edit "refactor parser for speed"
117
117
  ```
118
118
 
119
+ Amend the previous commit:
120
+
121
+ ```sh
122
+ # print only (useful for pasting into a GUI editor)
123
+ git-commit-message --amend "optional context"
124
+
125
+ # amend immediately
126
+ git-commit-message --commit --amend "optional context"
127
+
128
+ # amend immediately, but open editor for final tweaks
129
+ git-commit-message --commit --amend --edit "optional context"
130
+ ```
131
+
119
132
  Limit subject length:
120
133
 
121
134
  ```sh
@@ -165,6 +178,7 @@ git-commit-message --provider ollama --host http://192.168.1.100:11434
165
178
  - `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
166
179
  - `--debug`: print request/response details
167
180
  - `--commit`: run `git commit -m <message>`
181
+ - `--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`)
168
182
  - `--edit`: with `--commit`, open editor for final message
169
183
  - `--host URL`: host URL for providers like Ollama (default: `http://localhost:11434`)
170
184
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "git-commit-message"
3
- version = "0.8.0"
3
+ version = "0.8.1"
4
4
  description = "Generate Git commit messages from staged changes using LLM"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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,
@@ -31,6 +33,7 @@ class CliArgs(Namespace):
31
33
  __slots__ = (
32
34
  "description",
33
35
  "commit",
36
+ "amend",
34
37
  "edit",
35
38
  "provider",
36
39
  "model",
@@ -48,6 +51,7 @@ class CliArgs(Namespace):
48
51
  ) -> None:
49
52
  self.description: str | None = None
50
53
  self.commit: bool = False
54
+ self.amend: bool = False
51
55
  self.edit: bool = False
52
56
  self.provider: str | None = None
53
57
  self.model: str | None = None
@@ -99,6 +103,16 @@ def _build_parser() -> ArgumentParser:
99
103
  help="Commit immediately with the generated message.",
100
104
  )
101
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
+
102
116
  parser.add_argument(
103
117
  "--edit",
104
118
  action="store_true",
@@ -198,11 +212,19 @@ def _run(
198
212
 
199
213
  repo_root: Path = get_repo_root()
200
214
 
201
- if not has_staged_changes(repo_root):
202
- print("No staged changes. Run 'git add' and try again.", file=stderr)
203
- return 2
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
204
226
 
205
- diff_text: str = get_staged_diff(repo_root)
227
+ diff_text = get_staged_diff(repo_root)
206
228
 
207
229
  hint: str | None = args.description if isinstance(args.description, str) else None
208
230
 
@@ -299,9 +321,9 @@ def _run(
299
321
  print(message)
300
322
 
301
323
  if args.edit:
302
- rc: int = commit_with_message(message, True, repo_root)
324
+ rc: int = commit_with_message(message, True, repo_root, amend=args.amend)
303
325
  else:
304
- rc = commit_with_message(message, False, repo_root)
326
+ rc = commit_with_message(message, False, repo_root, amend=args.amend)
305
327
 
306
328
  return rc
307
329
 
@@ -0,0 +1,269 @@
1
+ """Git-related helper functions.
2
+
3
+ Provides repository root discovery, extraction of staged changes, and
4
+ creating commits from a message.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from subprocess import CalledProcessError, check_call, check_output, run
11
+
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
+
68
+ def get_repo_root(
69
+ cwd: Path | None = None,
70
+ /,
71
+ ) -> Path:
72
+ """Find the repository root from the current working directory.
73
+
74
+ Parameters
75
+ ----------
76
+ cwd
77
+ Starting directory for the search. Defaults to the current working directory.
78
+
79
+ Returns
80
+ -------
81
+ Path
82
+ The repository root path.
83
+ """
84
+
85
+ start: Path = cwd or Path.cwd()
86
+ try:
87
+ out: bytes = check_output(
88
+ [
89
+ "git",
90
+ "rev-parse",
91
+ "--show-toplevel",
92
+ ],
93
+ cwd=str(start),
94
+ )
95
+ except CalledProcessError as exc: # noqa: TRY003
96
+ raise RuntimeError("Not a Git repository.") from exc
97
+
98
+ root = Path(out.decode().strip())
99
+ return root
100
+
101
+
102
+ def has_staged_changes(
103
+ cwd: Path,
104
+ /,
105
+ ) -> bool:
106
+ """Check whether there are staged changes."""
107
+
108
+ try:
109
+ check_call(
110
+ ["git", "diff", "--cached", "--quiet", "--exit-code"],
111
+ cwd=str(cwd),
112
+ )
113
+ return False
114
+ except CalledProcessError:
115
+ return True
116
+
117
+
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(
145
+ cwd: Path,
146
+ /,
147
+ ) -> str:
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^"],
172
+ cwd=str(cwd),
173
+ check=False,
174
+ capture_output=True,
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
+
226
+ return out.decode()
227
+
228
+
229
+ def commit_with_message(
230
+ message: str,
231
+ edit: bool,
232
+ cwd: Path,
233
+ /,
234
+ *,
235
+ amend: bool = False,
236
+ ) -> int:
237
+ """Create a commit with the given message.
238
+
239
+ Parameters
240
+ ----------
241
+ message
242
+ Commit message.
243
+ edit
244
+ If True, use the `--edit` flag to open an editor for amendments.
245
+ cwd
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.
250
+
251
+ Returns
252
+ -------
253
+ int
254
+ The subprocess exit code.
255
+ """
256
+
257
+ cmd: list[str] = ["git", "commit"]
258
+ if amend:
259
+ cmd.append("--amend")
260
+
261
+ cmd.extend(["-m", message])
262
+ if edit:
263
+ cmd.append("--edit")
264
+
265
+ try:
266
+ completed = run(cmd, cwd=str(cwd), check=False)
267
+ return int(completed.returncode)
268
+ except OSError as exc: # e.g., editor launch failure, etc.
269
+ raise RuntimeError(f"Failed to run 'git commit': {exc}") from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-message
3
- Version: 0.8.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.
@@ -58,7 +58,7 @@ Generate a commit message from your staged changes using OpenAI, Google Gemini,
58
58
  ## Requirements
59
59
 
60
60
  - Python 3.13+
61
- - A Git repo with staged changes (`git add ...`)
61
+ - A Git repo with staged changes (`git add ...`) (or use `--amend` even if nothing is staged)
62
62
 
63
63
  ## Install
64
64
 
@@ -167,6 +167,19 @@ git-commit-message --commit "refactor parser for speed"
167
167
  git-commit-message --commit --edit "refactor parser for speed"
168
168
  ```
169
169
 
170
+ Amend the previous commit:
171
+
172
+ ```sh
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"
181
+ ```
182
+
170
183
  Limit subject length:
171
184
 
172
185
  ```sh
@@ -216,6 +229,7 @@ git-commit-message --provider ollama --host http://192.168.1.100:11434
216
229
  - `--chunk-tokens N`: token budget per diff chunk (`0` = single summary pass, `-1` disables summarisation)
217
230
  - `--debug`: print request/response details
218
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`)
219
233
  - `--edit`: with `--commit`, open editor for final message
220
234
  - `--host URL`: host URL for providers like Ollama (default: `http://localhost:11434`)
221
235
 
@@ -1,114 +0,0 @@
1
- """Git-related helper functions.
2
-
3
- Provides repository root discovery, extraction of staged changes, and
4
- creating commits from a message.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from pathlib import Path
10
- from subprocess import CalledProcessError, check_call, check_output, run
11
-
12
-
13
- def get_repo_root(
14
- cwd: Path | None = None,
15
- /,
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 = check_output(
33
- [
34
- "git",
35
- "rev-parse",
36
- "--show-toplevel",
37
- ],
38
- cwd=str(start),
39
- )
40
- except 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
- cwd: Path,
49
- /,
50
- ) -> bool:
51
- """Check whether there are staged changes."""
52
-
53
- try:
54
- check_call(
55
- ["git", "diff", "--cached", "--quiet", "--exit-code"],
56
- cwd=str(cwd),
57
- )
58
- return False
59
- except CalledProcessError:
60
- return True
61
-
62
-
63
- def get_staged_diff(
64
- cwd: Path,
65
- /,
66
- ) -> str:
67
- """Return the staged changes as diff text."""
68
-
69
- out: bytes = 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
- message: str,
85
- edit: bool,
86
- cwd: Path,
87
- /,
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 = 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