Git合并:删除我想要保留的文件!

90
如何在 Git 中合并两个分支,并保留一个分支中必要的文件?
当合并两个分支时,如果有一个文件在一个分支中被删除,而在另一个分支中没有被删除,那么这个文件最终也会被删除。
例如:
- 当你创建一个新分支时,主分支中存在一个文件。 - 由于我们还不需要它,你从主分支中删除了该文件。 - 在分支中进行更改以添加一个功能,这个功能依赖于该文件的存在。 - 在主分支中进行错误修复(不能被丢弃)。 - 在某一天进行合并,结果该文件消失了!
如何重现:
1.创建一个带有一个文件的 Git 存储库。
git init
echo "test" > test.txt
git add .
git commit -m "initial commit"
  • 创建一个分支

  • git branch branchA
    
  • 删除主分支中的文件

  • git rm test.txt
    git commit -m "removed file from master"
    
  • 在branchA中进行任何更改,但不要触及已删除的文件(它必须保持不变以避免冲突)

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

    在这里,我发现的任何合并这两个分支的方法都会删除test.txt文件。假设我们对branchA依赖这个文件,那么这是一个很大的问题。


    失败的示例:

    合并1

    git checkout branchA
    git merge master
    ls test.txt
    
    合并2
    git checkout master
    git merge branchA
    ls test.txt
    

    变基 1

    git checkout branchA
    git rebase master
    ls test.txt
    

    我相信你可以通过将branchA(只要它不是公共的)从最新的主分支上进行变基来解决这个问题,然后再进行合并。交互式变基将给你一个机会告诉git你绝对需要这个文件,然后合并应该就没有问题了。 - jchook
    8个回答

    36

    这是一个有趣的问题。因为您在创建BranchA之后删除了文件,然后将master合并到BranchA中,我不确定Git如何能够意识到存在冲突。

    在错误合并之后,您可以撤销,并重新合并,但需要添加回该文件:

    git checkout HEAD@{1} .
    git merge --no-commit master
    git checkout master test.txt
    git add test.txt
    git commit
    

    28
    手动复制并重新添加文件可能是一个可行的解决方法,但我希望知道如何通过git来处理这个问题。如果git对于这个分支/合并问题的回答是“备份并自己完成”,那么这基本上就破坏了我使用git的原因。 - drfloob
    4
    不需要手动备份,您可以从主分支中获取文件。我更新了示例。 - cmcginty
    HEAD@{1} 是什么意思? - shampoo
    在git合并命令中加入选项--no-ff可能是个好主意。请参考https://dev59.com/xmgu5IYBdhLWcg3w_L3J。 - ekangas
    17
    如果删除文件的列表很长怎么办?这有点不公平,不是吗?我无法说服自己。自动合并应该在这样一个巨大的冲突中停止。即使文件中只有一个字符的冲突,它也会停止合并。但如果所有字符都被删除,感觉就没问题了。我错了吗?抱歉,我不理解这个逻辑。 - shampoo
    如果您在提交中删除了许多文件,可以使用此答案列出它们,以便更轻松地复制粘贴:https://dev59.com/PHRC5IYBdhLWcg3wD8xV - Eric Burel

    9

    Casey的例子在我的情况下不起作用 - 我无法从master中检出test.txt,因为它已不再存在于该分支中:

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

    很高兴我能够从branchAHEAD中提取文件:

    $ git checkout branchA
    $ git merge --no-commit master
    $ git checkout HEAD test.txt
    $ git add test.txt
    $ git commit
    

    2
    如果我有成千上万个文件需要添加,该怎么办? - kabrice
    1
    你可以将任何提交的文件检出到当前分支。git checkout <commit hash or branch name> -- path/to/file。Git也接受glob模式。 - Todd

    6
    在这种情况下,为了快速解决问题,应该使用"git revert"命令还原删除的文件。
    在未来遇到这种情况时,更好的方式是确保新文件的创建发生在分支上。然后当你合并时,它会被添加到主干中,但在此期间主干中不会存在该文件。

    2
    删除文件的提交包含其他重要更改。由于我不能丢失这些其他更改,因此git-revert似乎不是一个选项。 - drfloob
    4
    在这种情况下,最简单的方法可能是执行 "git revert -n [commit]" 命令,该命令会打补丁并保留所有更改但不进行提交。然后使用 "git checkout HEAD [filenames]" 命令清除您不想要的更改,即除文件还原之外的所有更改,然后提交。 - Phil

    5

    我解决这个问题的方法很简单,就是修改我需要保留的文件(添加了一些必要的注释),然后将这些更改提交到目标分支,从而生成合并冲突,然后可以通过git add和普通提交轻松解决。

    我的历史记录大致如下。为了保护无辜者,分支名称已更改。

    1. 在主分支上创建并提交新功能的文件
    2. 意识到这个添加比原来计划的要复杂,因此创建了功能分支
    3. 从主分支中删除文件,以免干扰常规工作流程(如RB等)
    4. 时间过去了,在主分支上进行了更多的提交,但在功能分支上没有进行任何提交
    5. 恢复对该功能的工作,git merge master 在功能分支上导致原始文件被删除(当然),git reset --hard 回到合并之前的状态
    6. 应用上述解决方案

    2

    您需要修改分支中的文件,以便在主干中的删除操作中出现合并冲突。

    如果您在主干中的头文件中删除某个声明(因为没有任何东西需要它),并将对该声明的依赖项添加到分支中的某些非头文件中,则会发生完全相同的情况。合并时,由于分支不涉及(该部分)头文件,它将仅删除声明并导致问题。

    每当您拥有多个位置上的相互依存且需要保持同步的内容时,合并就很容易悄然引入问题。这只是您在合并时需要知道和检查的事情之一。理想情况下,您可以使用编译时断言或其他构建时间检查,使任何失败立即显现。


    2
    同样的问题也发生在git上。我正在一个分支上开发一个功能,但是我的工作决定将该功能暂时搁置。因此,相关的功能文件已从主分支中删除,以允许应用程序部署而不需要不必要的文件...目前为止。现在我的工作需要我完成旧的功能,但当我尝试将旧的分支合并或变基到当前的主分支时,它会删除我需要的文件。
    快速解决方案是:修改您需要在功能分支中保留的文件,添加并提交,然后与主分支合并。这将导致合并冲突而不是删除模式。现在,您可以保留对文件的更改。
    问题是git将HEAD中的文件删除视为未更改,并且将默认进入删除模式。具体来说,在您修改所需文件后,输出的是“在主分支中已删除并在HEAD中已修改”。
    重现解决方案:
    1. 将分支重置为较旧的版本,其中包含已删除文件:git reset --hard origin/old_branch 2. 修改您需要保留的文件。不要让它们保持原样。 3. 将当前的主分支合并到功能分支中:git merge master 4.

    自动合并失败;修复冲突,然后提交结果。

    5. 在修复冲突时保留“我们”的更改。 6. git add .git commit -m "fix deleted HEAD" 7. 完成合并:git merge master

    1

    与其反向合并,您可以从提交之前的文件中检出文件到当前合并的分支中(user Todd的建议):

      git checkout <提交哈希或分支名称> -- path/to/file

    例如:

      git checkout 08ac9cf08f -- classify.cpp

    然后您就可以推送您的分支了。


    0

    所以,我找到了另一个解决方案,经过测试它有效:

    1. git log master..feature/<branch> --oneline --reverse 列出自从创建该错误分支以来的所有提交(注意:--reverse会按照从较旧的提交到最新的提交的顺序列出)
    2. git show --name-only <hash> 列出通过前面命令获得的每个提交修改的所有文件
    3. 选择每个文件的最近提交(我在一个文本文件上手动进行了操作)
    4. 从主分支创建一个新分支并切换到该分支
    5. git checkout <most_recent_commit_hash> -- <file_path> 将每个文件手动添加到新分支
    6. 提交并推送

    这确实类似于“逐个手动添加文件”,但如果您想确保较旧的提交不会覆盖主分支的情况,这是正确的方法。
    顺便说一下,这当然可以编写成脚本。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接