Undo removal of file from a previous commit
I am using gitflow branching model for development.
I’ve branched from
feature/X and in the very first commit in
feature/X I also deleted some files (using
git rm <file>).
Now after a couple of other commits in that branch I have realized, that I need the files I have deleted earlier.
Here’s a short sample to illustrate what i mean for clarification:
- Remove a specific commit but keep all the commits afterward
- Perform an empty commit with mercurial
- How do I specify a merge-base to use in a 'hg merge'
- Is there a version control system for database structure changes?
- Git: merge hides certain changes
- Git hook to update various web folders based on branch pushed to remote server
git flow init -d echo "file contents" > file.txt git commit -m "Added file.txt" git checkout -b feature/X git rm file.txt echo "foo" > foo.txt git add --all git commit -m "Deleted file.txt and added another file" <some more commits in feature/X> git log -u ... commit 04948fc4fc36d83901a0862b057657f3ccb9cf0d Author: <...> Date: Wed Aug 10 12:26:58 2016 +0200 Deleted file.txt and added another file diff --git a/file.txt b/file.txt deleted file mode 100644 index d03e242..0000000 --- a/file.txt +++ /dev/null @@ -1 +0,0 @@ -file contents diff --git a/foo.txt b/foo.txt new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/foo.txt @@ -0,0 +1 @@ +foo ...
I don’t just want to re-add the file in a new commit to avoid issues when merging
develop while there has been some changes in
Is there any way to take out the removal of
file.txt from that earlier commit?
I have tried
git reset <sha-of-previous-commit> file.txt but that didn’t bring back the file to my working copy.
I know about the down sides of rewriting history that has been pushed to a remote already. However I know nobody has done any commits on
feature/X except me hence it should be safe for me to rewrite the history although it has been pushed already.
4 Solutions collect form web for “Undo removal of file from a previous commit”
I don’t just want to re-add the file in a new commit to avoid issues when merging feature/X to develop while there has been some changes in file.txt in develop branch.
Yes, you do want to just re-add the file in a new commit. git is wise about this (well, actually there is no special intelligence involved here, it just comes from the way git handles files).
git init test # init a repos, create test.txt cd test echo start > test.txt git add -A ; git commit -m 'x' git checkout -b bra # create branch, add a line echo line1 >> test.txt git add -A ; git commit -m 'x' git rm test.txt # delete file + commit git commit -m "x" echo start > test.txt # restore file and add a line echo line2 >> test.txt git add -A ; git commit -m 'x' git checkout master # back to master, add the same line and another one echo line2 >> test.txt echo line3 >> test.txt git add -A ; git commit -m 'x' git merge bra # merge cat test.txt start line2 <<<<<<< HEAD line3 ======= >>>>>>> bra
The conflict is as expected (sic!); the important part about it is that
line2 is not part of the conflict.
git merge did not care about all the shenannigans that happened to the file on the branch, it is only interested about the end result.
What you have now is a sequence of commits on some branch:
...--o--*--B1--B2--B3 <-- branch
* is a good commit, but
B1 is a “bad” commit (has files removed that you decided, now, you don’t want to remove after all).
You’ll need to re-do your bad commit(s) as good commit(s)—and also, to copy any good commits that come after a bad commit. In other words, you will basically repeat the following three steps as needed:
Copy the commit you don’t like to a new commit (but don’t actually commit it just yet). Starting from a new branch that will grow from the commit marked
git cherry-pick -nto copy a bad commit like
B1, but not actually commit it.
Then, restore the missing file(s):
git checkout -- <path>. (You can also do this with
git reset, but when we get to using
git rebase -ibelow, it gets too hard, so we’ll stick with
Finally, make the new commit that is a copy of the old commit:
git commit. Since this is a cherry pick, Git will have the old commit’s message ready to be edited.
Now the first bad commit
B1 has been copied to a new, corrected, good commit
...--o--*--B1--B2--B3 <-- branch \ G1 <-- new branch [new copies being made]
Now you can copy the second commit,
B2. If it has a deletion you don’t want, use the same three-part sequence (
commit). If it’s fine, leave out the
-n to just copy it and commit:
...--o--*--B1--B2--B3 <-- branch \ G1--G2 <-- new branch [new copies being made]
And now you can copy the third commit,
B3. As before, if it has something you need to change, you can fix it up along the way. (This assumes there are three commits in the
B1--...--Bn chain; if there are more, or fewer, adjust as needed.)
When everything is all done, you will need to convince Git to peel the
branch label off the old branch and paste it on the new one instead. There is a way to do that, but…
git rebase -i
There’s a much easier way to do it. The
git rebase command works by doing a series of cherry-picks.1 When the cherry-picks are all done, the
git rebase command peels off the old branch label, and pastes the label on the new branch it just built. This is exactly what you need to do, so all you have to do is convince
git rebase to let you tell it which commits are good commits, and which are bad ones, and to pause at bad ones.
This is where
git rebase -i comes in. It brings up your editor on a set of instructions. Initially, all the instructions are
pick: do a cherry pick of each commit. You can change any one instruction to
edit, which tells Git to do that particular cherry-pick, but then stop and let you fix / change things.
rebase does the cherry-pick without
-n: it makes the copy and commits it. So you have to tweak step 2 a bit: instead of
git checkout -- <path> to get the file back from
HEAD, you need to
git checkout HEAD^ -- <path> or
git checkout HEAD~1 -- <path>. This means “reach back into the previous commit, and get that version of the file.” Having done that, you can then run
git commit --amend to update the commit.
Then, continue the rebase process with
git rebase --continue. The rebase code will go on through more
picks to the next
edit, or if there are only
picks remaining, finish them all off and finish the rebase and move the branch label.
The main thing is to identify commit
*: the last “good” commit. Spelling out commit
* in some way lets you do the
git rebase -i. In our example, it’s three steps back from the current branch tip, so:
git rebase -i HEAD~3
will do the trick. If it’s more or fewer steps back, you’ll need to adjust the number after the tilde
Note that whatever you do, you will get copies of the original commits. Also,
git rebase normally removes merge commits when making copies. Both of these mean you must be cautious if you’ve made these commits available to anyone else (by
git push or similar), or if you have merges following the first bad commit you intend to “replace” (really, copy and then ignore the originals).
git rebase literally runs
git cherry-pick sometimes. Other times, it uses something else that’s pretty much the same anyway.
Git is not SVN, I think there shouldn’t be any problem if you just re-add the file in a new commit.
Unlike SVN, Git does not track files, it tracks content that happens to be stored in files.
If you delete and re-add a file in SVN their histories are not connected, but they are different files for SVN.
If you delete and re-add a file in Git, it is just content that is deleted and re-added, so a merge should work fine. A rebase could be problematic as there the commits are applied one-by one to the new base commit and thus you would have the edit / delete conflict. But on merge only the changes that were done should get applied as a whole and thus no problem should arise.
There are a couple of ways of restoring that file, depending on whether or not it’s safe to rewrite the history of the
Option 1: Restore the file in a new commit
If you’ve already pushed that commit, the easiest thing to do would be to simply retrieve the
foo.txt file from the parent of the commit that deleted it and commit it again.
For example, say that the SHA-1 of the commit that deleted the file is
git checkout feature/X git checkout 123abc^ path/to/file.txt git add path/to/file.txt git commit -m "Restore the file.txt file"
Option 2: Restore the file in the original commit
If, on the other hand, you haven’t pushed those commits, you’re able to safely rewrite the history of your local
feature/X branch to undo the removal of that file. In order to do that, you’ll have to do an interactive rebase and edit the commit that delete the file:
git checkout feature/X git rebase -i 123abc^
In the todo list, change the word on the left of that commit from
edit; then save the file and exit.
Once Git reaches the commit you want to edit, you can restore the deleted file by saying:
git checkout HEAD^ path/to/file.txt git add path/to/file.txt git commit --amend -C HEAD # where -C HEAD reuses the commit message of HEAD
Then finish up the rebase with:
git rebase --continue