I remember when I first decided to use Git. It was my junior year in college and I was working on a project alone. I had read a blog and it recommended a simple workflow I could follow. Pull. Branch. Make changes. Add. Commit. Merge. Push. I used it all the time and it worked smoothly. Except when it didn’t. Occasionally, my code would break and prevent me from continuing. More often than not, because I had glossed over a typo, written a broken commit, or flubbed resolving a conflict. Here, roughly, are the steps I’d take to fix it:
Commit what I had
Futz around with some ideas
Did it work? Commit it, breathe a sigh of relief.
Did it not work? Continue panicking, repeat step 1
Give up. Cry a little, delete my whole branch and start from scratch again.
At the end of the day my repo was either a complete mess of broken commits fixing broken commits or it was perfect because I had done the original work 5 times over until I got it right. Needless to say, this was a terrible way to write code, but because I was the only judge of my work and I eventually progressed, I became ok with some extra masochism in my life.
Fast forward a year or two later when I found myself committing code in a team environment that I eventually realized that it’s quite common to write imperfect history the first time and then correct it by taking advantage of Git’s history re-writing features. The commits I made while iterating on code didn’t have to be the commits I published. I was totally at liberty to modify or rework the history of commits stored in my local repository to my liking. And when I finally published my history all that mattered was that it was in a logical, working order. It felt a little like cheating and I was hesitant to take advantage of that, but to my pleasant surprise I found that Git actively encourages this approach.
Git provides a wealth of commands to aid you. Though, a lot of them can be daunting to use. Through rampant misuse, you could easily shoot yourself in the foot and make changes you didn’t want. But if that ever happens you can just undo or rewrite history. There are many ways to manipulate history, but I always keep in mind a small list of commands that I use for the more common situations. I consider them analogous to having a dashboard of reset buttons. I’d recommend reaching for them early and often. I have them listed below along with some illustrative thoughts to describe the potential situation in which they would useful.
Note: For those who need a refresher on Git terminology: Your working directory is the set of files you’ve checked out from your local Git store. You fiddle with these to make your changes. Once you’re satisfied with your changes you add them to your index (also called the staging area) by staging them with git add. Once you’re satisfied with your changes, you tell Git to package them up and save them into the Git store by creating a commit. HEAD is the top commit you are actively working off of. It is relative to the current branch.
“I just staged a change, but I’m having second thoughts. I think I’d rather undo it.”
A quick git reset HEAD <file> will unstage it and your changes will still be exactly where you left them.
“Hmm. I made a commit, but I accidentally left out some important changes.” OR “Ack! I just committed another typo.”
Don’t fret. There’s no need to create a new commit. Just git add the missing changes and then run git commit—amend. This discards the top commit on the branch and replaces it with a new commit containing the previous commit’s changes in addition to your newly added changes.
“Help! I just messed up this file and my undo button only goes back five changes.”
You can run git checkout—<file> to replace the current version of a file with an exact copy extracted from the Git store. If you need to grab a version from a commit other than HEAD use git checkout <commit>—<file>.
“I totally messed up my working directory. I feel like I wouldn’t miss any of my changes.”
Then wipe your history away with reckless abandon! If you truly don’t care about losing any outstanding changes you’ve made or new files you’ve created you can run git reset—hard <commit>. This will change HEAD to point to the given commit. That means your index is wiped clean and your working directory will now look exactly like it did at the time of that commit.
“I ended up committing some stuff I didn’t want to and I just realized it after I made another set of changes.”
And you definitely don’t want to lose those hard earned changes. But, you’re in luck. Running git reset—soft <commit> will set HEAD to point to that commit, but unlike—hard your working directory and your index won’t be altered. The changes in the previous commits will simply appear as staged changes in your index alongside your current changes.
“Wow. You’re never going to believe this, but I think I’ve lost a commit. Should I start making lost and found posters?”
No, but you should run git reflog. It’s Git’s own version of the lost and found. Every single commit you’ve ever made in the last 30 days is listed here. Depending on how long ago it was you might have to dig deep.
“So, I made this one commit and I’m realizing that it would be useful in another branch. It’s pretty deep in my history and I don’t want to merge/rebase because I’m not feeling particularly crazy right now and I’m kind of lazy.”
That sounds sensible. Just clone that commit with git cherry-pick <commit>. It’s important to keep in mind that the commit you created is a different commit even though the contents are the same. If you want to cherry-pick a range of commits all at once use git cherry-pick <earlier commit>..<later commit>.
“Ugh. I just made a bad commit and in a bout of madness decided to share it with everyone else. How do I get out of this without having to change my name and address?”
Use git revert <commit>. It applies the inverse of the commit by creating a new commit that effectively undoes the changes that were made.
“Crikey. I’ve just made a bunch of commits and my OCD is starting to kick in. I really wish they were in a different order.” OR “Yeah…that commit I made a while ago is looking mighty useless now.”
Interactive rebase to the rescue. Just run git rebase -i <commit>. An editor will pop up and you’ll be given the ability to discard, rearrange, or squash any of your commits since the one you specified. Remember to read the instructions carefully.
There are a wealth of books and resources on the web that cover each command more in depth. One of my favorites is Version Control with Git. It can be bought here: http://shop.oreilly.com/product/0636920022862.do. Another good one is the Git Pro book at http://git-scm.com/book. Just remember to use these powers responsibly. Everything you commit in your local repository is yours to mess around with however you please. But the second you publish it, it becomes everyone’s history. Please don’t be the guy who rewrites history for everyone.