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.

Lombard St., San Francisco by Omer Rana via Unsplash

Lombard St., San Francisco by Omer Rana via Unsplash

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 tell git to not parse any following arguments as command-line flags to itself, and to treat them all as positional arguments to the reset 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:

  1. fork the project to your own account (via GitHub UI)
  2. clone the project locally (via git clone)
  3. create a feature or bugfix branch (via git co -b)
  4. make your changes and commit (via git add, git ci)
  5. push your local branch to your fork (via git push)
  6. 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 pushed 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 local main 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.


  1. Mercurial users2 will point out that Mercurial automatically provides shortcuts in the form of “unique prefixes”, which Git does not. ↩︎

  2. FWIW, I also use Mercurial for some projects. Git and Mercurial each have their pros and cons. ↩︎ ↩︎

  3. hg s may refer to any of: serve, showconfig, status, or summary ↩︎

  4. Courtesy of this Stack Overflow answer↩︎

  5. 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 nearest WORKSPACE file), but we’re just using make test as an example, and you can run really any command from the top of the tree with that shortcut. ↩︎

  6. I prefer rebasing my branch on main rather than merging commits from main as it creates a cleaner, simpler history. ↩︎

  7. Full disclosure: yes, I am also a fan of Bazel. ↩︎