Wednesday, March 14, 2012

Git: retroactive branching

Say you're working along on a branch (let's say master), and suddenly you realize that this task is harder than you thought. You wish you had started a branch for this feature, back before you began it. Now you need to work on a bug instead, so you've got to put these changes away -- if only you had branched!

This is extremely easy to fix. First, I'll tell you what to type. Then, I'll tell you how it works, so that you can adjust the solutions to your own needs.

What to Type:

1) Commit your changes. It's ok if they're not finished; you'll come back to these later.
2) git branch feature_branch_name
3) git reset --hard origin/master

Bam, your changes are saved off on feature_branch_name, your current working directory matches what was in the origin repo last time you fetched (or pulled) from it, and you are ready to start work on the bug.

The details

1) After your changes are committed, you're still on the master branch. git status looks like:
# On branch master
# Your branch is ahead of 'origin/master' by 2 commits.
nothing to commit (working directory clean)
and in gitx (if you're on a Mac, please download gitx):
Notice that your master branch is ahead of origin/master.

2) git branch does exactly one thing: it creates a new label. It goes right where you are, on your most recent commit.
This operation does not check out the new branch; master is still your current branch. Git status has not changed, and gitx has that new label on it:

3) Now the real magic. "git reset --hard origin/master" does three things. Primarily, it moves the "master" label (your current branch) to wherever "origin/master" is. That's what reset does: it moves labels around.
The output is:
HEAD is now at 0562dac Some commit that's already been pushed to origin
This tells you the second thing it did: it moved the HEAD label, which marks where you're currently working. You're now working on master, which is now before you did your feature-related changes.
Git status looks the same. Gitx says:
That new_feature branch is sticking out up there, saving your work. But you're back at the "master" label. This is the third thing that your git reset command did, thanks to "--hard": it replaced everything in your working directory with what the repository looks like in origin's master branch.
"--hard" tells git to rejigger all your files.

Why is "--hard" not dangerous? well, it can be. But we're safe because
1) we committed changes
2) we gave that commit a label.
Git will always and forever remember the exact state of your code at any commit that has a label, or is in the history of any commit with a label. If a commit is reachable in the history from any reference (branch, HEAD, tag), then it is safe.

The critical point to this operation -- the reason this is really easy for me now, but was super-hard before I understood the commit graph -- is that we don't have to move any code. We don't have to move any commits to a branch: instead we move the labels around. Branches are simply pointers to a commit. We can move them around all day. In this example, we added a new pointer for the branch, moved the "master" pointer back to where it was before our changes, and replaced our working directory files with the older stuff.

This contrasts sharply with the overhead of branching in Subversion. Yay Git!


  1. Another way you might consider is just creating your branch with a dirty workspace:

    $> git co -b

    This creates the branch and moves you to it in one shot. Then make your commit on the branch afterward.

    Also, you could stash your change, create the branch, and pop the stash, though that is doing in 3 steps what you can just do in one.

    1. Blogspot ate my answer....

      $> git co -b (branch_name)

    2. Or just stashing your work while you work on the bug on a separate branch.

      There are many paths.

      My first suggestion, though, doesn't match your original problem of needing to work on a branch for your bug. git co -b (branch-name) keeps your changes available and just switches your branch for you. This isn't what you want when you have a bug to squash.

  2. Thanks for writing this up. I need to do this and this is a great starting point. I was wondering if I could use git stash to save the changes but I'm going to look into this. It looks like a natural use of Git internals to get the job done; I like it.