您从 cmets 注意到:
我说的确实是插入符号
和:
.. 我应该明确我的目的是稍后使用 Bitbucket API 来完成这项工作(而不是标准的 git 命令)...
这将使您的工作变得相当困难,除非您使用的 API 部分是 git clone。 :-) 究竟难度如何取决于您是否可以只了解每个提交对象的信息,或者您是否需要git cherry 的等价物。
Kevin's comment 是正确的:git log 或 git rev-list 做什么,以实现来自 gitrevisions 的 ^branch_exclude branch_include 或 branch_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 标志,而通过步行排除的提交设置了 UNINTERESTING。 relevant_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-pick 或 git rebase 或类似方式)。
(要查看 Git 实际用于对称差异的算法,请参阅源代码。UNINTERESTING 标志增加了另一个标志 BOTTOM 和“边界”提交——双方相遇的地方,允许一些父要避免的链 - 用另一个标志标记BOUNDARY 标志。您可以通过将--boundary 添加到您提供给git rev-list 的标志来查看这些提交。我发现--boundary 没有它有用可能看起来,因为从多个起点开始的广度优先搜索可以有“额外”的边界,这在以其他顺序完成的搜索中是不必要的。)
(在内部,BOTTOM 标志也用于--ancestry-path,以及“负面引用”列表,即^X 中的否定标记X 引用的提交既是否定的引用并作为“底部提交”,然后进入此列表。然后,rev-list walk 代码可以排除不具有所有这些“底部”提交作为祖先的提交。)