It's popular for people to do things like this in order to save a couple of keystrokes:
alias gco='git checkout'
Honestly, I'm not a fan, so I thought I'd see if I could do better.
What do I want to achieve?
I want to lower the effort in changing branches. It's a modest goal, and one that's theoretically addressed by the alias above. But say you want to switch between two long-names feature branches:
$ git branch
develop
feature/TCKT-123-bug-frobinator-crashes-doohickey
* feature/TCKT-124-feat-add-flux-capacity
Now, you can have some kind of tab completion installed for git, so you can type git checkout f<tab>3<tab>
and get where you want to go. That's one for the vimmers among us; let's say we don't care for that way of working and sweep that whole solution under the Rug Of Digression.
We can make our own command instead.
Let it work on as many systems as possible
Instead of using a shebang like #!/bin/bash
or #!/usr/bin/env bash
we'll use #!/bin/sh
. If we don't have any requirements beyond POSIX, why add any dependencies?
Here's a relevant Stack Overflow question about the recommended POSIX shebang but suffice it to say that this one is more likely to exist than bash
, and that env
might not even be available if /usr
isn't mounted yet.
#!/bin/sh
Ensure we're in a git repository
We want to bail out of the script early if it's not going to work.
We don't want to see an initial git error and then a bunch of artifacts from our script.
if ! git status >/dev/null; then
exit
fi
Pass through git's regular error messages and exit status
Redirecting STDOUT to /dev/null
means that if we are in a git repo, the command won't spew irrelevant status information to the console.
We could include a redirect for STDERR with 2>&1
, but if the command fails, we want it to display an error. Likewise, we could provide our own error code with something like exit 1
to show there's been a problem, but exit
on its own will pass through the previous error code. It's effectively the same as exit $?
. This way we don't need to worry about compatibility with other scripts, they will get the appropriate git exit code.
Add support for fuzzy matching
We can take the output from git branch
and pass it through fzf
which will do the heavy lifting for us.
if command -v fzf >/dev/null; then
branch="$(git branch -a | grep -oE '[^ ]+$' | fzf)"
if [ -n "$branch" ]; then
git checkout "$branch"
fi
This looks a little scary! It's searching for a branch that contains the first argument to the script, but what's that odd-looking grep
for?
-o
means to only return the bit of the line that matches the pattern, not the whole line as in grep
's default behaviour.
-E '[^ ]+$'
is an extended regex to grab the last word on the line1.
Why is this important? It's because if one of the branches is the current branch, then it will have a prefix like ' * '. Check the example with the feature branches above to see what I mean. We need to make sure we have the branch name and only the branch name.
Make it pretty!
We can use fzf
's --preview
option to get something helpful for each result. The obvious choice is a git show
of the branch:
git branch -a | grep -oE '[^ ]+$' | fzf --preview='git show --color=always --stat {}
Degrade gracefully if there's no available fuzzy matching tool
We can tell if a command is available like this2:
if command -v fzf >/dev/null; then
# Do stuff
fi
Use grep
to work with a partial match instead.
We'd like to be able to checkout a branch using a unique string, such as the ticket numbers in the example above. This uses a cut-down version of our fuzzy branch finding command from earlier.
matches=$(git branch | grep -i "$1" | grep -oE '[^ ]+$')
Do something sensible if multiple matches are found
We can count the matches with grep -c
and if it returns more than one, print3 the possibilities.
This time we don't need to enclude all the crazy regex, because we're not interested having a sanitised result.
count=$(git branch | grep -ci "$1")
if [ "$count" = "1" ]; then
shift
git checkout "$matches" "$@"
elif [ "$count" = "0" ]; then
printf "No match for '%s'" "$1"
else
printf "Multiple matches:\n"
for match in $matches; do
printf "%s\n" "$match"
done
fi
Do something sensible if no branch is specified
In the fuzzy route, we open fzf
by default, so what can we do that's as similar as possible? How about listing all local branches?
if command -v fzf >/dev/null; then
git branch -a
fi
Provide usage instructions if passed the --help
flag
We're going to use a case
statement as a little dispatcher.
Each possible command line argument will get matched against a different pattern and we'll use that to decide what to do next.
case "$1" in
--help|-h)
printf "Usage: %s [pattern]\n" "$(basename "$0")"
;;
# ...
esac
Here, we can explicitly catch both -h
and --help
and display a help message.
Safely handle unexpected flags
We don't have any other flags in this simple script, and we don't want to pass anything through to downstream executables like git
or grep
that starts with a -
in case they interpret it as a flag.
We're keeping things clean by quoting variables (hence "$1"
instead of $1
), but it pays to program defensively. Right? Right. I know you're with me on this one.
In fact, this is the only place we want to supply our own 'error' exit code.
case "$1" in
--help|-h)
printf "Usage: %s [pattern]\n" "$(basename "$0")"
;;
-*)
printf "Usage: %s [pattern]\n" "$(basename "$0")"
exit 1
;;
# ...
esac
It goes after the --help
pattern because otherwise that would take precedence.
Use this command to extend git.
If we call it git-co
and save it somewhere on our PATH
, then it will work exactly as if it was a native git subcommand4
Putting it all together
A little bit of nesting things and swapping conditionals and we get to the tl;dr of this post!
#!/bin/sh
# co: "Check Out".
# Checkout an existing branch based on a partial match.
#
# If FZF is installed, this script will use it.
if ! git status >/dev/null; then
exit
fi
case "$1" in
--help|-h)
printf "Usage: %s [pattern]\n" "$(basename "$0")"
;;
-*)
printf "Usage: %s [pattern]\n" "$(basename "$0")"
exit 1
;;
"")
if command -v fzf >/dev/null; then
branch="$(git branch -a | grep -oE '[^ ]+$' | fzf --preview='git show --color=always --stat {}')"
if [ -n "$branch" ]; then
# remove the "remotes/origin/..." branch name prefix.
branch="$(echo "$branch" | sed -e "s#^remotes/[^/]*/##")"
git checkout "$branch"
fi
else
git branch -a
fi
;;
*)
matches=$(git branch | grep -i "$1" | grep -oE '[^ ]+$')
count=$(git branch | grep -ci "$1")
if [ "$count" = "1" ]; then
shift
git checkout "$matches" "$@"
elif [ "$count" = "0" ]; then
printf "No match for '%s'" "$1"
else
printf "Multiple matches:\n"
for match in $matches; do
printf "%s\n" "$match"
done
fi
;;
esac
Postscript
This post was partly inspired by comments about fzf in Neovim for web development and the fact that there are a lot of posts about aliases here.
I've also talked more generally about extending command-line programs before.
Cover image by Zach Reiner on Unsplash
-
There are multiple ways to match the last word, this is just the way I happened to choose. ↩
-
command
is more portable thanwhich
, but for either, we have to redirect its STDOUT to keep our output clean. ↩ -
I'm using
printf
here because it's more portable thanecho
. ↩ -
Well, it won't support
git help co
, because we haven't gone so far as to make an installer and manual file.¯\_(ツ)_/¯
↩