click here for the tl;dr summary

Ah, git. Everyone's favorite command line revision control system. You hear horror stories of losing weeks of work on a bad merge, breaking production, or accidently overwriting the master branch with experimental code. No doubt it's a powerful system -- but with great power comes great responsibility.

I've been using git for a few years now and I can't imagine working on a project, large or small, without it. It took a bit of getting used to, but I've settled into what I think is a natural, simple, and effective workflow. Whenever someone asks me for help with git, I almost always end up recommending the same thing: Rebase.

Getting Started

First, you need to know the difference between an origin branch and your local branch. When you hook up your repository to your own git server or a service like GitHub, you have a copy of your project on your local machine as well as a copy on the remote server (generally referred to as "origin"). What you're doing when pushing in git is sending up some code in the form of "commits" to the remote server. Pulling is just what you'd expect, grabbing some code down from the remote server and adding it to your local copy. You can also create branches off of master, which helps keep experiments and features in your project even more separate. Got it? Good.

A lot of apps run production directly off the master branch, like this website. When I work on this site, the workflow is super simple. I just work on my local master branch and push up to my origin master branch, then SSH into the server, pull down the changes, and restart the app. Since I'm the only one who commits code to it, I don't have to worry about merges and conflicts. I can push and pull all day without issue.

The trickery and true power of git comes into play when you're working with other people. Say my friend or coworker has committed some code up to my repo in the master branch. On my local, I've got some commits as well. If I try to push, it'll give me an error about my branch not being up-to-date and that I need to fix it before I can successfully push my code up:

> git push origin master
To git@github.com:ibanez270dx/humani.se.git
  ! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:ibanez270dx/humani.se.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

This means I have to pull down the new code, merge it with mine, and then push it back up to the remote server.

At this point, you might be inclined to try your standard git pull. When I was starting out with git and working on a team of three, more often than not, it resulted in all kinds of merge errors and conflicts. You can end up with a crazy divergent version tree and spend a whole afternoon just trying to resolve conflicts and get the damn thing merged when all you really care about is getting your code out. Who needs that headache?

> git pull
Auto-merging app/assets/stylesheets/blog.scss
CONFLICT (add/add): Merge conflict in app/assets/stylesheets/blog.scss
Automatic merge failed; fix conflicts and then commit the result.

Just imagine a few days or even a week's worth of work that overlaps a bit with your coworker's code. The conflicts can add up and get unwieldy quickly and be pretty overwhelming. Behind the scenes, all git pull does is run git fetch and then a git merge. git fetch updates your local branch with changes from origin, but it does not apply the changes. git merge is responsible for that. How merge works is actually really interesting but a bit much to explain here. Check out Anders Kaseorg's excellent explanation on Quora. Long story short, for a beginner, this can be really confusing and you can end up with a version tree that is non-linear, making it more difficult to resolve conflicts. You can get a visual representation of your project's version tree like this:

> git log --oneline --graph --decorate --all

And my advice? Just rebase! To me, it's easier to understand and resolve conflicts, and keeps your git history linear. Plus, all you need is one flag: git pull --rebase.

Rebasing

When you rebase, what's actually going on is that git "rewinds" your branch back to head (in this case, of origin master) and then tries to apply your changes on top of them. This allows you to resolve any conflicts as they come up commit by commit. Git will tell you which files have conflicts, allows you to resolve them, and continue rebasing with git rebase --continue:

> git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: Tweak blog CSS
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging something
CONFLICT (add/add): Merge conflict in app/assets/stylesheets/blog.scss
Failed to merge in the changes.
Patch failed at 0001 Change font on blog page
The copy of the patch that failed is found in:
  /Users/jeff/Dev/humani.se/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

When you're done, you end up with everything from the branch you're pulling from with your working branch's commits neatly stacked on top. No need to create merge commits or anything, it keeps your version history nice and linear. For example, without any conflicts:

> git pull --rebase
remote: Counting objects: 28, done.
remote: Compressing objects: 100% (23/23), done.
remote: Total 28 (delta 9), reused 17 (delta 5)
Unpacking objects: 100% (28/28), done.
From github.com:ibanez270dx/humani.se
  71f093b..232241c master -> origin/master
First, rewinding head to replay your work on top of it...
Applying: Upgrade BCrypt.
Applying: Change blog font.

Now that my local master is up-to-date with my friend's code as well as mine, I can successfully push up to the remote server to submit my changes.

> git push origin master
Counting objects: 29, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (15/15), 1.75 KiB | 0 bytes/s, done.
Total 15 (delta 12), reused 0 (delta 0)
To git@github.com:ibanez270dx/humani.se.git
  1d966de..dee72b3 master -> master

If you're working on your own branch off of master, you can still take advantage of this approach. Git's rebase feature also lets you rebase with respect to any branch you like. Say I'm on branch jeff/experiment with some significant changes to the app. However, in the time since I branched off of master, the rest of the team has been working away committing code to it. At this point, these two branches have diverged and it might suck to get these back on track. This is probably the most common scenario and what I like to do in this situation is rebase my branch with respect to origin master:

> git pull --rebase origin master
From github.com:ibanez270dx/humani.se
  * branch master -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: Change blog font.

It's convenient if you have a reasonable number of commits, but sometimes it can turn into a headache if you have a lot of little commits that cause conflicts. Especially in experimental branches, where you might have a lot of commits changing a certain piece of code. This is where interactive rebasing comes in.

Interactive Rebasing

When developing a feature, I generally like to use a new branch and commit often. In doing so, I might end up with 30 or so commits by the time I'm done. That'll be a bit of a pain to search through and probably have changes that were later reverted or modified, which are ultimately pointless to keep track of. Interactive rebasing allows you to squash all those little commits into one or more big ones. In the following example, I'll squash the last 5 commits into 1. When you do this, ensure you have a good command line text editor set in your environment by typing > echo $EDITOR. I use vi.

> git rebase -i HEAD~5

pick 1039039 get assets working in production
pick 1d966de Upgrade bcrypt
pick dee72b3 Change blog fonts, add console CSS
pick f1d0ae2 Some example commit
pick 4105354 Another rebase example commit

# Rebase 7105829..4105354 onto 7105829
#
# 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

Here, you can change "pick" to "squash" as instructed like so:

pick 1039039 get assets working in production
squash 1d966de Upgrade bcrypt
squash dee72b3 Change blog fonts, add console CSS
squash f1d0ae2 Some example commit
squash 4105354 Another rebase example commit

Upon saving and quitting, you'll be able to edit the commit message:

# This is a combination of 5 commits.
# The first commit's message is:
get assets working in production

# This is the 2nd commit message:

Upgrade bcrypt

# This is the 3rd commit message:

Change blog fonts, add console CSS

# This is the 4th commit message:

Some example commit

# This is the 5th commit message:

Another rebase example commit

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# HEAD detached from 1039039
# You are currently editing a commit while rebasing branch 'jeff/experiment' on '7105829'.
#
# Changes to be committed:
#  (use "git reset HEAD^1 ..." to unstage)
#
#   modified: Gemfile
#   modified: Gemfile.lock
#   modified: app/assets/stylesheets/layouts/blog.scss
#   modified: app/views/layouts/application.html.erb
#   new file: foobar.txt
#   new file: something.txt

Change it to something like:

# This is a combination of 5 commits.
# The first commit's message is:
did a bunch of stuff

...and finally

[detached HEAD 55b6511] did a bunch of stuff
 6 files changed, 86 insertions(+), 46 deletions(-)
 create mode 100644 foobar.txt
 create mode 100644 something.txt
Successfully rebased and updated refs/heads/jeff/experiment.

Tada! I've now got all the code within those 5 commits compressed into one.

Summary / TL;DR

This is how I use git -- it's not a definitive guide nor necessarily "the right way" to do it, but I find this workflow feels pretty natural and easier to manage. Instead of pulling (fetching and merging) and creating extra commits to resolve merge conflicts, just pull using rebase. It keeps your branches clean of those pesky extra commits and your version tree nice and linear by rewinding your branch, updating it, and replaying your commits on top. When working on larger features, create your own branch off of master and keep it up-to-date by rebasing with respect to origin master. If you've got a ton of commits on a feature branch, slim it down by interactively rebasing before pushing it to master. That's it!