git-hunk 0.3.1__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_hunk-0.3.1 → git_hunk-0.4.1}/PKG-INFO +9 -5
- {git_hunk-0.3.1 → git_hunk-0.4.1}/README.md +8 -4
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_cli.py +56 -13
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_git.py +12 -5
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_ui.py +1 -1
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/skills/core/SKILL.md +2 -1
- {git_hunk-0.3.1 → git_hunk-0.4.1}/pyproject.toml +1 -1
- {git_hunk-0.3.1 → git_hunk-0.4.1}/.gitignore +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/LICENSE +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/__init__.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/__main__.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_hunk.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_lines.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_patch.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/_skills.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.1}/git_hunk/skills/logical-commits/SKILL.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-hunk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Non-interactive, programmatic git hunk staging with durable Hunk IDs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/wkentaro/git-hunk
|
|
6
6
|
Project-URL: Issues, https://github.com/wkentaro/git-hunk/issues
|
|
@@ -71,7 +71,7 @@ variant: organize a dirty working tree into correct, focused commits, once
|
|
|
71
71
|
following git-hunk's bundled skills and once restricted to bare Git. The
|
|
72
72
|
[checked-in eval harness](https://github.com/wkentaro/git-hunk/tree/0ef14bef657e187b5ab7283b0dbd64751a02736f/eval)
|
|
73
73
|
grades the exact resulting repository state — commit partition and order, final
|
|
74
|
-
tree, index, and leftovers. This table records the qualifying run; `
|
|
74
|
+
tree, index, and leftovers. This table records the qualifying run; `just eval`
|
|
75
75
|
reruns the protocol and prints a table in the same format.
|
|
76
76
|
|
|
77
77
|
| Task | git-hunk | bare Git |
|
|
@@ -370,12 +370,16 @@ content and creates new IDs.
|
|
|
370
370
|
Bug reports, feature requests, and pull requests are welcome on
|
|
371
371
|
[GitHub](https://github.com/wkentaro/git-hunk).
|
|
372
372
|
|
|
373
|
+
Install [just](https://just.systems/man/en/packages.html) 1.58.0 or newer and
|
|
374
|
+
[uv](https://docs.astral.sh/uv/getting-started/installation/). Recipes use Bash;
|
|
375
|
+
on Windows, install Git for Windows and make its Bash available on `PATH`.
|
|
376
|
+
|
|
373
377
|
```bash
|
|
374
378
|
git clone https://github.com/wkentaro/git-hunk.git
|
|
375
379
|
cd git-hunk
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
380
|
+
just setup # install dependencies
|
|
381
|
+
just test # run tests
|
|
382
|
+
just lint # run linters
|
|
379
383
|
```
|
|
380
384
|
|
|
381
385
|
## License
|
|
@@ -43,7 +43,7 @@ variant: organize a dirty working tree into correct, focused commits, once
|
|
|
43
43
|
following git-hunk's bundled skills and once restricted to bare Git. The
|
|
44
44
|
[checked-in eval harness](https://github.com/wkentaro/git-hunk/tree/0ef14bef657e187b5ab7283b0dbd64751a02736f/eval)
|
|
45
45
|
grades the exact resulting repository state — commit partition and order, final
|
|
46
|
-
tree, index, and leftovers. This table records the qualifying run; `
|
|
46
|
+
tree, index, and leftovers. This table records the qualifying run; `just eval`
|
|
47
47
|
reruns the protocol and prints a table in the same format.
|
|
48
48
|
|
|
49
49
|
| Task | git-hunk | bare Git |
|
|
@@ -342,12 +342,16 @@ content and creates new IDs.
|
|
|
342
342
|
Bug reports, feature requests, and pull requests are welcome on
|
|
343
343
|
[GitHub](https://github.com/wkentaro/git-hunk).
|
|
344
344
|
|
|
345
|
+
Install [just](https://just.systems/man/en/packages.html) 1.58.0 or newer and
|
|
346
|
+
[uv](https://docs.astral.sh/uv/getting-started/installation/). Recipes use Bash;
|
|
347
|
+
on Windows, install Git for Windows and make its Bash available on `PATH`.
|
|
348
|
+
|
|
345
349
|
```bash
|
|
346
350
|
git clone https://github.com/wkentaro/git-hunk.git
|
|
347
351
|
cd git-hunk
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
just setup # install dependencies
|
|
353
|
+
just test # run tests
|
|
354
|
+
just lint # run linters
|
|
351
355
|
```
|
|
352
356
|
|
|
353
357
|
## License
|
|
@@ -6,6 +6,7 @@ import stat
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from dataclasses import replace
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import Final
|
|
10
11
|
|
|
11
12
|
import click
|
|
@@ -19,7 +20,7 @@ from ._git import get_diff
|
|
|
19
20
|
from ._git import get_unmerged_files
|
|
20
21
|
from ._git import get_unsupported_changes
|
|
21
22
|
from ._git import get_untracked_files
|
|
22
|
-
from ._git import
|
|
23
|
+
from ._git import get_worktree_context
|
|
23
24
|
from ._git import stage_files
|
|
24
25
|
from ._git import unstage_added_files
|
|
25
26
|
from ._git import unstage_files
|
|
@@ -68,6 +69,9 @@ JSON_SCHEMA_VERSION: Final = 2
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
class CliError(Exception):
|
|
72
|
+
tip: str | None
|
|
73
|
+
usage: str | None
|
|
74
|
+
|
|
71
75
|
def __init__(
|
|
72
76
|
self,
|
|
73
77
|
message: str,
|
|
@@ -103,9 +107,9 @@ class CliGroup(click.Group):
|
|
|
103
107
|
ctx.exit(130)
|
|
104
108
|
|
|
105
109
|
|
|
106
|
-
def
|
|
110
|
+
def _require_worktree_context() -> tuple[str, str]:
|
|
107
111
|
try:
|
|
108
|
-
return
|
|
112
|
+
return get_worktree_context()
|
|
109
113
|
# Usually there is no worktree to anchor to, but rev-parse also refuses on
|
|
110
114
|
# dubious ownership or a bad config value, and those say how to fix them.
|
|
111
115
|
# Classifying git's English is what this avoids; passing it on is not.
|
|
@@ -243,10 +247,16 @@ def _make_targets(
|
|
|
243
247
|
|
|
244
248
|
|
|
245
249
|
def _select_hunks(
|
|
246
|
-
*,
|
|
250
|
+
*,
|
|
251
|
+
hunks: list[Hunk],
|
|
252
|
+
targets: list[_Target],
|
|
253
|
+
inventory_hunks: list[Hunk],
|
|
254
|
+
invocation_prefix: str,
|
|
255
|
+
worktree_root: str,
|
|
247
256
|
) -> list[Hunk]:
|
|
248
257
|
files = {h.file for h in hunks}
|
|
249
258
|
eligible_ids = {hunk.id for hunk in hunks}
|
|
259
|
+
resolved_worktree_root = Path(worktree_root).resolve()
|
|
250
260
|
selected: list[Hunk] = []
|
|
251
261
|
seen: set[str] = set()
|
|
252
262
|
for target in targets:
|
|
@@ -259,9 +269,32 @@ def _select_hunks(
|
|
|
259
269
|
if matches[0].id not in eligible_ids:
|
|
260
270
|
raise CliError(f"hunk '{target.arg}' is not eligible for this command")
|
|
261
271
|
else:
|
|
272
|
+
operand_path = resolved_worktree_root / target.path
|
|
273
|
+
if (
|
|
274
|
+
operand_path.is_dir()
|
|
275
|
+
and operand_path.resolve().is_relative_to(resolved_worktree_root)
|
|
276
|
+
and any(
|
|
277
|
+
target.path == "." or path.startswith(f"{target.path}/")
|
|
278
|
+
for path in files
|
|
279
|
+
)
|
|
280
|
+
):
|
|
281
|
+
raise CliError(
|
|
282
|
+
f"directory operands are not expanded: '{target.arg}'",
|
|
283
|
+
tip="pass exact Repository paths (a shell-expanded glob is "
|
|
284
|
+
"acceptable when it covers the intended tracked files)",
|
|
285
|
+
)
|
|
286
|
+
suggested_path = posixpath.normpath(
|
|
287
|
+
posixpath.join(invocation_prefix, target.path)
|
|
288
|
+
)
|
|
289
|
+
tip = (
|
|
290
|
+
"Repository paths are worktree-root-relative; "
|
|
291
|
+
f"did you mean '{suggested_path}'?"
|
|
292
|
+
if suggested_path in files
|
|
293
|
+
else "run 'git-hunk list' to see changed files and hunk ids"
|
|
294
|
+
)
|
|
262
295
|
raise CliError(
|
|
263
296
|
f"no changed file matches '{target.arg}'",
|
|
264
|
-
tip=
|
|
297
|
+
tip=tip,
|
|
265
298
|
)
|
|
266
299
|
for hunk in matches:
|
|
267
300
|
if hunk.id not in seen:
|
|
@@ -368,6 +401,7 @@ def _apply_selection(
|
|
|
368
401
|
targets: list[_Target],
|
|
369
402
|
selection: _Selection,
|
|
370
403
|
worktree_root: str,
|
|
404
|
+
invocation_prefix: str,
|
|
371
405
|
staged: bool,
|
|
372
406
|
cached: bool,
|
|
373
407
|
reverse: bool,
|
|
@@ -379,7 +413,11 @@ def _apply_selection(
|
|
|
379
413
|
hunks = [hunk for hunk in inventory.hunks if hunk.status == status]
|
|
380
414
|
diff_output = inventory.staged_diff if staged else inventory.unstaged_diff
|
|
381
415
|
selected = _select_hunks(
|
|
382
|
-
hunks=hunks,
|
|
416
|
+
hunks=hunks,
|
|
417
|
+
targets=targets,
|
|
418
|
+
inventory_hunks=inventory.hunks,
|
|
419
|
+
invocation_prefix=invocation_prefix,
|
|
420
|
+
worktree_root=worktree_root,
|
|
383
421
|
)
|
|
384
422
|
selected = _apply_line_filter(hunks=selected, selection=selection, reverse=reverse)
|
|
385
423
|
|
|
@@ -473,7 +511,7 @@ def _run_patch_command(
|
|
|
473
511
|
verb: str,
|
|
474
512
|
dry_run: bool,
|
|
475
513
|
) -> None:
|
|
476
|
-
worktree_root =
|
|
514
|
+
worktree_root, invocation_prefix = _require_worktree_context()
|
|
477
515
|
targets = _make_targets(
|
|
478
516
|
args=args,
|
|
479
517
|
worktree_root=worktree_root,
|
|
@@ -484,6 +522,7 @@ def _run_patch_command(
|
|
|
484
522
|
targets=targets,
|
|
485
523
|
selection=selection,
|
|
486
524
|
worktree_root=worktree_root,
|
|
525
|
+
invocation_prefix=invocation_prefix,
|
|
487
526
|
staged=staged,
|
|
488
527
|
cached=cached,
|
|
489
528
|
reverse=reverse,
|
|
@@ -571,7 +610,7 @@ def cmd_list(
|
|
|
571
610
|
print_help(HELP_LIST)
|
|
572
611
|
return
|
|
573
612
|
|
|
574
|
-
worktree_root =
|
|
613
|
+
worktree_root, _ = _require_worktree_context()
|
|
575
614
|
selected_paths = {
|
|
576
615
|
_make_repository_path(arg=path, worktree_root=worktree_root) for path in files
|
|
577
616
|
}
|
|
@@ -612,7 +651,7 @@ def cmd_show(
|
|
|
612
651
|
print_help(HELP_SHOW)
|
|
613
652
|
return
|
|
614
653
|
|
|
615
|
-
worktree_root =
|
|
654
|
+
worktree_root, _ = _require_worktree_context()
|
|
616
655
|
inventory = _get_inventory(worktree_root=worktree_root)
|
|
617
656
|
hunks = _filter_inventory_hunks(
|
|
618
657
|
inventory=inventory,
|
|
@@ -832,7 +871,7 @@ def cmd_discard(
|
|
|
832
871
|
|
|
833
872
|
|
|
834
873
|
@cli.command("commit", add_help_option=False)
|
|
835
|
-
@click.option("-m", "
|
|
874
|
+
@click.option("-m", "messages", multiple=True)
|
|
836
875
|
@click.option("-l", "line_spec", default=None)
|
|
837
876
|
@click.option("--include-matching", "include_matching", multiple=True)
|
|
838
877
|
@click.option("--exclude-matching", "exclude_matching", multiple=True)
|
|
@@ -843,7 +882,7 @@ def cmd_discard(
|
|
|
843
882
|
def cmd_commit(
|
|
844
883
|
*,
|
|
845
884
|
targets: tuple[str, ...],
|
|
846
|
-
|
|
885
|
+
messages: tuple[str, ...],
|
|
847
886
|
line_spec: str | None,
|
|
848
887
|
include_matching: tuple[str, ...],
|
|
849
888
|
exclude_matching: tuple[str, ...],
|
|
@@ -854,10 +893,13 @@ def cmd_commit(
|
|
|
854
893
|
if show_help:
|
|
855
894
|
print_help(HELP_COMMIT)
|
|
856
895
|
return
|
|
857
|
-
|
|
896
|
+
# Each -m is its own paragraph, as in git commit; git's own cleanup then
|
|
897
|
+
# collapses blank ones.
|
|
898
|
+
message = "\n\n".join(messages)
|
|
899
|
+
if not message.strip():
|
|
858
900
|
raise CliError("commit requires a message (-m)", usage=USAGE_COMMIT)
|
|
859
901
|
|
|
860
|
-
worktree_root =
|
|
902
|
+
worktree_root, invocation_prefix = _require_worktree_context()
|
|
861
903
|
commit_targets = _make_targets(
|
|
862
904
|
args=list(targets),
|
|
863
905
|
worktree_root=worktree_root,
|
|
@@ -883,6 +925,7 @@ def cmd_commit(
|
|
|
883
925
|
targets=commit_targets,
|
|
884
926
|
selection=selection,
|
|
885
927
|
worktree_root=worktree_root,
|
|
928
|
+
invocation_prefix=invocation_prefix,
|
|
886
929
|
staged=False,
|
|
887
930
|
cached=True,
|
|
888
931
|
reverse=False,
|
|
@@ -4,6 +4,8 @@ from typing import Final
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class GitCommandError(RuntimeError):
|
|
7
|
+
stderr: str
|
|
8
|
+
|
|
7
9
|
def __init__(self, command: tuple[str, ...], stderr: str, /) -> None:
|
|
8
10
|
self.stderr = stderr
|
|
9
11
|
super().__init__(f"git {' '.join(command)} failed: {stderr}")
|
|
@@ -17,8 +19,8 @@ class UnsupportedChange:
|
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def run_git(*args: str, worktree_root: str | None, input: str | None = None) -> str:
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
+
# A missing root is only for the bootstrap query that discovers both
|
|
23
|
+
# repository coordinates. Every later call anchors there.
|
|
22
24
|
# Git output and input may contain bytes that are not valid UTF-8 (e.g. a
|
|
23
25
|
# Latin-1 source file). surrogateescape round-trips those bytes losslessly
|
|
24
26
|
# so a rebuilt patch hands git back exactly what it emitted.
|
|
@@ -60,9 +62,14 @@ def get_diff(*, worktree_root: str, staged: bool) -> str:
|
|
|
60
62
|
return run_git(*args, worktree_root=worktree_root)
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
def get_worktree_context() -> tuple[str, str]:
|
|
66
|
+
# Ask Git for this coordinate because Windows can give Git and Python
|
|
67
|
+
# different spellings for the same directory.
|
|
68
|
+
output = run_git(
|
|
69
|
+
"rev-parse", "--show-toplevel", "--show-prefix", worktree_root=None
|
|
70
|
+
)
|
|
71
|
+
worktree_root, invocation_prefix = output.removesuffix("\n").split("\n", 1)
|
|
72
|
+
return worktree_root, invocation_prefix.removesuffix("/")
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
def get_untracked_files(*, worktree_root: str) -> list[str]:
|
|
@@ -438,7 +438,7 @@ can retry with [bold cyan]git commit[/bold cyan].
|
|
|
438
438
|
{USAGE_COMMIT}
|
|
439
439
|
|
|
440
440
|
[bold green]Options:[/bold green]
|
|
441
|
-
[bold cyan]-m[/bold cyan] [cyan]<msg>[/cyan] Commit message (required)
|
|
441
|
+
[bold cyan]-m[/bold cyan] [cyan]<msg>[/cyan] Commit message (required; repeat for more paragraphs)
|
|
442
442
|
{_LINE_OPT_ROW}
|
|
443
443
|
|
|
444
444
|
{_format_examples(rows=_EXAMPLES_COMMIT)}""" # noqa: E501
|
|
@@ -30,7 +30,8 @@ git-hunk list
|
|
|
30
30
|
`git-hunk commit <id-or-path>... -m <message>` stages exactly those hunks and
|
|
31
31
|
commits them; it aborts when the index already holds staged changes. A
|
|
32
32
|
Repository path selects every changed hunk in one exact file and beats an ID
|
|
33
|
-
when committing a whole file.
|
|
33
|
+
when committing a whole file. Repeat `-m` as with `git commit`: each one
|
|
34
|
+
becomes its own paragraph.
|
|
34
35
|
|
|
35
36
|
Committing a complete hunk leaves every other non-`conditional` ID valid, so a
|
|
36
37
|
whole plan of complete hunks chains in one call.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|