我可以追溯性地修复应该“移动并更新”的“删除添加”吗?

时间:2018-09-06 15:54:59

标签: git

我有一个基于基础的分支(1个提交),在该分支中,一系列已移动和更新的文件显示为delete + add。

是否可以追溯解决此问题?

我想知道如何创建一个新分支,从现有分支中挑选更改(但尚未提交)。在这一点上,我可以强制git将(删除+添加)视为move + update。这样的事情可能吗?

1 个答案:

答案 0 :(得分:1)

TL; DR

使用strategy选项参数进行重命名检测。根据您的Git年份,此值为-X find-renames=threshold-X rename-threshold=threshold。使用git diff确定合适的阈值;在git diff中,这是-M--find-renames参数。

请记住,樱桃拣选是通过合并实现的,合并基础是被樱桃拣选的提交的父级,--ours提交是HEAD的提交(通常), --theirs commit是您正在挑选的提交。

Git从未记录任何东西作为重命名操作。如果重命名文件并提交,Git只会记录一个新快照。

例如,考虑典型的Spot the Difference puzzle。系统会为您提供两张图片,并要求您找出不同之处。如果左侧图片为“之前”,右侧图片为“之后”,并且椅子不见了,您将说“椅子被移开”。如果另一把椅子出现在不同的位置,您可能会说“一把椅子被移走,另一把椅子被添加”。但是,如果两把椅子看上去相同?

怎么办?

您可能会说:删除椅子A,添加椅子B,就像当两把椅子看起来非常不同时一样。或者,您可以说主席A已经移动了 B位置!(但是真的吗?也许主席A被删除了,另外一个主席B被添加了,您只是无法分辨出两者之间的差异。正如我们即将看到的,这里还有一些更深层次的哲学问题。)

无论如何,Git的快照就像图片一样。它们从未包含任何运动!这取决于某人比较快照,即使该人本身就是Git。您告诉Git:比较快照A和快照B。如果A中的一个名称丢失了文件,Git会将文件报告为 moved 。内容在B,中以另一个名称出现,您已经告诉Git:“检查事物是否也已移动。”

这是您的基本git diff <commit-L> <commit-R>,其中使用-M--find-renames选项启用了重命名查找。 (这里L代表左侧,R代表右侧。)如果文件100%相同,Git将找到这样的重命名。但是,如果不是这样的话,那把椅子移动了却又刮伤了些什么呢?

如果Git符合最佳匹配标准,它将认为“移动文件”与某些原始文件“相同” 。本质上,Git首先找到所有似乎从提交L中消失的文件,以及所有所有似乎在提交R中创建的新文件。它将所有这些名称放入重命名候选队列

然后,对于每个这样的文件,Git会将所有L文件与所有R文件进行比较。 (您可能会猜到,这是相当大量的计算工作。这里有很多内部优化,包括首先对100%相同的对象进行快速检查,出于内部到Git的原因,这很容易。)Git计算a每个配对的相似性索引。如果相似性指数超过您选择的阈值,或者如果您未选择相似性阈值,则为50%-Git认为此配对为候选。 Git选择 best 这样的对,即相似度得分最高的那对。

找到最佳配对后,这两个文件将从重命名候选队列中删除。现在,这两个文件被标识为同一文件,或者在我们的椅子类比中,在左侧和右侧的图片中被标识为“同一把椅子”,在此过程中它们只是被移动并且可能会被划伤

我将其称为确定文件身份的过程。从哲学上讲,这是Git对the Ship of Theseus或更确切地说Grandfather's Axe paradox问题的解答。 “这是我祖父的斧头。父亲代替了手柄,我换了头,但仍然是同一根斧头!”一旦被确定为两个文件,它们就是相同文件。

为了速度起见,Git默认将提交L和R中的任何两个文件具有完全相同的名称配对为“相同”。使用git diff,您可以选择断开配对,以防万一。这样会将更多文件名放入重命名检测队列中,从而花费更长的时间。

关于git diff的全部内容; git merge呢? (还有为什么git merge在我挑樱桃的时候!)

稍后我们将介绍原因,但现在让我们谈谈git merge。当我们使用Git时,我们使用git merge组合更改,这些更改是跨两个不同的开发线(通常是两个不同的分支)完成的,通常由两个不同的人完成。为了合并这些更改,Git必须首先找到工作分开的点。这点是合并基础,并且由于Git完全是关于提交的,因此在两行之间找到 common commit

当我们将其绘制为提交图片时,这一切都非常有意义。每个提交都会记住它的 parent 提交(该提交恰好在此特定提交之前),因此我们可以从左到右绘制提交,左侧是较旧的提交,右侧是较新的提交,如下所示: / p>

...  <-o  <-o  <-o  ...

假设Alice和Bob都从一个共同的源代码仓库开始(例如,两者都在同一个Git资源库上运行git clone),以便它们有一些以最近结尾的提交>在master上提交:

...--F--G--H   <-- master

名称master包含某个提交H的实际哈希ID,Git将其称为分支的 tip

现在,爱丽丝做了一些工作,然后进行一两次新的提交。她的提交会获得新的,唯一的哈希ID,其他任何人都将永远不会使用它:

             I--J   <-- master (Alice's)
            /
...--F--G--H   <-- origin/master

与此同时,鲍勃做了一些工作并进行了一次或两次新提交,他的提交获得了新的唯一哈希ID,这些哈希ID不会被其他任何人使用:

             I--J   <-- [Alice's master]
            /
...--F--G--H   <-- origin/master
            \
             K--L   <-- master (Bob's)

一旦我们以某种方式将所有提交全部存储到一个公共存储库中,我们就会有两个分支,即Alice的管理员和Bob的管理员,并带有常见的开始commit < / em>,原始的master

             I--J   <-- alice/master
            /
...--F--G--H
            \
             K--L   <-- bob/master

只要我们有 commits ,无论我们是爱丽丝(Alice),鲍勃(Bob)还是第三人称卡罗尔(Carol),我们都可以这样做。提交很重要! 名称(这里我使用alice/masterbob/master来定位提交JL的位置只是为了帮助我们找到提交

现在非常明显的是,爱丽丝和鲍勃都从提交H开始,所以现在很容易看到 Git将爱丽丝的工作与鲍勃的工作合并:Git只需要< em> compare (即git diff)对H提交J以查看爱丽丝做了什么,并将HL进行比较以查看做什么鲍勃做到了。因此,Git做到了:

git diff --find-renames <hash-of-H> <hash-of-J>   # what Alice changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what Bob changed

请注意此处的--find-renames选项,该选项使用默认的“ 50%相似”度量来查找在Alice或Bob工作时重命名的所有文件。 (值得思考:为什么Git不需要查看任何 intermediate 提交?这一点尤其重要,因为在某些情况下它可能有助于重命名检测。Git不会这样做不过)

无论如何,Git现在可以组合这两组更改,将合并的一组更改应用于来自合并基础的快照。如果一切顺利,结果将作为新的 merge commit 提交,该新提交在我们当前的提交之后进行,这两个分支中的任何一个都附加了HEAD 1 < / sup>

运行git merge时,可以给Git一个-X rename-threshold参数,就像可以给git diff这样一个参数一样。合并只是将相同的数字传递给diff,以控制重命名检测器在确定文件身份时应严格或宽松。


1 我们没有插入HEAD,所以我们要添加到alice/masterbob/master中吗?直到Git自己进行提交,这才真正重要!好吧,那不是真的。在重命名冲突的情况下这很重要:如果Alice Bob都重命名了某个特定文件,那么Git应该使用哪个名称?默认情况下,它将使用HEAD提交中的任何名称。在更典型的合并冲突的情况下,它还会影响工作树文件的标记方式。


樱桃采摘(最终!)

使用git cherry-pick时,Git认为这是一种有趣的合并。让我们再次绘制一些提交链,看看它是如何工作的:

...--o--*--o--P--C--o--o   <-- branch-X
         \
          o--o--L   <-- branch-Y (HEAD)

此处的名称HEAD附加在branch-Y上,表示L是我们现在已签出的提交。该提交是--ours提交。上面提交的C是我们要挑选的对象(C代表Cherry),P是它的父对象。 (我知道P可以代表Pick,但是我需要给Parent一封信,所以P代表Parent,C代表Cherry。)其他大多数提交都没有兴趣-我们永远不需要它们的哈希ID,所以我们只是将其显示为o。我标记了一个*,因为它显然是合并基础,但实际上Git也不会使用它!

Git现在要做的是运行合并,就像我们运行了git merge一样,除了不是寻找合并基础(它将是提交*), Git只是使用P作为合并基础。 Git现在运行:

git diff --find-renames <hash-of-P> <hash-of-L>

看看我们 做了什么更改— Git将尝试保留这些更改!—然后:

git diff --find-renames <hash-of-P> <hash-of-C>

看看他们发生了什么变化,一次是我们在挑选樱桃。

Git现在将这些更改(就像任何合并一样)与潜在的合并冲突结合在一起。如您所见,--find-renames取决于提交PCL中存储的文件的相似性索引值。 Git 必须检测PL之间的重命名,以便将特定文件标识为相同文件,否则它将不知道如何将更改合并到该文件。