使Git在文件移动/重命名之间应用合并

时间:2018-09-21 07:22:02

标签: git git-merge

我确定我在这里做错了,但是我不确定是什么。

我有一个master和一个branch,它们最终将被合并回去,但目前两者都在进行开发。

这意味着我定期将master的最新更改合并到branch

问题在于branch中包含许多文件移动和重命名。

我当前的流程是:

  • branch
    • my-control.html重命名为my-control.js
    • 进行更改并提交-Git认为这是move而不是delete+add
    • 更新my-control.js
    • 提交my-control.js更改。
    • my-control.js现在有了my-control.html
    • 的新更改和历史记录
  • master
    • 更改为my-control.html
    • 提交更改
  • 返回branch
    • 合并来自master的更改

这就是问题发生的地方-有时我得到了我期望的my-control.js的更改,但大约有一半的时间我刚回到my-control.htmlbranch

发生这种情况时,my-control.js拥有所有历史记录,my-control.html拥有所有历史记录,再加上master的1或2次提交。

  • 我在做什么错了?
  • 为什么有时会发生这种情况并且有时会起作用?
  • 该如何解决?
  • 有没有办法告诉Git“不,这些更改应适用于此文件”?

1 个答案:

答案 0 :(得分:1)

背景:文件身份

这实际上归结为我所说的文件身份,这是一个困难的问题-不仅在Git中,总体上也很困难:请参见the Wikipedia article on the philosophical issue。但是,Git使其特别棘手,因为:

  

发生这种情况时,my-control.js拥有所有历史记录,而my-control.html拥有所有历史记录以及主服务器的1或2次提交。

Git没有文件历史记录。 Git只有提交历史。更准确地说,提交的历史记录和文件与此无关。提交包含文件,但这并不能以任何方式控制历史记录:提交就是历史记录。

例如,我对Missing deletion of lines in file history (git)的回答对此有更多了解。如果您要求Git在重命名中使用--follow一个文件,则Git会使用其历史记录简化来仅显示触及该命名文件的提交-并且当其中一个“触动”为“ Git检测到重命名”时,Git开始寻找此时新名称,并停止寻找旧名称。 (或者,由于Git倒退了,最好说它开始寻找旧名称,而不再寻找新名称。)

由于合并的一个分支可能具有“错误的”名称,因此该技术显然会在合并时失败。但是,无论如何,简化历史记录通常仅会影响合并的一小部分!

如果您不使用--follow,而是使用git log -- path(s)或等效名称,则Git根本不会理会重命名:它只是使用给定的一个或多个路径来简化历史记录。

一个稍微折磨的类比

  

我在做什么错了?

一无所有,也许一无所有。问题在于,Git有时可以(有时不能)识别出一个名叫Bob的文件和另一个名叫Robert的文件是同一个人。它可以或不能正确地识别文件对。鲍勃和罗伯特是同一个人吗?

  

为什么有时会发生这种情况并且有时会起作用?

至少有一个可靠的答案:Git 可以识别两个文件,如果它们足够相似,并且其他条件也成立。也就是说,您向Git显示了两个快照,其中包含一些文件(“人员”),并猜测谁是谁以及谁来回移动。如果在上一张照片中只有一个带有标签“ Bob”的文件,在下一张照片中只有一个带有标签“ Robert”的文件,则Git 可能能够检测到他们是同一个人,只要他没有失去四肢或获得额外的头部等。但是,如果两个图片都带有“ Bob”和“ Robert”两个名字标签的人,则Git将假定两个“ Bob”是同一个人,并且两个“ Robert”是同一个人,并且鲍勃从来都不是后来的罗伯特,反之亦然。

技术:git merge,提交图和git diff --find-renames

让我们看看git merge的工作原理。要到达那里,我们必须从两件事开始:提交图git diff --find-renames

提交图是合并的最重要的关键。如果是普通提交,则每次提交都记录其 parent 提交的原始哈希ID,或者如果是 merge commit ,则记录两者(或全部为 1 )的父母。通常,合并只有两个父级。让我们绘制一些提交图作为示例,并挑选出一些具体的提交来讨论。与其使用完整的,丑陋的哈希ID,不如使用大写字母来指定特定的提交(对于不太感兴趣的提交,则使用圆点)。我们将拥有分支branchmain,它们在提交B时分开,但过去至少合并了一次:

          o--o---D--o--o--E   <-- branch
         /        \
...--o--B--o----C--M--o--o--F   <-- main

当我们在提交M进行合并(用于合并)时,将branch合并到main中时,合并基础很明显:最后一个共享提交为B。提交B过去和现在都在两个分支上。因此,Git进行合并的方式是这样的:

git diff --find-renames <hash-of-B> <hash-of-C>   # what we did, on main
git diff --find-renames <hash-of-B> <hash-of-D>   # what they did, on branch

Git然后合并两组更改,将 combined 更改应用于保存在B中的快照,并进行合并提交M

因为M合并提交,所以它会记住两者 C D。当Git回顾历史(记住,由提交组成)时,只要它从M向后移动,就必须拜访双方父母。

我们现在将运行git checkout main; git merge branch。也就是说,我们将选择提交F作为当前提交,并要求Git将提交E合并到F中。 Git现在必须找到合并基础:两个分支上的最后一次提交。

您能猜出哪个提交是合并基础吗?这次不是B

查找合并基础完全是关于 reachability 的问题,我将把更完整的讨论外包给Think Like (a) Git,但答案是从F回到通过M,我们可以到达D,从E返回,我们可以直接沿着顶行到达D。因此,D是这次的合并基础。 Git再次运行两个git diff命令:

git diff --find-renames <hash-of-D> <hash-of-F>   # what we did on main
git diff --find-renames <hash-of-D> <hash-of-E>   # what they did on branch

每个diff都具有左侧的D提交和右侧的特定分支的 tip 提交。 Git会找到两组更改,包括检测重命名。因此,如果在基础提交和提示提交中有一些名称不同的文件,那么Git决定该 是另一个名称下的相同文件,例如,左照片中的Bob变成了Robert in正确的文件-然后Git将声明文件已重命名。

Git现在将使用基础(D)快照作为应用更改的基础来组合两组更改。如果更改包括“重命名文件”,Git也会进行重命名。如果文件的底部标记为Bob,而两个标记都标记为Robert,则两个diff都具有 same 重命名,一切都很好。如果仅一次更改就重命名了文件,则您获得的名称取决于合并时所处的分支:我们将Bob重命名为Robert,还是他们这样做吗?

如果Git 无法检测到重命名,那么事情真的很糟糕。如果Bob失去了一条手臂,而Git却不认识其中一张照片中标记为Robert的那个人是同一个人怎么办?


1 具有三个或更多父级的合并在Git中称为章鱼合并。 Linux具有一种66向合并,其中Linus Torvalds指出:that's not an octopus, that's a Cthulhu merge


您能做什么:相似度索引

  

该如何解决?

到目前为止,最简单的方法是避免重命名。 Git相信文件上的标签(路径名)是第一位的。如果基本提交和两个提示都具有名为bob.txt的文件,为什么,那一定是同一个人,鲍勃。没有什么可困惑的。

不过,重命名已经发生。解决该问题的一种方法是安排所有 future 合并使用新名称:如果文件应命名为robert,请确保每个 future 合并基础和 future 分支提示调用文件robert,不会造成混乱。

如果这不可能,那么自动化还有最后的希望:提供更多(或不同的)信息。实际上,使Git变得更聪明:告诉Git,即使Bob和Robert失去了四肢,它也应该与Bob和Robert匹配。

git diffgit merge中,Git在此处具有的标志是不同的,但是两者都使用相同的想法来设置相似性索引。当Git比较两个快照(两次提交)时,如果左侧缺少某些文件,而右侧显示了一些新文件,则Git会比较这些文件的 content

使用git diff --find-renames(或更短的git diff -M),您可以添加相似性索引阈值:

git diff -M10

例如。 M(或--find-renames=)之后的数字是将两个文件视为“同一”文件的最低要求相似度索引,即,由Git决定哪个船是(或曾经是)These修斯之船,或者一个戴着鲍伯名牌的人和一个戴着罗伯特名牌的人是同一个人。

Git对两个文件相似性的内部计算不会改变,但是Git的阈值,它声明这两个不同的文件实际上相同文件,做。降低阈值使Git非常高兴地识别名称不同的文件。提高它会使Git更加不情愿。

默认相似性阈值为50%-M50。逐字节完全相同的文件是100%匹配的。其他人不太相似/更不相似。实际公式是我对Trying to understand `git diff` and `git mv` rename detection mechanism的回答,但通常,找到可用数字的方法是在合并基础和两个分支技巧上使用git diff。将阈值设置得很低,运行git diff,Git会告诉您匹配的文件以及它们的实际相似之处。

(要查找合并基础,请运行git merge-base --all commit1 commit2,其中两个提交标识符将分支提示提交命名。您可以在此处使用分支名称,原始哈希ID或根据{{3 }}。然后,您将获得基础的哈希ID,您可以将其用作git diff的参数之一。)

您可以使用git merge-X find-renames=number提供相同的阈值。您可以使用一个非常小的数字,但这可能会发现太多重命名。要找出Git将认为重命名的内容,请使用git diff

如果其他所有方法都失败

如果以上条件都不足够(可能发生 ),那么您不是完全 不能选择:

  

有没有办法告诉Git“不,这些更改应适用于此文件”?

有一种完全手动的方法来进行文件合并,即:

  • 使用--no-commit告诉Git Git不应认为合并成功,从而开始合并。

  • 使用任何更简单的方法来解决所有问题。

  • 如果Git包含错误标识的文件,请从一些已知的或手动选择的合并基础提交中提取文件的合并基础版本。如果不是,它已经在Stage-1插槽的 index 中,因此您可以从那里提取它。无论哪种方式,都可以使用您可以使用的名称将文件提取到工作树中。例如:

    git show $hash:$basepath > file.base
    

    同样,将文件的“我们的”和“他们的”版本提取到工作树中:

    git show HEAD:file > file.ours
    git show MERGE_HEAD:$theirpath > file.theirs
    

    现在您已经拥有文件的所有三个版本,请使用the gitrevisions documentation对文件执行三向合并。在工作树中获得正确的合并结果后,将其放入正确的名称下,然后使用git add将其复制到索引中,以准备提交。确保从索引中删除任何错误的(--theirs)版本-git status会告诉您有关此类文件的信息(如果存在)。

合并完成后,使用git commit(或在新版本的Git中,git merge --continue)完成合并。

这-手动选择三个文件并在它们上使用某种合并程序-是我们在Git出现之前的糟糕日子里所做的。欢迎来到1990年代! :-)