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.
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
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
-mis for modified files
-ois for other files (also known as untracked)
-oto 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:
git addis always invoked, even if I cancel
- 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
-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
The final step to making this usable is to add it as a Git alias. So in my
[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.