git-hunk 0.3.1__tar.gz → 0.4.0__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.0}/PKG-INFO +1 -1
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_cli.py +47 -10
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_git.py +10 -5
- {git_hunk-0.3.1 → git_hunk-0.4.0}/.gitignore +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/LICENSE +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/README.md +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/__init__.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/__main__.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_hunk.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_lines.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_patch.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_skills.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/_ui.py +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/skills/core/SKILL.md +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/git_hunk/skills/logical-commits/SKILL.md +0 -0
- {git_hunk-0.3.1 → git_hunk-0.4.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: git-hunk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -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
|
|
@@ -103,9 +104,9 @@ class CliGroup(click.Group):
|
|
|
103
104
|
ctx.exit(130)
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
+
def _require_worktree_context() -> tuple[str, str]:
|
|
107
108
|
try:
|
|
108
|
-
return
|
|
109
|
+
return get_worktree_context()
|
|
109
110
|
# Usually there is no worktree to anchor to, but rev-parse also refuses on
|
|
110
111
|
# dubious ownership or a bad config value, and those say how to fix them.
|
|
111
112
|
# Classifying git's English is what this avoids; passing it on is not.
|
|
@@ -243,10 +244,16 @@ def _make_targets(
|
|
|
243
244
|
|
|
244
245
|
|
|
245
246
|
def _select_hunks(
|
|
246
|
-
*,
|
|
247
|
+
*,
|
|
248
|
+
hunks: list[Hunk],
|
|
249
|
+
targets: list[_Target],
|
|
250
|
+
inventory_hunks: list[Hunk],
|
|
251
|
+
invocation_prefix: str,
|
|
252
|
+
worktree_root: str,
|
|
247
253
|
) -> list[Hunk]:
|
|
248
254
|
files = {h.file for h in hunks}
|
|
249
255
|
eligible_ids = {hunk.id for hunk in hunks}
|
|
256
|
+
resolved_worktree_root = Path(worktree_root).resolve()
|
|
250
257
|
selected: list[Hunk] = []
|
|
251
258
|
seen: set[str] = set()
|
|
252
259
|
for target in targets:
|
|
@@ -259,9 +266,32 @@ def _select_hunks(
|
|
|
259
266
|
if matches[0].id not in eligible_ids:
|
|
260
267
|
raise CliError(f"hunk '{target.arg}' is not eligible for this command")
|
|
261
268
|
else:
|
|
269
|
+
operand_path = resolved_worktree_root / target.path
|
|
270
|
+
if (
|
|
271
|
+
operand_path.is_dir()
|
|
272
|
+
and operand_path.resolve().is_relative_to(resolved_worktree_root)
|
|
273
|
+
and any(
|
|
274
|
+
target.path == "." or path.startswith(f"{target.path}/")
|
|
275
|
+
for path in files
|
|
276
|
+
)
|
|
277
|
+
):
|
|
278
|
+
raise CliError(
|
|
279
|
+
f"directory operands are not expanded: '{target.arg}'",
|
|
280
|
+
tip="pass exact Repository paths (a shell-expanded glob is "
|
|
281
|
+
"acceptable when it covers the intended tracked files)",
|
|
282
|
+
)
|
|
283
|
+
suggested_path = posixpath.normpath(
|
|
284
|
+
posixpath.join(invocation_prefix, target.path)
|
|
285
|
+
)
|
|
286
|
+
tip = (
|
|
287
|
+
"Repository paths are worktree-root-relative; "
|
|
288
|
+
f"did you mean '{suggested_path}'?"
|
|
289
|
+
if suggested_path in files
|
|
290
|
+
else "run 'git-hunk list' to see changed files and hunk ids"
|
|
291
|
+
)
|
|
262
292
|
raise CliError(
|
|
263
293
|
f"no changed file matches '{target.arg}'",
|
|
264
|
-
tip=
|
|
294
|
+
tip=tip,
|
|
265
295
|
)
|
|
266
296
|
for hunk in matches:
|
|
267
297
|
if hunk.id not in seen:
|
|
@@ -368,6 +398,7 @@ def _apply_selection(
|
|
|
368
398
|
targets: list[_Target],
|
|
369
399
|
selection: _Selection,
|
|
370
400
|
worktree_root: str,
|
|
401
|
+
invocation_prefix: str,
|
|
371
402
|
staged: bool,
|
|
372
403
|
cached: bool,
|
|
373
404
|
reverse: bool,
|
|
@@ -379,7 +410,11 @@ def _apply_selection(
|
|
|
379
410
|
hunks = [hunk for hunk in inventory.hunks if hunk.status == status]
|
|
380
411
|
diff_output = inventory.staged_diff if staged else inventory.unstaged_diff
|
|
381
412
|
selected = _select_hunks(
|
|
382
|
-
hunks=hunks,
|
|
413
|
+
hunks=hunks,
|
|
414
|
+
targets=targets,
|
|
415
|
+
inventory_hunks=inventory.hunks,
|
|
416
|
+
invocation_prefix=invocation_prefix,
|
|
417
|
+
worktree_root=worktree_root,
|
|
383
418
|
)
|
|
384
419
|
selected = _apply_line_filter(hunks=selected, selection=selection, reverse=reverse)
|
|
385
420
|
|
|
@@ -473,7 +508,7 @@ def _run_patch_command(
|
|
|
473
508
|
verb: str,
|
|
474
509
|
dry_run: bool,
|
|
475
510
|
) -> None:
|
|
476
|
-
worktree_root =
|
|
511
|
+
worktree_root, invocation_prefix = _require_worktree_context()
|
|
477
512
|
targets = _make_targets(
|
|
478
513
|
args=args,
|
|
479
514
|
worktree_root=worktree_root,
|
|
@@ -484,6 +519,7 @@ def _run_patch_command(
|
|
|
484
519
|
targets=targets,
|
|
485
520
|
selection=selection,
|
|
486
521
|
worktree_root=worktree_root,
|
|
522
|
+
invocation_prefix=invocation_prefix,
|
|
487
523
|
staged=staged,
|
|
488
524
|
cached=cached,
|
|
489
525
|
reverse=reverse,
|
|
@@ -571,7 +607,7 @@ def cmd_list(
|
|
|
571
607
|
print_help(HELP_LIST)
|
|
572
608
|
return
|
|
573
609
|
|
|
574
|
-
worktree_root =
|
|
610
|
+
worktree_root, _ = _require_worktree_context()
|
|
575
611
|
selected_paths = {
|
|
576
612
|
_make_repository_path(arg=path, worktree_root=worktree_root) for path in files
|
|
577
613
|
}
|
|
@@ -612,7 +648,7 @@ def cmd_show(
|
|
|
612
648
|
print_help(HELP_SHOW)
|
|
613
649
|
return
|
|
614
650
|
|
|
615
|
-
worktree_root =
|
|
651
|
+
worktree_root, _ = _require_worktree_context()
|
|
616
652
|
inventory = _get_inventory(worktree_root=worktree_root)
|
|
617
653
|
hunks = _filter_inventory_hunks(
|
|
618
654
|
inventory=inventory,
|
|
@@ -857,7 +893,7 @@ def cmd_commit(
|
|
|
857
893
|
if message is None or not message.strip():
|
|
858
894
|
raise CliError("commit requires a message (-m)", usage=USAGE_COMMIT)
|
|
859
895
|
|
|
860
|
-
worktree_root =
|
|
896
|
+
worktree_root, invocation_prefix = _require_worktree_context()
|
|
861
897
|
commit_targets = _make_targets(
|
|
862
898
|
args=list(targets),
|
|
863
899
|
worktree_root=worktree_root,
|
|
@@ -883,6 +919,7 @@ def cmd_commit(
|
|
|
883
919
|
targets=commit_targets,
|
|
884
920
|
selection=selection,
|
|
885
921
|
worktree_root=worktree_root,
|
|
922
|
+
invocation_prefix=invocation_prefix,
|
|
886
923
|
staged=False,
|
|
887
924
|
cached=True,
|
|
888
925
|
reverse=False,
|
|
@@ -17,8 +17,8 @@ class UnsupportedChange:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def run_git(*args: str, worktree_root: str | None, input: str | None = None) -> str:
|
|
20
|
-
#
|
|
21
|
-
#
|
|
20
|
+
# A missing root is only for the bootstrap query that discovers both
|
|
21
|
+
# repository coordinates. Every later call anchors there.
|
|
22
22
|
# Git output and input may contain bytes that are not valid UTF-8 (e.g. a
|
|
23
23
|
# Latin-1 source file). surrogateescape round-trips those bytes losslessly
|
|
24
24
|
# so a rebuilt patch hands git back exactly what it emitted.
|
|
@@ -60,9 +60,14 @@ def get_diff(*, worktree_root: str, staged: bool) -> str:
|
|
|
60
60
|
return run_git(*args, worktree_root=worktree_root)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
def get_worktree_context() -> tuple[str, str]:
|
|
64
|
+
# Ask Git for this coordinate because Windows can give Git and Python
|
|
65
|
+
# different spellings for the same directory.
|
|
66
|
+
output = run_git(
|
|
67
|
+
"rev-parse", "--show-toplevel", "--show-prefix", worktree_root=None
|
|
68
|
+
)
|
|
69
|
+
worktree_root, invocation_prefix = output.removesuffix("\n").split("\n", 1)
|
|
70
|
+
return worktree_root, invocation_prefix.removesuffix("/")
|
|
66
71
|
|
|
67
72
|
|
|
68
73
|
def get_untracked_files(*, worktree_root: str) -> list[str]:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|