Powerful Git Macros For Automating Everyday Workflows

Wil Hall

I try to identify the things in my work that I do most often or those that are most error prone and automate those first. There is always room for improvement, but what you’ll actually need necessarily changes with what you’re working on and who you’re working with. So rather than finding the ‘perfect setup’, I like to let it develop and change naturally over time.

Everyday git workflows are a great candidate for automation. I spend a lot of time working with git each day. Most of that time is spent working with git on the command line, with the exception of Sublime Merge and Fork which I prefer for visual merging and staging of large or complex changesets.

I find working with git from the shell to be a more customizable experience than working in a graphical git client. You can perform common unixisms like piping its output to another command, or build your own macros that compose git functionality to automate things in your everyday workflow.

A comprehensive set of shell aliases

Either the zsh git plugin or bash-it is a good place to start if you want a comprehensive set of shell aliases for git. Most of the aliases provided don’t compose any behavior, but they save you a lot of characters for commands you type all the time. For example: tired of typing git commit? Now you can type gc instead.

These provide a few useful shell functions as well such as gwip and gunwip for automating the creation/deletion of WIP (work-in-progress) commits, but for the mostpart they are a collection of aliases for existing git commands.

For any commands that you do type out, you can avoid prefixing them with git by using gitsh - an interactive shell for git. You can even utilize your existing git aliases, modify your git config for the duration of the shell session, and more.

GitHub from the command line

If you use GitHub, it’s worth checking out hub, the official command line tool that allows you to perform everyday GitHub tasks from the shell such as creating pull requests or browsing issues.

From the help docs:

These GitHub commands are provided by hub:

   api            Low-level GitHub API request interface
   browse         Open a GitHub page in the default browser
   ci-status      Show the status of GitHub checks for a commit
   compare        Open a compare page on GitHub
   create         Create this repository on GitHub and add GitHub as origin
   delete         Delete a repository on GitHub
   fork           Make a fork of a remote repository on GitHub and add as remote
   gist           Make a gist
   issue          List or create GitHub issues
   pr             List or checkout GitHub pull requests
   pull-request   Open a pull request on GitHub
   release        List or create GitHub releases
   sync           Fetch git objects from upstream and update branches

Even some of the smallest conveniences like hub browse can be a huge time saver!

Whitespace diffs

When I want to get an overview of my changes while I am working, I’ll often look at a diff on the command line. I prefer looking at a whitespace diff because it reduces visual noise by hiding changes that only change whitespace characters. We can reduce even more visual noise by hiding the +/- indicators in the diff. My whitespace diff alias looks like the following:

[alias]
    wdiff = diff -w --word-diff=color

The -w ignores whitespace, and --word-diff=color distinguishes additions and deletions by color only.

There are two macros I find useful for working with the root of the current git repository - either to cd back to it, or to run commands on it.

The first is a git alias that outputs the root directory using the rev-parse –show-toplevel flag:

[alias]
    root = rev-parse --show-toplevel

The second is a shell alias which provides an easy way to cd back to the root directory:

alias grcd='cd $(git root)'

Setting the upstream branch

When you have a local branch, git needs to know which remote branch (if any) should be used when performing operations like push or pull. As a result, you’re probably familiar with messages like the following:

fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin master

And if you’re like me, you’ve copied the suggested push --set-upstream command thousands of times.

I have since defined an git alias to automate this; I prefer it as a seperate command, but you could integrate it into a push alias as well:

[alias]
    set-upstream = !git branch --set-upstream-to=origin/`git symbolic-ref --short HEAD`

In the above, we use the symbolic-ref command to resolve the current branch name; HEAD refers to the ref we want to resolve the name of (the tip of the current branch) and passing the –short option shortens (for example) refs/heads/master to master giving us the non-fully-qualified branch name.

We then pass the current branch name to the --set-upstream-to flag as the fully-qualified name of the remote branch (in our example: origin/master).

Getting a good look at branches

You can list all branches in a repository with git branch, but the output leaves much to be desired.

In the thoughtbot dotfiles we have a git branches alias which lists out all the remote branches along with the last modification date and author, sorted in descending order by last commit date:

[alias]
    branches = for-each-ref --sort=-committerdate --format=\"%(color:blue)%(authordate:relative)\t%(color:red)%(authorname)\t%(color:white)%(color:bold)%(refname:short)\" refs/remotes

It looks complex, but most of it is the --format option. It uses git’s for-each-ref command to loop over all refs matching the provided pattern. In this case, we want all refs matching refs/remotes (for all the remote branches) and we specify --sort=-committerdate to sort descending (note the preceding -) by committerdate.

Two big benefits of this alias are:

  1. It provides more information than git branch, but keeps the output concise
  2. It uses colors to help visually distinguish the output, improving its readability

A similar alias I like to define for working with branches allows us to see the ten most recently used local branches, sorted descending by last commit date. I often use this to remind myself what I’ve been working on lately:

[alias]
    mru = for-each-ref --sort=-committerdate --count=10 refs/heads/ --format='%(HEAD) %(color:yellow)%(refname:short)%(color:reset) - %(color:red)%(objectname:short)%(color:reset) - %(contents:subject) - %(authorname) (%(color:green)%(committerdate:relative)%(color:reset))'

This is very similar to the branches alias above, except we pass refs/heads to get the local branches instead of the remote branches, limit the number of results with --count=10, and show some more information in the formatted output.

It looks like this:

A screenshot of the output of `git mru`

Querying changes to repository files

I frequently want to answer the question: When was this file added? You might also want to answer similar questions about deletions, renames, or really any type of change. It’s easy to do with the git log option –diff-filter.

I define an alias whatadded to answer the first question, because it’s the one I ask most frequently:

[alias]
    whatadded = log --diff-filter=A

Now I can just run git whatadded src/some/file.

The A means to filter to only commits that add the specified file, but you can pass other arguments too like D for deletions or M for modifications. You can also compose these to see, for example, when a file was added, deleted, or renamed:

git log --diff-filter=ADR src/some/file

Finding the difference between the current branch and its base branch

Let’s say you have a branch feature branched off of develop. At some point they diverge, and you want a quick overview of how they differ in terms of commits. You could run git log feature and git log develop and compare the two. But git can also do that for you!

I define two aliases for this purpose: gbc (git branch changes) to see what the current branch has that the base branch does not, and gbbc (git base branch changes) to see what the base branch has that the current branch does not. Both are intended to be used while you have the feature branch checked out, and require you to specify the base branch (such as develop) as an argument.

Their definitions are below:

gbc() {
  git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative $@..$(git rev-parse --abbrev-ref HEAD)
}

gbbc() {
  git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative $(git rev-parse --abbrev-ref HEAD)..$@
}

From a feature branch, we can see what changes the current branch has that master does not using gbc master:

A screenshot of the output of 'gbc master'

From a feature branch, we can see what changes master has that the current branch does not using gbbc master:

A screenshot of the output of 'gbbc master'

We use rev-parse again to resolve the name of the current branch, and $@ refers to whatever argument we pass when calling the function. The –graph and –pretty flags are just changing the visuals of the output. –abbrev-commit shows us shortened commit hashes, and –date=relative shows us relative dates.

If we have feature checked out and we run gbc develop, we are effectively running git log develop..feature. And if we run gbbc develop it’s the same as git log feature..develop. Except you don’t have to type all that out!

These commands can be especially useful when you’re trying to find the right commit hash for a rebase, such as when using git rebase –onto to specify which commit you want a rebase to start from.

Rebasing a branch

Updating a feature branch with changes from the base branch

First, let’s look at incorporating changes from a base branch develop into a feature branch feature. We want to make sure we have the latest changes from both the base branch and our feature branch, and then we want to perform the rebase. Let’s call it gqrb (git-quick-rebase):

gqrb() {
    git set-upstream
    git fetch origin "$@:$@" && git pull && git rebase "$@"
}

Now with feature checked out we can run gqrb develop to incorporate changes from develop into feature.

Firstly we run git set-upstream because this will fail if we don’t have an upstream branch. Next, we run git fetch for only the develop branch. Then we pull the current branch (feature). And finally, we run git rebase develop. If any of those steps fails, && will stop execution of the subsequent commands.

If you find yourself wanting to do this sort of thing with a dirty working tree, you might want to consider adding the –autostash flag to the rebase, which stashes changes before the rebase and reapplies them after it completes.

Reworking the commits on a feature branch to reword, squash, or fixup

For rebase operations that involve commits on the current branch, I often want to perform an interactive rebase, but I only care about rebasing with commits that are exclusive to the current branch.

For example, I might branch feature off of develop and make two commits, and now I want to squash those two commits. I could run git rebase -i HEAD~2, or we can build a macro that works for any number of commits and any action so that you don’t have to go hunting for the commit you’re looking for on your branch.

I call this function gbir (git-branch-interactive-rebase):

gbir() {
  git rebase -i --autosquash $(git merge-base --fork-point "$@" $(git rev-parse --abbrev-ref HEAD))
}

In our example, if you have feature checked out you can run gbir develop to drop into an interactive rebase with all the commits from feature since it diverged from develop.

We use rev-parse again to get the current branch name, and then pass the argument (develop) and the current branch name to the git merge-base –fork-point command which finds the common ancestor commit between the two branches.

We then pass that ancestor commit to rebase -i to drop into the interactive rebase with commits since the common ancestor.

I like to add the –autosquash flag to the rebase here so that if I have run git commit --squash or git commit --fixup on the branch, starting a rebase this way with the –fixup flag will automatically perform the squash and fixup operations.

Quick fixups

Using the gbir function from above, it’s pretty painless to perform fixups on a branch. But sometimes I want to make a fixup to the last thing I commited, and so it feels a little heavy-handed to commit it and then rebase it away. That’s two whole commands! Could we make a single command to accomplish that?

If we have some working tree changes we want to apply to the last commit (without changing its message) we could do something like this:

gfu() {
  git commit --amend --no-edit
}

And then we can just run gfu to apply the changes in our working tree to the last commit. This is conceptually the same as performing a fixup on the last commit, but we use the –no-edit flag along with the amend to avoid changing the commit message.

Build your own!

Hopefully some of these macros will prove to be useful in your git workflow as well. But even more importantly, I hope they will inspire you to build your own. All of these solutions started out as roadbumps that I kept running into. If you run into those as well, I encourage you to write them down, and work to build things that help you overcome them.