Until last night I lived in fear of tildes, carats, resets and reverts in Git. I cargo culted, I destroyed, I laid waste the tidy indicies, branches and trees Git so diligently tried to maintain. Then Zach Holman gave a talk at Paperless Post. It was about Git secrets. He didn't directly cover these topics but he gave an example that made me realize it was time to learn.
A better undo
Generally, when I push out bad code, I panic, hit
git reset --hard HEAD^, push and clean up the pieces later. I don't even really know what most of that means. Notational Velocity seems to be fond of it ... in that I just keep copying it from Notational Velocity and pasting it. Turns out, this is dumb. I've irreversibly lost the faulty changes I made. I'll probably even make the same mistakes again. It's like torching your house to get rid of some mice.
Enter Holman. He suggests a better default undo.
git reset --soft HEAD^. Says it stages the last commit's changes so you can push two commits ago. Then work from your changes that broke the build. Okay. I need to figure out what this all means.
The flavors of reset
Turns out, there are bunches of resets. Vanilla,
--patch. Oh God, what do they all do. The reset reference is ... a bit obscure. So here's the lowdown
vanilla: If you just
git resetit will unstage all staged files.
git reset HEAD fileunstages just a single file
--mixed: Uncommit all changes back to the specified location. Take all the changes since them and puts them in the working tree. Nothing is staged. This is a good reset to use if you, say, commit three files, push and realize only one has an error. You can
git reset --mixed HEAD^,
git add good_1 good_2,
git commit "wheat from the chaff",
git push origin master. Now your working directory still has the bad file with the bad changes, put you have pushed the good stuff.
--hard: Uncommit all changes back to the specified location and throw them away. This is great when you want to forget the last hour, toss all your work and start from square one. Pretty much never the right solution.
--soft: Uncommit all changes back to the specific location and stages their files for commit. Very similar to --mixed. In fact this might be better in the case that you commit a lot of files and only one is broken. You can
git reset --soft HEAD^and then
git reset HEAD bad_file. Now you have your good changes staged. Commit, push, and deal with the delinquent.
--merge: Okay, this one's a little black magic. It's disallowed in many cases because you can't merge with local changes. It's really only used when you merge in changes, the merge fails and you want to undo.
--keep: I honestly lost steam by this one. It seems to mainly be used when you git tag things and mess up. I don't really do that so...
--patch: This is interactive reset. It's for warlocks. You can interactively select which hunks to leave alone and which hunks to undo. This is a bit of a different paradigm than the other reset commands as you are specifying what to undo rather than what to keep.
Oh cool but what's 'HEAD^'
Carats and tildes are relative commit markers in Git. They both mean "parent" but in a different way.
Most commonly used, they are the same. HEAD^1 (or HEAD^ for short) is the same as HEAD
1 (HEAD). Always.
The difference comes when they stack. So HEAD^2 means "The second parent of HEAD". This only means anything if there's been a merge. In a merge, the main branch is parent #1; the merged in branch is parent 2. So, HEAD^2 will be the merged parent, whereas HEAD^1 will be the parent merged into.
Now, HEAD^^ is not the same as HEAD^2. HEAD^^ means the first parent of the first parent. This is just shorthand for HEAD^1^1. Evaluate from left to right.
But HEAD^^ is ugly. Isn't there something nicer. Yes. Tilde. Tilde ALWAYS means first parent. so HEAD~2 == HEAD^^.
Finally, you can stack tildes and carats. Say you wanted the merged in branch of two heads ago. That would be HEAD~2^2. I'm sure you will be doing that a lot. Not really.
That's from a good piece on carats and tildes
Great, great. So what about revert.
If reset is rampaging boar running back in time and rooting up changes, revert is a dainty little flower child, skipping back in time and plucking out changes.
It's also real simple.
git revert sha means "commit the opposite of the changes in sha" on top of the tree. So say you made ten commits and three ago you messed one little thing up.
git revert HEAD~3 will commit the opposite of that commit on top. You have undone it.
You cannot revert if you have changes in your working directory (it must be the same as HEAD).
More to come
We haven't even gotten to git checkout. But if we've covered the rampaging boar and the dainty flower child, we have definitely run out of attention for the Liam Neeson with a Swiss Army Knife that is
git checkout. More later.