【问题标题】:Find first common child of two commits查找两个提交的第一个公共孩子
【发布时间】:2012-06-10 22:39:04
【问题描述】:
           :
           A
T         / \
i        B   C
m        :   :
e        D   E
          \ /
|          F
V          :

git merge-base B E 允许找到两个提交的共同祖先A。有没有办法找到两个分支再次合并的提交F

【问题讨论】:

  • 这张图很棒,你是怎么做出来的?
  • 如果有多个“第一个”普通孩子怎么办?例如,可能有一个提交是 B 和 E 的合并。
  • 你可以像这个人那样破解git rev-list --children的输出:stackoverflow.com/questions/1761825/…
  • 我使用 vim 手工制作了图表。 @svick 所以可以有多个没有防御顺序的普通孩子。
  • @bara:啊,所以诀窍是使用 vim。

标签: git merge branch git-branch


【解决方案1】:

这个问题不一定有唯一的答案,因此您必须决定一些约束和/或启发式方法,或者接受多个“下游”合并的可能性。问题的核心与多个合并基候选的问题相同——使用git merge-base --all 将它们全部列出,否则 Git 只会选择其算法中最先弹出的那个。我们可以这样做,或者找到所有最佳合并候选者。

你画了我通常喜欢横向渲染的东西,例如:

  B--...--D
 /         \
A           F--G--H   <-- branch1
 \         /
  C--...--E   <-- branch2

但我们可能有这个:

  B--C---D--E--...   <-- branch1
 /    \ /
A      X
 \    / \
  F--G---H--I--...   <-- branch2

在这种情况下,如果您允许同时考虑 branch1branch2,则合并 DH 同样是“分支重新合并的地方”的理想候选者。即使你不这样做,如果branch2 稍后合并回branch1

  B--C---D--E---J--...   <-- branch1
 /    \ /      /
A      X      /
 \    / \    /
  F--G---H--I--...   <-- branch2

那么就从branch1 开始(或结束于)DH 都是同样好的候选者。

无论如何,我们在这里需要的是枚举以您要考虑的一个或所有分支结尾的提交。为此,我们可以使用,例如:

git rev-list --ancestry-path ^B ^E branch1 branch2

这会发现提交是branch1的祖先branch2,也是提交B的后代提交E。 p>

要真正得到正确答案,我们想添加--children。这样,我们将获得每个提交的哈希 ID,以及该提交的子节点。 Git 通过在遍历链接时反转从子节点到父节点的反向连接来实现--children,这已经足够了;但我们不会看到提交BE。这是一个问题。要显示它们,我们可以添加--boundary。这并不理想:--boundary 有时会包含一些我们不想要的提交。幸运的是,它们都标有-,因此我们可以通过剔除那些不是我们关心的提交来排除额外的边界提交。

我不打算展示这些,但如果你这样做了,你现在将有一个列表,每行一个条目,每个节点(顶点)及其连接到其子节点的边。你现在可以问由这些 (V,E) 集合形成的 DAG 的 LCA 是什么?

如果我们可以只使用 Git 的 LCA 算法就好了,但是 Git 没有办法在任意图上调用它——我们只能在提交时调用它,并且实际的提交有父级,而不是子级。所以你必须自己写。请参阅Algorithm to find lowest common ancestor in directed acyclic graph?(不幸的是,没有接受的答案)。 This algorithm looks correct at first blush;它在图中具有 LCA 的两个标准定义之一。

但是,如果我们愿意接受一个不太好的答案,我们可以通过添加 --topo-order 得到在大多数情况下可能就足够的东西(以确保父母在他们所有的孩子之后出来)和--merges(省略所有不是合并提交的内容)。这将获得所有合并的列表。

我在这里做了一个带有简单案例的测试仓库:

$ git log --all --decorate --oneline --graph
* 91fcef6 (HEAD -> master) J
* d1e5905 I
*   5bf18a0 merge
|\  
| * 49b2ba7 (sidebr) D
| * 725e5ea C
| * 36b830d (tag: B) B
* | 198a982 (tag: G) G
* | 216bc01 F
* | e905e59 E
|/  
* 5df9428 initial

所以我现在可以使用BG 命名提交B 和G,而我想要的“朝这个方向移动”的分支就是master。所以:

$ git rev-list --topo-order --merges --ancestry-path ^B ^G master
5bf18a0797dfd78107928a9a4095f357cfabe914

这里的 last 行是“最接近”两个提交的合并。在这种情况下,这也是唯一的行,这就是我们想要的合并。

这里的缺陷一旦我们画出来就很明显了。假设我有一个更复杂的图表,例如:

      I--J
     /    \
    H      M--N
   / \    /    \
  /   K--L      \
 /               \
A                 P--Q  <-- master
 \               /
  \   C--D      /
   \ /    \    /
    B      G--O
     \    /
      E--F

如果我现在运行git rev-list --topo-order --merges --ancestry-path ^B ^H master,我将枚举提交P,然后以某种顺序同时枚举GM。所以 last 行要么是 commit G 要么是 commit M,虽然这两个 都是 合并,但它们不符合正确的标准:它们不不要合并BH。只有提交 P 才会这样做。

因此,要检查您是否有正确的答案(不处理多个 LCA 问题),您应该获取此 git rev-list 命令的每个输出行,可能以相反的顺序(考虑添加 --reverse),然后查看如果两个提交都是每个提交的祖先。像GM 这样的“内部”合并将只有一个提交作为祖先。要进行 is-ancestor 测试,请使用 git merge-base --is-ancestor:

if git merge-base --is-ancestor $commit1 $mergecommit &&
       git merge-base --is-ancestor $commit2 $mergecommit; then
    ... we've found a correct candidate
else
    ... move on to another candidate
fi

【讨论】:

  • 我在这里使用了你的代码:gist.github.com/CMCDragonkai/bfb4529f2384948c45f8b1f97b237f69。但是我发现当使用--reverse 时,只有最后一行成功了。所有先前可能的合并提交都不是两个提交的后代。而且我有 3000 次可能的合并提交。
  • 请注意,这只查找合并提交的共同后代。如果我们想在它们不是合并提交的情况下找到共同的后代,那么它可以对任何 2 个提交任意工作。那么我想我们只是删除--merges 标志?
  • @CMCDragonkai:如果您开始的两个提交本身不在不同的“行”上,我不清楚您所说的“普通孩子”是什么意思。例如,您是在询问提交K-L 的“第一个共同孩子”吗?那将是提交L 本身,因为LK 的后代。如果您希望允许这种事情,只需检查 K 和/或 L 是否是另一对的祖先;如果是这样,无论哪一个不是另一个的祖先,都是共同的孩子。
  • 如果提交 在不同的行上,并且您从廉价的 git rev-list ... --merges 选项中删除了 --merges,您将选择其中一个后代两次提交。例如,给定上图,并提交KI,你会发现提交JI 的子级)或LK 的子级),它们不满足“可以达到两个提交”测试。但是,添加git merge-base --is-ancestor 测试会淘汰它,然后还会淘汰另一个,因此毕竟会找到提交M
  • git rev-list 中省略--merges 的问题在于,当我们将KL 作为输入时,它根本不会列出L。为了修复那个,我们需要避免敲掉有问题的两个提交,并且只敲掉它们的父母。这很容易:使用^{hash}^@;请参阅 gitrevisions 文档。
【解决方案2】:

this answer 改编 all.awk 以同时携带每个 ref 的行号,然后当您遇到父母双方时,请查看他们共同的 refs。

【讨论】:

    【解决方案3】:

    哎呀。没有仔细阅读。

    提交中的唯一信息是其父(或父)的 id。您无法从父提交获取子提交(这是存储库的定向部分,即 DAG)。

    再看一下 - 看起来 git log 的 --ancestry-path 选项可以做到这一点。例如给出:

    * 85d26ab When compiling vim, also compile & install gvim
    *   3146e5d Merge remote-tracking branch 'origin/devel' into deve
    |\
    | * 28d08e5 rebasing-merge: specify all commits explicitly
    * | 006d11d Help 'file' find its magic file
    |/
    * e68531d (tag: Git-1.7.6-preview20110720) Update submodules
    

    我们可以使用这两个提交的所有子节点

    git log --oneline --ancestry-path B..E
    

    如果你把它反过来并选择第一个——那就是 F。

    git rev-list --reverse --ancestry-path 28d08e5..006d11d | head -1
    

    在我的情况下返回 3146e5d。

    【讨论】:

    • 这似乎不太可能工作:--ancestry-path 将输出限制为既是右侧哈希 (006d11d) 的祖先又是左侧哈希 (28d08e5) 的后代的提交,所以由于3146e5d 不是006d11d 的祖先,它不应该被打印出来。确实可以告诉git rev-list --ancestry-path 反转它所遍历的父
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-11
    • 1970-01-01
    • 2013-12-12
    • 1970-01-01
    • 2012-02-09
    相关资源
    最近更新 更多