为什么这个 r1..r2 修订范围的输出包括可从 r2 访问的提交?

Why does the output of this r1..r2 revision range include commits reachable from r2?

在下图中,r2 (HEAD) 和 r1 (75ec2933~1) 都可以访问最后两个提交。

> git log --oneline --graph develop topic 75ec2933~1..HEAD
* 91cc860a (HEAD -> topic) 
* 1048e4d1 
* 1c28716e
| * f4a483cc (develop)
| *   b7cb53e6 
| |\
| |/
|/|
* | c7a197bd 
* | 3935a1a7 
| *   ad27a1fc 
| |\
| |/
|/|
* | 75ec2933 Merge branch 'develop' 
| * 5e55f38f 
|/
* 2effd96f          <--------------- 75ec2933~1 is r1
* ae6c987e 
* ecc2b546 

我预计最后两次提交不会成为输出的一部分,因为 the git-log documentation says that we can use a revision range to "Show only commits in the specified revision range." Further, the revision range documentation say this about the r1..r2 notation:

...you can ask for commits that are reachable from r2 excluding those that are reachable from r1 by ^r1 r2 and it can be written as r1..r2.

所以我的问题是关于为什么我们可以看到最后两个提交,它们似乎可以从 r1.

访问

进一步调查

原来 75ec2933~1 不是 2effd96f,而是 887b3cfa。上图隐藏了这一点,这导致我对 r2.

感到困惑
> git log ecc2b546..3935a1a7 --oneline --graph
* 3935a1a7
*   75ec2933 Merge branch 'develop' 
|\  
| * 2effd96f 
| * ae6c987e 
* 887b3cfa 
* 62e6be09 

我不得不在这里猜测一下(更新:确认),但我认为我们有这种情况:

  • 提交 75ec2933 是一个合并提交,即有 两个 parents.
  • Parent #1 有一些未知的哈希 ID(更新:887b3cfa)。
  • Parent #2 是 2effd96f.

如果是这种情况,表达式 75ec2933~1..HEAD 排除 parent #1,但不排除 parent #2。可以通过运行ning:

查到
git rev-parse 75ec2933^@

(注意插入符号或帽子 ^ 后的 @ 后缀)。对于结果 git log 输出有相当长的解释。不过,为了演示它,我将使用 Git 存储库来代替 Git 本身,因为这是我手边的一个。

例子

这是我在 Git 存储库中针对 Git 的不同合并提交执行此操作时发生的情况:

$ git rev-parse a562a11983^@
7fa92ba40abbe4236226e7d91e664bbeab8c43f2
ad6f028f067673cadadbc2219fcb0bb864300a6c

这里commit a562a11983是一个合并,with parents 7fa92ba40a and ad6f028f06.

如果我 运行 git log --decorate --oneline --graph 在 Git 存储库上 Git,允许 git log 从提交 b5101f9297 开始(一个旧的master 提示——我已经好几个星期没有为 Git 更新我的 Git 存储库了),结果是这样开头的:

* b5101f9297 (HEAD -> master) Fourth batch after 2.20
*   a562a11983 Merge branch 'it/log-format-source'
|\  
| * ad6f028f06 log: add %S option (like --source) to log --format
* |   7fa92ba40a Merge branch 'js/add-e-clear-patch-before-stating'
|\ \  
| * | fa6f225e01 add --edit: truncate the patch file
* | |   371820d5f1 Merge branch 'bc/tree-walk-oid'
|\ \ \  
| * | | 974e4a85e3 cache: make oidcpy always copy GIT_MAX_RAWSZ bytes
| * | | ea82b2a085 tree-walk: store object_id in a separate member
| * | | f55ac4311a match-trees: use hashcpy to splice trees
| * | | 36775ab524 match-trees: compute buffer offset correctly when splicing
| * | | 0a3faa45b1 tree-walk: copy object ID before use
| | |/  
| |/|   
* | |   a6e3839976 Merge branch 'jt/upload-pack-deepen-relative-proto-v2'

使用 git log --decorate --oneline --graph a562a11983^1..HEAD 将其修整为:

* b5101f9297 (HEAD -> master) Fourth batch after 2.20
* a562a11983 Merge branch 'it/log-format-source'
* ad6f028f06 log: add %S option (like --source) to log --format

请注意,这个图形形状看起来简单多了!我已经删除了提交 a562a11983 但没有提交 ad6f028f06,因此看起来提交 a562a11983 有一个 parent、ad6f028f06,尽管它实际上有两个。实际上,git log --graph 欺骗了我们。

在深入研究 git log 本身的具体细节之前,还有一些项目值得注意。首先,gitrevisions notation 中的语法 r1..r2 等同于 r2 ^r1。事实上,如果我们用git rev-parse扩展语法,就是我们看到的:

git rev-parse a562a11983^1..HEAD
b5101f929789889c2e536d915698f58d5c5c6b7a
^7fa92ba40abbe4236226e7d91e664bbeab8c43f2

HEAD是以b5101开头的提交哈希,a562a11983^1(后缀^和数字1)是以7fa92b...开头的提交注意我们在这里使用插入符 ^ 作为 后缀 ,而不是 前缀; 插入符作为 prefix 表示 not,即排除修订,但作为后缀的插入符号引入了许多其他 gitrevisions 说明符之一,例如 @{commit},当然还有特定 parent.

的数字选择

另一个是每个提交记录 零个或多个 parent 哈希 ID。大多数提交都有 one parent ID。您在存储库中所做的第一次提交没有 parent,原因很简单,它 不能 有任何 parent: parent 新提交的 ID 必须是现有的、有效的提交哈希 ID。没有 parents 的提交称为 root 提交。一些提交,通常是由 git merge 做出的提交,有两个 parent,你可以使 multi-armed octopus merges 有三个或更多 parent秒。根据定义,任何具有两个或更多 parent 哈希 ID 的提交都是 merge commit.

因为大多数提交都有一个 parent,我们通常从此类提交链的末尾开始,通常用分支标签标记,例如 master,然后我们可以向后工作一次提交:

... <-F <-G <-H   <-- master (HEAD)

这里存储在分支名称master中的哈希ID表示为大写字母H。我们说名称 master 指向 哈希 ID 为 H 的提交。 Commit H 本身存储其 parent commit G 的 hash ID,后者存储 commit F 的 hash ID,依此类推。因此,通过从 H 开始并向后工作到 G,然后是 F,依此类推,Git 可以向我们展示 [=234] 的历史 - 提交=]可从 名称 master.

访问

最后一项是git log实际上需要很多starting-points(我们可能想称它们为ending-points,但是Git 向后工作)。每个指定修订的参数——但不是那些由于被前缀 ^ 否定而消除修订的参数——提供了这样一个起点。如果您不提供任何起点,git log 将使用 HEAD 作为起点。

git log 如何遍历历史然后显示图表

如果我们有一个简单的线性链,如:

...--F--G--H   <-- master (HEAD)

那么我们的工作,如果我们想效仿 git log,就很容易了。我们从提交 H 开始并显示它。我们现在已经完成了 H,所以我们退一步回到它的 parent G。我们显示 G,然后返回 F。我们重复这个直到我们到达一个根提交,它没有 parent 让我们停止,或者直到用户退出 git log.

但是假设我们有一个包含合并提交的图表:

       I--J
      /    \
...--H      M--N   <-- master (HEAD)
      \    /
       K--L

我们将从显示提交 N 开始,然后转到 M 并显示它。1 然后我们将转到 ...等等,我们是走到J,还是L

为了处理这个问题,git log 所做的是保持一个优先队列 尚未显示的提交, 也遍历提交图一一次提交。所以当你 运行 git log 没有额外的参数,或者使用 HEADmaster 作为参数时,git log 将提交 N 放入队列.

当队列中只有一个提交时,工作很简单:从队列中取出一个提交,显示它,如果没有,则将其 parent(s) 放入队列之前在 git log 期间看到过(通常是这种情况)。当队列中有 多个 提交时,git log 会在队列的 front 中获取一个,即带有最高优先级。

因此,如果您 运行 git log <start-point-1> <start-point-2> <start-point-3>,Git 所做的就是将 所有三个 起点放入优先队列。由于您的实际命令是:

git log --oneline --graph develop topic 75ec2933~1..HEAD

我们有三个起点,即developf4a483cc)、topicHEAD75ec2933~1是对某些散列的负引用ID)。事实证明,HEADtopic 都命名为提交 91cc860a,因此队列最终只有两个提交。

--graph 选项稍微修改了优先级队列。默认情况下,具有最高 date 的提交——即,最远的未来,或至少过去的——排在队列的前面。对于 --graph--topo-order,同样的规则已就位,但添加了一条附加规则:在将显示其所有 children 之前,不能显示 parent 提交,已经展示过了。在这种情况下,额外的异常此时没有影响,因为 91cc860af4a483cc 没有 parent/child 关系。

所以 git log 从这两者中较晚的日期开始,即 91cc860a 又名 HEADtopic。 Git 使用单个 * 打印此提交并找到其进入队列的 parent、1048e4d11048e4d1 也比 f4a483cc 更新,所以 Git 接下来显示它。它是上一次提交的直接 parent,所以现在是时候显示那个了。这持续了一段时间,以便我们看到:

* 91cc860a (HEAD -> topic) 
* 1048e4d1 
* 1c28716e

1c28716e 有 parent c7a197bd 并且 c7a197bdf4a483cc 的祖先,因此无论日期如何,它都不能显示。 Git 现在开始显示 f4a483cc,这是一个普通的提交:

| * f4a483cc (develop)

f4a483cc的parent是b7cb53e6所以b7cb53e6进入队列。该提交具有 c7a197bd 作为祖先,因此 Git 显示 b7cb53e6 接下来:

| *   b7cb53e6 

...和b7cb53e6本身就是合并,将其parent的c7a197bdad27a1fc放入队列。但是 c7a197bd 已经在队列中了,所以什么也没有发生。

现在 c7a197bd 位于队列的 前面 ,因此 git log 显示了它。它是 1c28716e 的第一个也是唯一的 parent 和 b7cb53e6 的第二个 parent,所以 git log --graph 以这种稍微时髦的方式显示它:

| |\
| |/
|/|
* | c7a197bd 

right-extending 向下的腿显示此 second-parent-ness。 straight-down 分支最终将连接到 b7cb53e6 的第一个 parent。

同样的模式持续了一段时间,但随后我们遇到了不幸的情况:

* | 3935a1a7 
| *   ad27a1fc 
| |\
| |/
|/|
* | 75ec2933 Merge branch 'develop' 
| * 5e55f38f 
|/
* 2effd96f          <--------------- ???

此时,Git 已显示提交 75ec2933(其中有两个 parent,887b3cfa 即 parent #1 和 2effd96f 即 parent #2)。 Git 本来887b3cfa放入队列,但我们告诉它不要这样做:^75ec2933~1表示^887b3cfa,表示不显示 887b3cfa,这使其不在队列中。因此显示 75ec2933,队列包含提交哈希 ID 5e55f38f2effd96f。 Git 显示 5e55f38f,这让它可以继续前进到 2effd96f。当git log --graph显示那个时,它甚至看到还有第二个cut-offparent,所以它画错了,因为如果 parent 不存在。2


1值得注意的是:当 git log 显示普通提交时,如果 -p 有效,它会将提交与其(单个)parent 以便将提交(实际上是快照)显示为更改。但是当它遇到合并提交时,它不知道要使用哪个 parent 作为差异,所以它根本懒得做差异!您可以通过额外的 git log 选项强制它显示一个或多个差异。

2公平地说,此时的in-memory表示可能没有第二个parent. git log 代码包含一些用于历史简化的 "parent rewriting" 代码,这可能也是这里触发的。


结论

我通常告诉人们,如果 git log 显示奇怪的结果,他们应该添加 --graph 以使其既绘制图形——好吧,无论如何都是粗略的 ASCII 近似值——并遵守图形拓扑在遍历提交时,以便显示通常至关重要的 parent/child 关系。不幸的是,当您使用否定来剪掉部分图形时,这可能会欺骗 graph-drawing 代码来绘制谎言。可能 graph-drawing 代码应该显示真实的 multi-parent 情况,因此被迫将最后一部分绘制得更像这样:

|/|
* | 75ec2933 Merge branch 'develop' 
|\|
| |\
| * | 5e55f38f 
|/ /
* | 2effd96f 
* | ae6c987e 
* | ecc2b546 

但是没有,所以unless/until有人可以将这个添加到Git,我们只需要看t 对于这种情况。