git-hunk 0.2.0__tar.gz → 0.3.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.
Files changed (53) hide show
  1. {git_hunk-0.2.0 → git_hunk-0.3.0}/.gitignore +4 -0
  2. git_hunk-0.3.0/PKG-INFO +380 -0
  3. git_hunk-0.3.0/README.md +352 -0
  4. git_hunk-0.3.0/git_hunk/_cli.py +881 -0
  5. git_hunk-0.3.0/git_hunk/_git.py +201 -0
  6. git_hunk-0.3.0/git_hunk/_hunk.py +525 -0
  7. git_hunk-0.3.0/git_hunk/_lines.py +334 -0
  8. git_hunk-0.3.0/git_hunk/_patch.py +123 -0
  9. git_hunk-0.3.0/git_hunk/_skills.py +71 -0
  10. git_hunk-0.3.0/git_hunk/_ui.py +457 -0
  11. git_hunk-0.3.0/git_hunk/skills/core/SKILL.md +100 -0
  12. git_hunk-0.3.0/git_hunk/skills/logical-commits/SKILL.md +30 -0
  13. {git_hunk-0.2.0 → git_hunk-0.3.0}/pyproject.toml +25 -4
  14. git_hunk-0.2.0/.gitattributes +0 -1
  15. git_hunk-0.2.0/.github/workflows/lint.yml +0 -13
  16. git_hunk-0.2.0/.github/workflows/publish.yml +0 -16
  17. git_hunk-0.2.0/.github/workflows/test.yml +0 -19
  18. git_hunk-0.2.0/.python-version +0 -1
  19. git_hunk-0.2.0/Makefile +0 -41
  20. git_hunk-0.2.0/PKG-INFO +0 -190
  21. git_hunk-0.2.0/README.md +0 -163
  22. git_hunk-0.2.0/assets/teaser.png +0 -0
  23. git_hunk-0.2.0/git_hunk/_cli.py +0 -323
  24. git_hunk-0.2.0/git_hunk/_git.py +0 -59
  25. git_hunk-0.2.0/git_hunk/_hunk.py +0 -249
  26. git_hunk-0.2.0/git_hunk/_lines.py +0 -112
  27. git_hunk-0.2.0/git_hunk/_patch.py +0 -28
  28. git_hunk-0.2.0/git_hunk/_ui.py +0 -240
  29. git_hunk-0.2.0/skills/git-hunk/SKILL.md +0 -62
  30. git_hunk-0.2.0/taplo.toml +0 -3
  31. git_hunk-0.2.0/tests/__init__.py +0 -0
  32. git_hunk-0.2.0/tests/conftest.py +0 -44
  33. git_hunk-0.2.0/tests/e2e/__init__.py +0 -0
  34. git_hunk-0.2.0/tests/e2e/conftest.py +0 -46
  35. git_hunk-0.2.0/tests/e2e/discard_test.py +0 -41
  36. git_hunk-0.2.0/tests/e2e/error_test.py +0 -72
  37. git_hunk-0.2.0/tests/e2e/list_test.py +0 -153
  38. git_hunk-0.2.0/tests/e2e/show_test.py +0 -85
  39. git_hunk-0.2.0/tests/e2e/stage_test.py +0 -105
  40. git_hunk-0.2.0/tests/e2e/unstage_test.py +0 -40
  41. git_hunk-0.2.0/tests/unit/__init__.py +0 -0
  42. git_hunk-0.2.0/tests/unit/_hunk/__init__.py +0 -0
  43. git_hunk-0.2.0/tests/unit/_hunk/id_test.py +0 -80
  44. git_hunk-0.2.0/tests/unit/_hunk/parse_diff_test.py +0 -84
  45. git_hunk-0.2.0/tests/unit/_hunk/split_test.py +0 -149
  46. git_hunk-0.2.0/tests/unit/_lines/__init__.py +0 -0
  47. git_hunk-0.2.0/tests/unit/_lines/filter_test.py +0 -86
  48. git_hunk-0.2.0/tests/unit/_lines/parse_spec_test.py +0 -59
  49. git_hunk-0.2.0/tests/unit/_patch_test.py +0 -81
  50. git_hunk-0.2.0/uv.lock +0 -791
  51. {git_hunk-0.2.0 → git_hunk-0.3.0}/LICENSE +0 -0
  52. {git_hunk-0.2.0 → git_hunk-0.3.0}/git_hunk/__init__.py +0 -0
  53. {git_hunk-0.2.0 → git_hunk-0.3.0}/git_hunk/__main__.py +0 -0
@@ -4,6 +4,7 @@
4
4
  /*.egg-info/
5
5
 
6
6
  # Test / lint caches
7
+ __pycache__/
7
8
  /.coverage
8
9
  /.mypy_cache/
9
10
  /.pytest_cache/
@@ -16,3 +17,6 @@
16
17
 
17
18
  # Tools
18
19
  /.claude/
20
+
21
+ # Worktrees
22
+ /.wt/
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-hunk
3
+ Version: 0.3.0
4
+ Summary: Non-interactive, programmatic git hunk staging with durable Hunk IDs.
5
+ Project-URL: Homepage, https://github.com/wkentaro/git-hunk
6
+ Project-URL: Issues, https://github.com/wkentaro/git-hunk/issues
7
+ Project-URL: Repository, https://github.com/wkentaro/git-hunk
8
+ Author: Kentaro Wada
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: automation,diff,git,hunk,staging,version-control
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Version Control :: Git
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: click>=8
26
+ Requires-Dist: rich>=13
27
+ Description-Content-Type: text/markdown
28
+
29
+ # git-hunk
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/git_hunk.svg)](https://pypi.org/project/git-hunk/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/git_hunk.svg)](https://pypi.org/project/git-hunk/)
33
+ [![License](https://img.shields.io/pypi/l/git_hunk.svg)](https://pypi.org/project/git-hunk/)
34
+ [![Build](https://github.com/wkentaro/git-hunk/actions/workflows/test.yml/badge.svg)](https://github.com/wkentaro/git-hunk/actions/workflows/test.yml)
35
+
36
+ Non-interactive, programmatic alternative to `git add -p`.
37
+
38
+ Every staged or unstaged Hunk gets a durable ID so you can inspect, filter, and
39
+ stage changes without interactive prompts. Duplicate Hunks get unique
40
+ Conditional IDs.
41
+
42
+ <img src="https://raw.githubusercontent.com/wkentaro/git-hunk/main/assets/teaser.png" alt="git-hunk teaser" width="800">
43
+
44
+ ## Why?
45
+
46
+ `git add -p` requires interactive input. That makes it unusable for:
47
+
48
+ - **AI agents** (Claude Code, Codex, etc.) that need to split changes into logical commits
49
+ - **Scripts & CI/CD** that automate commit organization
50
+ - **Editor integrations** that want hunk-level staging without shelling out to a TUI
51
+
52
+ `git-hunk` solves this by assigning each staged or unstaged Hunk a durable ID
53
+ and exposing simple stage/unstage/discard commands.
54
+
55
+ ## Eval
56
+
57
+ One agent (Claude Code 2.1.226, `claude-sonnet-5`, reasoning effort `high`)
58
+ attempted the same eight tasks from identical repository state, three times per
59
+ variant: organize a dirty working tree into correct, focused commits, once
60
+ following git-hunk's bundled skills and once restricted to bare Git. The
61
+ [checked-in eval harness](https://github.com/wkentaro/git-hunk/tree/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval)
62
+ grades the exact resulting repository state — commit partition and order, final
63
+ tree, index, and leftovers. This table records the qualifying run; `make eval`
64
+ reruns the protocol and prints a table in the same format.
65
+
66
+ | Task | git-hunk | bare Git |
67
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
68
+ | [split_refactor_vs_feature](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/split_refactor_vs_feature.py) | PASS 3/3 · 3c · 4t | PASS 3/3 · 3c [2-3] · 4t [3-4] |
69
+ | [separate_mixed_hunks](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/separate_mixed_hunks.py) | PASS 3/3 · 3c [3-4] · 4t [4-5] | MIXED 1/3 partition · 15c [11-20] · 16t [12-21] |
70
+ | [drop_debug_lines](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/drop_debug_lines.py) | PASS 3/3 · 3c · 4t | MIXED 2/3 partition · 16c [8-19] · 17t [9-20] |
71
+ | [protect_unrelated_work](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/protect_unrelated_work.py) | PASS 3/3 · 3c · 4t | PASS 3/3 · 2c [2-3] · 3t [3-4] |
72
+ | [split_single_hunk](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/split_single_hunk.py) | PASS 3/3 · 5c [3-5] · 6t [4-6] | PASS 3/3 · 14c [9-15] · 15t [10-16] |
73
+ | [separate_formatter_noise](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/separate_formatter_noise.py) | PASS 3/3 · 4c [3-4] · 5t [4-5] | PASS 3/3 · 16c [9-30] · 17t [10-31] |
74
+ | [pick_duplicate_hunk](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/pick_duplicate_hunk.py) | PASS 3/3 · 3c · 4t | PASS 3/3 · 10c [9-17] · 11t [10-18] |
75
+ | [commit_parseable_subset](https://github.com/wkentaro/git-hunk/blob/9829b7ac6fd871cd874d47e7424c9082bd58dbaf/eval/tasks/commit_parseable_subset.py) | PASS 3/3 · 5c [3-6] · 6t [4-7] | PASS 3/3 · 14c [12-15] · 15t [13-16] |
76
+ | **total** | **8/8 · 29c [24-31] · 37t [32-39]** | **6/8 (2 mixed) · 90c [62-122] · 98t [70-130]** |
77
+
78
+ `c` = tool calls, `t` = turns; a cell reports the median of its three repeats
79
+ with the observed range in brackets, dropped where every repeat agreed, and the
80
+ pass column counts passing repeats. `MIXED j/3` means the variant passed j of its
81
+ three repeats, and `partition` names the failure: the commits made do not match
82
+ the required change groups. The cost column is omitted: bare Git runs second in
83
+ each pair and partly reads the prompt cache the git-hunk run warmed, so raw costs
84
+ are not order-neutral
85
+ ([#224](https://github.com/wkentaro/git-hunk/issues/224)). Three samples per task
86
+ variant, dated 2026-08-09 at commit `9829b7a`.
87
+
88
+ ## Install
89
+
90
+ Requires Git 2.28 or later. `git-hunk` forces canonical diff paths with
91
+ `git diff --no-relative`, which earlier versions of Git do not accept.
92
+
93
+ ```bash
94
+ pip install git-hunk
95
+ ```
96
+
97
+ Or with [uv](https://docs.astral.sh/uv/):
98
+
99
+ ```bash
100
+ uv tool install git-hunk
101
+ ```
102
+
103
+ Verify it works:
104
+
105
+ ```bash
106
+ git-hunk --version
107
+ ```
108
+
109
+ > [!TIP]
110
+ > To try the latest development version (the head of `main` on GitHub) before
111
+ > it is published:
112
+ >
113
+ > ```bash
114
+ > uv tool install git+https://github.com/wkentaro/git-hunk
115
+ > ```
116
+
117
+ ### For AI agents
118
+
119
+ A usage guide ships inside the CLI, so agents (Claude Code, Codex, etc.) can
120
+ load it on demand. It always matches the installed version, so it never goes
121
+ stale:
122
+
123
+ ```bash
124
+ git-hunk skills get core
125
+ ```
126
+
127
+ `core` covers the tool itself. A separate `logical-commits` skill covers how to
128
+ group hunks into commits and order them; it is optional, so a project that
129
+ already defines its own commit conventions can load `core` alone:
130
+
131
+ ```bash
132
+ git-hunk skills # list available skills
133
+ git-hunk skills get core logical-commits # load both
134
+ ```
135
+
136
+ `git-hunk --help` points here first.
137
+
138
+ ## Quick start
139
+
140
+ ```bash
141
+ # See all hunks across staged, unstaged, and untracked files
142
+ git-hunk list
143
+
144
+ # Show the diff for a specific hunk
145
+ git-hunk show d161935
146
+
147
+ # Stage specific hunks, then commit
148
+ git-hunk stage d161935 a3f82c1
149
+ git commit -m "feat: add validation for user input"
150
+
151
+ # Stage the remaining hunks
152
+ git-hunk stage e7b4012
153
+ git commit -m "fix: handle empty response in API client"
154
+ ```
155
+
156
+ ## Usage
157
+
158
+ ### Repository paths
159
+
160
+ A Repository path is relative to the worktree root, uses `/`, and has the same
161
+ meaning from every invocation directory. Every path in output and every file
162
+ operand for `list`, `stage`, `unstage`, `discard`, and `commit` is a Repository
163
+ path. A leading `./` and internal `..` components are normalized. Absolute paths
164
+ and paths that escape the worktree are rejected.
165
+
166
+ File operands select one exact changed file. Directories, globs, and Git pathspec
167
+ syntax are not expanded. Quote operands that contain shell metacharacters so the
168
+ shell passes them unchanged. For example, from `sub/`, `same.txt` selects the
169
+ file at the worktree root, while `sub/same.txt` selects the file inside `sub/`.
170
+ `show` remains ID-only.
171
+
172
+ ### Unsupported repository states
173
+
174
+ git-hunk rejects detected rename, copy, and unmerged index states before it
175
+ writes inventory output or changes the repository. This prevents partial JSON,
176
+ partial inventory, false clean results, and partial mutation. Resolve an
177
+ unmerged index with Git before retrying. Full rename and copy support is not yet
178
+ available; it remains tracked in [#53](https://github.com/wkentaro/git-hunk/issues/53).
179
+
180
+ ### Hunk IDs
181
+
182
+ A canonical Hunk ID is a full SHA-256 value. JSON returns it in full. Human
183
+ output shows the shortest unambiguous prefix of at least seven characters, and
184
+ commands accept unambiguous prefixes without case sensitivity. IDs are
185
+ calculated from the combined staged and unstaged inventory, including when a
186
+ status filter shows only one side.
187
+
188
+ An Unchanged Hunk keeps its ID when it moves completely between staged and
189
+ unstaged state or when other complete Hunks move. A partial-line operation
190
+ creates new Hunks with new IDs.
191
+
192
+ Hunks with the same Repository path and patch content form a Duplicate Hunk
193
+ group. Each member gets a unique Conditional Hunk ID, shown with a
194
+ `conditional` label in human output and `"id_stability": "conditional"` in JSON.
195
+ The ID can change when its Duplicate Hunk group changes. After a partial-line
196
+ operation or an operation on a Conditional Hunk ID, address anything remaining
197
+ by Repository path, which is ID-independent, or run `git-hunk list` again for
198
+ the new IDs.
199
+
200
+ ### List hunks
201
+
202
+ ```bash
203
+ git-hunk list # all hunks (unstaged + staged + untracked)
204
+ git-hunk list --unstaged # unstaged hunks only
205
+ git-hunk list --staged # staged hunks only
206
+ git-hunk list src/foo.py src/bar.py # specific files
207
+ git-hunk list --json # JSON output for scripting
208
+ ```
209
+
210
+ ### Show hunks
211
+
212
+ ```bash
213
+ git-hunk show # show all hunks (staged + unstaged)
214
+ git-hunk show d161935 # show a single hunk
215
+ git-hunk show d161935 a3f82c1 # show multiple hunks
216
+ git-hunk show --staged # show all staged hunks
217
+ git-hunk show --unstaged # show all unstaged hunks
218
+ ```
219
+
220
+ ### Stage, unstage, discard
221
+
222
+ ```bash
223
+ git-hunk stage d161935 # stage a hunk
224
+ git-hunk stage d161935 a3f82c1 # stage multiple hunks
225
+ git-hunk stage d161935 -l 3,5-7 # stage specific lines only
226
+ git-hunk stage d161935 --exclude-matching debug # stage all but lines containing "debug"
227
+ git-hunk stage d161935 --include-matching xfail # stage only lines containing "xfail"
228
+ git-hunk unstage d161935 # move back to working tree
229
+ git-hunk unstage d161935 -l 3,5-7 # unstage specific lines only
230
+ git-hunk discard d161935 # restore from the index
231
+ git-hunk discard d161935 -l ^3,^5-7 # discard excluding specific lines
232
+ ```
233
+
234
+ `--include-matching` / `--exclude-matching` select changed lines by content
235
+ instead of line number (literal substring by default, `--regex` for regular
236
+ expressions). Both are repeatable and OR'd, case-sensitive, and error if nothing
237
+ matches. They are mutually exclusive with `-l` and with each other.
238
+
239
+ Line selection accepts any subset of a pure addition or pure deletion. Selecting
240
+ one side of a one-for-one replacement is rejected, because it would leave a
241
+ deletion-only or addition-only half; select both lines, match text they share,
242
+ or pass `--allow-one-sided` when that half is what you want. A grouped
243
+ replacement with multiple deleted or added lines must be selected as a whole or
244
+ not selected, and `--allow-one-sided` does not relax that. Numeric range
245
+ endpoints are checked against the Hunk before expansion, and no-newline state is
246
+ preserved for each patch side. Submodule pointer changes and whole-file Hunks do
247
+ not support line selection. Select the Hunk as a whole.
248
+
249
+ A binary, mode-only, type, or empty tracked file change is a whole-file Hunk.
250
+ Plain output labels empty tracked changes as `Empty file (added)` or
251
+ `Empty file (deleted)`. When one file has a mode change and text edits, the mode
252
+ and each text range are separate Hunks. Selecting text does not apply the mode
253
+ change, and selecting the mode Hunk does not apply text.
254
+
255
+ ### Commit
256
+
257
+ ```bash
258
+ git-hunk commit d161935 -m "fix: ..." # stage a hunk and commit it in one step
259
+ git-hunk commit d161935 -l 3,5-7 -m "..." # stage specific lines and commit
260
+ git-hunk commit d161935 --exclude-matching debug -m "..." # commit all but matching lines
261
+ ```
262
+
263
+ `commit` aborts if anything is already staged, so the commit contains exactly
264
+ the selected hunks. It accepts the same `-l`, `--include-matching`,
265
+ `--exclude-matching`, `--regex`, and `--allow-one-sided` selection options as
266
+ `stage`.
267
+
268
+ ### JSON output
269
+
270
+ ```bash
271
+ git-hunk list --json # inventory: every hunk, no body
272
+ git-hunk show <id> --json # the same hunks plus a structured per-line body
273
+ ```
274
+
275
+ Both emit a versioned envelope (`schema_version` is currently `2`) so consumers
276
+ can depend on a stable shape. `list --json` is a lean inventory and carries no
277
+ body; `show --json` adds a structured `lines` array. A `show --json` hunk
278
+ (`list --json` is identical but without the `lines` field):
279
+
280
+ ```json
281
+ {
282
+ "schema_version": 2,
283
+ "hunks": [
284
+ {
285
+ "id": "d161935000000000000000000000000000000000000000000000000000000000",
286
+ "id_stability": "stable",
287
+ "file": { "text": "src/main.py" },
288
+ "status": "unstaged",
289
+ "change_kind": "M",
290
+ "a_mode": "100644",
291
+ "b_mode": "100644",
292
+ "binary": false,
293
+ "header": "@@ -10,3 +10,5 @@",
294
+ "context_before": { "text": "def main():" },
295
+ "additions": 2,
296
+ "deletions": 0,
297
+ "lines": [
298
+ { "n": 1, "op": " ", "content": { "text": " x = 1" } },
299
+ { "n": 2, "op": "+", "content": { "text": " y = 2" } }
300
+ ]
301
+ }
302
+ ]
303
+ }
304
+ ```
305
+
306
+ | Field | Type | Description |
307
+ | ---------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
308
+ | `schema_version` | int | Envelope version; bumped on any incompatible change to the shape below. |
309
+ | `hunks` | array | The hunks (empty array when there are no changes). |
310
+ | `id` | string | Full canonical SHA-256 Hunk ID; empty for an `untracked` entry, which no command can address. Human output uses a unique prefix of at least seven characters. |
311
+ | `id_stability` | string | `stable` or `conditional`. An untracked inventory entry reports `stable`, but its empty `id` remains unaddressable. |
312
+ | `file` | union | Repository path of the changed file, as a byte-safe `{text\|bytes}` union (see below). |
313
+ | `status` | string | One of `staged`, `unstaged`, `untracked`. |
314
+ | `change_kind` | string | Git status letter: `A` added, `D` deleted, `M` modified, `T` typechange (`R`/`C` reserved and currently rejected). Always present. |
315
+ | `a_mode` | string \| null | 6-digit octal git mode on the pre-image side; `null` when that side does not exist. |
316
+ | `b_mode` | string \| null | 6-digit octal git mode on the post-image side; `null` when that side does not exist. |
317
+ | `binary` | bool | Whether the change is binary. Always present. |
318
+ | `header` | string \| null | The bare `@@ -a,b +c,d @@` range for a text hunk; `null` for a whole-file hunk (binary, mode-only, type, or empty tracked file change) or an `untracked` inventory entry. |
319
+ | `context_before` | union \| null | The function/section name after a text hunk's `@@` header, as a `{text\|bytes}` union; `null` for a text hunk without a heading, a whole-file hunk, or an `untracked` inventory entry. |
320
+ | `additions` | int | Number of added lines. |
321
+ | `deletions` | int | Number of removed lines. |
322
+ | `lines` | array | `show --json` only. The structured body; `[]` for a whole-file hunk. See below. |
323
+
324
+ A `lines` entry is `{ "n", "op", "content", "no_newline"? }`:
325
+
326
+ | Field | Type | Description |
327
+ | ------------ | ------ | --------------------------------------------------------------------------------------------------- |
328
+ | `n` | int | 1-based position within the hunk body — the index `-l` line selection uses. Counts every body line. |
329
+ | `op` | string | `" "` context, `"+"` addition, `"-"` deletion. |
330
+ | `content` | union | The line text **without** its leading op character, as a `{text\|bytes}` union. |
331
+ | `no_newline` | bool | Present and `true` only when the line has no trailing newline; consumes no `n`. |
332
+
333
+ Any field carrying arbitrary git/source bytes (`file`, `context_before`,
334
+ `lines[].content`) is a byte-safe `{text | bytes}` union: `{"text": "..."}` for
335
+ valid UTF-8, else `{"bytes": "<base64>"}`. It is always an object, so consumers
336
+ have one code path and strict JSON parsers never see a lone surrogate.
337
+
338
+ Adding a new field is backward-compatible and does not change `schema_version`;
339
+ renaming, removing, or changing the type of an existing field bumps it. (Before
340
+ `schema_version` existed, `list --json` returned a bare array.)
341
+
342
+ ## Comparison
343
+
344
+ | | Interactive | Programmatic | Hunk IDs | Line-level control | JSON output |
345
+ | ---------------- | ----------- | ------------ | -------- | ------------------ | ----------- |
346
+ | `git add -p` | Yes | No | No | Yes | No |
347
+ | `git add <file>` | No | Yes | No | No | No |
348
+ | **`git-hunk`** | **No** | **Yes** | **Yes** | **Yes** | **Yes** |
349
+
350
+ ## How it works
351
+
352
+ 1. Rejects detected rename, copy, and unmerged states.
353
+ 2. Parses staged and unstaged `git diff` output into one combined Hunk inventory.
354
+ 3. Assigns each Hunk a full canonical SHA-256 ID and a unique human prefix.
355
+ 4. Gives members of a Duplicate Hunk group unique Conditional Hunk IDs.
356
+ 5. For staging, reconstructs a minimal patch and pipes it through `git apply --cached`.
357
+ 6. For discarding, reconstructs a reverse patch and applies it to the working tree.
358
+
359
+ Text IDs use the Repository path and patch body, including context and newline
360
+ state. They exclude `@@` ranges, section headings, and staged state. Whole-file
361
+ IDs include the actual binary, mode, or type change. This keeps an Unchanged
362
+ Hunk stable while complete Hunks move. A partial operation changes the patch
363
+ content and creates new IDs.
364
+
365
+ ## Contributing
366
+
367
+ Bug reports, feature requests, and pull requests are welcome on
368
+ [GitHub](https://github.com/wkentaro/git-hunk).
369
+
370
+ ```bash
371
+ git clone https://github.com/wkentaro/git-hunk.git
372
+ cd git-hunk
373
+ make setup # install dependencies
374
+ make test # run tests
375
+ make lint # run linters
376
+ ```
377
+
378
+ ## License
379
+
380
+ MIT ([LICENSE](https://github.com/wkentaro/git-hunk/blob/main/LICENSE))