图0:Linux 内核 Git 历史记录中,最大最奇怪的提交信息是这样的

我们通常认为 git merges 有两个父节点。例如,由我写的最新的 Linux 内核合并操作是提交2c5d955,这是 4.10-rc6 版本发行前准备工作的一部分。它有两个父节点:

2c5d955 Merge branch 'parisc-4.10-3' of ...
|
*- 2ad5d52 parisc: Don't use BITS_PER_LONG in use ...
*- 53cd1ad Merge branch 'i2c/for-current' of ...

Git 还支持章鱼式的合并,这意味着可以有超过两个父节点的合并。这对于我们那些从事小型项目开发的人来说,这似乎很奇怪:与三四个父节点合并会不会令人感到困惑?这得看情况而定。有时候,一个内核的维护者需要一次同时合并几十个单独的历史记录。一个接着一个的30个合并提交比起单独的一个30路(30个父节点)合并更加令人困惑,特别是当30路合并没有冲突的时候。

章鱼式合并可能比你想象地更常见。在内核的提交历史记录中有649,306个提交。其中 46,930 (7.2%) 个提交是合并提交。在合并提交中,有 1,549 (3.3%) 是章鱼式合并。(截止到我当前的 git HEAD 指向的提交 566cf87 。)

$ git log --oneline | wc -l
   649306
$ git log --oneline --merges | wc -l
   46930
$ git log --oneline --min-parents=3 | wc -l
    1549

作为比较,Rails 的所有提交中的 20% 是合并提交 (12,401/63,111),但没有一个章鱼式合并。Rails 大概更能代表一般的工程; 我认为大多数的 git 用户甚至都不知道章鱼式合并。

现在,显而易见的问题是: 这些章鱼式合并的规模有多大? 在这里每行开头的 “>” 是续行符;这个命令写了总共5行。这篇文章中的所有命令都是我在做实验的时候输入到终端里面的,所以它们未必容易看懂。我对于结果更感兴趣,贴出代码只是为了满足那些好奇的人。

$ (git log --min-parents=2 --pretty='format:%h %P' |
>  ruby -ne '/^(w+) (.*)$/ =~ $_; puts "#{$2.split.count} #{$1}"' |
>  sort -n |
>  tail -1)
66 2cde51f

66 个父节点!这么多的父节点,这个提交到底发生了什么?

$ git log -1 2cde51f
commit 2cde51fbd0f310c8a2c5f977e665c0ac3945b46d
Merge: 7471c5c c097d5f 74c375c 04c3a85 5095f55 4f53477
2f54d2a 56d37d8 192043c f467a0f bbe5803 3990c51 d754fa9
516ea4b 69ae848 25c1a63 f52c919 111bd7b aafa85e dd407a3
71467e4 0f7f3d1 8778ac6 0406a40 308a0f3 2650bc4 8cb7a36
323702b ef74940 3cec159 72aa62b 328089a 11db0da e1771bc
f60e547 a010ff6 5e81543 58381da 626bcac 38136bd 06b2bd2
8c5178f 8e6ad35 008ef94 f58c4fc4 2309d67 5c15371 b65ab73
26090a8 9ea6fbc 2c48643 1769267 f3f9a60 f25cf34 3f30026
fbbf7fe c3e8494 e40e0b5 50c9697 6358711 0112b62 a0a0591
b888edb d44008b 9a199b8 784cbf8
Author: Mark Brown <[email redacted for privacy]>
Date:   Thu Jan 2 13:01:55 2014 +0000
 
    Merge remote-tracking branches [65 remote branch names]

这让许多历史记录可视化工具都无法正常运行,引出了 Linus Torvalds 的一个回应:

我刚刚从 Takashi 那收到了一些消息,因此我看到了你的合并提交 2cde51fbd0f3 。这个提交有 66 个父节点。

[…]

它被拉取(pulled)了,并且状况良好,但显然它在 “章鱼式合并很好” 和 “上帝” 之间做到了平衡,这不是一个章鱼式合并,这是一个克苏鲁(一个章鱼头人神的巨人)式的合并。

正如我所看到的,这个有66个父节点的不同寻常的提交在某种程度上只是对于ASoc代码修改的正常合并。ASoc 代表了芯片上的ALSA系统。ALSA系统是音频子系统;“单片系统是集成在单片硅芯片上计算机的术语。综上所述,ASoc 是对嵌入式设备的声音支持系统。

那么这样的合并多久会发生一次呢?永远都不会发生。规模排第二的合并是 fa623d1 ,它仅仅有 30 个父节点。然而,因为足够的背景知识,从 30 到 66 之间的巨大差距并不会令人感到惊讶。

一次 git 提交的父节点的数量大概是一种单侧分布(通常非正式的说法是幂律分布,因为这里对这个不感兴趣,所以不必严格正确)。软件系统的许多属性都属于单侧分布。等一下;我将会生成一个图表来确定…(大量严格的图表确定了)。是的,它确实是单侧分布:

简单地说来, “单侧分布”意味着小事件比大事件多得多,而且大事件的最大规模是没有界限的。内核的提交历史中包含了 45,381 个两个父节点的合并,但仅仅有一个 66个父节点的合并。假如考虑足够多的开发历史记录的话,我们可能会看到多于66个父节点的合并。

单个函数或是单个模块的代码行数也是单侧分布(大多数函数和模块都很小,但是其中一些很大;想想 web app 中的 “User” 类)。同样,对于模块的变化率(大多数模块都不会经常变动,只有其中一些会不断改动;再想想 “User” 类)。这些分布在软件开发中无处不在,而且经常在下面的双对数坐标图中呈直线分布。

对于父节点的数量最大的提交,我们就讨论到这。那么在差异方面的合并又怎样呢?在差异方面,我的意思是被合并的两个分支之间的差异。我们可以通过简单地比较合并节点的父节点,然后统计它们差异的行数来衡量这一点。

例如,如果一个分支一年前从 master 分支分离出去,改变了一行代码,之后被合并回 master 分支,在这段时间内对于 master 分支的所有修改都会被统计到,同样分离出去的分支上的改变也会被统计到。我们可以引出更直观的差异概念,但因为 git 不会保留分支的元数据,所以它们很难或者说是几乎不可能计算出来。

在任何情况下,作为计算差异的起点,下面是最近内核合并的差异:

$ git diff $(git log --merges -1 --pretty='format:%P') | wc -l
     173

在英语中,这个命令的含义是这样的:“比较最近合并的两个父节点,然后统计差异的行数。”为了找出差异最多的合并,我们可以遍历每个合并提交,用类似的方法统计差异的行数。然后,作为一个测试,我们将搜索所有合并中恰好有 2,000 行差异的分支。

$ (git log --merges --pretty="%h" |
   while read x; do
     echo "$(git diff $(git log --pretty=%P $x -1) | wc -l)" $x
   done > merges.txt)
$ sort -n merges.txt | grep 'b2000b'
    2000 3d6ce33
    2000 7fedd7e
    2000 f33f6f0

(这个命令需要花费很长的时间运行: 我想大约需要12小时,尽管我已经减少了许多。)

我认为合并的差异大小遵循单侧分布,就像父节点数量的统计一样。所以它应该在双对数坐标图表中显示为一条直线。让我检查一下….对了:

我把差异的大小定在1000行左右(注:前面用2000行,得到的数据太少),否则没有足够的样本来生成有效的曲线。

右下角难看的原因部分是因为量化问题,另一部分是由于缺乏大量的提交导致样本数量较小,与之前的图表情况一样。

现在,显而易见的问题是: 提交历史中差异最大的合并是哪个?

$ sort -n merges.txt | tail -1
 22445760 f44dd18

22,445,760 行差异!这看起来根本不可能这么大-因为差异的行数比整个内核源代码的行数都大。

Greg Kroah-Hartman 在2016年9月19做了这一提交,当时正处在 4.8-rc6 版本的开发期间。Greg 是 Linus Torvalds 的 “副官” 之一 – 他(Linus)最亲近,最值得信赖的开发者。简单地说,副官们构成了内核 pull request 树中的第一层。Greg 负责维护内核的稳定分支,驱动程序内核,USB 子系统和其他几个子系统。

在更加仔细的研究这个合并之前,我们需要一点背景知识。通常我们把合并作为菱形分支模式(先分支,然后合并,见下图)的一部分:

  A
 / 
B   C
  /
  D

在2014年,Greg 开始在一个新的仓库开发 Greybus (移动设备总线),这就好像是他创建了一个全新的项目一样。最后,Greybus 的开发工作完成时,它就被合并到了内核中。但因为它是从一个崭新的仓库开始的,所以它和内核的中的其他源代码没有共同的历史记录。所以除了2005年我们公认的初始提交之外,这个合并为内核又添加了一个 “初始提交”。这个仓库现在有两条独立的初始提交,而不是通常的菱形分支模式:

  A
 / 
B   C

通过查看合并提交的两个父节点中分别存在多少文件,我们可以看到一些蛛丝马迹:

$ git log -1 f44dd18 | grep 'Merge:'
Merge: 9395452 7398a66
$ git ls-tree -r 9395452 | wc -l
   55499
$ git ls-tree -r 7398a66 | wc -l
     148

一条分支存在大量的文件,因为它包含了整个内核的源文件。而另一条仅仅包含了很少的文件,因为它包含的只是 Greybus 的历史记录。

像章鱼式合并一样,这会让一些 git 用户感到奇怪。但是内核开发人员是专家级的 git 用户,并倾向于放弃使用这个功能,但绝对不是盲目的放弃

最后一个问题:这种情况到底发生了多少次?内核中有多少独立的 “初始化” 提交?事实上是四次:

$ git log --max-parents=0 --pretty="format:%h %cd %s" --date=short
a101ad9 2016-02-23 Share upstreaming patches
cd26f1b 2014-08-11 greybus: Initial commit
be0e5c0 2007-01-26 Btrfs: Initial checkin, basic working tree code
1da177e 2005-04-16 Linux-2.6.12-rc2

如果我们要画出这些提交,为了清楚起见,我们忽略所有其他的历史记录,如下图:

566cf87 (the current HEAD)
| | | |
| | | *- a101ad9 Share upstreaming patches
| | |
| | *- cd26f1b greybus: Initial commit
| |
| *- be0e5c0 Btrfs: Initial checkin, basic working tree code
|
*- 1da177e Linux-2.6.12-rc2

这四个提交中的每一个都是离当前内核版本库 HEAD 节点很遥远的祖先节点,并且都没有父节点。从 git 的角度来看,内核历史“开始”了不同的四次,所有的这些提交最终都被合并在一起。

这四个提交中的第一个(在我们输出的底部)是2005年的初始化提交,也就是我们通常认为的初始化提交。第二个是文件系统 btrfs 的开发,它是独立仓库完成的。第三个是 Greybus,同样也是独立仓库完成的,我们之前已经说过。

第四个初始化提交,a101ad9,很奇怪,正如下面看到的:

$ git show --oneline --stat a101ad9
a101ad9 Share upstreaming patches
 README.md | 2 ++
 1 file changed, 2 insertions(+)

它刚创建了一个 README.md 文件。但随后,它就立即被合并到正常的内核历史的提交 e5451c8 中了!

$ git show e5451c8
commit e5451c8f8330e03ad3cfa16048b4daf961af434f
Merge: a101ad9 3cf42ef
Author: Laxman Dewangan <[email protected]>
Date:   Tue Feb 23 19:37:08 2016 +0530

为什么有人会创建一个只包含两行文本的 README 文件的初始化提交,然后立即合并到主线的历史记录中呢?我想不出任何理由,所以我怀疑这是一个意外!但它没有造成任何危害;它只是很奇怪。(更新:这是个意外,Linus用他一贯的方式回应了。)

顺便提一句,这也是历史记录中差异数量排第二的提交,仅仅因为它是一个不相关提交的合并,就像我们仔细研究过的 Greybus 的合并一样。

现在你知道了:Linux 内核的 git 历史记录一些最奇怪的事情。一共有 1,549 个章鱼式合并,其中有一个是拥有 66 个父节点的提交。差异最多的合并有 22,445,760 行差异,尽管它有点技术性因为它和仓库的其他部分没有公共的历史记录。内核拥有四个独立的初始化提交,其中一个是失误导致的。尽管上面的这些都不会出现在绝大多数的 git 仓库中,但是所有的这些功能都是在 git 的设计范围之内的。

余下全文(1/3)

本文最初发表在伯乐在线,文章内容属作者个人观点,不代表本站立场。

分享这篇文章:

请关注我们:

发表评论

电子邮件地址不会被公开。 必填项已用*标注