Mind Your Git Manners

If you've read The Clean Coder, you probably know that writing clean code is one of the marks of a professional developer. However a true professional doesn't stop with the code, but keeps everything around the code clean, including Git history.

Git is a wonderful tool for source control, but many people don't take advantage of some of the simplest features that can keep their source code repositories clean. Too many developers out there are only using the basic features: push, pull, commit.

Pull

Let's start with the standard `git pull`.

This command will pull in any commits that are on the remote branch, but not on your local branch yet. This sounds OK, but this type of pull will perform a merge, which mixes up the git history.

Take this series of commits:

##Remote
commit 6e98f52
Author: Some guy
Date: Wed Sep 26 16:43:07 2012
    Something cool

commit 8954ad8
Author: Some guy
Date: Mon Sep 24 14:34:28 2012
    Something not so cool
##Local
commit 7623bc9
Author: Me
Date: Tues Sep 25 12:08:22 2012
    My new stuff

commit 634f4de
Author: Me
Date: Sun Sep 23 15:33:27 2012
    WIP: starting new stuff

With a standard pull, your changes get inserted in the history based on their timestamps, like so:

commit 6e98f52
Author: Some guy
Date: Wed Sep 26 16:43:07 2012
    Something cool

commit 7623bc9
Author: Me
Date: Tuesday Sep 25 12:08:22 2012
    My new stuff

commit 8954ad8
Author: Some guy
Date: Mon Sep 24 14:34:28 2012
    Something not so cool

commit 34f4de
Author: Me
Date: Sun Sep 23 15:33:27 2012
    WIP: starting new stuff

But this reflects the time that commits are made, not the time that the remote branch receives the commits. It can be a bit confusing when you make some commits, pull from the remote branch, then push your commits up, only to see them buried under several others.

So what does Git offer us to help with this confusion? Enter,

git pull --rebase

When pulling with the rebase option, all of your commits that aren't synced with the remote branch will be taken to the side, while your local branch gets up-to-date. Then your changes are put back on top, making the timeline reflect when changes are made to the remote branch.

So with our earlier example, the history would end up looking like this,

commit 7623bc9
Author: Me
Date: Tuesday Sep 25 12:08:22 2012
    My new stuff

commit 34f4de
Author: Me
Date: Sun Sep 23 15:33:27 2012
    WIP: starting new stuff

commit 6e98f52
Author: Some guy
Date: Wed Sep 26 16:43:07 2012
    Something cool

commit 8954ad8
Author: Some guy
Date: Mon Sep 24 14:34:28 2012
    Something not so cool

with the newest changes to the remote branch on top.

…And Squash

There may still be something that doesn't look quite right, though. Our commits are in a logical order, but the commits that were pushed include a work-in-progress commit.

We want master to only have commits that reflect a working system. That way, we can be assured that everything will work if we need to roll back to a certain commit. Since WIP commits are inherently incomplete, we need a way to keep them off of master. For this we use,

git rebase -i <commit-sha>

Another rebase? Yes, but this is slightly different. This command, called an interactive rebase, will allow you to squash all of your WIP commits into a single commit that holds all of the changes for your completed feature.

Once you pull in any changes from the remote branch (using the --rebase option of course), run this rebase command with the SHA of the commit previous to your first WIP commit, and follow the instructions.

If we look at the history from earlier (after the `git pull --rebase`), we would run

git rebase -i 6e98f52

This will bring up a dialog that allows you to squash commits (along with some other options). We want "pick" the first commit in the list and "squash" the rest, like so

pick 7623bc9 My new stuff
squash 34f4de WIP: starting new stuff

# Rebase 52be3ac.. 34f4de onto 52be3ac
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Once you do that, you will be able to make a new message for the combined commits by commenting out the other messages and typing a new one for your complete feature.

You should, however, be careful not to squash too many commits. Commits should be fairly small, so they can be easily understood and easily reverted if necessary. Squashing a whole week's worth of commits could lead to one giant commit with a never-ending diff.

…And Push

git push --force origin master

NO! Never, ever do this. Force pushing rewrites history, which can cause commits (and whole features) to be lost. Force push all you want to your own branches that don't matter, but never force push to master. Just stick to the standard `git push`.

…And That's All (For Now)

These are only a few useful Git commands that I use every day (with the exception of force push, which I never use). There are many more commands that you can use to make your git history clean and easy to follow. Take some time to investigate a few more commands offered by Git to be a considerate Git user.

I only ask that you fully read the documentation on a Git command before using it. Using a Git command that you don't understand can end up being painful for not just you, but everyone who works on the same project.

Git Resources

Kevin Liddle, Software Craftsman

Kevin Liddle is a craftsman and enjoys physics and trivia.