1

我有一个重新定位的分支(1 次提交),其中一系列已移动和更新的文件显示为删除 + 添加。

有没有办法追溯解决这个问题?

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

4

1 回答 1

1

TL;博士

摆弄重命名检测的策略选项参数。根据您的 Git 年份,这可以是或. 用于确定适当的阈值;这是or参数。-X find-renames=threshold-X rename-threshold=thresholdgit diffgit diff-M--find-renames

请记住,樱桃挑选是作为合并实现的,合并基础是被挑选的提交的父级,--ours提交是HEAD提交(像往常一样),--theirs提交是你挑选的提交。

Git 从不将任何内容记录为重命名操作。如果你重命名一个文件并提交,Git 只会记录一个新的快照。

例如,考虑一下典型的Spot the Difference 谜题。给您两张图片,并要求您找出不同之处。如果左边的图片是“之前”,右边的图片是“之后”,而椅子不见了,你会说“椅子被移走”。如果另一把椅子出现在不同的位置,您可能会说“一把椅子被移除,另一把椅子被添加”。但是如果两把椅子看起来一样呢?

你可以说:椅子 A 被移除,椅子 B 被添加,就像你在两把椅子看起来非常不同时所做的那样。或者,您可以说椅子 A 已移至位置B! (但真的是这样吗?也许椅子 A 被移除了,并且添加了不同的椅子 B,你就是无法区分。这里有一些更深层次的哲学问题,正如我们即将看到的那样。)

无论如何,Git 的快照就像图片一样。它们不包含任何动作,永远!这取决于比较快照的人,即使那个人是 Git 本身。你告诉 Git:对我来说,比较快照 A 和快照 B。如果文件从 A 中的一个名称中丢失, Git 将报告文件已移动,并且完全相同的内容出现在 B 中的另一个名称下,已经告诉Git:“检查东西,看看它们是否也移动了。”

这是你的 basic git diff <commit-L> <commit-R>,使用-Mor--find-renames选项启用重命名查找。(这里的 L 代表左侧,R 代表右侧。)如果文件 100% 相同,Git 会找到这样的重命名。但如果他们不是——如果椅子移动了,但在途中出现了一些划痕怎么办?

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

然后,对于每个这样的文件,Git 将所有 L 文件与所有 R 文件进行比较。(正如您可能猜到的,这是相当计算密集型的。这里有一堆内部优化,包括首先快速检查 100% 相同,这对于 Git 内部的原因来说非常容易。)Git 计算一个每个配对的相似度指数。如果相似度指数超过你选择的阈值——如果你没有选择一个阈值,则超过 50%——Git 认为这个配对是候选。Git 会选择最好的一对,也就是相似度得分最高的一对。

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

我称之为确定文件身份的过程。从哲学上讲,这是 Git对忒修斯之船 问题的回答,或者更通俗地说,是祖父的斧头悖论。“这是我祖父的斧头,我父亲换了柄,我换了头,但还是原来的斧头!” 两个文件一旦被识别为同一个文件。

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

就是这样git diff;怎么样git merge?(为什么git merge当我在摘樱桃的时候!)

我们稍后会解释原因,但现在让我们谈谈git merge。当我们使用 Git 时,我们习惯于git merge两个不同的开发线(通常是两个不同的分支)中完成的更改组合在一起,通常是由两个不同的人完成的。为了组合这些变化,Git 必须首先找到工作分歧的点。 这一点是合并基础,由于 Git 是关于提交的,这相当于在两条工作线之间找到共同的提交。

当我们将其绘制为提交的图片时,这一切都很有意义。每个提交都会记住它的提交——在这个特定提交之前的提交——所以我们可以从左到右绘制提交,左边是旧的提交,右边是新的提交,如下所示:

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

假设 Alice 和 Bob 都从一个共同的源存储库开始——git clone例如,它们都在同一个 Git 存储库上运行——因此他们有一些以最近一次提交结尾的提交master

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

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

现在 Alice 做了一些工作并做出一两次新的提交。她的提交获得了新的、唯一的哈希 ID,其他任何人都不会在其他任何地方使用这些 ID:

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

与此同时,Bob 做了一些工作并进行了一两次新的提交,他的提交获得了新的、唯一的哈希 ID,其他任何人都不会在其他任何地方使用这些 ID:

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

一旦我们以某种方式将所有提交集中到一个公共存储库中,我们就有两个分支,Alice 的 master 和 Bob 的 master,它们有一个共同的起始提交,原始的master

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

无论我们是 Alice、Bob 还是某个第三人称 Carol,只要我们有commits ,我们都可以做到这一点。提交是最重要的!这些名称——我在这里使用alice/masterbob/master定位提交J——L只是为了帮助我们找到提交。

现在很明显 Alice 和 Bob 都是从 commit 开始的H,所以现在很容易看出Git如何将 Alice 的工作与 Bob 的工作合并:Git 只需要比较git diff——即——commit来查看 Alice 做了什么,H然后比较看看鲍勃做了什么。所以 Git 这样做:JHL

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 不需要查看任何中间提交?这尤其重要,因为在某些情况下,它可能有助于进行重命名检测。不过,Git 不会这样做。)

无论如何,Git 现在组合了这两组更改,将组合的一组更改应用到合并库中的快照。结果,如果一切顺利,将作为一个新的合并提交提交,该提交在我们当前的提交之后——这两个分支中的任何一个都HEAD附加到它上面。1

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


1我们没有画HEAD进去,所以我们要添加alice/masteror bob/master?在 Git 自己进行提交之前,这并不重要!嗯,这并不完全正确。重命名冲突的情况很重要:如果 AliceBob 都重命名了某个特定文件,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提交。上面的CommitC是我们想要挑选的那个(C 代表 Cherry),并且P是它的父级。(我知道P它可以代表 Pick,但我需要一个字母来代表 Parent,所以 P 代表 Parent,C 代表 Cherry。)大多数其他提交都是无趣的——我们从不需要它们的哈希 ID,所以我们只是将它们显示为o. 我标记了一个*,因为它是明显的合并基础,但实际上 Git 也不会使用它!

Git 现在要做的是运行一个合并,就像我们运行 一样git merge,只是它没有找到合并基础,即 commit *,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取决于存储在提交、和中的文件的相似性索引值。Git必须检测和之间的重命名,以便将特定文件识别为同一个文件,否则它将不知道如何组合对该文件的更改。PCLPL

于 2018-09-06T17:48:52.737 回答