4 min read

On Patching Git Working Tree Files

Easily one of my favorite Git commands, git add has an awesome switch -p (long form --patch) to patch hunks of files that are currently in the working tree. This means that instead of undoing changes in a text editor to isolate only the changes that you know you want to stage or any other laborious hack to accomplish the same thing, you can add various bits of changes of any “dirty” file in your working tree to only index (stage) those particular bits while ignoring everything else. Known as hunks, these changes can be staged however you like, skipping some or all, and thus allows you to customize a commit from a working tree that could contain many bits from many files that aren’t yet ready for staging.

Although I just mentioned the -p switch to patch, I usually invoke the git add command with the -i flag (long form --interactive) to drop me into a menu that allows me to select only the files that contain the hunks that I wish to stage. In contrast, invoking git add -p would bypass the menu but then would have me work through every file in the working tree. Since I usually know exactly what hunks I want stage, I just find it easier to select those files individually.

Let’s walk through an interactive session.

First, I invoke the command that brings up the menu. Although I use many of these options quite frequently, I am only going to describe the patch option. I highly recommend, however, that you familiarize yourself with what the other commands do. It will save you a lot of time!

~/projects/dotfiles:master$ g add -i
           staged     unstaged path
  1:    unchanged        +2/-2 bash/.bash_aliases
  2:    unchanged        +1/-1 bash/.bash_env
  3:    unchanged        +3/-2 bash/.bash_functions
  4:    unchanged        +2/-2 tmux/linux/.tmux.conf
  5:    unchanged        +2/-2 vim/.vim.autocmd
  6:    unchanged        +4/-0 vim/.vim.mappings

*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help
What now> 

Choose the patch option:

What now> 5
           staged     unstaged path
  1:    unchanged        +2/-2 bash/.bash_aliases
  2:    unchanged        +1/-1 bash/.bash_env
  3:    unchanged        +3/-2 bash/.bash_functions
  4:    unchanged        +2/-2 tmux/linux/.tmux.conf
  5:    unchanged        +2/-2 vim/.vim.autocmd
  6:    unchanged        +4/-0 vim/.vim.mappings
Patch update>> 

To select more than one file, separate each with a comma:

Patch update>> 3,6
           staged     unstaged path
  1:    unchanged        +2/-2 bash/.bash_aliases
  2:    unchanged        +1/-1 bash/.bash_env
* 3:    unchanged        +3/-2 bash/.bash_functions
  4:    unchanged        +2/-2 tmux/linux/.tmux.conf
  5:    unchanged        +2/-2 vim/.vim.autocmd
* 6:    unchanged        +4/-0 vim/.vim.mappings
Patch update>> 

The asterisks obviously designate the chosen files to patch, so hit enter again (i.e., at the blank line) to begin patching those files:

diff --git a/bash/.bash_functions b/bash/.bash_functions
index 6a98e53..e9df391 100644
--- a/bash/.bash_functions
+++ b/bash/.bash_functions
@@ -41,11 +41,12 @@ bp() {
         echo "$(tput setaf 1)[ERROR]$(tput sgr0) Not enough arguments."
         echo "Usage: bp <filename>"
     else
-
         # Let's not overwrite an existing file. `stat` and test the process exit code.
         stat "$1" &> /dev/null
 
-        if [ $? -eq 1 ]; then
+       # Check exit code of `stat` call before proceeding.
+       # An error (return value of 1 in this case) means that the file does not exist.
+        if [ "$?" -eq 1 ]; then
             case "$1" in
                 *.elm)  vim -c ":read ~/templates/elm.txt" "$1" ;;
                 *.html) vim -c ":read ~/templates/html.txt" "$1" ;;
Stage this hunk [y,n,q,a,d,/,s,e,?]? 

As you can see, you’re presented with a diff (patch) of the first hunk for which you must make a decision…to patch or not to patch? The options are intuitive, y for yes, n for no, etc.

One of the options that I use quite frequently is s, or split. This comes in really handy for when the hunk contains a mix of changes I want to stage and ones that I don’t. Very often, splitting the hunk into finer-grained hunks will allow me to index just the line that I want (although this isn’t always the case).

Once you’ve worked your way through the selections and staged your hunks, git will drop you back to the menu to wait upon your next selection. You can quit at this point.

*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help
What now> q
Bye.

If you invoke the same command again, the menu helpfully shows what has been indexed and what is still in the working tree. Neat! You can revert and do it all again…weeeeeeeeeeee!

~/projects/dotfiles:master$ !git
git add -i
           staged     unstaged path
  1:    unchanged        +2/-2 bash/.bash_aliases
  2:    unchanged        +1/-1 bash/.bash_env
  3:        +3/-2      nothing bash/.bash_functions
  4:    unchanged        +2/-2 tmux/linux/.tmux.conf
  5:    unchanged        +2/-2 vim/.vim.autocmd
  6:        +4/-0      nothing vim/.vim.mappings

*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help
What now> 

At this point, you can do a git status and see the results of your labors.

~/projects/dotfiles:master$ g s
## master...origin/master
 M bash/.bash_aliases
 M bash/.bash_env
M  bash/.bash_functions
 M tmux/linux/.tmux.conf
 M vim/.vim.autocmd
M  vim/.vim.mappings

Finally, if you’re interested you can find my .gitconfig on my GitHub.