1

I am using gitflow branching model for development. I've branched from develop to 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:

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 feature/X to develop while there has been some changes in file.txt in develop branch.

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.

EDIT 1:
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.

Anticom
  • 975
  • 13
  • 29

4 Answers4

1

What you have now is a sequence of commits on some branch:

...--o--*--B1--B2--B3   <-- branch

where * 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:

  1. 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 *, use git cherry-pick -n to copy a bad commit like B1, but not actually commit it.

  2. 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 -i below, it gets too hard, so we'll stick with git checkout.)

  3. 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 G1:

...--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 (cherry-pick -n, checkout, 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.

Slightly annoyingly, 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 ~ character.

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).


1In fact, git rebase literally runs git cherry-pick sometimes. Other times, it uses something else that's pretty much the same anyway.

torek
  • 448,244
  • 59
  • 642
  • 775
1

Answer

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).

Example

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.

AnoE
  • 8,048
  • 1
  • 21
  • 36
  • So tl;dr; is if i delte a file and just restore it later on in the same branch git "ignores" it during merge? Is there some kind of internal squashing happening for diff reasons before the actual merge? – Anticom Aug 10 '16 at 12:08
  • Yes, you could call it "internal squashing". The merge compares 3 files: the current one on the source branch, and the current one on the target branch, and the one on the common parent. What happened inbetween does not matter at all. – AnoE Aug 10 '16 at 21:34
  • Thank you for elaborating on this. – Anticom Aug 11 '16 at 08:25
0

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.

Vampire
  • 35,631
  • 4
  • 76
  • 102
0

There are a couple of ways of restoring that file, depending on whether or not it's safe to rewrite the history of the feature/X branch.

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 123abc:

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 pick to 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
Enrico Campidoglio
  • 56,676
  • 12
  • 126
  • 154