为什么这个 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
没有额外的参数,或者使用 HEAD
或 master
作为参数时,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
我们有三个起点,即develop
(f4a483cc
)、topic
和HEAD
(75ec2933~1
是对某些散列的负引用ID)。事实证明,HEAD
和 topic
都命名为提交 91cc860a
,因此队列最终只有两个提交。
--graph
选项稍微修改了优先级队列。默认情况下,具有最高 date 的提交——即,最远的未来,或至少过去的——排在队列的前面。对于 --graph
或 --topo-order
,同样的规则已就位,但添加了一条附加规则:在将显示其所有 children 之前,不能显示 parent 提交,已经展示过了。在这种情况下,额外的异常此时没有影响,因为 91cc860a
和 f4a483cc
没有 parent/child 关系。
所以 git log
从这两者中较晚的日期开始,即 91cc860a
又名 HEAD
和 topic
。 Git 使用单个 *
打印此提交并找到其进入队列的 parent、1048e4d1
。 1048e4d1
也比 f4a483cc
更新,所以 Git 接下来显示它。它是上一次提交的直接 parent,所以现在是时候显示那个了。这持续了一段时间,以便我们看到:
* 91cc860a (HEAD -> topic)
* 1048e4d1
* 1c28716e
1c28716e
有 parent c7a197bd
并且 c7a197bd
是 f4a483cc
的祖先,因此无论日期如何,它都不能显示。 Git 现在开始显示 f4a483cc
,这是一个普通的提交:
| * f4a483cc (develop)
f4a483cc
的parent是b7cb53e6
所以b7cb53e6
进入队列。该提交具有 c7a197bd
作为祖先,因此 Git 显示 b7cb53e6
接下来:
| * b7cb53e6
...和b7cb53e6
本身就是合并,将其parent的c7a197bd
和ad27a1fc
放入队列。但是 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 5e55f38f
和 2effd96f
。 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 对于这种情况。
在下图中,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
没有额外的参数,或者使用 HEAD
或 master
作为参数时,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
我们有三个起点,即develop
(f4a483cc
)、topic
和HEAD
(75ec2933~1
是对某些散列的负引用ID)。事实证明,HEAD
和 topic
都命名为提交 91cc860a
,因此队列最终只有两个提交。
--graph
选项稍微修改了优先级队列。默认情况下,具有最高 date 的提交——即,最远的未来,或至少过去的——排在队列的前面。对于 --graph
或 --topo-order
,同样的规则已就位,但添加了一条附加规则:在将显示其所有 children 之前,不能显示 parent 提交,已经展示过了。在这种情况下,额外的异常此时没有影响,因为 91cc860a
和 f4a483cc
没有 parent/child 关系。
所以 git log
从这两者中较晚的日期开始,即 91cc860a
又名 HEAD
和 topic
。 Git 使用单个 *
打印此提交并找到其进入队列的 parent、1048e4d1
。 1048e4d1
也比 f4a483cc
更新,所以 Git 接下来显示它。它是上一次提交的直接 parent,所以现在是时候显示那个了。这持续了一段时间,以便我们看到:
* 91cc860a (HEAD -> topic)
* 1048e4d1
* 1c28716e
1c28716e
有 parent c7a197bd
并且 c7a197bd
是 f4a483cc
的祖先,因此无论日期如何,它都不能显示。 Git 现在开始显示 f4a483cc
,这是一个普通的提交:
| * f4a483cc (develop)
f4a483cc
的parent是b7cb53e6
所以b7cb53e6
进入队列。该提交具有 c7a197bd
作为祖先,因此 Git 显示 b7cb53e6
接下来:
| * b7cb53e6
...和b7cb53e6
本身就是合并,将其parent的c7a197bd
和ad27a1fc
放入队列。但是 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 5e55f38f
和 2effd96f
。 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 对于这种情况。