So you switched to git. You thought this is fool-proof version control. You will never lose work again. While this is generally true, there are a lot of things you can do to increase the chance of losing changes. Even though it is unlikely you lost work for good, more than often recovering from a git action is not obvious at all. There is no simple undo command in git. Instead, you have to come up with a series of commands to reverse the effect. For the first part, we will cover some practices on how to not lose work in the first place. In the second part, we will go over some common recovery scenarios.

Clean working copy

One very good rule of thumb is to have a clean working copy. A clean working copy means that a git status will report exactly that:

nothing to commit, working directory clean

Do not be afraid to commit. While a commit in other, centralized version control systems like svn is the same as publishing changes, a git commit is rather light-weight: Not everything you commit has to be pushed and published. Therefore, commit as early and often as possible. Every time the ‘yes, it works!’ moment occurs, commit. That creates checkpoints to come back to later in case you screw up. It also makes sure that git has a record of those changes. It is harder to lose commits than it is to accidentally override changes in the working copy.

Don’t wait to commit, because it might make a dirty, imperfect commit. I guarantee you, before you have a perfect commit ready, you will have found a way to misplace uncommitted work. There is always time to clean up commits later. Feature branches exist as your personal playground and are meant to have dirty commits on them. Constantly caring for clean commits on a feature branch is the same as constantly cleaning your desk in the middle of work. You would not do that. You would clean the desk when the work is done. The same applies to feature branches. When you ship a feature, then is the time to clean.

Needn’t the force be with you

Some git commands offer the possibility to make them work in situations where they usually would refuse to work for good reasons. I am talking about the --force switch. Especially for newcomers, this is a simple quick fix to problems. If you have to use a command with the --force switch, you are probably doing something wrong or you are that advanced that you should not be reading this post anyway. Most of the times though, this is just an indicator that either something went wrong before or a concept of git is not clear to the user. In any of those cases, forcing the command to work is not a sustainable solution.

A good example is git-push. If you make changes on a shared branch and a colleague will push her changes in the meantime, you will not be able to push your changes later. Instead, you have to fetch your coworker’s changes and merge them or base your changes upon them. This is due to the graph nature of git. Developers used to centralized version control might be confused by this as they tend to have a linear understanding of commits. If you push --force your changes now – without incorporating your coworker’s changes – the result will be that her set of changes is left dangling, unreachable from any branch. The good news is; this situation is recoverable. But it is a pain. And it deserves its own post.

Besides push, there is clean. By design, this commands only works with the --force flag. Clean will delete all untracked files. This action is not recoverable. While cleaning untracked files is sometimes necessary to obtain a clean working copy, there are better alternatives:

git stash -u

This will stash untracked (-u) files instead of deleting them. Think of a stash like an ultra-light-weight commit. You can easily get those changes and untracked files back but they do not pollute your regular branches.

If you must use the force, try to do a dry-run first. Commands like git-push (flag -n) offer the possibility to get an overview of the effects of running that command without actually running it. Remember how someone accidentally push-forced to over 50+ repositories, causing some headaches for the Jenkins’ developers? Better call -n (with git push --force).

Keep in mind, the --force flag is a good indicator that you need to pay attention. You are very likely to run an irreversible or hard to undo command.

Git undo examples

We shed some light on best-practices to avoid losing work in the first place. But when worst comes to worst, it is good to have some arsenal of undo magic at your disposal. More so, since there is no straight-forward undo in git. Instead, you have to think of a command or a series of command that will reverse the effect of your wrong commands. Let us start with some simple examples and work our way up to more advanced ones.

Unstage a file

The stage or index holds all your changes that you want to commit. If you changed somefile.txt and added it to the index with git-add, you can reverse the effect by running a git-reset on that file.

git add wrongfile.txt
# Undo with:
git reset wrongfile.txt

Changing the last, unpushed commit

With this technique you can change the last, unpushed commit This is helpful if your last commit did not contain all changes or you misspelled the commit message. Git has the possibility of amending a commit.

> git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       first
#       second
nothing added to commit but untracked files present (use "git add" to track)

> git add first_file.txt
> git commit -m 'missspelled massage'

# Correct your errors
> git add second_file.txt
> git commit --amend -m 'correct message'

Amending a commit aggregates additional changes on top of an existing commit plus the possibility to alter the commit message. It also helps if you just want to change the last commit message. The important thing: Only do this with unpushed commits. Since the commit hash changes with an amend, work based upon the original commit, would be left dangling.

Drop the last, unpushed commit

Another case could be that you want to entirely discard of an unpushed commit.

git reset HEAD^

This discards the commit by rewiring the current branch to the parent of the last commit. The working copy will still contain the changes of the dropped commit, though. In order to discard of those as well, you can stash them:

git stash -u

Modify a series of unpushed commits (Rebasing)

We can also change older, unpushed commits. Consider a local branch that is 10 commits ahead of its tracking branch. For this we are going to use one of the most powerful tools git has to offer. This is a good point to make sure you have a clean working copy beforehand. Recovering from botched usage will be cleaner in that case.

> git status
# On branch master
# Your branch is ahead of 'origin/master' by 10 commits.
#   (use "git push" to publish your local commits)
#
nothing to commit, working directory clean

At this point we notice we made a typo in the code 9 commits ago. Since we care about clean commits on the master branch, we want to tidy up this error before pushing everything. For that there is interactive rebasing:

> git rebase -i HEAD~10

In this form, git-rebase can be thought of taking a series of commits, ripping them off at the specified position, changing them, and reapply the altered commits where they had been torn off originally. The above command tells git that we want to rip off the commits at the 10th ancestor, meaning we are able to edit the last 10 commits. Your editor will pop up and allow you to chose an action for every commit:

# In your $EDITOR
pick c8c2e42 Increase font size in body text
pick c5c5527 Corrected idiom
pick cbbf166 Added gravatar
pick 19cf87f Aligned the footers right side
pick fc7ccf9 Adapted the about page
pick 7d866c3 Finished draft
pick 22ac6b8 Published first post
pick 29ec1f9 Properly style the excerpt in the index
pick 24aa361 Enabled comments in first post
pick 79266da Current state of git draft

# Rebase cdd1533..79266da onto cdd1533
#
# 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

You can read the lines from top to bottom as a recipe. The commands are fairly self-explanatory. You chose any of the commands by replacing the default word ‘pick’ with any of the words listed in the explanatory comment below. You can also reorder the lines to reorder the commits. But beware that situations occur, where commits might not apply cleanly in changed order.

Since we want to change the ninth commit ago, we would replace the first ‘pick’ with the word ‘edit’. After saving and closing our editor, git will get to work and apply the recipe that we provided from top to bottom.

Stopped at c8c2e42... Increase font size in body text
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

The commit we wanted to change, is now pointed to by HEAD. So we can proceed, like described above for amending the last commit. After you are content with your changes, you can call git –rebase continue. All the following commits will now change their hash, since the parent no longer is the same. This is the reason, why this will only work for unpushed changes.

Undo pushed commits

There is only one way to undo pushed commits: Revert them.

git revert c8c2e42

This will create a counter commit with the exact inverse of the diff contained in the original commit. Just like you would not go and strike through an erroneous transaction in a bank account ledger, you would not simply make a pushed git commit disappear. Instead, you would create a counter-transaction which in git is a git-revert.

Undo a merge

This is practically the same as undoing the last commit. A merge is almost like a regular commit but it has two parents. Undoing it is the same as resetting the branch to the first parent.

git reset HEAD^1

This would of course leave the working copy dirty with the changes of the merge. If you had a clean working copy before (advisable), you could have called reset with the --hard flag. If you want to be extra careful, you can clean up the working copy by stashing:

git stash -u

In case you ran into a merge with nasty conflicts and you want to back out of it, the merge operation also features an --abort flag.

git merge --abort

Undo a rebase

Since I showed you rebase, I also owe you a way to undo a rebase-SNAFU. Rebasing commits does not change old commits, but rather abandons them and creates new ones. Therefore, our task boils down to find out where a branch pointed to before. For that, git has a tool called reflog. Reflog will tell you where your HEAD pointed to and what actions caused it to point somewhere else. This will show you the last 30 days of changes to your HEAD pointer per default. Old, dangling commits hang around for 90 days before they fall victim to the garbage collection.

> git reflog
345fae3 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
79266da HEAD@{1}: cherry-pick
24aa361 HEAD@{2}: cherry-pick
29ec1f9 HEAD@{3}: cherry-pick
22ac6b8 HEAD@{4}: cherry-pick
7d866c3 HEAD@{5}: cherry-pick
fc7ccf9 HEAD@{6}: cherry-pick
19cf87f HEAD@{7}: cherry-pick
cbbf166 HEAD@{8}: cherry-pick
c5c5527 HEAD@{9}: cherry-pick
c8c2e42 HEAD@{10}: cherry-pick
45f2312 HEAD@{11}: commit-amend: added some forgotten changes
ab4381a HEAD@{12}: checkout: moving from master to ab4381a
79266da HEAD@{13}: commit: The commit message
{...}

This is an excerpt from the reflog with a rebase. The rebase finished in the first line. Before that, it cherry-picked some old commits and amended one. The last line represents a commit that happened before the rebase. The hash on the left side is the result of the command on the right-hand side. Therefore, 79266da represents the commit we want to go back to in order to undo the rebase. This can be accomplished with a reset:

git reset 79266da

Summary

The most important technique for a painless git experience is maintaining a clean working copy by committing early and often. As soon as git has a commit of your changes, it is hard to lose them. Furthermore, you should avoid using the --force switch with commands. There are better options available in most scenarios, like stashing your changes or merging before pushing. Many actions can be undone as shown afore. If you ever completely lose track of what happened, git-reflog provides a good overview of what actions have been performed on your working copy.

What are your best practices when using git? What were the most difficult undo scenarios you encountered? Let me know in the comments!