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.
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.
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.
git branch -dBut 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
git reset --hard ORIG_HEADThis 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>.
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.
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.
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
:
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.
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
git reset --hard HEADIf 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.
git cleanIn 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.
git add new files by mistake. How do I undo?git reset HEAD -- <file>git reset <file> is the opposite of git add <file>.
git rm --cached <file>git rm --cahced can be also used for this situation.
git add -p <file>, but there are some mistakings. How do I undo?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.
git checkout -- <file>This is a variant of Q5.
git checkout HEAD -- <file>This is a variant of Q5.
git checkout -p HEAD -- <file>With -p option, Git will ask you which changes to keep or to
delete.
git rebase -i. How do I stop rebasing and restore the working tree before rebasing?git rebase --abortBy the way, you should check out messages from git rebase -i – it has
already told you how to abort rebasing.
git rebase, but I found serious mistakes later. How do I undo this rebasing?git reset --hard ORIG_HEADThis situation is a variant of Q2.
git reflog and git reset --hardSometimes 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.
git reset --hard HEAD~4, but I ran git reset --hard HEAD~44 by mistake. How do I undo this?git reset --hard HEAD@{1}This situation is a variant of Q4.
git reset HEAD~1For example:
$ git reset HEAD~1
$ git add foo bar
$ git commit -m 'First part'
$ git add -u
$ git commit -m 'Second part'
git stash save and git stash popgit
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.
$ 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
This might be the best solution for most cases.
git filter-branch then git push -fWarning: 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.
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.
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.
git merge?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.