This blog post is an adaptation of an old presentation on some useful concepts and commands to keep your git history clean.

Goal

A clean git history, i.e.:

* cf43bac (HEAD, not-verbose) Title and Goal slides.
* e831535 Import project scaffolding
* 0d858b4 (master) initial commit

not:

* eb911c7 (HEAD, verbose) add bad example to gaol
* c01aacd add good example to goal
* f74382c fix type: Workflwo
* b9b376a Title and Goal slides.
* 7f492c6 Remove the example slide scaffolding
* 16ccdbb Import project scaffolding
* 0d858b4 (master) initial commit

Why?

A clean history = easier to see what happened when and why. e.g.:

A good git blame:

192f7d3c (Joe Crobak 2014-10-26 21:41:00 +0000 22) class: center, middle
192f7d3c (Joe Crobak 2014-10-26 21:41:00 +0000 23)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 24) # Git Rebase Workflow
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 25)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 26) ---
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 27)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 28) # Goal
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 29)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 30) A clean git history, i.e.:
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 31)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 32) ```
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 33) * cf43bac (HEAD, not-verbose) Title and Goal slides.
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 34) * e831535 Import project scaffolding
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 35) * 0d858b4 (master) initial commit
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 36) ```
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 37)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 38) not:
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 39)
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 40) ```
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 41) * eb911c7 (HEAD, verbose) add bad example to gaol
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 42) * c01aacd add good example to goal
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 43) * f74382c fix type: Workflwo
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 44) * b9b376a Title and Goal slides.
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 45) * 7f492c6 Remove the example slide scaffolding
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 46) * 16ccdbb Import project scaffolding
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 47) * 0d858b4 (master) initial commit
dafc1e27 (Joe Crobak 2014-10-26 21:46:03 +0000 48) ```
192f7d3c (Joe Crobak 2014-10-26 21:41:00 +0000 49)

Lines 24-49 are clearly one commit—you can tell by looking at the shas on the left side.

Versus an unclean history

16ccdbb4 (Joe Crobak 2014-10-26 21:41:00 +0000 22) class: center, middle
16ccdbb4 (Joe Crobak 2014-10-26 21:41:00 +0000 23)
f74382c7 (Joe Crobak 2014-10-26 21:46:36 +0000 24) # Git Rebase Workflow
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 25)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 26) ---
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 27)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 28) # Goal
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 29)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 30) A clean git history, i.e.:
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 31)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 32) ```
c01aacdc (Joe Crobak 2014-10-26 21:49:45 +0000 33) * cf43bac (HEAD, not-verbose) Title and Goal slides.
c01aacdc (Joe Crobak 2014-10-26 21:49:45 +0000 34) * e831535 Import project scaffolding
c01aacdc (Joe Crobak 2014-10-26 21:49:45 +0000 35) * 0d858b4 (master) initial commit
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 36) ```
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 37)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 38) not:
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 39)
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 40) ```
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 41) * eb911c7 (HEAD, verbose) add bad example to gaol
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 42) * c01aacd add good example to goal
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 43) * f74382c fix type: Workflwo
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 44) * b9b376a Title and Goal slides.
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 45) * 7f492c6 Remove the example slide scaffolding
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 46) * 16ccdbb Import project scaffolding
e449027d (Joe Crobak 2014-10-26 21:50:05 +0000 47) * 0d858b4 (master) initial commit
b9b376a1 (Joe Crobak 2014-10-26 21:46:03 +0000 48) ```
16ccdbb4 (Joe Crobak 2014-10-26 21:41:00 +0000 49)

Here the changes look like they were done piecemeal. See all the different shas on the left side. Is that information useful?

Let's look at the commit for the line with the title: f74382c7. What happened in that commit?

$ git show f74382c7
commit f74382c73b96f1f68cb1da7edeac14e0952ff6aa
Author: Joe Crobak <joe@undefined>
Date:   Sun Oct 26 21:46:36 2014 +0000

    fix type: Workflwo

diff --git a/presentation.html b/presentation.html
index 119f7c5..6786114 100644
--- a/presentation.html
+++ b/presentation.html
@@ -21,7 +21,7 @@

 class: center, middle

-# Git Rebase Workflwo
+# Git Rebase Workflow

 ---

Not very useful! (note also, that I had a typo in my commit message... I couldn't even spell 'typo' correctly).

Utility of git commits

Think about what is useful to the future you or your teammate. e.g.

  • why was a block of code was added?
  • when was a bug introduced?
  • can the commits be used to build a changelog?

How to keep git history clean

Go read about writing good git commit messages: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html

tl;dr:

  • First line is < 50 chars (used in git rebase -i, git log --pretty=oneline)
  • One item/feature per commit.
  • Descriptive commit messages.
  • Also, consider linking to the User Story/Ticket/etc item for the commit. This adds additional context for someone exploring a commit that changed a line of code.

Example of a good commit

Adding new feature 'bar'

Feature 'bar' improves response time by 10ms by x, y, and z.

Refs: http://jira.apache.org/path/to/jira/ticket.

Tools

Let's get a tour of several tools that we can use to keep our history clean. Here are a few:

  • git add --patch
  • git commit --amend
  • git cherry-pick
  • git rebase -i
  • git reset --soft
  • git reflog / git reset --hard

Caveat: these should be used before pushing to a remote—especially if someone else is using your remote branch! If you've already pushed to a remote, consider making a new one.

git add --patch

Useful when you have unrelated changes in a file (e.g. you fixed an unrelated typo and you want to keep it separate, or you combined a refactor and a new function). Drops you into an interactive session to select your changes for a single commit.

Ref: http://www.codeproject.com/Articles/650440/Git-Quick-Reference-Interactive-Patch-Staging-with

git commit --amend

Useful when you found a typo, have a quick fix, or need to fix the commit message of the previous commit. Applies staged changes (if any) and lets you ammend the commit message.

git cherry-pick

Useful when you want to pull apart a single branch into multiple branches so that you can submit separate branches/pull requests. Be careful about introducing merge conflicts, these should be truly independent commits.

e.g. let's say we have a branch that has two commits that implement separate features. We'd like to turn this branch into two separate feature branches (foo-feature-branch and bar-feature-branch):

[my-feature-branch] ~/code/example $ git lol
* 12d9b25 (HEAD, my-feature-branch) adding new feature 'bar'
* b68f1fa adding new feature 'foo'
* 94ba2ee (master) initial commit

Create a new feature branch for foo and cherry-pick the foo commit onto it.

[my-feature-branch] ~/code/example $ git checkout -b foo-feature-branch master
Switched to a new branch 'foo-feature-branch'
[foo-feature-branch] ~/code/example $ git cherry-pick b68f1fa
[foo-feature-branch ca2d000] adding new feature 'foo'
 1 file changed, 1 insertion(+)
 create mode 100644 foo.txt

Create a new feature branch for bar and cherry-pick the bar commit onto it.

[foo-feature-branch] ~/code/example $ git checkout -b bar-feature-branch master
Switched to a new branch 'bar-feature-branch'
[bar-feature-branch] ~/code/example $ git cherry-pick 12d9b25
[bar-feature-branch f0e95ca] adding new feature 'bar'
 1 file changed, 1 insertion(+)
 create mode 100644 bar.txt

Result:

[bar-feature-branch] ~/code/example $ git lol bar-feature-branch
* f0e95ca (HEAD, bar-feature-branch) adding new feature 'bar'
* 94ba2ee (master) initial commit
[bar-feature-branch] ~/code/example $ git lol foo-feature-branch
* ca2d000 (foo-feature-branch) adding new feature 'foo'
* 94ba2ee (master) initial commit

Note: this is only possible if you break up your commits initially!

git rebase -i

Useful when you have a bunch of local commits that you want to reorder or squash.

Run git rebase -i $commit (where $commit is an ancestor on your branch). Your editor will come up with something like:

pick 16ccdbb Import project scaffolding
fixup 7f492c6 Remove the example slide scaffolding
pick b9b376a Title and Goal slides.
fixup f74382c fix type: Workflwo

# Rebase 0d858b4..f74382c onto 0d858b4
#
# 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
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Gotchas / notes

  • You can only rebase -i if you have a "clean" working directory.
  • git rebase -i pops you into your editor. Ensure you have $EDITOR set to something useful.
  • If you end up with a conflict (usually when reordering commits), you can abort the rebase with git rebase --abort.
  • This doesn't work well with merge commits (git rebase on the base branch rather than merge).
  • It rewrites history, so beware.
  • Prefix your commit messages with fixup: or squash: so they're easy to identify.

git reset --soft

Useful when you have a lot of local commits that are actually one commit. Do a "soft" reset to the commit before you started your work—all work ends up staged. Example:

current status:

[test] ~/code/example $ git lol
* 2c5d91c (HEAD, test) fixup: another fixup for bar
* 3706d1c fixup: bar
* 18af5f4 fixup: bar
* f0e95ca (bar-feature-branch) adding new feature 'bar'
* 94ba2ee (master) initial commit

reset to before I started working on bar:

[test] ~/code/example $ git reset --soft 94ba2ee

[test●] ~/code/example $ git lol
* 94ba2ee (HEAD, test, master) initial commit

[test●] ~/code/example $ git status
On branch test
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

  new file:   bar.txt

bar is staged, let's commit it:

[test●] ~/code/example $ git commit -m "add new feature 'bar'"

[test ae12373] add new feature 'bar'
 1 file changed, 1 insertion(+)
 create mode 100644 bar.txt

git reflog and git reset --hard

If things are screwed up, DON'T PANIC. The git reflog is your friend. It retains info on all local commits. For example, if I wanted to undo the git reset --soft from before:

Check the git reflog to find my old HEAD:

$ git reflog
ae12373 HEAD@{0}: commit: add new feature 'bar'
94ba2ee HEAD@{1}: reset: moving to 94ba2ee
2c5d91c HEAD@{2}: commit: fixup: another fixup for bar
3706d1c HEAD@{3}: commit (amend): fixup: bar
18dfb5d HEAD@{4}: commit: fixup: x
18af5f4 HEAD@{5}: commit: fixup: bar

Run git reset --hard to restore to 2c5d91c:

[test] ~/code/example $ git reset --hard 2c5d91c
HEAD is now at 2c5d91c fixup: another fixup for bar
[test] ~/code/example $ git lol
* 2c5d91c (HEAD, test) fixup: another fixup for bar
* 3706d1c fixup: bar
* 18af5f4 fixup: bar
* f0e95ca (bar-feature-branch) adding new feature 'bar'
* 94ba2ee (master) initial commit

Summary

Git is a powerful tool with a steep learning curve. This should be enough to help you get started with some of git's more advanced features. If you have questions or suggestions, you can find me on twitter!

Appendix: My ~/.gitconfig

The following config sets up colors in git status, highlights white space in diffs, adds a number of usueful aliases (git lol is my favorite, if you haven't noticed), and more.

$ cat ~/.gitconfig
[user]
  email = joe@undefined
  name = Joe Crobak
# great tips from http://cheat.errtheblog.com/s/git and
# http://mislav.uniqpath.com/2010/07/git-tips/
[color]
    ui = auto
[color "branch"]
    current = red bold
    local = blue
    remote = green
[color "diff"]
    meta = black
    frag = magenta
    old = red
    new = green
    whitespace = red reverse
[color "status"]
    added = green bold
    changed = yellow bold
    untracked = cyan bold
[core]
    # tabs are an error, as are trailing spaces
    whitespace=tab-in-indent,trailing-space
  excludesfile = /Users/joe/.gitignore
[alias]
    st = status
    ss = status -sb
    ci = commit
    br = branch
    co = checkout
    df = diff
    lg = log -p
    ls = log --oneline --decorate
    lol = log --graph --decorate --pretty=oneline --abbrev-commit
    lola = log --graph --decorate --pretty=oneline --abbrev-commit --all
    ls = ls-files
[difftool "sourcetree"]
    cmd = opendiff \"$LOCAL\" \"$REMOTE\"
    path =
[mergetool "sourcetree"]
    cmd = /Applications/SourceTree.app/Contents/Resources/opendiff-w.sh \"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"
    trustExitCode = true
[filter "media"]
  clean = git-media-clean %f
  smudge = git-media-smudge %f