我有一个项目,branch-1
和branch-2
在branch-1
中,我复制了一个文件以再次使用它: 以前
root/
code.cpp
后
root/
src/
code.cpp
test/
code.cpp
意外地,当合并到 master 时(我无权更改),git 识别为rename {root/ => root/test/}code.cpp
.
现在每次我更改代码时都会code.cpp
branch-2
并挑选branch-1
,所有更改都将应用于test/code.cpp
而不是src/code.cpp
我该如何纠正?
如果您需要在事后合并文件中的工作,请考虑git merge-file
(见下文)。 但是"我该如何解决这个问题"的答案是:你没有。 Git 实际上首先没有文件重命名,所以没有什么可修复的!
现在,你可能会反对,嘿,Git 本身在这里说"重命名"。 确实如此。 但这并不是因为文件被重命名了。 这是因为 Git 正在测试——当你要求它告诉你两个提交的时候——看看它是否可以假装文件被重命名了。 如果 Git 认为说"重命名此文件"会产生更短、更清晰的输出,Git 会这么说。
您可以使用--no-renames
选项将其关闭:
git diff --no-renames <commit-hash-ID-1> <commit-hash-ID-2>
现在,当 Git 比较两个提交时,它永远不会说"重命名"。 相反,它会说"删除文件"和"添加文件"。
当 [I] 合并时
对于git merge
,请使用-X no-renames
而不是--no-renames
。 (为什么这是一个与git diff
的--no-renames
不同的选项拼写主要是历史的,以及Git通常糟糕的用户体验设计。
和樱桃采摘
挑选代码使用 Git的合并引擎,所以在这里,再次使用-X no-renames
使 Git 停止调用"重命名"。
长:所有原始细节
我们将从git merge
的概述开始,因为所有这些东西都使用 Git 的合并引擎。
Git 的自动组合工作方法
当您运行git merge other
时,Git 执行以下操作:
- 找到当前提交(通过
HEAD
); - 找到
other
指定的提交:这可以是分支名称或原始提交哈希ID,或者git rev-parse
真正可以接受的任何内容; - 使用这两个提交,找到第三个提交,即合并库。
为了用常规(和实际1)合并来说明这一点,让我们考虑以下图形片段:
o--...--L <-- br1 (HEAD)
/
...--o--B
o--...--R <-- br2
也就是说,您当前"在"分支br1
,使用提交L
(L
这里代表一个丑陋的大哈希ID,我们将像git mergetool
一样称LOCAL
,或者像git restore
一样称--ours
)。 您要求 Git 通过git merge br2
合并提交R
。 Git 使用历史记录(从提交到早期提交的父链接)来查找第三个提交,我在这里称之为B
。 这是合并基础。 它是"最佳公共(共享)提交",或者从技术上讲,是运行最低公共祖先算法的输出。阿拉伯数字
实际上,Git 现在运行:
git diff --find-renames <hash-of-B> <hash-of-L>
看看"我们"在"我们的"分支上发生了什么变化br1
. 使用-X no-renames
会使 Git 省略--find-renames
部分。
然后 Git 运行第二个git diff
命令:
git diff --find-renames <hash-of-B> <hash-of-R>
查看"他们"在"他们的"分支上发生了什么变化br2
,因为相同的起点(提交B
)。 同样,使用-X no-renames
将禁用--find-renames
步骤。
通过--find-renames
步骤,Git 有时会将一些B
并在L
和/或R
中删除的文件与一些不在B
中但在L
和/或R
中新创建的文件配对。 Git 会将其称为重命名,并尝试保留此重命名更改。如果没有--find-renames
步骤,Git 将不会进行此配对,这避免了 Git 找到错误对时出现的问题,但现在会给您留下"文件已删除"。
在所有情况下(无论是否--find-renames
),Git 现在都有一系列可能性:
- 该文件存在并且在所有三个提交(
B
、L
和R
)中具有相同的名称; - 文件在
B
到L
和/或B
到R
过渡中重命名和/或删除; - 文件是在
B
到L
和/或B
到R
过渡中的一个或两个中从头开始创建的。
第一种情况是最简单和最常见的一种,Git 通常可以很好地处理。 如果 Git 不知道如何组合B
to-L
和B
to-R
更改,则第二种和第三种情况可能会导致 Git 因合并冲突而停止。
如果确实存在文件的三个版本(包括重命名),我们现在有"同一文件"的"三个版本"(即使文件在L
和/或R
中具有不同的文件名)。 Git 现在尝试合并在B
到L
和/或B
到R
差异中所做的任何内容更改。 如果一方触摸了内容而另一方没有,Git 将从接触内容的一侧获取更改。 如果双方都没有触及内容,Git 将采用文件的三个版本中的任何一个。如果双方都接触了内容,Git 会对三个输入文件使用git merge-file
来尝试合并更改。
git merge-file
的结果可以是"成功合并"或"检测到合并冲突"。 如果检测到合并冲突,Git:
- 将其合并的最佳近似写到工作树,并且
- 将三个输入文件保留在其索引中,使用非零暂存号。
使用git ls-files --stage --unmerged
,你可以在 Git 的索引中看到文件的三个版本;你可以使用git checkout-index
或类似(包括git mergetool
)来获取这三个版本。
但是,当文件被删除(或被检测为)时,至少有一个版本不在索引中。 您仍然可以从索引中恢复文件的合并基版本,这对于合并特别方便,因为识别提交B
可能很棘手。 有关详细信息,请参阅此处的git checkout-index
文档。
如果文件实际上没有被删除,而是被重命名(Git 只是错误地识别了正确的新文件)和/或有需要合并的工作,你现在必须绕过 Git 的正常自动化。
1当不需要真正的合并时,git merge
命令有时会使用快捷方式。 我们不会在这里介绍这种情况,因为它不适用于任何问题情况。
2如果 LCA 算法发出多个提交哈希 ID,事情就会变得复杂;我在这里不涉及这种情况。
<小时 />使用git merge-file
当 Git 对三个文件内容进行自动三向合并时,它将识别一个"侧"(例如,我们的/左侧/本地)接触而另一侧没有更改的部分,并将进行更改。 只有在"我们的"和"他们的"更改实际重叠或相邻的情况下,Git 才会声明合并冲突。
但是,当 Git 无法识别正确的输入文件时,它的自动合并是无用的。 对于这些情况,您只需提取三个输入。 然后,您可以将这三个文件提供给git merge-file
命令:
git merge-file ours base theirs
或:
git merge-file foo.LOCAL foo.BASE foo.REMOTE
(正如git mergetool
所说的那样)。 Git 将尽最大努力对ours
(或foo.LOCAL
)文件进行相同的三向合并,将base
和theirs
文件视为合并基础和"远程"。
git merge-file
命令将使用相同类型的冲突标记;若要使其将正确的文本放入冲突标记中,请使用-L
选项。
使合并提交结果
使用正确的名称获得正确的文件内容后,请使用git add
(以及git rm
,如果必要和适当)告诉 Git 你已成功合并更改。 然后使用git merge --continue
或git commit
完成合并。
如果git merge -X no-renames
尝试提交合并是因为 Git 认为它已经成功完成了合并,即使它实际上并没有正确地这样做,请使用--no-commit
选项来防止git merge
进行提交。
樱桃采摘
如上所述,樱桃采摘实际上是使用合并引擎实现的。 运行:
git cherry-pick <commit-specifier>
运行与git merge
相同的更改组合代码,但有一个更改:Git 使用指定提交的父级作为合并基础。
那是:
--ours
提交是像往常一样HEAD
提交;--theirs
提交是你像往常一样指定的提交;但是- 合并基是指定提交的(单个)父级。
其他一切都像常规合并一样工作,直到合并完成。 然后,你运行git cherry-pick --continue
(或者再次,git commit
)来让 Git 进行新的提交,而不是git merge --continue
。 与git merge
不同,新提交将是一个普通(单亲)提交。 与git merge
一样,如果樱桃采摘认为它可以自己做樱桃采摘,但会出错,您可以使用--no-commit
来防止这种情况发生。
这是@torek答案的补充
对于@torek答案,使用git merge-file
需要3个文件,他没有提到如何获取这3个文件
git merge-file <target> <base> <changed>
正如 git-merge-file 所记录的那样,git-merge-file
会将更改从<base>
更改为<changed>
到<target>
对于这种情况:<target>
将被src/code.cpp
,<changed>
将被code.cpp
(-X no-rename
文件code.cpp
将被标记为deleted by us
),但我们没有<base>
文件。
为了解决这个问题,我添加以下内容.gitconfig
:
[mergetool "manual"]
cmd = echo "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
然后我跑git merge-tool --tool=manual code.cpp
.当工具询问合并是否成功时,我按 Ctrl-C 打断它。现在我只剩下 4 个文件,"*_备份_*"*_基本_*"*_本地_*"*_远程_*"。<base>
将是"*_BASE_*">
现在我可以正常运行git-merge-file
。