Skip to content

shhac/git-hunk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

128 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-hunk

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

Demo

Why

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.

Install

Homebrew

brew install shhac/tap/git-hunk

GitHub Releases

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/

Build from source

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

Help

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.

Usage

List hunks

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.

View hunk diff

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.

Stage hunks

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)

Unstage hunks

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

Restore worktree

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

Count hunks

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).

Check hunk validity

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).

Stash hunks

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

Typical workflow

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 format

--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 hash
  • file -- file path
  • start_line -- first line of the hunk range
  • end_line -- last line of the hunk range
  • summary -- 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

Diff output

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 @@
...

Context lines

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.

Verbosity

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.

Line selection

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.

How hashing works

Each hunk gets a SHA-1 hash computed from:

SHA1(file_path + '\0' + stable_line + '\0' + diff_lines)
  • file_path -- canonical path from the diff
  • stable_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.

Color output

Output is colorized when stdout is a TTY:

  • SHA hashes in yellow (in list, diff, add, reset, restore, and stash output)
  • 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.

Handles

  • Modified files (single and multi-hunk)
  • New files (via git add -N)
  • Untracked files (shown by default, --tracked-only/--untracked-only to 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 --all or --file without SHAs
  • Per-line staging via sha:line-spec syntax
  • Configurable context lines via -U<n> / -U <n> / --unified=<n> / --unified <n>
  • Hash validity checking via check with --exclusive support
  • Worktree restore via restore with --dry-run preview and --force for untracked
  • Hunk stashing via stash with native 3-parent format for untracked files
  • Executable bit preservation in stash

Testing

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)"

License

MIT

About

Non-interactive, deterministic hunk staging for git

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors