Article summary
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:
git add
is always invoked, even if I cancelfzf
.- 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.
Getting `xargs: invalid option — ‘o’` with xargs v4.6.0
xargs doesn’t seem to know the -o option on any of my machines, also it still executes git add after exiting fzf
what fixed it for me was replacing -o with -r