当重命名文件并使用旧名称创建新文件时,什么工作流程可以缓解 git 合并冲突?

What workflow can mitigate git merge conflicts when a file is renamed and a new file is created with the old name?

问题

  1. 我有根分支 masterfeature 从主分支。
  2. master 有文件 A
  3. feature 已将文件 A 移动到 B 并创建了一个新文件 A
  4. master 中针对文件 A 的工作需要合并到 feature 分支文件 B
  5. master 合并到 feature 会导致合并冲突。 master 文件 A 尝试合并到 feature 文件 A,即使 master Afeature A不再相关。 我如何告诉 git 将 master A 合并到 feature B

重现步数

在控制台中:

mkdir git_rename_demo
cd git_rename_demo

git init

echo "LineB1\nLineB2\nLineB3\nLineB4" > A.txt
git add A.txt
git commit -m "Add A"

git checkout -b rename_A_to_B
git mv A.txt B.txt
echo "LineA1\nLineA2\nLineA3" > A.txt
git add A.txt
git commit -m "Moved Old A to B and Added New A"

git checkout master
echo "LineB5\nLineB5" >> A.txt
git add A.txt
git commit -m "Added More LineBs to A"

git checkout rename_A_to_B
git merge master

场景

我有一个 master 分支和一个 feature 分支。 master 上的文件 A 包含执行 "A" 相关逻辑的代码。

feature分支上,发现文件A作为文件名没有意义,因为代码与"B"逻辑更相关。同时,编写了与 feature 分支中的 "A" 逻辑相关的新代码。为解决此问题,文件 A 已重命名为文件 B,这实际上意味着文件 A 中所有与原始 "B" 相关的逻辑已移至新文件 B。所有新的 "A" 相关逻辑都添加到一个名为 A 的新文件中,有效地替换了旧的 A 文件。

master 分支上继续工作,向仍然存在的 A 文件添加更多 "B" 逻辑。这是 feature 分支上的文件,已重命名为 B.

master 的工作需要合并到 feature 的时候到了,因为功能将继续与 master 分开开发,直到以后。将 master 合并到 feature 会导致上述冲突。我们需要继续允许从事 master 的开发人员针对 A 进行 "B" 相关工作,并能够将 A 文件工作合并到 feature 分支 B 文件,而不必每次都手动解决冲突。

我不确定您是否可以为此找到自动化解决方案。 但是您可以从 git diff 创建补丁并手动更新 git apply 在功能分支中。

TL;DR

没有很好的方法来处理这个问题,但您可以手动完成。见下文。

不要太担心 b运行ch 名称。不要担心提交;合并基于提交,而不是 b运行ch 名称。 B运行ch names 只是帮助你,Git, find 特定的提交。根据定义,b运行ch 名称始终包含 b运行ch 中 last 提交的原始提交哈希 ID。添加新提交包括:

  1. 通过名称检查 b运行ch,以便 Git 知道在步骤 3 中更新哪个 b运行ch 名称。这使提示b运行ch 的提交是 current 提交。特殊名称 HEAD 现在附加到 b运行ch 名称,因此,HEAD select 是 current 提交,这是 b运行ch.

  2. 中的最后一次(或 tip)提交
  3. 以通常的方式进行新的提交。 Git 将创建提交,将其父项设置为当前提交。这个新提交将获得自己唯一的哈希 ID,不同于之前的每次提交,也不同于以后的每次提交。

  4. 此时,在进行了新提交后,Git 将 new 提交的哈希 ID 写入 b运行ch 名称: 附有 HEAD 的那个。所以现在 b运行ch 名称指向 b运行ch 中的 last(提示)提交。那个新的提交指向过去的提示; b运行ch 现在是一个更长的提交时间。

无论何时进行真正的合并(有一些伪合并),都会涉及三个提交:

  • 合并基础,这是其他两个提交的最佳共同祖先;
  • 你的当前提交(或HEAD),通常是b运行ch;1的提示和
  • 在你的命令行中提交你 select:通常,另一个 b运行ch.

Git 自行查找合并基础提交。您只需命名另一个提交即可。如果您想提前查看哪个提交是合并基础,您可以 运行:

git merge-base --all other

其中 other 是您计划提供给 git merge 命令的任何内容,Git 从中选择第三次提交。


1另一种选择是你可能处于detached HEAD状态,其中特殊名称HEAD只是包含提交本身的原始哈希 ID。无论哪种方式,HEAD 命名 当前提交 。当您在 b运行ch 上时,它只是 使用 b运行ch 名称。但通常当你 运行 合并时,你在 b运行ch 上。


简述合并的工作原理,这也是您的问题所在

想象一个简化的提交时间轴,后面的提交向右,我们用单个大写字母替换提交哈希,以便更容易讨论它们。我们可能有:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

此处名称 branch1 找到,或 指向 ,我们当前的提交,其哈希 ID 为 J。名称 branch2 指向提交 L。我们想将 b运行ch branch2 合并到 branch1,这实际上意味着将提交 L 合并到提交 J 以生成新的合并提交 M. Git 自动 找到最好的 shared 提交,即提交 H:最后一次提交 both b运行ches.

为了将我们的更改与他们的更改合并,Git 必须弄清楚我们更改了什么,以及他们更改了什么。由于每个提交都包含所有文件的完整快照,这相对容易:Git 将提交 H 中的快照与我们 HEAD 提交 J 中的快照进行比较,以查看我们 发生了什么变化。而且,Git 还将比较 HL 中的快照,以查看 它们 发生了什么变化。

您可以 运行 这两个 git diff 您自己的命令:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

这就是您的问题所在: --find-renames 在第一个 git diff 中会 找到该文件合并基础中的 A,大概在他们的提交中也称为 A,现在在您的 HEAD 提交中称为 B。所以 diff 会配对 "A in base, B in ours" 并比较内容。这就是 我们 改变的地方:内容改变了,文件的 name 也改变了。

第二个 git diff 会配对 "A in base, A in theirs" 看看他们改变了什么。当合并到 combine 更改时,一切都会起作用。合并后的更改为:

  • 无论我们更改了什么,如果有的话,加上重命名文件;
  • 无论他们改变了什么,如果有的话。

Git 会将这些组合更改应用于提交 H 中的快照,这将重命名文件。

但是对于Git首先要找到重命名,Git一定不能配对提交 H 中的文件 A--ours 提交 (J) 中名为 A 的文件。如果存在名为 A 的文件,Git 会自动假定 它是 "the same" 文件。

当您像这样手动 运行 git diff 时,您可以添加一个额外的选项来告诉 Git 假设。当 git merge 运行s git diff 本身时,它从不提供此选项。

请注意,对于所有其他文件,一切正常。假设在文件 F1 中,它在所有三个提交中都具有相同的名称,您更改了一些前面的行,并且他们更改了一些后面的行。 Git 将 both 更改应用到 HF1 的副本,然后移动到下一个文件。

不过,在某些时候,Git 可能会命中某些文件——可能是 F2——你和他们在其中更改了 相同 行。 Git 现在会停下来让你收拾残局。

Git 给你留下了 四个 个文件,而不只是一个,里面乱七八糟:

  • 在 Git 的 index.
  • 中有来自合并库的文件 F2 的副本
  • Git 的索引中有一份来自您提交的文件 F2 的副本。
  • 在 Git 的索引中有一份文件 F2 的副本。
  • 最后,Git 在工作树中合并这些更改的文件方面做出了最大努力。

工作树文件是您习惯使用的文件。它们就在那里,可供查看、编辑和编译,或者您对文件执行的任何操作。

Git 索引中的副本甚至很难看到,但您可以使用 git showgit checkout 将它们全部取出。 git mergetool 命令执行此操作,将名为 F 的文件提取到 F.BASE、F.LOCAL 和 F.REMOTE ... 然后尝试 运行 上的合并工具这三个文件产生合并文件F,然后删除这三个文件。所以 mergetool 几乎可以,但是它做的太多了。

您可以求助于 git show 方法:

git show :1:F > F.LOCAL    # :1: means the merge base version
git show :2:F > F.OURS     # :2: means our version, like `git checkout --ours`
git show :3:F > F.THEIRS   # :3: means their version, like `git checkout --theirs`

但在这种情况下,Git 正在合并 错误的文件 ,所以这并没有直接帮助。不过,知道这一点很重要。

以上是关于哪里出了问题。这是您可以做的

你运行:

git merge other

Git 找到了三个提交:基础、HEAD 和其他。它 运行 两个 git diff,从基地到 HEAD 看你做了什么,从基地到其他人看他们做了什么。

这两个差异中的一个应该找到重命名,但没有。然后它尝试将来自基础的 A 与来自本地/--ours 提交的 A 和来自其他/--theirs/远程提交的 A 合并。

您可以提取 A 的三个版本,然后将它们合并 "by hand",有点像 git mergetool 所做的。但这里真正的技巧是 根本不需要 A.LOCAL,你想要 B.LOCAL 或 B.OURS 取决于你喜欢怎么称呼它.

已经 B.LOCAL。它只是被称为B。所以你想要的是:

  • 提取文件 A 的合并基础版本:使用 git show :1:A > A.base
  • 提取他们的文件版本A:使用git show :3:A > A.other
  • 合并这三个文件,将合并结果写入文件B.

好的,前两个要点很简单。但是现在我们还是要合并三个文件。好吧,如果你有喜欢的合并工具,你可以直接使用它!如果没有,我们可以让 Git 执行它,与 Git 自动执行的方式相同,但我们控制输入文件。我们只要使用命令:

git merge-file B A.base A.other

将我们现有的 B 与他们的 AA.other 合并。像往常一样可能存在合并冲突;它们现在将以通常的方式修复。

当然 Git 弄乱了我们的文件 A,但我们也可以解决这个问题:

git checkout --ours A

从索引中提取 :2:A 到工作树中,然后是:

git add A

删除三个 :1::2::3: 插槽条目,并将 A 的签出索引版本 2 作为到-提交文件 A.

如果您必须经常这样做,您可以编写其中的一部分(除了解决任何实际冲突)。