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.
Navigating to the root of a repository
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:
- It provides more information than
git branch
, but keeps the output concise - 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:
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
:
From a feature branch, we can see what changes master
has that the current
branch does not using 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.