88

How can you merge two branches in git, retaining necessary files from a branch?

When merging two branches, if a file was deleted in one branch and not in another, the file is ultimately deleted.

For example:

  • A file exists in master when you make a new branch
  • you remove the file from master since we don't need it (yet)
  • you make changes in the branch to add a feature, which relies on the file existing
  • you make bug fixes in master (cannot be discarded)
  • you merge some day, and the file is gone!

How to Reproduce:

  1. Create a git repo with one file.

    git init
    echo "test" > test.txt
    git add .
    git commit -m "initial commit"
    
  2. Create a branch

    git branch branchA
    
  3. Delete the file in master

    git rm test.txt
    git commit -m "removed file from master"
    
  4. Make ANY changes in branchA that don't touch the deleted file (it has to be unchanged to avoid Conflict)

    git checkout branchA
    touch something.txt
    git add .
    git commit -m "some branch changes"
    

From here, any way I've found to merge these two branches, the test.txt file is deleted. Assuming we were relying on the file for branchA, this is a big problem.


Failing examples:

Merge 1

git checkout branchA
git merge master
ls test.txt

Merge 2

git checkout master
git merge branchA
ls test.txt

Rebase 1

git checkout branchA
git rebase master
ls test.txt
drfloob
  • 3,154
  • 1
  • 24
  • 31
  • I believe you can solve this problem by rebasing branchA (as long as it's not public) from latest master before merging. An interactive rebase will give you a chance to tell git that you absolutely do want the file, then the merge should go without a hitch. – jchook Jun 15 '17 at 15:26

8 Answers8

36

This is an interesting issue. Because you deleted the file after BranchA was created, and then are merging master into BranchA, I'm not sure how Git would be able to realize there is a conflict.

After the bad merge you can undo, and then re-merge, but add back the file:

git checkout HEAD@{1} .
git merge --no-commit master
git checkout master test.txt
git add test.txt
git commit
cmcginty
  • 113,384
  • 42
  • 163
  • 163
  • 27
    manually copying off and re-adding the file(s) would be a fine workaround, but I'd like to know how to do this through git. If git's answer to this branch/merge issue is "make a backup and do it yourself", it pretty much defeats my reasons for using git. – drfloob Sep 10 '09 at 21:09
  • 4
    no, you don't need a manual backup. You can get the file out of the master branch. I updated the example. – cmcginty Sep 11 '09 at 00:40
  • what does it mean HEAD@{1} ? – shampoo Apr 23 '13 at 13:27
  • It is probably a good idea to add the option, --no-ff, to the git merge command. see http://stackoverflow.com/questions/10935226/git-interactive-merge – ekangas Jul 11 '14 at 21:01
  • 17
    What if the list of deleted files is huge? It's kind of not fair. Isn't it? I can not convince myself otherwise. Auto merge is supposed to stop at such a HUGE conflict. It stops even if there is just one character conflict in the file, But if All the characters are deleted, it feels that's fine. Am I wrong? Sorry, I don't get the logic. – shampoo Jun 08 '15 at 07:32
  • If you have a lot of file deleted in a commit you can use this answer to list them, so that copy pasting is easier: https://stackoverflow.com/questions/424071/how-to-list-all-the-files-in-a-commit – Eric Burel Jan 09 '19 at 13:55
9

Casey's example didn't work for my case - I couldn't checkout test.txt from master, because it was no longer in that branch:

$ git checkout master test.txt
error: pathspec 'test.txt' did not match any file(s) known to git.

Happily I could pull the file out of branchA's own HEAD:

$ git checkout branchA
$ git merge --no-commit master
$ git checkout HEAD test.txt
$ git add test.txt
$ git commit
Alex Dean
  • 15,575
  • 13
  • 63
  • 74
6

For a quick fix in this case, "git revert" the commit that deleted the file.

When this situation comes up in the future, the better way to handle it is to ensure that the creation of the new file happens on the branch. Then it gets added on master when you merge, but you don't have the file lying around in master in the meantime.

Phil
  • 4,767
  • 1
  • 25
  • 21
  • 2
    the commit that deleted the file(s) included other nontrivial changes. git-revert doesn't seem to be an option, since I can't lose those other changes. – drfloob Sep 10 '09 at 21:28
  • 4
    In that case, the easiest way might be to do "git revert -n [commit]", which patches in all the changes but doesn't commit them. Then use "git checkout HEAD [filenames]" to wipe the changes you don't want, i.e. everything except the file restore, then commit. – Phil Sep 10 '09 at 21:37
5

My solution to this was to simply modify the files I needed to keep (added a comment which was needed anyway) and commit those changes on the target branch, thus generating a merge conflict which could easily be resolved with a git add and a normal commit.

My history went something like this. Branch names have been changed to protect the innocent.

  1. create and commit files for a new feature to master
  2. realize this addition going to be more involved than originally planned, thus, branched to feature_branch
  3. Removed files from master so as not to disrupt normal workflow with RBs and such
  4. Time passes, more commits on master, none on feature_branch
  5. Resume work on the feature, git merge master on feature_branch causes original files to be removed (of course), git reset --hard to before the merge
  6. Applied the solution described above
Alex S
  • 25,241
  • 18
  • 52
  • 63
2

You need to modify the file in the branch, so that there's a merge conflict with the delete in the trunk.

The exact same thing will happen if you, for example, delete a declaration for something in a headerfile in the trunk (because nothing needs it), and add a dependency on that declaration to some non-header file(s) in the branch. When you merge, since the branch doesn't touch (that part of) the header, it will just delete the declaration and things will break.

Whenever you have stuff in multiple places that is interdependent and needs to be kept in sync, its very easy for a merge to silently introduce problems. Its just one of the things you have to know about and check when merging. Ideally, you use compile-time asserts or other build time checks that will make any failures immediately apparent.

Chris Dodd
  • 2,920
  • 15
  • 10
2

Same issue with git. Was working on a feature in a branch, then my job decided to sideline the feature for later. Thus the relevant feature files were deleted from master branch to allow app deployment without unnecessary files ... for the time being. Now my job needs me to finish the old feature and when I try to merge or rebase the old branch into the current master it deletes the files I need.

The quick solution is: Modify the files you need to keep in your feature branch, add and commit, and then merge with your master. This will now cause a merge conflict as opposed to deletion mode when merging. Now you can keep "our" changes to the files.

The problem is git sees the file deletion in HEAD as not being changed and will default into deletion mode. Specifically: deleted in master and modified in HEAD is outputted after you modify the files you need.

Reproduce the solution:

  1. Reset a branch to the older version with deleted files: git reset --hard origin/old_branch
  2. Modify the files you need to keep. Do not leave them the same.
  3. Merge your current master into the feature branch: git merge master

Automatic merge failed; fix conflicts and then commit the result.

  1. Keep "our" changes when fixing conflicts.
  2. git add . and git commit -m "fix deleted HEAD"
  3. Finish merging: git merge master
hygtfrde
  • 130
  • 3
1

Instead reversing merging, you could checkout file(s) from just before commit into your current merged branch (user Todd's suggestion):

  git checkout <commit hash or branch name> -- path/to/file

For example,

  git checkout 08ac9cf08f -- classify.cpp

and then you could push your branch.

Cloud Cho
  • 1,594
  • 19
  • 22
0

So, I've found another solution, which I've tested and it works:

  1. git log master..feature/<branch> --oneline --reverse List all commits of the bad branch, since it was created from master (NOTE: --reverse will give you a list from the older commit, to the newest at the bottom)
  2. git show --name-only <hash> List all files modified by every commit you obtained with the command before
  3. Take the most RECENT commit, of each file (I did it manually on a text file).
  4. Create a new branch from master and switch into it.
  5. git checkout <most_recent_commit_hash> -- <file_path> Add manually each file to the new branch
  6. Commit and push

This is for sure like "adding manually files one by one" but if you want to be sure that older commits don't prevail to the master situation, this is the way to go.
BTW, It could for sure be scripted.

bersi
  • 75
  • 1
  • 8