Non-interactive, deterministic hunk staging for git. Enumerate hunks, select by content hash, stage or unstage them -- no TUI required.
Website: git-hunk.paulie.app
git add -p is interactive. It requires a human driving a terminal. LLM agents,
shell scripts, and CI pipelines can't use it.
git-hunk provides an enumerate-then-select workflow: list available hunks with stable content hashes, then stage or unstage specific hunks by hash. Hashes are deterministic and remain stable as other hunks are staged or unstaged, so multi-step staging workflows produce consistent results.
brew install shhac/tap/git-hunk
Download a prebuilt binary from Releases:
# macOS (Universal)
curl -L https://github.com/shhac/git-hunk/releases/latest/download/git-hunk-macos-universal.tar.gz | tar xz
# Linux x86_64
curl -L https://github.com/shhac/git-hunk/releases/latest/download/git-hunk-x86_64-linux.tar.gz | tar xz
# Move to PATH
mv git-hunk ~/.local/bin/
Requires Zig 0.15.2 or later.
git clone https://github.com/shhac/git-hunk.git
cd git-hunk
zig build -Doptimize=ReleaseFast
cp zig-out/bin/git-hunk ~/.local/bin/
Once on your PATH, git-hunk works as a git subcommand:
git hunk list
git-hunk --help # global help (all commands overview)
git-hunk <command> --help # per-command help (flags, examples, behavior)
git-hunk help <command> # same as above
When using git-hunk as a git subcommand, git hunk --help opens the man page
(standard git behavior). Use git hunk help [command] for inline help instead.
git hunk list # unstaged hunks (includes diff content)
git hunk list --oneline # compact one-line-per-hunk output
git hunk list --staged # staged hunks
git hunk list --file src/main.zig # filter by file
git hunk list --porcelain # machine-readable output
git hunk list -U1 # finer-grained hunks
git hunk list --tracked-only # exclude untracked files
git hunk list --no-color # disable color output
Example output:
a3f7c21 src/main.zig 12-18 Add error handling
@@ -12,1 +12,2 @@ fn handleRequest()
const result = try parse(input);
+if (result == null) return error.Invalid;
b82e0f4 src/main.zig 45-52 Replace old parser
@@ -45,1 +45,1 @@
-old_parser(input);
+new_parser(input);
Each hunk shows a 7-character content hash, file path, line range, summary, and
inline diff content. Use --oneline for compact output without diffs.
Untracked files are included by default. Use --tracked-only or
--untracked-only to filter.
git hunk diff a3f7c21 # show diff for one hunk
git hunk diff a3f7 b82e # show multiple hunks
git hunk diff a3f7c21 --staged # show a staged hunk
git hunk diff a3f7 --file src/main.zig # restrict match to file
git hunk diff a3f7c21 --porcelain # machine-readable output
git hunk diff a3f7:3-5 # preview specific lines (hunk-relative)
Prints the full unified diff content for the specified hunks. With line
selection syntax (sha:lines), shows numbered lines with selection markers.
git hunk add a3f7c21 # stage one hunk
git hunk add a3f7 b82e # stage multiple (prefix match, min 4 chars)
git hunk add a3f7c21 --file src/main.zig # restrict match to file
git hunk add --all # stage all unstaged hunks
git hunk add --file src/main.zig # stage all hunks in a file
git hunk add a3f7:3-5,8 # stage specific lines from a hunk
git hunk add a3f7c21 --porcelain # machine-readable output
Output shows the applied (input) and result (output) hashes:
staged a3f7c21 → 5e2b1a9 src/main.zig
With --verbose, summary counts and hints are included:
staged a3f7c21 → 5e2b1a9 src/main.zig
1 hunk staged
hint: staged hashes differ from unstaged -- use 'git hunk list --staged' to see them
When staging causes adjacent hunks to merge, consumed hashes appear with +:
staged a3f7c21 +xxxx123 → 5e2b1a9 src/main.zig
1 hunk staged (1 merged)
git hunk reset a3f7c21 # unstage from index
git hunk reset a3f7 b82e # unstage multiple
git hunk reset --all # unstage everything
git hunk reset --file src/main.zig # unstage all hunks in a file
git hunk reset a3f7c21 --porcelain # machine-readable output
git hunk restore a3f7c21 # restore one unstaged hunk from worktree
git hunk restore a3f7 b82e # restore multiple hunks
git hunk restore --all # restore all unstaged changes
git hunk restore --file src/main.zig # restore all hunks in a file
git hunk restore a3f7:3-5 # restore specific lines from a hunk
git hunk restore --dry-run a3f7c21 # preview what would be restored
git hunk restore a3f7c21 --porcelain # machine-readable output
Reverts specific worktree changes to match the index. The destructive counterpart
to add/reset. Staged changes are unaffected.
Untracked files require --force to restore (they are deleted permanently):
git hunk restore --force a3f7c21 # restore/delete an untracked file
Output:
restored a3f7c21 src/main.zig
1 hunk restored
With --dry-run, shows what would be restored without modifying files:
would restore a3f7c21 src/main.zig
1 hunk would be restored
git hunk count # count unstaged hunks
git hunk count --staged # count staged hunks
git hunk count --file src/main.zig # count hunks in a file
Outputs a bare integer. Always exits 0 (zero hunks is a valid count).
git hunk check a3f7c21 # verify hash still exists
git hunk check a3f7 b82e # check multiple hashes
git hunk check --exclusive a3f7 b82e # assert these are the ONLY hunks
git hunk check --staged a3f7c21 # check against staged hunks
git hunk check --porcelain a3f7 b82e # machine-readable results
Validates that hunk hashes exist in the current diff. Exits 0 if all
hashes are valid, 1 if any are stale or ambiguous. Silent on success
in human mode. With --exclusive, asserts the provided hashes are the
only hunks (scoped by --file if given).
git hunk stash a3f7c21 # stash one hunk, remove from worktree
git hunk stash a3f7 b82e # stash multiple hunks
git hunk stash --all # stash all tracked unstaged hunks
git hunk stash --all -u # stash all including untracked files
git hunk stash push --include-untracked # same as -u (explicit push keyword)
git hunk stash --file src/main.zig # stash hunks in one file
git hunk stash -m "wip: auth refactor" # custom stash message
git hunk stash pop # restore most recent stash
Saves selected hunks into a real git stash entry and removes them from the
worktree. Compatible with git stash list, git stash show, and git stash pop.
Auto-generates a stash message from affected file paths unless -m is provided.
Like git stash, --all excludes untracked files by default. Use -u /
--include-untracked to include them. Explicit hash targeting always works
for untracked hunks regardless of -u. Untracked files are stored using git's
native 3-parent stash format, so git stash pop restores them correctly.
Note: stash operates on whole hunks only (line selection syntax is not supported).
Output:
stashed a3f7c21 src/main.zig
stashed b8e4d2f src/args.zig
With --verbose, summary counts and hints are included:
stashed a3f7c21 src/main.zig
stashed b8e4d2f src/args.zig
2 hunks stashed
hint: use 'git stash list' to see stashed entries, 'git hunk stash pop' to restore
git hunk list # see what changed (with inline diffs)
git hunk diff a3f7c21 # inspect a specific hunk
git hunk add a3f7c21 # stage it
git hunk add b82e0f4 # stage another
git hunk list --staged --oneline # verify staged
git commit -m "feat: add error handling"
--porcelain outputs tab-separated fields, one hunk per line, with no headers
or alignment padding:
sha\tfile\tstart_line\tend_line\tsummary
Fields:
sha-- 7-character content hashfile-- file pathstart_line-- first line of the hunk rangeend_line-- last line of the hunk rangesummary-- first changed line, function context (fallback), "new file", or "deleted"
Line ranges are mode-aware: for unstaged hunks they refer to worktree lines, for staged hunks they refer to HEAD lines. This ensures hashes and ranges remain stable as other hunks are staged or unstaged.
Example:
a3f7c21 src/main.zig 12 18 Add error handling
b82e0f4 src/main.zig 45 52 Replace old parser
list shows inline diff content by default. Use --oneline to suppress it.
In human mode, each hunk's diff lines are indented by 4 spaces:
a3f7c21 src/main.zig 12-18 Add error handling
@@ -12,1 +12,2 @@ fn handleRequest()
const result = try parse(input);
+if (result == null) return error.Invalid;
In porcelain mode, the raw diff lines follow the metadata line verbatim, with records separated by a blank line:
a3f7c21 src/main.zig 12 18 Add error handling
@@ -12,1 +12,2 @@ fn handleRequest()
const result = try parse(input);
+if (result == null) return error.Invalid;
b82e0f4 src/main.zig 45 52 Replace old parser
@@ -45,1 +45,1 @@
...
By default, git-hunk respects git's diff.context setting (default: 3 lines).
Override with -U<n>, -U <n>, --unified=<n>, or --unified <n>:
git hunk list -U1 # finer-grained hunks (no space)
git hunk list -U 1 # finer-grained hunks (with space)
git hunk list --unified=0 # zero context (maximum granularity)
git hunk list --unified 0 # zero context (with space)
The -U/--unified flag is available on all commands. Context must be consistent
within a workflow -- hashes change with different context values.
By default, git-hunk prints action output (what was done) to stdout. Summary
counts and hints are shown with --verbose / -v:
git hunk add a3f7c21 --verbose # includes summary count and hints
git hunk list --verbose # includes "N hunks across M files" summary
Use --quiet / -q to suppress all output except errors:
git hunk add a3f7c21 --quiet # stage silently (exit code only)
git hunk count --quiet # no output (use exit code)
The --verbose and --quiet flags are available on all commands and are
mutually exclusive.
Stage or preview specific lines from a hunk using sha:line-spec syntax:
git hunk diff a3f7:3-5 # preview lines 3-5 (hunk-relative)
git hunk diff a3f7:3-5,8 # preview lines 3-5 and 8
git hunk add a3f7:3-5 # stage only lines 3-5
Line numbers are 1-based and relative to the hunk body (line 1 is the first
line after the @@ header). Use diff to preview what would be staged before
running add.
When showing a line selection, lines are numbered with > markers indicating
which lines are selected:
--- a/src/main.zig
+++ b/src/main.zig
1 @@ -12,3 +12,4 @@ fn handleRequest()
2 const result = try parse(input);
> 3 +if (result == null) return error.Invalid;
4 return result;
Unselected - lines become context in the patch; unselected + lines are
dropped. This produces a valid partial patch that git apply can process.
Each hunk gets a SHA-1 hash computed from:
SHA1(file_path + '\0' + stable_line + '\0' + diff_lines)
file_path-- canonical path from the diffstable_line-- the line number from the side that doesn't shift during staging. For unstaged hunks this is the new (worktree) side; for staged hunks this is the old (HEAD) side.diff_lines-- only the+and-lines (context lines excluded)
Because the hash uses the stable line number and the actual diff content, it remains constant as other hunks in the same file are staged or unstaged. This means you can list hashes, stage some hunks, and the remaining hashes stay the same.
Output is colorized when stdout is a TTY:
- SHA hashes in yellow (in
list,diff,add,reset,restore, andstashoutput) - Added lines (
+) in green - Removed lines (
-) in red
Color is disabled automatically when piping output. Use --no-color to disable
explicitly, or set the NO_COLOR environment variable. The --no-color flag is
accepted by all commands.
- Modified files (single and multi-hunk)
- New files (via
git add -N) - Untracked files (shown by default,
--tracked-only/--untracked-onlyto filter) - Deleted files
- Renamed files (with content changes)
- Files with C-quoted paths (tabs, backslashes)
- Files with no trailing newline
- Prefix matching (minimum 4 hex characters)
- Ambiguous prefix detection
- Bulk staging via
--allor--filewithout SHAs - Per-line staging via
sha:line-specsyntax - Configurable context lines via
-U<n>/-U <n>/--unified=<n>/--unified <n> - Hash validity checking via
checkwith--exclusivesupport - Worktree restore via
restorewith--dry-runpreview and--forcefor untracked - Hunk stashing via
stashwith native 3-parent format for untracked files - Executable bit preservation in stash
Integration tests use a deterministic fixture repo created by tests/setup-repo.sh
(3 files, 30 lines each, 2 commits):
REPO="$(bash tests/setup-repo.sh)"
MIT
