【问题标题】:Git log ^ algorithmGit日志^算法
【发布时间】:2026-01-26 22:50:01
【问题描述】:

我正在使用“git log ^”命令来确保在两个分支之间(比如说当前版本和上一个版本)在上一个版本中没有提交不是当前版本的一部分(我们使用提交消息中的记录号,作为比较的基础)。

我基本上想知道命令背后的算法是什么。有谁知道它是如何工作的?
我想你可以得到两个分支的所有提交并一一比较。但是,如果历史很长,这可能是一个漫长的过程。
但我认为有一种更聪明的方法可以做到这一点。想法是找到共同的祖先,但不确定这是否可行以及如何完成。

如果有人能启发我,那就太好了:)

【问题讨论】:

  • Git 历史被表示为直接图,因此您可以从一个分支进行呼吸优先搜索/深度优先搜索,看看它是否遇到另一个分支的头部。如果是这样,它也必须包含其他分支中的所有提交。
  • git log not 是指git log br1 ^br2 吗? (也就是说,前缀帽子或插入符号^ 是“不”。)或者您的意思是git log br1 --not br2? (这是表达几乎相同事物的不同语法。)但是没有-- 或拼写为^ 的普通单词“not”只是一个常规的旧分支或标记名称。
  • @Kevin 你的描述真的很有趣。那么它如何找到丢失提交的列表呢? :))
  • @torek 我说的确实是插入符号:)
  • @SupunWijerathne 对于每个分支,您可以执行 BFS/DFS 并将遇到的所有提交放入一个集合中。这将为您提供两组,每组包含可从一个分支访问的提交。然后,您可以遍历其中一组中的所有提交,并查看另一组是否也包含该提交。任何不在两个集合中的提交都只能从其中一个分支访问。

标签: git algorithm


【解决方案1】:

您从 cmets 注意到:

我说的确实是插入符号

和:

.. 我应该明确我的目的是稍后使用 Bitbucket API 来完成这项工作(而不是标准的 git 命令)...

这将使您的工作变得相当困难,除非您使用的 API 部分是 git clone。 :-) 究竟难度如何取决于您是否可以只了解每个提交对象的信息,或者您是否需要git cherry 的等价物。

Kevin's comment 是正确的:git loggit rev-list 做什么,以实现来自 gitrevisions^branch_exclude branch_includebranch_exclude..branch_include 语法,是在提交图上运行广度和/或深度优先搜索. (根据revision.c中的代码,主要是广度优先。)

当然,这需要构建提交图,或者至少需要足够的提交图。

在 Git 内部,每个提交都由其哈希 ID 命名。每个提交一个小文本对象组成,其中包括提交的父哈希 ID(每个父哈希 ID)。这两个部分,加上一个起点,例如 HEAD 提交的 ID 或某个分支名称的 ID,是我们进行最简单的遍历所需的全部,即标准的 git rev-list <startpoint> 图遍历。

请注意,大多数提交只有一个父级:这些是普通提交。图中至少有一个提交有 no 父级,并且是“根”提交。在存储库中进行的第一次提交始终是根提交(您可以使用git checkout --orphan 或使用 Git 的管道命令创建更多根)。一些提交是合并:它们有两个或多个父级。

简单的图形遍历

有很多图游走算法(参见 Sedgewick、Aho,当然还有 Knuth 的各种书籍),但 Git 使用一个非常简单的方法开始:为遇到的每个提交保留一个内存数据结构 (struct commit)到目前为止,一旦“看到”对象就标记它。为了遍历图,给定一些提交哈希 H,我们将 H 放在“提交访问”队列中。然后,在 not-quite-C 中:

while (there are commits in the queue) {
    struct commit *c = lookup_by_id(remove_first_id_from_queue());
    if (c->flags & SEEN)
        ... we already saw this, so do nothing ...
    else {
        ... print this commit ...
        c->flag |= SEEN; /* now we've seen it! */
        for (h in all C's "parent" hashes)
            append_hash_to_queue(h);
    }
}

队列是处理合并提交的地方,它是广度优先搜索。当我们从一个空队列开始,并将一个普通的提交放入其中时,我们访问该提交并将该提交的单个父级添加到队列中,然后立即从队列中删除父级并访问父级,将 its 父级添加到队列中,依此类推。这只是线性行走。当我们击中根提交时,该过程停止。但是当我们点击 merge 提交时,我们将 both 父级添加到队列中,然后开始走“两边”,轮流访问“第一个父级”并将其父级排队( s),然后是“第二个父母”并对其父母进行排队,依此类推。在某些时候,我们会将已经在队列中或已经被看到的提交排队。当我们稍后到达重新排队或重新看到的提交时,我们只需跳过它。

要实现“可从标识符 include 但不能从标识符 exclude 访问的提交”,我们可以使用相同的算法,但经过修改:首先我们遍历所有从 @987654340 查找的提交@,设置 SEEN 标志但不打印任何内容;然后我们将include 的哈希放入队列中,再次遍历,并打印我们这次访问的提交,而我们还没有已经通过排除访问过。 p>

第一个遍历,排除提交,我们可以称之为“消极遍历”,第二个,包括提交,我们可以称之为“积极遍历”。

(此算法非常次优,因此不是 Git 使用的。如果您阅读代码,您会看到明确排除的提交实际上设置了 UNINTERESTING | BOTTOM 标志,而通过步行排除的提交设置了 UNINTERESTINGrelevant_commit 函数及其调用者中有一些有趣的(嗯)代码,它们有助于在负游走期间修剪图形的大片区域,而不必经过它们。诀窍是我们必须知道是否有多种方法达到我们现在想要修剪的点。如果没有,标记一次提交是安全的,这将避免在正向遍历期间遍历它及其所有父项。)

Git 樱桃

git cherry(或git rev-list --cherry-mark,或任何类似项目中的任何一个)所做的事情比上面的简单步行要复杂得多。要实现git cherry 或等效项,我们不仅需要提交graph,还需要存储库中的剩余对象,因为现在我们想要标记“在”一组但不是“在”另一组。

在我们到达那里之前,我们需要定义 对称差异,它在 Git 语法中表示为 A...B(三个点而不是两个点)。对称差异的核心是“包括可从左侧名称 访问的提交, 或从右侧名称访问的提交,但 不是 em> 来自两个的名字。”同样,我们可以使用一个非常简单的图遍历算法(尽管 Git 没有这样做,因为这个算法效率太低了):从A 开始遍历,用“在左侧看到”标志标记每个“看到”提交。然后,从B 步行,用“在右侧看到”标志标记每个新看到的提交,与“在左侧看到”标志分开(这样我们就可以用两个标志标记所有可以从两个名称访问的提交)。

现在我们对所有存储的提交对象进行最后一次遍历(不是来自两个名称,就在struct commit 内部对象之上),并且仅将“在左侧看到但在右侧未看到”的提交打印为左侧提交,并仅将“在右侧看到但在左侧未看到”的提交打印为右侧提交。这给了我们对称的差异。

不过,要实现git cherry,我们还需要更进一步。对于每个单亲提交,我们获得一个 changeset。 (通常,我们完全忽略合并,尽管可以将合并变成针对其父级一个的变更集。)变更集基本上是git diff <parent-id> <commit-id> 的结果。

现在我们遇到了困难的部分:每个变更集都可以转换为哈希 ID,就像 Git 将所有 Git 存储库对象转换为哈希 ID 一样。如果我们小心地这样做,以一种对行号不敏感的方式(并忽略提交消息,以及作者和日期等),我们最终会得到 Git 所说的 patch ID

为了实现git cherry,我们用补丁ID 标记每个“边”上的所有普通提交。然后我们可以在打印每个提交时轻松检查每个左侧补丁 ID 是否在所有右侧补丁 ID 的集合中,反之亦然。这使我们能够找到“丢失的”提交,即使提交已被复制(通过 git cherry-pickgit rebase 或类似方式)。

(要查看 Git 实际用于对称差异的算法,请参阅源代码。UNINTERESTING 标志增加了另一个标志 BOTTOM 和“边界”提交——双方相遇的地方,允许一些父要避免的链 - 用另一个标志标记BOUNDARY 标志。您可以通过将--boundary 添加到您提供给git rev-list 的标志来查看这些提交。我发现--boundary 没有它有用可能看起来,因为从多个起点开始的广度优先搜索可以有“额外”的边界,这在以其他顺序完成的搜索中是不必要的。)

(在内部,BOTTOM 标志也用于--ancestry-path,以及“负面引用”列表,即^X 中的否定标记X 引用的提交既是否定的引用并作为“底部提交”,然后进入此列表。然后,rev-list walk 代码可以排除不具有所有这些“底部”提交作为祖先的提交。)

【讨论】:

  • 非常感谢@torek 的详尽解释!我预计它会很复杂,我有我的答案;)我可能不得不重新考虑使用 Bitbucket 来做到这一点的想法......我需要消化一下所有这些:D
【解决方案2】:

您可以使用cherry 命令查找丢失的提交。

例如,假设您的第一个分支是 r1,最新的是 r2。

git checkout r1

git 樱桃 r2

这将报告 r2 分支中不存在的所有提交。

这里是文档https://git-scm.com/docs/git-cherry

【讨论】:

  • 你确实是对的。但是,我应该明确指出,我的目的是稍后使用 Bitbucket API(而不是标准的 git 命令)来完成这项工作,而樱桃不存在。从我看到的 API 中,我可以得到提交列表,但不是像樱桃这样的“进化”命令......
  • @Xendar 您的程序从 Bitbucket 完全克隆您正在分析的存储库,然后在克隆的存储库上运行 git 命令以获取您需要的信息是否合理?这意味着您必须向 Bitbucket 发出一个非常大的请求,而不是许多小请求。
  • 是的,这是我现在或多或少正在做的事情,使用 JGit API,但是,它并没有涵盖所有 Git 功能。理想情况下,我希望避免必须在后面管理文件系统,因此希望使用 Bitbucket API。这个假设当然可以辩论;)