Git 像单个文件一样合并基础

Git merge-base like for a single file

有没有命令可以在两个分支中找到一个文件的共同祖先?

假设有一个文件在两个分支中被独立修改。我想找到两个分支通用的该文件的最新版本。我相信这归结为在两个分支中找到文件的单父提交。

但是,merge-base 只允许为提交查找父提交,而不是文件。我试图指定最后两次提交修改各自分支中的文件,但我得到的父提交不在任何一个分支中该文件的更改历史记录中,这可能是由于提交通常包含对多个文件的更改一个文件。

Is there any command to find a common ancestor of a file in two branches?

不,或者是,或者也许:这取决于你的意思。

Say there is a file that was modified independently in two branches. I want to find the last version of that file common to both branches. I believe this boils down to finding the single parent commit for the file in both branches.

文件没有父提交。只有提交有父提交。

更糟糕的是,每次提交都会存储每个文件(即提交时属于暂存区的每个文件)。因此,从某种意义上说,这要么是每次提交,要么是常规的普通合并基础。显然这不是你的意思,所以让我们看看在这里还能说些什么。

让我们来做一个思想实验。假设你有两个分支提示 br1br2 最终有一个共同的祖先提交:

       o--o--o--Y   <-- br1
      /
...--X
      \
       o--o--o--Z   <-- br2

再考虑一个更复杂的图,它仍然有一个共同的祖先和两个分支提示:

         o
        / \
       o   o--o--Y   <-- br1
      / \ /
...--X   o
      \
       o--o--o--Z   <-- br2

鉴于图表的方式和 git merge 的工作方式,"regular" 合并(或使用 git merge-base)将找到合并基础 X,其中我认为大多数人会同意 X 中的某些文件传播(可能重命名)到 Y,也传播到 Z,在 [=18] 中有一个共同的祖先=].这个共同的祖先可能出现在 YZ 中的不同路径名下(甚至在 Y Z 中)但是它仍然是共同的祖先,因此它被用作合并基础版本。

这里有一个问题:git 不记录重命名。相反,它 "discovers" 它们每次都会产生差异。为了发现 X 中的文件 generic/b.c 现在是 Y 中的 specific/b.c,git 必须将 X 下的整个树与Y 下的整棵树。这意味着它必须找到提交 X.

这对于常规合并来说并不难,因为它使用提交图:它从提交 YZ 开始,并向后遍历历史以找到最近的公共提交(这里当然是X)。一旦我们知道(或 git 知道)使用 X,它就会产生两个差异,X-vs-YX-vs-Z,然后它可以将更改合并到公共文件的 contents,而不管它在 [=20] 中的 path 是什么=] 和 Z.

(交叉合并还有一个次要问题,其中可能有多个最近的共同提交,但我们现在可以忽略它。)

如果我们(至少暂时)放弃寻找重命名的想法,我们可以,给定一些路径 p,使用不同的方法,我think 你问的是:

  • 对于 XY 之间的每个提交 cy(包括 X 并从 Y),以及 XZ 之间的每个提交 cz(同样从 Z 向后工作),比较 <em>cy</em>/<em>p</em><em>cz</em>/<em>p</em>.
  • 当这两个路径的内容相等时,声明提交相等。

请注意,这会将 X 的路径版本 pX 的版本(当然是相同),并且还针对任一提交链中的每个版本,同时还将每个版本与其他版本进行比较。

制作完这个完整的矩阵(我们稍后可以优化),我们现在可以找到许多 "interesting" 提交:

  1. 最后一次提交 cyX-to-Y 链中 p X 中的内容相同(这是该链中最新的提交,p 未更改)
  2. 最后一次提交 czX-to-Z 链中 p X 中的内容相同(另一个链中最新的未更改)
  3. 最早的 cy 其中 p 与提交中的内容相同 Y(这是最后一次在 X-to-Y 链中修改路径 p
  4. 最早的 cz 其中 p 与提交中的内容相同 Z
  5. 任一链中的任何提交与 p 的内容与另一链中的任何提交相同。

我想您可能正在考虑在这里找到第 1 项和第 2 项。不过,还不清楚 为什么 。如果您只关心存储在路径 p 下的内容,我们已经确定(上文)这两个提交在 [= 下存储相同的内容44=] 正如您在 X 中找到的那样。所以 X:<em>p</em> 是 "just as good" 识别这些内容,你不妨使用 commit X.

如果你说的是寻找项目 3 和 4,那么又不是很清楚 为什么 ,因为我们已经确定它们与 [=181= 的内容相同]p 作为他们的最高提交,所以 Y:<em>p</em>Z: <em>p</em> 对于识别这些内容同样有用。

但也许您正在处理第 5 项:在路径 p 下的内容相同的两条链上提交(因为另一个提交在其他链),但不一定与最尖端提交中的内容相同。

可以有很多这样的对。例如,假设在 Xgit merge-base 找到的绝对共同祖先)中,路径 p 有五行。然后,在朝着 Y 前进的过程中,该路径中的第一个提交删除了最后一行。同时在 X-to-Z 序列中,几次提交保留所有 5 行,然后删除最后一行。现在这个版本的 p 在两条开发线中都是一样的,直到下一次提交修改 p。假设在 X-to-Z 序列中删除了另一行。然后在 X-to-Y 序列中,同一行被删除;然后,两次提交都删除了更多行,直到最终文件在一个或两个分支提示处完全为空。

定义"nearest"还有另一个问题。我们再看一下比较复杂的X-to-Y的图片段,但是放几个比较有区别的字母:

         R
        / \
       P   T--o--Y   <-- br1
      / \ /
...--X   S

假设路径 p 在提交 RS 中具有相同的内容,但在 P 中不同和 T。两者与 XY 的图形距离相同。只要你关心路径p,这可能是无关紧要的,但它确实表明不一定存在唯一的提交.


在我深入了解您想要使用的几个命令之前,为了解决您要解决的问题,我说了很多废话。

使您更接近解决方案的命令(甚至可能一直到那里,具体取决于您想要什么,尽管您似乎可能需要使用其他命令,有些甚至不需要 git 命令)是 git rev-list。这可以找到修改了特定路径的提交(与那些提交的父提交相比;请注意,通常必须特别处理合并,因为它们有多个父提交)。如果您 do 使用一个或多个路径来限制 git rev-list 列出的修订,请注意它将执行 "history simplification" 以便从其输出中省略一些提交。根据您希望如何处理 DAG 级分支(例如更复杂的 X-to-Y 链中的分支),这可能正是您想要的。

基本上,git rev-list X..Y -- path 将找到可从 Y 访问的提交,不包括可从 X 访问的修改 path 的提交,其中 "modify" 表示 "a diff against the parent shows a change to that path"。 (关于它如何处理合并,请参阅文档。)列出提交的顺序取决于您选择的排序(有或没有拓扑约束;请参阅 "Commit Ordering" 部分)。

如果用 X..Z 重复此操作,您可以找到哪些提交修改了那里的路径。

这两个 git rev-list 基本上是从 X 到两个分支提示的整个修订链,但是因为它们允许您将它们的输出限制为 "commits that modify some path(s)",所以它们可以优化我在思想实验中概述的过程。

您可能希望在此处包括提交 X。默认情况下,rev-list 不会:您可以更早地开始一个提交(在 X 的父级),但是如果 X 本身是一个合并,这可能会失败;或者您可以使用 --boundary,它指示 rev-list 包含提交 X 的 SHA-1(以 - 为前缀)。

查看某个路径下存储的内容在两次不同的commit中是否相同——显然这里两次使用相同的commit ID内容是相同的,但仍然有效——你可以比较存储的 blob 的 SHA-1 ID:

path=dir/file
...
rev_a=...   # something from git rev-list, for instance
rev_b=...
if [ $(git rev-parse ${rev_a}:${path}) = $(git rev-parse ${rev_b}:${path} ]; then
    ... the contents match ...
else
    ... the contents differ (at least slightly) ...
fi
其中

None 将检测重命名;为此,您必须使用成熟的 git diff(打开重命名检测)。