Automating git add -p

Devin Rich builds his own dynamic OUYA API server, and you will be able to edit existing game data and add new ones with it.

While building that, some changes to the 1200+ OUYA game data files were necessary, and I got those changes as one big commit. It added missing uuid for all developers, but also trimmed some text fields here and there.

I love to have sensible and clean commits that do one thing, and so I wanted to have one commit that fixed the UUIDs, and one for the rest. Using git add -p is the normal way to go when splitting up commits into several managable ones, but this was not possible with over 900 changed files.

grepdiff to the rescue

After some searching I found grepdiff, which is part of patchutils.

It allows me to find all changes that match a given regex in a patch file. In the end it shows you the files that contained the match - but it is also able to create a new patch with only the matching lines in it! The magic parameter for this is --output-matching=hunk.

Example

A simple file:

$ cat gamelist
best=Bloo Kid 2
second=Babylonian Twins
third=SNES9x

Now we change second and third place:

$ git diff
diff --git gamelist gamelist
index b0aeab0..548adad 100644
--- gamelist
+++ gamelist
@@ -1,3 +1,3 @@
-best=Bloo Kid 2
+best=Hidden in plain sight
 second=Babylonian Twins
-third=SNES9x
+third=Bomb Squad

My goal is now to only add the change to the "best" line to the git staging area. We do not want a unified diff here as shown above, because that gives us both changes in one hunk. Instead I use 0 lines of context:

$ git diff -U0
diff --git gamelist gamelist
index b0aeab0..548adad 100644
--- gamelist
+++ gamelist
@@ -1 +1 @@
-best=Bloo Kid 2
+best=Hidden in plain sight
@@ -3 +3 @@ second=Babylonian Twins
-third=SNES9x
+third=Bomb Squad

Now grepdiff can be used to get only those hunks that contain best:

$ git diff -U0 | grepdiff best --output-matching=hunk
diff --git gamelist gamelist
index b0aeab0..548adad 100644
--- gamelist
+++ gamelist
@@ -1 +1 @@
-best=Bloo Kid 2
+best=Hidden in plain sight

This patch file can now be piped into git apply to stage it:

$ git diff -U0\
 | grepdiff best --output-matching=hunk\
 | git apply --cached --unidiff-zero -p0
 
$ git diff --cached
diff --git gamelist gamelist
index b0aeab0..02e36aa 100644
--- gamelist
+++ gamelist
@@ -1,3 +1,3 @@
-best=Bloo Kid 2
+best=Hidden in plain sight
 second=Babylonian Twins
 third=SNES9x
 
$ git diff
diff --git gamelist gamelist
index 02e36aa..548adad 100644
--- gamelist
+++ gamelist
@@ -1,3 +1,3 @@
 best=Hidden in plain sight
 second=Babylonian Twins
-third=SNES9x
+third=Bomb Squad

-p0 is necessary because I use the diff.noprefix config option that omits the leading a/ and b/ in the filenames in diffs.

--unidiff-zero is needed because we used zero lines of context.

Written by Christian Weiske.

Comments? Please send an e-mail.