我们知道git是一个分布式版本控制系统,本地目录.git/objects/扮演着数据库(键值对数据库)的角色,里面有blobtreecommittag类型,每一次提交保存的都是完整数据的一次快照。而我一直好奇的有两个点:一是这个全量数据中会记录authorcommit time等信息吗?二是像git cherry-pickgit rebase这些指令可以将某一次提交的改动作为操作单元是如何实现的,按照blob的定义,每一次commit也是保存的全量数据呀。

存储全量数据的好处显而易见,要复现某一次历史时刻的提交commit,只需要按图索骥,根据commit->tree->tree/blob对象找到对应数据文件即可,这样不用遍历所有历史提交记录,这样提升了效率。另外git blame是我们平时用于查看代码作者和提交时间的,那blame的执行是否需要往前追溯历史去查找代码作者和比较时间呢,其底层具体是怎么执行的呢?git cherry-pick指令可以将某一次提交记录使用到另一个分支上,那么各位可否想过,如果每一次提交都是保存的全量快照数据,那么git是如何实现git cherry-pick的呢?

带着好奇心,我们一步一步来解答这些问题。 首先是我们验证下git确实没有单独保存文件各行的author和commit时间(这样做不合理,存储量也大)

authorcommit time只存储在commit对象中

验证很简单,找到几种类型的对象,看下具体的存储内容即可,这里图方便,就从GIT官网摘了相关数据过来(https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)

  • blob对象,存储具体文件数据
  • tree对象,存储文件目录
  • commit对象,存储某一次提交信息 我们重点看下commit对象的数据
1
2
3
4
5
6
tree 0af6e9faec5aeeac05de91104c9247689be6d78e
parent 575655271438f8a43f67b70e9bba2bc17702128f
author xieli <xieli@tencent.com> 1640530195 +0800
committer xieli <xieli@tencent.com> 1640530195 +0800

version 3

这是我所找到的所有有关authorcommit time唯一的数据了。那么git blame也一定是通过commit对象拿到的信息了。

git blame是根据比对tree对象和parent的差异获取到authorcommit time的。

有两个方法可以验证,我们先看一个简单的方法,根据我们所掌握的知识点,因为author等信息都只存储在commit对象中,那么git blame要获取某一行代码的作者等信息只能对比连续两次commit中文件中代码获得 假设当前tree对象如下:

1
2
3
tree 22222
parent 11111
author zhangsan ...

在提交commit22222中文件file1如下:

1
2
version 1
version 2

在提交commit11111中文件file1如下:

1
version 1

两者对比,可知version 2是这次提交新增的,那么这一行就可以标记为zhangsan,至于version 1的作者就得再继续往前追溯了。 这里啰嗦了很多,主要是想把这里的逻辑说清楚。按照我们这个逻辑,假设我们当前仓库版本只有一个提交历史记录的情况下,执行git blame应该就只会显示最近一次提交的author了。

  • 方法一

我们可以找到一个有很多历史提交记录的仓库,使用git clone --depth=1 ....命令拉取代码,然后执行git blame就能发现所有的文件的author信息都是最近一次提交者。这就验证了我们的猜想。

  • 方法二

我们可以玩一点复杂的,来验证我们的猜想,这里会啰嗦一些,大体思路是这样的: 1、我们连续创建两次commit,修改同一个文件,在同一个文件中得到两行不同提交日期的记录 2、我们修改第一次提交所指向的blob数据,让其存储的数据变为另外一个内容,这样也就改变了diff的内容,那么文件中的两行数据的提交author都是相同的了。

下面我们详细来操作下:

git init .,同时在另个窗口执行watch -n 3 tree .git/objects/查看 objects下的变动

echo "version 1" > file1.txt; git add file1.txt; git commit -m "version 1";

可以通过git cat-file -p 83baae等查看存储信息

echo "version 2" >> file2.txt; git add file1.txt; git commit -m "version 2";

git blame file1.txt

git_blame 可以看到两行代码的提交时间不一致

我们再创建一个blob对象,以下命令会往objects文件下写入一个新的blob对象

1
2
echo "fake version"|git hash-object -w --stdin
8ef8975141a1dbe936f16042792d839091dff7c2

我们再将第一次提交的file1blob文件修改为指向我们新创建的blob对象

1
2
rm -rf .git/objects/83/baae61804e65cc73a7201a7252750c76066a30
mv .git/objects/8e/f8975141a1dbe936f16042792d839091dff7c2 .git/objects/83/baae61804e65cc73a7201a7252750c76066a30

git blame file1.txt会发现显示的author信息已经变了

1
2
8945edb2 (xieli 2021-12-27 00:36:04 +0800 1) version 1
8945edb2 (xieli 2021-12-27 00:36:04 +0800 2) version 2

这也就验证了我们的想法。

同理git cherry-pick方法也是通过对比两次相邻commit得到的差异部分,再将差异部分应用到当前分支上面。