2 Comments

Using Fuzzy-Find to Enhance Git Add

A frequent part of my daily workflow involves adding files to Git. Since I like to keep commits relatively small and focused, that often means adding only a subset of changed files in my working tree. With the git command-line client, that is usually accomplished by copying and pasting the files I want from the git status output. A small annoyance perhaps, but since it’s something I do quite often, I have been looking for a way to improve it.

Discovery

My first idea was to modify the output of git status and assign numbers to each file listed, something like the way git clean -i works when you choose “select by numbers.” But in my search to see if anyone had already solved this problem, I stumbled upon fzf.

fzf is not a Git-specific tool, but rather a command-line fuzzy-find tool that’s true to the Unix philosophy of “do one thing and do it well.” At its core, fzf accepts a list of strings and presents a fuzzy-find interface for selecting from that list. It has some great potential applications that are outside the scope of this post, but for now, I’ll just focus on leveraging it to work with git add.

Integration

Without any command-line arguments, fzf will index the current directory. Thanks to its shell integration, I can just hit Ctrl+T to invoke it. Fortunately, fzf also supports multiple selection, so I can fuzzy-find to narrow down the list, then hit tab to select each line I want.

So far, that means I can type git add <Ctrl+T> and pick from all of the files in the current directory. This is a good start, but I’d like to narrow the initial list of options to just the files that have changed (i.e. the output of git status). I can get a script-friendly list of those files from Git with:


git ls-files -m -o --exclude-standard
  • -m is for modified files
  • -o is for other files (also known as untracked)
  • --exclude-standard modifies -o to exclude untracked files according to the standard “ignore” rules

Great! We can start fzf in multi-pick mode using its -m flag, so let’s try this:


git add $(git ls-files -m -o --exclude-standard | fzf -m)

This works, but it does have some issues:

  1. git add is always invoked, even if I cancel fzf.
  2. It won’t work on files with spaces in their names.

The first problem can be solved by shuffling the order in which we invoke things. Instead of starting a sub-shell to generate a list of options, we can pipe everything through xargs, like so:


git ls-files -m -o --exclude-standard | fzf -m | xargs git add

The second is a classic problem of piping lists of things around as plain text. Many utilities have an option that will delimit list items using the NUL character (ASCII code 0) instead of whitespace, and fortunately fzf supports this as well.


git ls-files -m -o --exclude-standard | fzf -m --print0 | xargs -0 git add

Now we are in pretty good shape! But what happens when I want to use git add in “patch” mode (with --patch or -p), to interactively prompt before staging each hunk? With the current invocation, this won’t work since git add is launched by xargs and no longer has a connection to the console.

A little perusing of the xargs man page reveals its -o option, which will reconnect git add to the console! I also found the -t option which causes xargs to echo the command before running.


git ls-files -m -o --exclude-standard | fzf -m --print0 | xargs -0 -o -t git add -p

Usage

The final step to making this usable is to add it as a Git alias. So in my ~/.gitconfig:


[alias]
fza = "!git ls-files -m -o --exclude-standard | fzf --print0 -m | xargs -0 -t -o git add"

To use the alias, I can run git fza in most cases, or git fza -p to use patch mode.

It may seem like a small thing, but small optimizations add up when you use them frequently.