I have a bunch of useful Git shortcuts (aliases, really) in my
~/.gitconfig
, so I thought I would share a few that I use quite often in my
workflow. If you haven’t used Git shortcuts, these are aliases to help you type
a much smaller command for those commands that you type regularly.
I should note that all of these shortcuts are aliases in the [alias]
section
of my ~/.gitconfig
. I will be repetitive so that it’s clear in each code
section that I’m talking about the [alias]
section of my config, and not
commands you need to run at your shell prompt, but note that you can combine
all the aliases you like into a single [alias]
section, which I’ll show at
the end of the post.
Foreword
This post is not an intro or a tutorial on Git, and it assumes that you are comfortable with Git commands, branches, etc. If not, I highly recommend starting by learning Git and using it for a bit to become profficient with it, before optimizing your Git usage with the suggestions below.
Further, this post also assumes that you’re using Git via the command-line and that you use the terminal quite a bit; if you’re primarily using Git via GUI and you use a graphical IDE, these shortcuts may not be applicable to your typical workflow.
Basic Git aliases
Let’s get a few basic ones out of the way; these are just simple and obvious:
[alias]
b = branch
d = diff
s = status
So, given the above config, when I type git b
, that is equivalent to me
typing git branch
and so on. I don’t even use Tab to autocomplete
it, I just run it as-is.1
Folks who regularly work with Mercurial2 are comfortable with automatic
shortcuts hg d
and hg di
for hg diff
; however, hg s
is ambiguous3, so
hg st
is the shortcut for hg status
. We can add that here to enable us to
do the same in Git without thinking about it much:
[alias]
st = status
For the fans of the version control systems CVS and Subversion, these are must-haves, for muscle memory, if not nostalgia:
[alias]
ci = commit
co = checkout
This already helps quite a bit with repetitive typing, but let’s keep going.
Let’s say you’ve added all the changes in your workspace to your index. Now
git diff
(or git d
if you’re using the alias above) shows nothing. But
there are changes there, and we want to see them!
These changes can be found via git diff --staged
(synonym
for git diff --cached
), but who wants to type that out in
full every time? Into the aliases they go, both of them:
[alias]
dc = diff --cached
ds = diff --staged
The command git add
can be used to stage a file, but what about
unstaging? Why isn’t there a command for that? Well… there is a command
for that, and it looks as follows:
$ git reset HEAD [...]
Which brings us to the next question: why isn’t it just called unstage
? Let’s
add it:
[alias]
unstage = reset HEAD --
Note: the
--
argument is to tellgit
to not parse any following arguments as command-line flags to itself, and to treat them all as positional arguments to thereset
command instead. You can omit this in most cases, but using it here means that you won’t run into issues if, by change, you have a filename that starts with a dash (-
).This is not Git-specific (see the POSIX docs4); this behavior is supported by many other command-line tools and command-line argument parsing libraries as well.
Is git reset HEAD --
(or even the simpler version git reset HEAD
) as simple
or as eloquent as git unstage
? No, of course not, which is why we’ve fixed
that with an alias.
So, how can we use this alias? Here’s an example:
$ git add foo
# foo is staged to be committed
$ git unstage foo
# foo is no longer staged to be committed
In fact, when you run git status
after git add foo
, Git will tell you that
to undo this action, you can run git reset HEAD [...]
(though it won’t tell
you about the --
above), but this makes it simple to remember and easy to
use.
Running shell commands
What if you want to use an alias to run a different command, whether a different Git subcommand or an entirely different command (not even Git itself), or a set of commands in sequence, just in the shell directly?
Git has an escape hatch for that: just prefix your command with !
and it’ll
be executed as a shell command. In that case, you’ll have to prefix Git
commands with the literal git
, of course.
As a basic example, if we wanted git ls
to run ls -FC --color=auto
, we can
configure it as follows:
[alias]
ls = !ls -FC --color=auto # Note: this is incorrect; see below
However, note that these commands run from the top-level of the repository,
which may not be your current directory. However, Git proviles a useful
environment variable, $GIT_PREFIX
(which can also be obtained via git rev-parse --show-prefix
) which shows the path to the current directory from
the top of the repository, which you can use in your commands, e.g.,
[alias]
ls = !ls -FC --color=auto ${GIT_PREFIX}
If you want to run several commands from that directory, you can cd
into it
first and then run your full command sequence:
[alias]
ls = !cd ${GIT_PREFIX} && command1 && command2
How can we make use of this command-running functionality, especially from the top of the tree?
Testing via git
Let’s say you’re deep in your development tree, and you want to run all tests, from the top-level directory of your repo. If you’re using an IDE, you’ve probably already configured it appropriately, and a single button or a keypress will initiate a full build + test cycle, but how can we do this from the command line?
$ cd ../../..
$ pwd
# no wait, that's not the root of the repo yet
$ cd ..
$ make test
Sound familiar? Let’s see if we can fix this.
Sure, maybe Git is not a build tool, but why not use the functionality above to
our advantage? We can add a very simple test
alias:
[alias]
test = !make test
Now we can simply run git test
from any directory, and it will be equivalent
to running make test
from the top of the source tree, but without having to
figure out how many directories we are from the root of the tree and manually
change directories, or write shell scripts, etc.
This is also very handy because you’re defining shortcuts in a global file
(~/.gitconfig
) which means it’s automatically available to any Git repo you
are using, and you don’t need to do any per-project customization.5
Local per-repo configuration
In case you have some projects that are using make
and others using different
build systems, instead of adding the alias to your global config in
~/.gitconfig
, you can add it to the specific clone of a project, and only
visible to yourself, the same way that you can configure your name, email, etc.
on a per-repo basis.
For example, if you have a make
-based project, you can add a local alias via:
$ git config alias.test '!make test'
For a Go-based project, which doesn’t need any build/test system, you can use:
$ git config alias.test '!go test ./...'
Now you can run tests in different repos, regardless of their testing
infrastructure simply by running git test
, from any directory in the tree!
Remember that if you want to run tests in the current directory, you can use
cd ${GIT_PREFIX}
first.
That simplifies cognitive overhead, especially if you’re working with multiple projects using different languages and tooling.
Git automation workflows
Let’s say you’re contributing to a project on GitHub. To do that, you’ll probably do the following:
- fork the project to your own account (via GitHub UI)
- clone the project locally (via
git clone
) - create a feature or bugfix branch (via
git co -b
) - make your changes and commit (via
git add
,git ci
) - push your local branch to your fork (via
git push
) - create a pull request (via GitHub UI)
Along the way, you may also want to update your branch by rebasing it on the
latest changes that happened in the main
branch periodically.6
For simplicitly, I keep my local main
branch free of any changes, so it
mirrors the upstream main
branch as-is, so I periodically update the local
main
branch, and then git rebase
my feature branch on it. Thus, my actual
workflow would be as follows:
$ git checkout main
$ git checkout -b new-feature
# make some changes
$ git add [...]
$ git ci [...]
$ git push
fatal: The current branch git-shortcuts has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin new-feature
Aside: simplifying git push
I used to just copy-paste the command and run it as-is, and then I got tired of it, so I automated it:
[alias]
# pobr: (p)ush (o)rigin (br)anch
pobr = !git push --set-upstream origin "$(git rev-parse --abbrev-ref HEAD)"
Later, I learned that you can just make this behavior the default via a global config, so with just a single command:
$ git config --global push.default current
git push
now does all the work, and I don’t need the git pobr
alias
anymore.
Updating the main
branch
After I’ve git push
ed my local branch, created a PR, and got some comments on
it, I may realize that the main
branch on the project already has some
newer changes which I want to incorporate into my branch. As I mentioned
earlier, I prefer to rebase my commits on the latest changes in upstream
main
, so I want to do the following:
$ git checkout main
$ git pull --ff-only upstream main
# optional: `git push origin` to keep origin up-to-date with upstream
$ git checkout new-feature
$ git rebase main
I typically only combine the first half of these steps into a single handy
shortcut, as rebase
may require more hands-on interaction:
[alias]
# puma: (p)ull (u)pstream (ma)in
puma = !git checkout main && git pull --ff-only upstream main && git push
Note that I use the
--ff-only
(fast-forward only) flag to ensure that my localmain
branch is clean, as I don’t want to create complex merge trees in my repo, which will then flow through to each of my feature/bug-fix branches.
When I am working on my own projects, where my repo on GitHub is already the
canonical source, there’s no upstream
, just origin
, so while I will still
use PRs to run tests on my feature or bug fix branches before merging them into
main
, I still need to update my local main
branch (I never develop on
main
, even for my own repos).
Thus, instead of updating my local main
from upstream
, I will update it
from origin
:
[alias]
# poma: (p)ull (o)rigin (ma)in
poma = !git checkout main && git pull --ff-only origin main
Putting it all together into ~/.gitconfig
That’s all the shortcuts for now!
Let’s put everything together from the above snippets. I’m not including the
pobr
alias since you can avoid it with a global config, and I’m also skipping
the example commands I added with the test
alias, as you’ll have to create
customized version of it for your specific project, but the rest can be used
as-is, so you can safely copy-paste this entire section and add it to your
config, and adjust accordingly to fit your work style.
Here’s the [alias]
section from my ~/.gitconfig
:
[alias]
b = branch
d = diff
s = status
st = status
ci = commit
co = checkout
dc = diff --cached
ds = diff --staged
unstage = reset HEAD --
# puma: (p)ull (u)pstream (ma)in
puma = !git checkout main && git pull --ff-only upstream main && git push
# poma: (p)ull (o)rigin (ma)in
poma = !git checkout main && git pull --ff-only origin main
Hope this was helpful and inspired you to add a few time-saving (and error-avoiding) shortcuts to your Git setup! Let me know what you use that helps you with your Git workflows on Twitter or Hacker News.
Mercurial users2 will point out that Mercurial automatically provides shortcuts in the form of “unique prefixes”, which Git does not. ↩︎
FWIW, I also use Mercurial for some projects. Git and Mercurial each have their pros and cons. ↩︎ ↩︎
hg s
may refer to any of:serve
,showconfig
,status
, orsummary
↩︎Courtesy of this Stack Overflow answer. ↩︎
Fans of Bazel7 will point out that Bazel lets you run all tests recursively from any directory via the syntax
bazel test //...
which means “run all tests, recursively, from the top of this tree (which ends when you find the nearestWORKSPACE
file), but we’re just usingmake test
as an example, and you can run really any command from the top of the tree with that shortcut. ↩︎I prefer rebasing my branch on
main
rather than merging commits frommain
as it creates a cleaner, simpler history. ↩︎Full disclosure: yes, I am also a fan of Bazel. ↩︎