Git Rebase Workflow
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:
orsquash:
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