Skip to content

wkentaro/git-hunk

Repository files navigation

git-hunk

PyPI Python License Build

Non-interactive, programmatic alternative to git add -p.

Every hunk gets a stable, content-based ID so you can inspect, filter, and stage changes without interactive prompts.

git-hunk teaser

Why?

git add -p requires interactive input. That makes it unusable for:

  • AI agents (Claude Code, Codex, etc.) that need to split changes into logical commits
  • Scripts & CI/CD that automate commit organization
  • Editor integrations that want hunk-level staging without shelling out to a TUI

git-hunk solves this by assigning each hunk a stable ID and exposing simple stage/unstage/discard commands.

Install

pip install git-hunk

Or with uv:

uv tool install git-hunk

Verify it works:

git-hunk --version

For AI agents

A usage guide ships inside the CLI, so agents (Claude Code, Codex, etc.) can load it on demand. It always matches the installed version, so it never goes stale:

git-hunk skills get core

git-hunk --help points here first.

Quick start

# See all hunks across staged, unstaged, and untracked files
git-hunk list

# Show the diff for a specific hunk
git-hunk show d161935

# Stage specific hunks, then commit
git-hunk stage d161935 a3f82c1
git commit -m "feat: add validation for user input"

# Stage the remaining hunks
git-hunk stage e7b4012
git commit -m "fix: handle empty response in API client"

Usage

List hunks

git-hunk list                          # all hunks (unstaged + staged + untracked)
git-hunk list --unstaged               # unstaged hunks only
git-hunk list --staged                 # staged hunks only
git-hunk list src/foo.py src/bar.py    # specific files
git-hunk list --json                   # JSON output for scripting

Show hunks

git-hunk show                          # show all hunks (staged + unstaged)
git-hunk show d161935                  # show a single hunk
git-hunk show d161935 a3f82c1          # show multiple hunks
git-hunk show --staged                 # show all staged hunks
git-hunk show --unstaged               # show all unstaged hunks

Stage, unstage, discard

git-hunk stage d161935                 # stage a hunk
git-hunk stage d161935 a3f82c1         # stage multiple hunks
git-hunk stage d161935 -l 3,5-7        # stage specific lines only
git-hunk stage d161935 --exclude-matching debug    # stage all but lines containing "debug"
git-hunk stage d161935 --include-matching xfail    # stage only lines containing "xfail"
git-hunk unstage d161935               # move back to working tree
git-hunk unstage d161935 -l 3,5-7      # unstage specific lines only
git-hunk discard d161935               # restore from HEAD
git-hunk discard d161935 -l ^3,^5-7    # discard excluding specific lines

--include-matching / --exclude-matching select changed lines by content instead of line number (literal substring by default, --regex for regular expressions). Both are repeatable and OR'd, case-sensitive, and error if nothing matches. They are mutually exclusive with -l and with each other.

Commit

git-hunk commit d161935 -m "fix: ..."      # stage a hunk and commit it in one step
git-hunk commit d161935 -l 3,5-7 -m "..."  # stage specific lines and commit

commit aborts if anything is already staged, so the commit contains exactly the selected hunks.

JSON output

git-hunk list --json     # inventory: every hunk, no body
git-hunk show <id> --json # the same hunks plus a structured per-line body

Both emit a versioned envelope (schema_version is currently 2) so consumers can depend on a stable shape. list --json is a lean inventory and carries no body; show --json adds a structured lines array. A show --json hunk (list --json is identical but without the lines field):

{
  "schema_version": 2,
  "hunks": [
    {
      "id": "d161935",
      "file": { "text": "src/main.py" },
      "status": "unstaged",
      "change_kind": "M",
      "a_mode": "100644",
      "b_mode": "100644",
      "binary": false,
      "header": "@@ -10,3 +10,5 @@",
      "context_before": { "text": "def main():" },
      "additions": 2,
      "deletions": 0,
      "lines": [
        { "n": 1, "op": " ", "content": { "text": "    x = 1" } },
        { "n": 2, "op": "+", "content": { "text": "    y = 2" } }
      ]
    }
  ]
}
Field Type Description
schema_version int Envelope version; bumped on any incompatible change to the shape below.
hunks array The hunks (empty array when there are no changes).
id string Stable, content-based hunk id (7-char SHA-256 prefix); accepts prefixes.
file union Path of the changed file, as a byte-safe {text|bytes} union (see below).
status string One of staged, unstaged, untracked.
change_kind string Git status letter: A added, D deleted, M modified, T typechange (R/C reserved). Always present.
a_mode string | null 6-digit octal git mode on the pre-image side; null when that side does not exist.
b_mode string | null 6-digit octal git mode on the post-image side; null when that side does not exist.
binary bool Whether the change is binary. Always present.
header string | null The hunk's bare @@ -a,b +c,d @@ range; null for a whole-file (binary, mode-only, or type) change.
context_before union | null The function/section git names after the @@ header, as a {text|bytes} union; null when there is none.
additions int Number of added lines.
deletions int Number of removed lines.
lines array show --json only. The structured body; [] for a whole-file hunk. See below.

A lines entry is { "n", "op", "content", "no_newline"? }:

Field Type Description
n int 1-based position within the hunk body — the index -l line selection uses. Counts every body line.
op string " " context, "+" addition, "-" deletion.
content union The line text without its leading op character, as a {text|bytes} union.
no_newline bool Present and true only when the line has no trailing newline; consumes no n.

Any field carrying arbitrary git/source bytes (file, context_before, lines[].content) is a byte-safe {text | bytes} union: {"text": "..."} for valid UTF-8, else {"bytes": "<base64>"}. It is always an object, so consumers have one code path and strict JSON parsers never see a lone surrogate.

Adding a new field is backward-compatible and does not change schema_version; renaming, removing, or changing the type of an existing field bumps it. (Before schema_version existed, list --json returned a bare array.)

Comparison

Interactive Programmatic Hunk IDs Line-level control JSON output
git add -p Yes No No Yes No
git add <file> No Yes No No No
git-hunk No Yes Yes Yes Yes

How it works

  1. Parses git diff output into individual hunks
  2. Assigns each hunk a stable, content-based ID (SHA-256 prefix)
  3. For staging: reconstructs a minimal patch and pipes it through git apply --cached
  4. For discarding: reconstructs a reverse patch and applies it to the working tree

IDs are stable across partial staging -- they are derived from the changed lines, not the @@ line numbers that shift as you stage hunks.

Contributing

Bug reports, feature requests, and pull requests are welcome on GitHub.

git clone https://github.com/wkentaro/git-hunk.git
cd git-hunk
make setup   # install dependencies
make test    # run tests
make lint    # run linters

License

MIT (LICENSE)

About

Non-interactive git hunk staging for AI agents.

Resources

License

Stars

Watchers

Forks

Contributors