例子
这里做些例子展示,方便理解。
不开LFS
git init
: 提交大文件,文件166m
git add isbin.bin
git commit -m "初始化"
: --------------- 本地仓库大小 321 MB (≈实际文件166+ git log变更166)---------------
: 修改文件
echo a >> isbin.bin
git add isbin.bin
git commit -m "修改大文件"
: --------------- 大小 476 MB (约等 321MB + 166M)---------------
: 二次修改文件
echo b>>isbin.bin
echo b>>text.txt
git add isbin.bin text.txt
git commit -m "修改大文件2次"
: --------------- 大小 631 MB (约等 478MB + 166M) ---------------
: 推送远端(使用本地裸仓库,git init --bare 创建,方便演示)
git push --progress "D:\lfs\noLFSbare" master
: 从远端克隆(使用本地裸仓库,方便演示)
git clone D:\lfs\noLFSbare --progress
: --------------- clone 库的大小 631 MB ;源文件和庞大的git log变更历史 ---------------
开LFS
git init
: 开启 lfs 功能及文件追踪
git lfs install
git lfs track "*.bin"
git add .gitattributes
git commit -m "LFS初始化"
: 上传大文件,文件166m
git add isbin.bin
git commit -m "初始化"
: --------------- 大小 333 MB (另外加上166m git lfs 缓存) ---------------
: 修改文件
echo a >> isbin.bin
git add isbin.bin
git commit -m "修改大文件"
: --------------- 大小 499 MB (另外加上166m *2 git lfs 缓存)---------------
: 二次修改文件
echo a >> isbin.bin
git add isbin.bin
git commit -m "修改大文件"
: --------------- 大小 666 MB (另外加上166m *3 git lfs 缓存)---------------
: 推送远端(使用本地裸仓库,git init --bare 创建,方便演示)
git push --progress "D:\lfs\LFSbare" master
: 从远端克隆(使用本地裸仓库,方便演示)
git clone D:\lfs\LFSbare --progress
:--------------- clone下来的仓库 大小 333 MB (真实文件另外加上166m git lfs 缓存)
:真实文件是缓存拷贝出来,其实clone下载只有一半---------------
: clone 下来的新仓库 回退版本
git reset --hard HEAD~1
: --------------- 大小 499 MB ---------------
: 回退版本时从lfs 下载上一版本的文件做本地缓存,并替换工作目录下的文件
例子小结
需要注意的是一个混淆点,GIT LFS不能缩减本地仓库的大小,只有在频繁变更二进制文件,才可以有效缩减的远端仓库大小,提高clone或者pull的速度!
详解
查看git log历史中的大文件文件,我们可以看到这些庞大的二进制文件,都转换成文件指针文本,大致格式如下。
version https://git-lfs.github.com/spec/v1
oid sha256:efe51849f1f4d4003a64ba0d430425e991d5f9c1c64691663ad58192b3bb8932
size 174684142
那问题来了,我们在例子中使用的提交命令是明明一样的。LFS是那是如何将文件做转换的呢?
GIT HOOK 及GIT attributes提供无感使用体验
在一个本地仓库执行git lfs install或者clone带lfs的仓库时,在.git\hooks文件夹中有一下四个文件
执行对应git操作时会自动或前或后执行对应的钩子函数,查看里面的内容也非常简单,判断git lfs是否存在并且执行配套的git lfs命令。
而另一个则是 GIT attributes文件,lfs 跟踪文件时,添加一个文件.attributes文件,简单来说,就是对bin文件,git 识别成文本,指定filter、merge及diff文件比对规则,其中filter用来执行检出和stage时源文件和文件指针转换的操作。
*.bin filter=lfs diff=lfs merge=lfs -text
git hook与 attributes都是git的原生特性。
lfs 源文件缓存及文件指针对应规则
从上面的一些说明,我们可以已经可以对 git lfs的性质做一下简单的总结了,他只是一个git的外挂式的插件,或者说一个enhancer,利用git原生特性,让git 不在管理真实的二进制文件,而是一个pointer性质的文件。
那lfs如何根据这些文件指针获取真正的源文件的呢?
同样,git lfs 默认会在当前仓库的.git目录创建lfs文件夹,这里面存储着git lfs的各个版本的源文件缓存,需要注意这些是缓存,而不是git的一部分,这也是能提高git性能的根本原因,这些缓存删除对git没有任何影响,上面的例子中我们可以看到,clone 带lfs的仓库,虽然只会下载最新版本的文件指针指向的文件版本,而历史文件只有当我们做另外的reset或者checkout之类的操作是,才会去下载。
指针获取对应文件规则:
文件指针有一个oid 对象id,对应的文件则是.git/lfs/objects/OID[0:2]/OID[2:4]/OID
例如
version https://git-lfs.github.com/spec/v1
oid sha256:efe51849f1f4d4003a64ba0d430425e991d5f9c1c64691663ad58192b3bb8932
size 174684142
则对应的文件缓存应当在**.git\lfs\objects\ef\e5**
文件名efe51849f1f4d4003a64ba0d430425e991d5f9c1c64691663ad58192b3bb8932
如果不存在则会像lfs服务器下载。
走服务器的http请求大概如下,主要还是根据sha256算出来的oid。
// POST https://lfs-server.com/objects/batch
// Accept: application/vnd.git-lfs+json
// Content-Type: application/vnd.git-lfs+json
// Authorization: Basic … (if needed)
{
“operation”: “download”,
“transfers”: [ “basic” ],
“ref”: { “name”: “refs/heads/main” },
“objects”: [
{
“oid”: “12345678”,
“size”: 123
}
],
“hash_algo”: “sha256”
}