<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vitaly Ostanin</title>
    <description>The latest articles on DEV Community by Vitaly Ostanin (@vitaly-ostanin).</description>
    <link>https://dev.to/vitaly-ostanin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4014849%2F82984c94-ff0e-4d0f-8117-7521dadf7d35.jpg</url>
      <title>DEV Community: Vitaly Ostanin</title>
      <link>https://dev.to/vitaly-ostanin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vitaly-ostanin"/>
    <language>en</language>
    <item>
      <title>hunkpick: a non-interactive git add -p for scripts and coding agents</title>
      <dc:creator>Vitaly Ostanin</dc:creator>
      <pubDate>Sat, 04 Jul 2026 10:15:08 +0000</pubDate>
      <link>https://dev.to/vitaly-ostanin/hunkpick-a-non-interactive-git-add-p-for-scripts-and-coding-agents-1f72</link>
      <guid>https://dev.to/vitaly-ostanin/hunkpick-a-non-interactive-git-add-p-for-scripts-and-coding-agents-1f72</guid>
      <description>&lt;p&gt;Staging a &lt;em&gt;subset&lt;/em&gt; of your changes is a routine part of making clean commits. Git's built-in tool for it is &lt;code&gt;git add -p&lt;/code&gt; — you walk through each hunk and answer &lt;code&gt;y&lt;/code&gt;/&lt;code&gt;n&lt;/code&gt;/&lt;code&gt;s&lt;/code&gt;. That works at a keyboard, but it is interactive by design: you cannot drive it from a shell script, a Makefile, or an automated coding agent that needs to stage a precise set of changes without a human at the prompt.&lt;/p&gt;

&lt;p&gt;The traditional &lt;strong&gt;non-interactive&lt;/strong&gt; answer is &lt;a href="https://linux.die.net/man/1/filterdiff" rel="noopener noreferrer"&gt;&lt;code&gt;filterdiff&lt;/code&gt;&lt;/a&gt; from the &lt;a href="https://cyberelk.net/tim/software/patchutils/" rel="noopener noreferrer"&gt;patchutils&lt;/a&gt; suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff path | filterdiff &lt;span class="nt"&gt;--hunks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1,3 | git apply &lt;span class="nt"&gt;--cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;filterdiff&lt;/code&gt; selects whole hunks by their position in the diff. The problem: a single hunk often contains several unrelated change runs separated by a few context lines. If you only want one of them, &lt;code&gt;filterdiff&lt;/code&gt; cannot help — the whole hunk is in or out.&lt;/p&gt;

&lt;h2&gt;
  
  
  hunkpick
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/VitalyOstanin/hunkpick" rel="noopener noreferrer"&gt;hunkpick&lt;/a&gt; is a small Rust CLI that fills that gap. It is a pure &lt;code&gt;stdin → stdout&lt;/code&gt; filter: it reads a unified diff, emits the subset you asked for, and leaves applying it to you (&lt;code&gt;git apply --cached&lt;/code&gt;). It never calls &lt;code&gt;git&lt;/code&gt; to produce the diff, so it works with any diff source — git, Mercurial, SVN, or plain &lt;code&gt;diff -u&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What it adds over &lt;code&gt;filterdiff&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-split into sub-hunks.&lt;/strong&gt; Each hunk is decomposed into minimal sub-hunks, one per contiguous run of &lt;code&gt;+&lt;/code&gt;/&lt;code&gt;-&lt;/code&gt; lines, each addressable by a stable per-file index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-file addressing.&lt;/strong&gt; In a multi-file diff selectors are prefixed with the path (&lt;code&gt;path:1,3&lt;/code&gt;, &lt;code&gt;path:*&lt;/code&gt;) so they stay unambiguous; for a single-file diff the prefix is optional and you can write the bare &lt;code&gt;1,3&lt;/code&gt; used in the examples below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content ids.&lt;/strong&gt; Every sub-hunk carries a &lt;code&gt;@&amp;lt;id&amp;gt;&lt;/code&gt; derived from the file path and its changed lines only — not the context or the &lt;code&gt;@@&lt;/code&gt; line numbers. The id survives a re-diff even when staging a neighbour renumbers indices or rewrites surrounding context, so an agent can capture it once and keep using it across a staging loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine-readable listing.&lt;/strong&gt; &lt;code&gt;list --json&lt;/code&gt; gives structured output for tooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in verification.&lt;/strong&gt; The result diff is checked for internal consistency by default; &lt;code&gt;--verify-result-diff-git&lt;/code&gt; additionally runs &lt;code&gt;git apply --check&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single cross-platform binary&lt;/strong&gt;, including Windows — no Unix toolchain to install.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  List, then select
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See the addressable sub-hunks&lt;/span&gt;
git diff src/main.rs | hunkpick list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;src/main.rs
&lt;/span&gt;  [1] 6860edc905028034 @@ -1,5 +1,5 @@  +1 -1  -    let a = 0;
  [2] 5bb69992111224fd @@ -6,5 +6,5 @@  +1 -1  -    let d = 0;
  [3] 369095db02d2feec @@ -11,3 +11,3 @@  +1 -1  -    let h = 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;[n]&lt;/code&gt; is a per-file index; the &lt;code&gt;@&amp;lt;id&amp;gt;&lt;/code&gt; after it is the content id (below). The preview is the first changed line of the sub-hunk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage sub-hunks 1 and 3, skipping the middle one&lt;/span&gt;
git diff src/main.rs | hunkpick &lt;span class="k"&gt;select &lt;/span&gt;1,3 | git apply &lt;span class="nt"&gt;--cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sub-hunks 1 and 3 are now staged; sub-hunk 2 is left in the working tree — exactly the kind of split &lt;code&gt;filterdiff&lt;/code&gt; cannot express, because all three live in one hunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting one file's changes into several commits
&lt;/h2&gt;

&lt;p&gt;The content id is what makes an automated staging loop reliable. Bare indices renumber as you stage, but a &lt;code&gt;@&amp;lt;id&amp;gt;&lt;/code&gt; stays valid across the re-diff — capture the listing once and keep using the ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Capture the ids once.&lt;/span&gt;
git diff src/indicator.js | hunkpick list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;src/indicator.js
&lt;/span&gt;  [1] 3a02b1b8b5f9bd02 @@ -1,3 +1,3 @@  +1 -1  -  const a = 0;
  [2] 0759ce5f8024a473 @@ -4,2 +4,2 @@  +1 -1  -  const b = 0;
  [3] 31aa062fa4795c80 @@ -6,2 +6,2 @@  +1 -1  -  const c = 0;
  [4] 0d33a7656bae5ea9 @@ -8,3 +8,3 @@  +1 -1  -  const d = 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. Stage and commit each group by @id, re-running git diff each round.&lt;/span&gt;
git diff src/indicator.js | hunkpick &lt;span class="k"&gt;select&lt;/span&gt; @3a02b1b8b5f9bd02 | git apply &lt;span class="nt"&gt;--cached&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix: ..."&lt;/span&gt;

git diff src/indicator.js | hunkpick &lt;span class="k"&gt;select&lt;/span&gt; @0759ce5f8024a473 @31aa062fa4795c80 | git apply &lt;span class="nt"&gt;--cached&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: ..."&lt;/span&gt;

&lt;span class="c"&gt;# 3. Whatever is left is the last group.&lt;/span&gt;
git diff src/indicator.js | hunkpick &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'*'&lt;/span&gt; | git apply &lt;span class="nt"&gt;--cached&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: ..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commits: &lt;code&gt;fix&lt;/code&gt; gets sub-hunk 1, &lt;code&gt;feat&lt;/code&gt; gets 2 and 3, and &lt;code&gt;chore&lt;/code&gt; gets the remaining 4 via &lt;code&gt;*&lt;/code&gt;. Between steps the bare indices would renumber, but each &lt;code&gt;@&amp;lt;id&amp;gt;&lt;/code&gt; stays valid across the re-diff, so the listing captured once keeps working.&lt;/p&gt;

&lt;h2&gt;
  
  
  hunkpick vs filterdiff
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;filterdiff&lt;/th&gt;
&lt;th&gt;hunkpick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Select whole hunks&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with any diff source&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address sub-hunks by per-file index&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-split hunks at change-run boundaries&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable content ids across re-diffs&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine-readable listing (JSON)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in result verification&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split an addition-only block by line range&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;hunkpick
&lt;span class="c"&gt;# or a prebuilt binary:&lt;/span&gt;
cargo binstall hunkpick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Links: &lt;a href="https://github.com/VitalyOstanin/hunkpick" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://crates.io/crates/hunkpick" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; · &lt;a href="https://docs.rs/hunkpick" rel="noopener noreferrer"&gt;docs.rs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this project was developed with the help of an AI coding assistant, and this article was drafted with LLM assistance and edited by the author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>git</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
