Git Undo 999 — How do I undo this operation?

2011-10-26 22:08:09 +0900 / tag:git / Comments

Git is one of the best version control systems at present. One of the important features of Git is that it provides some way to undo most operations. In practice, we often mistake something, but Git provides way to undo such mistakings without any loss. This is a great merit.

Though Git’s commands are incredibly useful, it’s hard for newcomers to make a good combination of Git’s commands to finish their own tasks, like shell commands. Recent Git’s command-line interface is well polished, but there is no command such as git undo-what-i-did, because things to be undone are varied for each situation. So that we have to execute a bunch of commands to do what we want.

In the past 3 years, I was often asked advice to undo something from my friends and my colleagues. The following is the list of situations I have experienced ever.

Q1. I committed several lines to check new features of library X. But I don’t want to keep the commits anymore. How do I delete them?

One might prefer committing each logical change to recover later, like the following:

$ $EDITOR
$ git commit -am 'Enable feature X'
$ $EDITOR
$ git commit -am 'Enable feature Y'
$ $EDITOR
$ git commit -am 'Disable feature Z'

But one often wants to delete these commits from the history.

A1. git reset --hard HEAD~<n>

In this situation, you can use git reset --hard to delete commits from the history. If there are 3 commits to delete, execute the following command:

$ git reset --hard HEAD~3

HEAD~<n> means the commit that is the n-th generation grand-parent of the HEAD, and HEAD means the most recent commit of the current branch. You can use these notations for other commands that take commits as their parameters. For example: git show HEAD~1.

A1’. git branch -d

But in this situation, you should consider creating a branch for temporary work and deleting the branch afterwards. For example:

$ git branch experimental
$ git checkout experimental

$ $EDITOR
$ git commit -am 'foo'
$ $EDITOR
$ git commit -am 'bar'
$ $EDITOR
$ git commit -am 'baz'

$ git checkout master
$ git branch -d experimental

Q2. I’ve merged a topic branch into the master branch, but the topic branch is not mature yet. How do I undo the last merge?

A2. git reset --hard ORIG_HEAD

This is a variant of Q1. Use the following command:

$ git reset --hard ORIG_HEAD

ORIG_HEAD means the previous state of HEAD. git commit, git reset, git merge and other commands update HEAD. You can use ORIG_HEAD to refer the commit before running such commands.

Note that ORIG_HEAD will be updated by several commands. You should run git reset --hard ORIG_HEAD as soon as possible if you want to undo the last merge. Once you run some command such as git commit after the last merge, ORIG_HEAD will be updated to refer the last merge commit.

If you want undo the last merge and some operations after the last merge, run git reset --hard HEAD~<n>, or check git log to find the proper commit then run git reset --hard <commit-ish>.

Q3. A serious bug is found after release of my product. The cause of the bug is in the commit which I did 30 days ago. How do I revert this mistake?

In this situation, you should not use git reset and other commands which rewrite the history. Because the product is already realeased, and your colleagues have run git pull your changes. Though it’s possible to use git reset and others to rewrite the history, your colleagues will be confused by the rewriting. In practice, it’s very hard to rewrite the history because there are many merges after the buggy commit.

A3. git revert <commit-ish>

Therefore, you should make a commit which reverts the buggy commit, then git push it to share your colleagues. Git provides the command for this situation. Use the following command:

$ git revert <commit-ish>

Where <commit-ish> is the ID of the buggy commit to be reverted. git revert generates a patch to revert changes in <commit-ish> then commits it.

Q4. I should run git commit, but I did git commit --amend by mistake. How do I undo this overwriting?

One of the useful idioms about Git is git commit --amend which overwrites the last commit. For example:

$ git commit -m 'Implement X'

$ $EDITOR foo  # Fix a typo in code.
$ git add foo
$ git commit -m 'Implement X' --amend

$ $EDITOR bar  # Fix minor mistakes in code.
$ git add bar
$ git commit -m 'Implement X' --amend

$ git add baz  # Forget to add...
$ git commit -m 'Implement X' --amend

So you can avoid messing up the commit log by boring commits such as “Fix a typo”.

Though git commit --amend is useful, it overwrites the last commit. So that it should be used carefully. But sometimes you will run commands like the following:

$ $EDITOR foo  # Implement brand-new feature.
$ git add foo
$ git commit -m 'Implement Y' --amend  # Eh?

If --amend is not passed, this commit can be undone by git reset HEAD~1. But it’s not proper for this situation, because HEAD~1 means the parent commit (B) of the last commit (C’), not the last commit before git commit --amend (C).

C'   HEAD (after git commit --amend)
|
| C  the commit which HEAD refered before git commit --amend
|/
B    HEAD~1 (after git commit --amend)
|
|
A
:

A4. git reset HEAD@{1}

Don’t worry. Git provides the way to undo this situation. Use the following command:

$ git reset HEAD@{1}

HEAD@{<n>} means the n-th prior value of HEAD. So that you can use HEAD@{1} to refer the commit (C) which HEAD refered before the last operation (in this situation, git commit --amend) updating HEAD.

This feature is called reflog which is available since version 1.4.0. You can also use some fancy notation like master@{yesterday} via reflog.

Q5. My working directory is messed up for some reason. How do I clean up?

A5. git checkout -- .

Normally git checkout is the command to switch the current branch. But git checkout can also be used to update files in the working directory from index or from a commit. Use the following command to clean up all files in the working directory from the index:

$ git checkout -- .

If you want to clean up specific files, use git checkout as follows:

$ git checkout -- foo bar baz

If you want to replace the content of files with specific version, use git checkout like the following:

$ git checkout master~1 -- foo

A5’. git reset --hard HEAD

If you git add some changes into the index and you want to clean up the index too, use the following command:

$ git reset --hard HEAD

On some environments, especially slow file IO like Cygwin, git reset --hard HEAD is very faster than git checkout HEAD -- ..

Note that git reset updates HEAD. Use it carefully.

Q6. Now there are many garbage files in the working directory. How do I delete them?

A6: git clean

In this situation, garbage files are not maintained by Git. Such files can be deleted by git clean.

But files not maintained by Git cannot be recovered after deletion. So that git clean doesn’t delete files by default. You should run:

$ git clean -n

to check what files to be deleted, then run:

$ git clean -f

to delete the files actually.

Q7. I ran git add new files by mistake. How do I undo?

A7. git reset HEAD -- <file>

git reset <file> is the opposite of git add <file>.

A7’. git rm --cached <file>

git rm --cahced can be also used for this situation.

Q8. I ran git add -p <file>, but there are some mistakings. How do I undo?

A8. git reset HEAD -- <file>

Unlike Q7, git rm --cached <file> cannot be used for this situation, because git rm is the command to remove files from the repository.

Q9. I deleted some files by mistake. How do I undo?

A9. git checkout -- <file>

This is a variant of Q5.

Q10. I edited many portions of some file, but I want to reset the file with the last committed version now. How do I reset?

A10. git checkout HEAD -- <file>

This is a variant of Q5.

Q11. I edited about 10 functions in foo.c, but I want to revert 3 of the functions to the last committed version. How do I revert?

A11. git checkout -p HEAD -- <file>

With -p option, Git will ask you which changes to keep or to delete.

Q12. I’m confused in the middle of git rebase -i. How do I stop rebasing and restore the working tree before rebasing?

A12. git rebase --abort

By the way, you should check out messages from git rebase -i – it has already told you how to abort rebasing.

Q13. I ran git rebase, but I found serious mistakes later. How do I undo this rebasing?

A13. git reset --hard ORIG_HEAD

This situation is a variant of Q2.

A13’. git reflog and git reset --hard

Sometimes you notice a mistake after two or more git rebase. If so, use git reflog to find the commit before multiple times of rebasing, then git reset the current branch to that state.

Q14. Arrrrgggh! I want to do git reset --hard HEAD~4, but I ran git reset --hard HEAD~44 by mistake. How do I undo this?

A14. git reset --hard HEAD@{1}

This situation is a variant of Q4.

Q15. The last commit has two logical changes. I want to split the commit. How can I do?

A15. git reset HEAD~1

For example:

$ git reset HEAD~1
$ git add foo bar
$ git commit -m 'First part'
$ git add -u
$ git commit -m 'Second part'

Q16. I worked on a feature branch and there are uncommitted changes, but I have to release the product now. I have to remove the chanes before switching branch, but I want to restore the changes later. How can I do?

A16. git stash save and git stash pop

git stash is the command for this situation. Use git stash save to remove uncommitted changes. The changes can be restored later by git stash pop.

Q17. I commited a file with a secret password for a long time ago, and I pushed it to the public repository. How do I rewrite this embarrassed history?

$ git checkout master

$ echo 'id=who' >conf
$ echo 'password=secret' >>conf
$ git add conf
$ git commit -m 'Add conf'

$ echo 'width=1024' >>conf
$ git commit -am 'Update conf 1'

$ echo 'height=768' >>conf
$ git commit -am 'Update conf 2'

$ echo 'color=256' >>conf
$ git commit -am 'Update conf 3'

$ git push origin master

For example, you notice that raw password should not be written into conf file. You’ll want to delete it first:

$ sed -e '/^password=/d' conf >,conf
$ mv ,conf conf
$ git commit -am 'Remove the secret password'
$ git push origin master

But it’s still possible to see the password from older commits:

$ git show HEAD~1:conf | grep 'password'
password=secret

A17. Change the actual password as soon as possible.

This might be the best solution for most cases.

A17’. git filter-branch then git push -f

Warning: Dangerous operation – you should be aware what you do.

In this situation, you can use git filter-branch to rewrite up the whole history. To remove raw password in conf file from the whole history, run the following command:

$ git filter-branch \
> --tree-filter 'sed -e "/^password=/d" <conf >,conf; mv ,conf conf' \
> master

Then confirm that conf file doesn’t contain raw password anymore:

$ git log -p master -- conf

If everything is okay, overwrite pushed content in the public repository:

$ git push origin master -f

Note that the above operation overwrites already published content by your own convenience. So that others who cloned the public repository will be very confused. You should be aware what you do, and you have to inform others.

Q18. I’ve merged feature-x into master, but there are too many conflicts. Most conflicts can be resolved by restoring file content from one branch. How do I do?

Suppose that you’ve run into the following problem:

$ git checkout master

$ git merge feature-x
Auto-merging SOMEFILE
CONFLICT (content): Merge conflict in SOMEFILE
Automatic merge failed; fix conflicts and then commit the result.

$ git diff | wc -l
12000

$ git diff | grep '<<<<<<<' | wc -l
8000

Conflicted files are automatically updated with <<<<<<< and >>>>>>> lines which are markers to denote conflicted lines. It’s not a big problem if the number of conflicted parts is few enough. But in this situation, you have too many conflicted parts to edit manually.

A18: git checkout --ours <file> and/or git checkout --theirs <file>

This is a variant of Q5. Use git checkout like git checkout master -- SOMEFILE. But Git knows what branch you’ve merged, so that you can use the following commands:

$ git checkout --ours -- SOMEFILE

--ours means the branch which is currently checked out. In this situation, --ours means master.

$ git checkout --theirs -- SOMEFILE

--theirs means the branch which was merged into the currently checked out branch. In this situation, --theirs means feature-x.

Q19: I’m confused about resolving conflicted files. How do I restore conflicted files to the state just after git merge?

A19: git checkout --merge <file>

This is a variant of Q18. You can use git checkout with --merge option to restore the content of conflicted files to the state just after git merge.