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