Git 进阶教程(一) | 大文件存储详解
唠唠闲话
最近接触的项目用 LFS 来管理文件,特此学习和记录,本文转载自知乎。
相关阅读
BitBucket: https://www.atlassian.com/git/tutorials/git-lfs
知乎:https://zhuanlan.zhihu.com/p/146683392
前置知识
以下转载自知乎,中间做了少量修改和加粗。
内容概要:
- Git 仓库中如何管理大的二进制文件
- 什么是 Git LFS
- Git LFS 是如何工作的
- Git LFS 的安装
什么是 Git LFS
Git 是分布式版本控制系统,在克隆过程中会将仓库的整个历史记录传输到客户端。对于包涵大文件(尤其是经常被修改的大文件)的项目,初始克隆需要大量时间,因为客户端会下载每个文件的每个版本。Git LFS(Large File Storage)是由 Atlassian, GitHub 以及其他开源贡献者开发的 Git 扩展,它通过延迟地(lazily)下载大文件的相关版本来减少大文件在仓库中的影响,具体来说,大文件是在 checkout 的过程中下载的,而不是 clone 或 fetch 过程中下载(这意味着你在后台 fetch 远端仓库内容到本地时,并不会下载大文件内容,而是在你 checkout 到工作区的时候才会真正去下载大文件的内容)。
Git LFS 通过将仓库中的大文件替换为指针(pointer)文件来做到这一点。在正常使用期间,你不会看到这些指针文件,因为它们是由 Git LFS 自动处理的:
-
当执行
git add
命令,添加文件到仓库时,Git LFS 用一个指针替换其内容,并将文件内容存储在本地 Git LFS 缓存中(本地 Git LFS 缓存位于仓库的.git/lfs/objects
目录中)。
-
当你推送新的提交到服务器时,新提交引用的所有 Git LFS 文件都会从本地 Git LFS 缓存传输到绑定到 Git 仓库的远程 Git LFS 存储(即 LFS 文件内容会直接从本地 Git LFS 缓存传输到远程 Git LFS 存储服务器)。
-
当你 checkout 一个包含 Git LFS 指针的提交时,指针文件将替换为本地 Git LFS 缓存中的文件,或者从远端 Git LFS 存储区下载。
关于 LFS 的指针文件
LFS 的指针文件是一个文本文件,存储在 Git 仓库中,对应大文件的内容存储在 LFS 服务器里,而不是 Git 仓库中,下面为一个图片 LFS 文件的指针文件内容:
1 | version https://git-lfs.github.com/spec/v1 |
指针文件很小,内容为 key-value 格式。第一行为指针文件规范 URL,第二行为文件的对象 id,也即 LFS 文件的存储对象文件名,可以在 .git/lfs/objects
目录中找到该文件的存储对象,第三行为文件的实际大小(单位为字节)。所有 LFS 指针文件都是这种格式。
Git LFS 是无缝的:在你的工作副本中,你只会看到实际的文件内容。这意味着你不需要更改现有的 Git 工作流程就可以使用 Git LFS。你只需按常规进行 git checkout
、编辑文件、git add
和 git commit
。而 git clone
和 git pull
将明显更快,因为你只下载实际 checkout 的提交所引用的大文件版本,而不是曾经存在过的文件的每一个版本。
为了使用 Git LFS,你需要一个支持 Git LFS 的托管服务器,例如 Bitbucket Cloud 或 Bitbucket Server(GitHub、GitLab也都支持 Git LFS)。仓库用户将需要安装 Git LFS 命令行客户端,或支持 Git LFS 的 GUI 客户端,例如 Sourcetree。
安装 Git LFS
这节做了删改。
-
Ubuntu 可以执行
1
sudo apt-get install git-lfs
-
Mac 可以执行
1
brew install git-lfs
-
Windows 可以在 Git LFS 官网 下载安装包,然后安装。
一旦安装好了 Git LFS,运行 git lfs install
来初始化 Git LFS。你只需要运行该命令一次,当克隆包含 Git LFS 内容的仓库时,Git LFS 将自动进行自我引导启用。
实践部分
创建新的 Git LFS 仓库
要创建新的支持 Git LFS 的仓库,你需要在创建仓库后运行 git lfs install
1 | # 初始化 Git 仓库 |
这将在你的仓库中安装一个特殊的 pre-push Git 钩子,该钩子将在你执行 git push
的时候传输 Git LFS 文件到服务器上。
所有 Bitbucket Cloud 仓库已自动启用 Git LFS 。对于 Bitbucket Server,你需要在仓库设置中启用 Git LFS:
当你的仓库初始化了 Git LFS 后,你可以通过使用 git lfs track
来指定要跟踪的文件。
克隆现有的 Git LFS 仓库
安装 Git LFS 后,你可以像往常一样用 git clone
命令来克隆 Git LFS 仓库。在克隆过程的结尾,Git 将 checkout 默认分支(通常是 master),并且将自动为你下载完成 checkout 过程所需的所有 Git LFS 文件。例如:
1 | git clone git@bitbucket.org:tpettersen/Atlasteroids.git |
仓库里有 4 个 PNG 文件被 Git LFS 跟踪。执行 git clone
命令时,在从仓库中检出指针文件的时候,Git LFS 文件被一个一个下载下来。
加快克隆速度
如果你正在克隆包含大量 LFS 文件的仓库,显式使用 git lfs clone
命令可提供更好的性能:
1 | git lfs clone git@bitbucket.org:tpettersen/Atlasteroids.git |
git lfs clone
命令不会一次下载一个 Git LFS 文件,而是等到检出(checkout)完成后再批量下载所有必需的 Git LFS 文件。这利用了并行下载的优势,并显著减少了产生的 HTTP 请求和进程的数量(这对于提高 Windows 的性能尤为重要)。
拉取并检出
就像克隆一样,你可以使用常规的 git pull
命令拉取 Git LFS 仓库。拉取完成后,所有需要的 Git LFS 文件都会作为自动检出过程的一部分而被下载。
1 | git pull |
不需要显式的命令即可获取 Git LFS 内容。然而,如果检出因为意外原因而失败,你可以通过使用 git lfs pull
命令来下载当前提交的所有丢失的 Git LFS 内容:
1 | git lfs pull |
加快拉取速度
像 git lfs clone
命令一样,git lfs pull
命令批量下载 Git LFS 文件。如果你知道上次拉取以来已经更改了大量文件,希望禁用 checkout 期间的自动下载,这可以通过在调用 git pull 命令时使用 -c
选项覆盖 Git 配置来完成:
1 | git -c filter.lfs.smudge= -c filter.lfs.required=false pull |
然后再显式地使用 git lfs pull
命令来批量下载 Git LFS 内容:
1 | git lfs pull |
由于这条命令太长,可以通过 git config alias
来设置简写
1 | git config --global alias.plfs "\!git -c filter.lfs.smudge= -c filter.lfs.required=false pull && git lfs pull" |
当需要下载大量的 Git LFS 文件时,这将大大提高性能(同样,尤其是在 Windows 上)。
使用 Git LFS 跟踪文件
当向仓库中添加新的大文件类型时,你需要通过使用 git lfs track 命令指定一个模式来告诉 Git LFS 对其进行跟踪:
1 | git lfs track "*.ogg" |
请注意,"*.ogg"
周围的引号很重要。省略它们将导致通配符被 shell 扩展,并将为当前目录中的每个 .ogg
文件创建单独的条目:
1 | # 你大概不希望这样做 |
Git LFS 支持的模式与 .gitignore
支持的模式相同,例如:
1 | # 追踪所有 .ogg 文件 |
需要注意的是,Git LFS 不支持像 .gitignore
那样的负模式(negative patterns)。
运行 git lfs track
后,你会在你的运行命令的仓库中发现名为 .gitattributes
的新文件。.gitattributes
是一种 Git 机制,用于将特殊行为绑定到某些文件模式。Git LFS 自动创建或更新 .gitattributes
文件,以将跟踪的文件模式绑定到 Git LFS 过滤器。但是,你需要将对 .gitattributes
文件的任何更改自己提交到仓库:
1 | git lfs track "*.ogg" |
1 | git add .gitattributes |
1 | git commit -m "Track ogg files with Git LFS" |
为了便于维护,通过始终从仓库的根目录运行 git lfs track
,将所有 Git LFS 模式保持在单个 .gitattributes
文件中。然而,你可以通过调用不带参数的 git lfs track
命令来显示 Git LFS 当前正在跟踪的所有模式的列表(以及它们在其中定义的 .gitattributes
文件):
1 | git lfs track |
你可以通过从 .gitattributes
文件中删除相应的行,或者通过运行 git lfs untrack
命令来停止使用 Git LFS 跟踪特定模式:
1 | git lfs untrack "*.ogg" |
1 | git diff |
运行 git lfs untrack
命令后,你自己必须再次提交 .gitattributes
文件的更改。
提交和推送
你可以按常规方式提交并推送到包含 Git LFS 内容的仓库。如果你已经提交了被 Git LFS 跟踪的文件的变更,则当 Git LFS 内容传输到服务器时,你会从 git push 中看到一些其他输出:
1 | git push |
如果由于某些原因传输 LFS 文件失败,推送将被终止,你可以放心地重试。与 Git 一样,Git LFS 存储也是内容寻址的(而不是按文件名寻址):内容是根据密钥存储的,该密钥是内容本身的 SHA-256 哈希。这意味着重新尝试将 Git LFS 文件传输到服务器总是安全的;你不可能用错误的版本意外覆盖 Git LFS 文件的内容。
在主机之间移动 Git LFS 仓库
要将 Git LFS 仓库从一个托管提供者迁移到另一个托管提供者序,你可以结合使用指定了 -all
选项的 git lfs fetch
和 git lfs push
命令。
例如,要将所有 Git 和 Git LFS 仓库从名为 github 的远端移动到名为 bitbucket 的远端:
1 | # 新建一个 bare clone 的 GitHub 仓库 | 不能 add/commit/checkout |
获取额外的 Git LFS 历史记录
Git LFS 通常仅下载你实际在本地检出的提交所需的文件。但是,你可以使用 git lfs fetch --recent
命令强制 Git LFS 为其他最近修改的分支下载额外的内容:
1 | git lfs fetch --recent |
这对于在外出午餐时批量下载新的 Git LFS 内容很有用,或者如果你打算与队友一起审查工作,并且由于网络连接受限而无法在以后下载内容时,这将非常有用。
Git LFS 会考虑包含最近提交超过 7 天的提交的任何分支或标签。 你可以通过设置 lfs.fetchrecentrefsdays
属性来配置被视为最近的天数:
1 | # 设置为最近 10 天的分支或标签 |
默认情况下,git lfs fetch --recent
将仅对“最近”分支或标记(tag)的最新提交下载 Git LFS 内容。
但是,你可以通过配置 lfs.fetchrecentcommitsdays
属性,将 Git LFS 配置为在最近的分支和标签上下载更早提交的内容:
1 | # 下载最近的分支或标记的最新 3 天的 Git LFS 内容 |
请谨慎使用此设置:如果分支移动很快,则可能会导致下载大量数据。 但是,如果你需要查看分支上的插页式更改,跨分支的 cherry-pick
提交或重写历史记录,它可能会很有用。
如在主机之间移动 Git LFS 仓库中所述,你还可以选择使用 git lfs fetch --all
获取仓库的所有 Git LFS 内容:
1 | git lfs fetch --all |
删除本地 Git LFS 文件
你可以使用 git lfs prune
命令从本地 Git LFS 缓存中删除文件:
1 | git lfs prune |
这将删除所有被认为是旧的本地 Git LFS 文件。不被以下方式引用的文件称为旧文件:
- 当前 checkout 的提交
- 尚未推送(到 origin,或任何
lfs.pruneremotetocheck
设置的)的提交 - 最近一次提交
默认情况下,最近的提交是最近十天内创建的任何提交。 通过添加以下内容计算得出:
- 获取额外的 Git LFS 历史记录 中所讨论的
lfs.fetchrecentrefsdays
属性的值(默认为 7) - 一直到
lfs.pruneoffsetdays
属性的值(默认为 3)
你可以配置 prune 偏移量以将 Git LFS 内容保留更长的时间:
1 | # 保留最近 4 周的提交(7 + 21) |
与 Git 的内置垃圾收集不同,Git LFS 内容不会自动修剪,因此,定期运行 git lfs prune
命令是保持本地仓库大小减小的好主意。
你可以使用 git lfs prune --dry-run
来测试 prune 操作将产生什么效果:
1 | git lfs prune --dry-run |
以及使用 git lfs prune --verbose --dry-run
命令精确查看哪些 Git LFS 对象将被修剪:
1 | git lfs prune --dry-run --verbose |
--verbose
模式输出的长十六进制字符串是要 prune 的 Git LFS 对象的 SHA-256 哈希(也称为对象 ID 或 OID)。 你可以使用“查找路径”中描述的技术或引用 Git LFS 对象的提交来查找有关将被修剪的对象的更多信息。
作为附加的安全检查,你可以使用 --verify-remote
选项在删除之前,检查远程 Git LFS 存储区是否具有你的 Git LFS 对象的副本:
1 | git lfs prune --verify-remote |
这将使修剪过程明显变慢,但是你可以从服务器上恢复所有修剪的对象,从而使你高枕无忧。 你可以通过全局配置 lfs.pruneverifyremotealways
属性为系统永久启用 --verify-remote
选项:
1 | git config --global lfs.pruneverifyremotealways true |
或者,你可以通过省略上述命令中的 --global
选项,仅对当前仓库启用远端校验。
从服务器删除远端 Git LFS 文件
Git LFS 命令行客户端不支持删除服务器上的文件,因此如何删除他们取决于你的托管服务提供商。
在 Bitbucket Cloud 中,你可以通过 仓库设置> Git LFS 查看和删除 Git LFS 文件:
请注意,每个 Git LFS 文件均通过其 SHA-256 OID 进行索引;通过 UI 看不到引用每个文件的路径。这是因为在许多不同的提交中,可能对应有许多引用对象的不同路径,因此查找它们将是一个非常缓慢的过程。
要确定给定的 Git LFS 文件实际包含什么,你有三个选项可用:
- 在 Bitbucket Git LFS UI 的左栏中查看文件预览图像和文件类型
- 使用 Bitbucket Git LFS UI 右栏中的链接下载文件-搜索引用 Git LFS 对象的 SHA-256 OID 的提交,如下一节所述
查找引用 Git LFS 对象的路径或提交
如果你有一个 Git LFS SHA-256 OID,你可以使用 git log --all -p -S
命令确定哪些提交引用了它:
1 | git log --all -p -S 3b6124b8b01d601fa20b47f5be14e1be3ea7759838c1aac8f36df4859164e4cc |
此 git log
咒语会通过添加或删除包含指定字符串(Git LFS SHA-256 OID)的行(-S)的任何分支(–all)上的提交生成补丁(-p)。
该补丁将向你显示 LFS 对象的提交和路径,以及添加它的人和提交时间。 你可以简单地检出提交,Git LFS 将在需要时下载文件并将其放置在你的工作副本中。
如果你怀疑特定的 Git LFS 对象位于当前的 HEAD 或特定的分支中,则可以使用 git grep
查找引用它的文件路径:
1 | # 在 HEAD 中查找特定对象的 OID |
1 | # 在 "power-ups" 分支上查找特定对象的 OID |
你可以用任何包含 Git LFS 对象的 ref,commit 或 tree 替换 HEAD 或 power-ups。
包含/排除 Git LFS 文件
在某些情况下,你可能指向为特定提交下载可用的 Git LFS 内容的子集。例如,在配置 CI 构建以运行单元测试时,你可能只需要源代码,因此可能要排除构建代码不需要的重量级文件。
你可以使用 ``git lfs fetch -X(或
–exclude` )排除模式或子目录:
1 | git lfs fetch -X "Assets/**" |
或者,你可能只想包含特定的模式或子目录。例如,音频工程师可以使用 git lfs fetch -I
(或 --include
)命令仅获取 ogg
和 wav
文件:
1 | git lfs fetch -I "*.ogg,*.wav" |
如果你将包含和排除合并在一起使用,则只会获取与包含模式匹配,但与排除模式不匹配的文件。例如,你可以使用以下方法获取 Assets
目录中除 gif 文件之外的所有内容:
1 | git lfs fetch -I "Assets/**" -X "*.gif" |
排除和包含支持与 git lfs track
和 .gitignore
相同的模式。 你可以通过设置 lfs.fetchinclude
和 lfs.fetchexclude
配置属性:
1 | git config lfs.fetchinclude "Assets/**" |
锁定 Git LFS 文件
不幸的是,没有解决二进制合并冲突的简便方法。 使用 Git LFS 文件锁定,你可以按扩展名或文件名锁定文件,并防止二进制文件在合并期间被覆盖。
为了利用 LFS 的文件锁定功能,你首先需要告诉 Git 哪些类型的文件是可锁定的。 在下面的示例中,在 git lfs track
命令后附加了 --lockable
标志。下边命令既将 PSD 文件存储在 LFS 中,又将它们标记为可锁定。
1 | git lfs track "*.psd" --lockable |
然后将以下内容添加到 .gitattributes
文件中:
1 | *.psd filter=lfs diff=lfs merge=lfs -text lockable |
在准备对 LFS 文件进行更改时,你将使用 lock 命令以便将文件在 Git 服务器上注册为锁定的文件。
1 | git lfs lock images/foo.psd |
一旦不再需要文件锁定,你可以使用 Git LFS 的 unlock 命令将其移除。
1 | git lfs unlock images/foo.psd |
与 git push
类似,可以使用 --force
标志覆盖 Git LFS 文件锁。 除非你完全确定自己在做什么,否则不要使用 --force
标志。
1 | git lfs unlock images/foo.psd --force |
Git LFS 如何工作
如果你想了解有关 clean 和 smudge filter,pre-push 钩子以及 Git LFS 背后的其他有趣的计算机科学的更多信息,请查看来自 Atlassian 在 LinuxCon 2016 的 Git LFS 的演示文稿。
施工完毕。。。