Git | 如何迁出非常大的仓库

2021 06 21, Mon

我们最近在搞ChromeOS,其中一部分核心就是Chrome。但是这部分仓库太大了,单单Git对象就有20多G,迁出后算上gclient拉下来的第三方库,大约会占60G空间。对于有一些同事,配备的电脑只有20G的空间来拉仓库,这就变得有点尴尬。类似的问题还有怎么快速clone一个Linux内核的仓库。

经过简单的搜索,得出一个可行的方法: 稀疏迁出 ( sparse checkout ) 浅克隆 ( shallow clone )

tl;dr:

sparse checkout: git sparse-checkout –help

shallow clone: git clone –depth=N …

Git是怎么样工作的

要解释什么是稀疏迁出和浅克隆,我们就需要先理解git是怎么工作的。

Git的设计目标

Git是一个版本管理系统,需要高效的支持这些事情:

  • 非线性的开发:开发人员可以同时开发不同的东西,而且互不干扰。
  • 分布式:可以在不同的机器上开发
  • 对大项目友好
  • 尽可能使用现有的协议
  • 对历史的验证:git commit有哈希作为验证历史的手段,还可以对commit进行签名
  • 高效的存储:文件会被用zlib压缩起来存储,而且每隔一段时间git会把以前的对象都打成一个大包一起压缩

Git的存储

针对这些需求,Torvalds设计了一套基于 哈希树 ( Merkle Tree ) 的存储系统。

简单说哈希树就是一颗树,每个节点带了以所有子节点为基础计算的哈希值,以此验证哈希树的完整性。如果一个节点的哈希值被修改了,那么父节点的哈希值就需要重新计算。如果哈希值验证不通过,说明存储系统的数据有损坏。

Git利用哈希树为基础,节点中存储不同类型的数据。数据的类型包括

  • blob: 全量存储文件内容
  • tree: 存储目录中的文件列表及目录列表
  • commit: 一次提交
  • packfile: 压缩过的上面的节点数据

也就是说,我们每commit一次,都会产生一个commit对象,挂到commit所在的树上,新commit会指向父commit以及一个tree对象,tree对象描述了包含的文件以及子目录对应的tree和blob的哈希。

以此为基础Git有branch(branch head)、HEAD以及tag的概念,三个都是指向某个commit的指针,或者说指向,区别在于

  • branch:一个仓库有不同的分支,开发者可以在不同的分支上工作,可以在仓库干净的情况下自由切换分支来做不同的事情。branch的head会指向某个commit树上的某个节点
  • HEAD:HEAD指的是当前所在分支的头部。
  • tag:tag和分支无关,直接指向一个commit,不随着branch改变,通常用来标记版本。

通过哈希树以及三种指向,git实现了一套版本控制系统,对仓库的修改实际上就是在commit tree上新记一笔。

由于git会定期对本地的对象进行gc,不需要的commit被移除,过多的零散对象会被打包压缩,所以git中的blob即使存储了全量数据,也会因为压缩不会占用太多空间。

由Git的对象存储引发的问题

1、Git全量存储文件,而且传输的时候一般是选出来有关的commit拉取,所以很难提前压缩好对象然后传输。所以为了高效的传输,需要实时对对象压缩 2、仓库太大的时候,checkout占空间很麻烦,所以很多时候我们想只checkout一个子目录 3、仓库一直在开发、历史久远的话,历史修改会占非常多的空间,拉取Linux内核这样的老仓库会占用非常多的带宽

为了解决这几个问题,Git产生了shallow clone和sparse checkout两个特性。其实都很简单,但是需要Git托管服务支持。

稀疏迁出

原理

所谓稀疏迁出很简单,就是告诉Git,我只拉取路径符合某个模式的文件。

比如说我的博客,有content/posts/...,有content/images/...,有src/....。我就想改content/posts/下面的文件,那么我就告诉git,我只要content/posts/*这些文件。然后Git就只会迁出目录符合content/posts/*的所有文件。同时在提交时,git会忽略所有不符合这些模式的文件,也就是说我无法提交content/posts目录以外的文件。

具体是怎么用的

稀疏迁出主要是针对拉仓库说的。

做什么

  • 在拉仓库的时候指定–no-checkout,并且通过–filter=blob:none不下载任何blob,让下载的数据量最小
  • 初始化,设置sparse checkout相关的配置项
    • 之前的很多博客会告诉你用git config设置sparse checkout,但是现在有了git sparse-checkout …命令,不需要
  • 添加目录的模式
  • 迁出代码

怎么做

用我的博客为例

git clone --no-checkout --filter=blob:none git@gitee.com:jeffguorg/blog.jeffthecoder.xyz.git
git sparse-checkout init
git sparse-checkout add 'content/*'
git checkout

问题

在没有做额外配置、只是开启了稀疏迁出的仓库中,有所有的commit历史,有tree的信息,有需要的文件的信息,所以稀疏迁出可以正常看到历史,可以提交代码,也可以正常推送git提交,但是因为缺少其他路径上的文件信息,所以本地无法对没有拉取的目录和文件进行分支合并,必须在一台有所有文件内容的机器上,比如说托管的服务器上,才能做这类操作。

浅克隆

稀疏迁出是通过减少目录和文件的数量减少资源消耗的情况,而浅克隆是另一种情况:通过减少本地拉取的地址来减少资源消耗。

原理

之前提到了git实际上是一个数据直接指向tree、间接指向blob列表的commit树,单个commit已经有所有的信息去迁出仓库的文件。所以实际上如果不考虑其他问题,只需要一个commit的信息就足以拿到仓库最新的状态,就可以构建项目啊什么的。

Git拿到单个commit,就可以用commit指向的tree对象拿到子目录啊、文件列表啊什么的,然后拉下来这些blob,展开就好了。

具体怎么工作的

简单说就是clone时加上–depth=N即可。

git clone --depth

问题

由于没有历史commit的信息,所以无法正常的做任何和历史有关的操作,比如说git log看不到历史,git merge没法工作。

shallow clone只适合看,要操作就需要通过git pull --unshallow来拉取历史。