Git LFS

Git LFS

什么是 Git LFS?

Git 是一个分布式版本控制系统,这意味着在克隆流程中,存储库的全部历史记录都会传输到客户端。对于包含大型文件的项目,尤其是定期修改的大文件,初始克隆可能需要大量时间,因为每个文件的每个版本都必须由客户端下载。Git LFS(大型文件存储)是由 Atlassian、GitHub 和其他一些开源贡献者开发的 Git 扩展,它通过延迟下载存储库中的相关版本来减少大型文件的影响。具体而言,大型文件是在签出流程中下载的,而不是在克隆或提取流程中下载的。

Git LFS 通过用小指针文件替换存储库中的大文件来实现此目的。正常使用期间,您永远不会看到这些指针文件,因为它们是由 Git LFS 自动处理的:

  1. 当您将文件添加到存储库时,Git LFS 会用指针替换其内容,并将文件内容存储在本地 Git LFS 缓存中。

    git lfs - git add
  2. 当您将新提交推送到服务器时,新推送的提交引用的任何 Git LFS 文件都将从您的本地 Git LFS 缓存传输到与您的 Git 存储库绑定的远程 Git LFS 存储。

    git lfs - git push
  3. git lfs - git checkoutgit lfs - git checkout当您签出包含 Git LFS 指针的提交时,它们将被本地 Git LFS 缓存中的文件或从远程 Git LFS 商店下载的文件替换。git lfs - git checkout

Git LFS 是无缝的:在您的工作副本中,您只能看到实际文件内容。这意味着您可以在不更改现有 Git 工作流程的情况下使用 Git LFS,只需照常进行 git checkout、编辑、git addgit commit 即可。git clonegit pull 操作的速度会显著加快,因为您只下载实际查阅提交引用的大文件版本,而不是文件的所有版本。

要使用 Git LFS,您需要一个支持 Git LFS 的主机,例如 Bitbucket CloudBitbucket Data Center。存储库用户需要安装 Git LFS 命令行客户端,或者安装支持 Git LFS 的 GUI 客户端,例如 Sourcetree。有趣的事实:发明 Sourcetree 的 Atlassian 开发人员 Steve Streeting 也是 Git LFS 项目的主要贡献者,因此 Sourcetree 和 Git LFS 的合作相当不错。

什么是 Git LFS?

安装 Git LFS

  1. 安装 Git LFS 有三种简单的方法:

    a. 使用您最喜欢的软件包管理器进行安装。git-lfs 软件包适用于 Homebrew、MacPorts、dnf 和 packagecloud;或

    b. 从项目网站下载并安装 Git LFS;或

    c. 安装 Sourcetree,这是一款与 Git LFS 捆绑在一起的免费 Git GUI 客户端。

  2. git-lfs 进入您的路径后,运行 git lfs install 来初始化 Git LFS(如果您安装了 Sourcetree,可以跳过这个步骤):

     $ git lfs install Git LFS 已初始化。 

    您只需运行一次 git lfs install 即可。您的系统初始化后,Git LFS 将在您克隆包含 Git LFS 内容的存储库时自动引导。

创建一个新的 Git LFS 存储库

要创建一个新的 Git LFS 感知存储库,您需要在创建存储库后运行 git lfs install:

# initialize Git
$ mkdir Atlasteroids
$ cd Atlasteroids
$ git init
Initialized empty Git repository in /Users/tpettersen/Atlasteroids/.git/
  
# initialize Git LFS
$ git lfs install
Updated pre-push hook.
Git LFS initialized.

这会在存储库中安装一个特殊的 pre-push Git 钩子,它会在您 git push 时将 Git LFS 文件传输到服务器。

所有 Bitbucket Cloud 存储库都会自动启用 Git LFS。对于 Bitbucket Data Center,您需要在存储库设置中启用 Git LFS:

Bitbucket Server Git LFS

为您的存储库初始化 Git LFS 后,您可以使用 git lfs track 指定要跟踪的文件。

克隆现有 Git LFS 存储库

安装 Git LFS 后,您可以使用 git clone 像往常一样克隆 Git LFS 存储库。克隆流程结束时,Git 将查阅默认分支(通常是 main 分支),完成签出流程所需的任何 Git LFS 文件都将自动为您下载。例如:

$ git clone git@bitbucket.org:tpettersen/Atlasteroids.git
Cloning into 'Atlasteroids'...
remote: Counting objects: 156, done.
remote: Compressing objects: 100% (154/154), done.
remote: Total 156 (delta 87), reused 0 (delta 0)
Receiving objects: 100% (156/156), 54.04 KiB | 31.00 KiB/s, done.
Resolving deltas: 100% (87/87), done.
Checking connectivity... done.
Downloading Assets/Sprites/projectiles-spritesheet.png (21.14 KB)
Downloading Assets/Sprites/productlogos_cmyk-spritesheet.png (301.96 KB)
Downloading Assets/Sprites/shuttle2.png (1.62 KB)
Downloading Assets/Sprites/space1.png (1.11 MB)
Checking out files: 100% (81/81), done.

这个存储库中有四个 PNG 正由 Git LFS 跟踪。运行 git clone 时,每次下载一个 Git LFS 文件,因为指针文件已从存储库中签出。

加快克隆速度

如果您要克隆一个包含大量 LFS 文件的存储库,那么显式的 git lfs clone 命令可以提供更好的性能:

$ git lfs clone git@bitbucket.org:tpettersen/Atlasteroids.git
Cloning into 'Atlasteroids'...
remote: Counting objects: 156, done.
remote: Compressing objects: 100% (154/154), done.
remote: Total 156 (delta 87), reused 0 (delta 0)
Receiving objects: 100% (156/156), 54.04 KiB | 0 bytes/s, done.
Resolving deltas: 100% (87/87), done.
Checking connectivity... done.
Git LFS: (4 of 4 files) 1.14 MB / 1.15 MB

git lfs clone 命令不是一次下载一个 Git LFS 文件,而是等到签出完成,然后批量下载所有所需的 Git LFS 文件。这利用了并行下载的优势,显著减少了 HTTP 请求和生成进程的数量(这对于提高 Windows 性能尤其重要)。

拉取并签出

就像克隆一样,您可以使用普通的 git pull 从 Git LFS 存储库中拉取。拉取完成后,所有需要的 Git LFS 文件都将作为自动签出流程的一部分进行下载:

$ git pull
Updating 4784e9d..7039f0a
Downloading Assets/Sprites/powerup.png (21.14 KB)
Fast-forward
 Assets/Sprites/powerup.png      |    3 +
 Assets/Sprites/powerup.png.meta | 4133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 4136 insertions(+)
 create mode 100644 Assets/Sprites/projectiles-spritesheet.png
 create mode 100644 Assets/Sprites/projectiles-spritesheet.png.meta

无需显式命令即可检索 Git LFS 内容。但是,如果由于意外原因导致签出失败,则可以使用 git lfs pull 下载当前提交中任何缺失的 Git LFS 内容:

$ git lfs pull
Git LFS: (4 of 4 files) 1.14 MB / 1.15 MB

加快拉取

就像 git lfs clone 一样,git lfs pull 会批量下载您的 Git LFS 文件。如果您知道自上次提取后有大量文件发生了变化,则可能希望在签出时禁用自动 Git LFS 下载,然后使用明确的 git lfs pull 批量下载您的 Git LFS 内容。这可以通过在调用 git pull 时使用 -c 选项覆盖您的 Git 配置来完成:

$ git -c filter.lfs.smudge= -c filter.lfs.required=false pull && git lfs pull

由于这相当麻烦,您可能需要创建一个简单的 Git 别名来为您执行 Git 和 Git LFS 的批量提取:

$ git config --global alias.plfs "\!git -c filter.lfs.smudge= -c filter.lfs.required=false pull && git lfs pull"
$ git plfs

需要下载大量 Git LFS 文件时(同样,尤其是在 Windows 上),这将极大地提高性能。

使用 Git LFS 跟踪文件

向存储库添加新类型的大文件时,您需要告诉 Git LFS 通过使用 git lfs track 命令指定模式来跟踪它:

$ git lfs track "*.ogg"
Tracking *.ogg

请注意,"*.ogg" 周围的引号很重要。省略它们将导致通配符被 shell 扩展,并且会为每个 .ogg 创建单独的条目您当前目录中的文件:

# probably not what you want
$ git lfs track *.ogg
Tracking explode.ogg
Tracking music.ogg
Tracking phaser.ogg

Git LFS 支持的模式与 .gitignore 支持的模式相同,例如:

# track all .ogg files in any directory
$ git lfs track "*.ogg"
  
# track files named music.ogg in any directory
$ git lfs track "music.ogg"
  
# track all files in the Assets directory and all subdirectories
$ git lfs track "Assets/"
  
# track all files in the Assets directory but *not* subdirectories
$ git lfs track "Assets/*"
  
# track all ogg files in Assets/Audio
$ git lfs track "Assets/Audio/*.ogg"
  
# track all ogg files in any directory named Music
$ git lfs track "**/Music/*.ogg"
  
# track png files containing "xxhdpi" in their name, in any directory
$ git lfs track "*xxhdpi*.png

这些模式是相对于您运行 git lfs track 命令的目录的。为了简单起见,最好从存储库的根目录运行 git lfs track。请注意,Git LFS 不支持负面模式,与 .gitignore 不同。

运行 git lfs track 后,您会注意到一个名为 .gitattributes 的新文件在您运行命令的目录中。.gitattributes 是一种 Git 机制,用于将特殊行为绑定到特定的文件模式。Git LFS 会自动创建或更新 .gitattributes 文件,将跟踪的文件模式绑定到 Git LFS 筛选器。但是,您需要自己将对 .gitattributes 文件的任何更改提交到您的存储库:

$ git lfs track "*.ogg"
Tracking *.ogg
  
$ git add .gitattributes
  
$ git diff --cached
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b6dd0bb
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.ogg filter=lfs diff=lfs merge=lfs -text
  
$ git commit -m "Track ogg files with Git LFS"

为了便于维护,最简单的做法是将所有 Git LFS 模式保存在单个 .gitattributes 文件中,通过始终从存储库的根目录运行 git lfs track 来实现。但是,您可以显示当前由 Git LFS(和其定义所在的 .gitattributes 文件)跟踪的所有模式的列表,方法是通过调用不带参数的 git lfs track

$ git lfs track
Listing tracked paths
    *.stl (.gitattributes)
    *.png (Assets/Sprites/.gitattributes)
    *.ogg (Assets/Audio/.gitattributes)

您只需从 .gitattributes 文件中删除相应的行,或者运行 git lfs untrack 命令,即可停止使用 Git LFS 跟踪特定模式:

$ git lfs untrack "*.ogg"
Untracking *.ogg
$ git diff
diff --git a/.gitattributes b/.gitattributes
index b6dd0bb..e69de29 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +0,0 @@
-*.ogg filter=lfs diff=lfs merge=lfs -text

运行 git lfs untrack 后,您必须自己再次提交对 .gitattributes 的更改。

提交和推送

您可以像往常一样提交并推送到包含 Git LFS 内容的存储库。如果您已提交对 Git LFS 跟踪的文件的更改,则在 Git LFS 内容传输到服务器时,您将看到 git push 的一些额外输出:

$ git push
Git LFS: (3 of 3 files) 4.68 MB / 4.68 MB                                                                                               
Counting objects: 8, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 1.16 KiB | 0 bytes/s, done.
Total 8 (delta 1), reused 0 (delta 0)
To git@bitbucket.org:tpettersen/atlasteroids.git
   7039f0a..b3684d3  main -> main

如果由于某种原因传输 LFS 文件失败,则推送将被中止,您可以放心地重试。与 Git 一样,Git LFS 存储也是内容可寻址的:内容是根据密钥存储的,密钥是内容本身的 SHA-256 哈希。这意味着再次尝试将 Git LFS 文件传输到服务器始终是安全的,您不能意外地用错误的版本覆盖 Git LFS 文件的内容。

在主机之间移动 Git LFS 存储库

要将 Git LFS 存储库从一个托管提供商迁移到另一个托管提供商,您可以结合使用 git lfs fetchgit lfs push 并指定 --all option

例如,要将所有 Git 和 Git LFS 存储库从名为 github 的远程存储库移动到名为 bitbucket 的远程存储库😉:

# create a bare clone of the GitHub repository
$ git clone --bare git@github.com:kannonboy/atlasteroids.git
$ cd atlasteroids
  
# set up named remotes for Bitbucket and GitHub
$ git remote add bitbucket git@bitbucket.org:tpettersen/atlasteroids.git
$ git remote add github git@github.com:kannonboy/atlasteroids.git
  
# fetch all Git LFS content from GitHub
$ git lfs fetch --all github
 
# push all Git and Git LFS content to Bitbucket
$ git push --mirror bitbucket
$ git lfs push --all bitbucket

获取额外的 Git LFS 历史记录

Git LFS 通常只下载您在本地实际签出的提交所需的文件。但是,您可以强制 Git LFS 使用 git lfs fetch --recent 为其他最近修改的分支下载额外内容:

$ git lfs fetch --recent
Fetching main
Git LFS: (0 of 0 files, 14 skipped) 0 B / 0 B, 2.83 MB skipped                                                                           Fetching recent branches within 7 days
Fetching origin/power-ups
Git LFS: (8 of 8 files, 4 skipped) 408.42 KB / 408.42 KB, 2.81 MB skipped
Fetching origin/more-music
Git LFS: (1 of 1 files, 14 skipped) 1.68 MB / 1.68 MB, 2.83 MB skipped

这对于外出吃午饭时批量下载新的 Git LFS 内容非常有用,或者如果您打算审核队友的工作,但由于互联网连接有限而无法下载内容。例如,开始工作之前,您可能希望运行 git lfs fetch --recent

Git LFS 将包含超过七天的提交的任何分支或标记视为最近提交。您可以通过设置 lfs.fetchrecentrefsdays 属性来配置被视为最近的天数:

# download Git LFS content for branches or tags updated in the last 10 days
$ git config lfs.fetchrecentrefsdays 10

默认情况下,git lfs fetch --recent 只会在最近的分支或标记的顶端下载提交的 Git LFS 内容。

git lfs - git lfs fetch --recent

但是,您可以通过配置 lfs.fetchrecentcommitsdays 属性将 Git LFS 配置为下载最近分支和标记上之前提交的内容:

# download the latest 3 days of Git LFS content for each recent branch or tag
$ git config lfs.fetchrecentcommitsdays 3

谨慎使用此设置:如果您的分支移动速度很快,这可能会导致大量数据被下载。但是,如果您需要查看分支上的插页式广告更改、跨分支挑选提交或重写历史记录,它可能会很有用。

git lfs - git lfs fetch --recent commits

正如在主机之间移动 Git LFS 存储库中所述,您也可以选择提取存储库的所有 Git LFS 内容,只需使用 git lfs fetch --all

$ git lfs fetch --all
Scanning for all objects ever referenced...
✔ 23 objects found                                                                                                                      
Fetching objects...
Git LFS: (9 of 9 files, 14 skipped) 2.06 MB / 2.08 MB, 2.83 MB skipped

删除本地 Git LFS 文件

您可以使用 git lfs prune 命令从本地 Git LFS 缓存中删除文件:

$ git lfs prune
✔ 4 local objects, 33 retained                                                                                                         
Pruning 4 files, (2.1 MB)
✔ Deleted 4 files

这将删除所有被认为是文件的本地 Git LFS 文件。旧文件是指任何被引用的文件:

  • 当前已签出的提交
  • 尚未推送的提交(到 origin,或者 lfs.pruneremotetocheck 设置的任何位置)
  • 最近的提交

默认请假下,最近的提交是最近天内创建的任何提交。这是通过以下内容相加计算得出的:

git lfs prune

您可以配置修剪偏移以将 Git LFS 内容保留更长时间:

# don't prune commits younger than four weeks (7 + 21)
$ git config lfs.pruneoffsetdays 21

与 Git 的内置垃圾回收不同,Git LFS 内容不会自动修剪,因此定期运行 git lfs prune 是缩小本地存储库大小的好主意。

您可以使用 git lfs prune --dry-run 测试一下修剪操作会产生什么效果:

$ git lfs prune --dry-run
✔ 4 local objects, 33 retained                                                                                                         
4 files would be pruned (2.1 MB)

究竟哪些 Git LFS 对象会被 git lfs prune --verbose --dry-run 修剪:

$ git lfs prune --dry-run --verbose
✔ 4 local objects, 33 retained                                                                                                         
4 files would be pruned (2.1 MB)
 * 4a3a36141cdcbe2a17f7bcf1a161d3394cf435ac386d1bff70bd4dad6cd96c48 (2.0 MB)
 * 67ad640e562b99219111ed8941cb56a275ef8d43e67a3dac0027b4acd5de4a3e (6.3 KB)
 * 6f506528dbf04a97e84d90cc45840f4a8100389f570b67ac206ba802c5cb798f (1.7 MB)
 * a1d7f7cdd6dba7307b2bac2bcfa0973244688361a48d2cebe3f3bc30babcf1ab (615.7 KB)

--verbose 模式输出的长十六进制字符串是要修剪的 Git LFS 对象的 SHA-256 哈希(也称为对象 ID 或 OID)。您可以使用查找引用 Git LFS 对象的路径或提交中描述的技术来查找有关将被修剪的对象的更多信息。

作为额外的安全检查,您可以使用 --verify-remote 选项来检查远程 Git LFS 存储在修剪 Git LFS 对象之前是否有这些对象的副本:

$ git lfs prune --verify-remote
✔ 16 local objects, 2 retained, 12 verified with remote                                                                                             
Pruning 14 files, (1.7 MB)
✔ Deleted 14 files

这会使修剪流程显著变慢,但知道任何已修剪的对象都可以从服务器上恢复,因此您可以高枕无忧。通过在全局范围内配置 lfs.pruneverifyremoteaways 属性,您可以为系统永久启用 --verify-remote 选项:

$ git config --global lfs.pruneverifyremotealways true 

或者,您可以通过省略上述命令中的 --global 选项来仅对上下文存储库启用远程验证。

从服务器删除远程 Git LFS 文件

Git LFS 命令行客户端不支持从服务器上修剪文件,因此如何删除它们取决于您的托管服务提供商。

在 Bitbucket Cloud 中,您可以通过存储库设置 > Git LFS 查看和删除 Git LFS 文件:

Bitbucket Cloud - 从服务器上删除 lfs

请注意,每个 Git LFS 文件都是按其 SHA-256 OID 索引的,引用每个文件的路径在用户界面中不可见。这是因为在许多不同的提交中可能有许多不同的路径可以引用给定对象,因此查找它们将是一个非常缓慢的过程。

要确定给定的 Git LFS 文件实际包含什么,有三个选项:

  • 查看 Bitbucket Git LFS 用户界面左栏中的文件预览图像和文件类型
  • 使用 Bitbucket Git LFS UI 右栏中的链接下载文件-搜索引用 Git LFS 对象的 SHA-256 OID 的提交,如下一节所述

查找引用 Git LFS 对象的路径或提交

如果您有 Git LFS SHA-256 OID,您可以使用 git log --all -p -S 来确定哪些提交引用了它:

$ git log --all -p -S 3b6124b8b01d601fa20b47f5be14e1be3ea7759838c1aac8f36df4859164e4cc
commit 22a98faa153d08804a63a74a729d8846e6525cb0
Author: Tim Pettersen <tpettersen@atlassian.com>
Date:   Wed Jul 27 11:03:27 2016 +1000
 
    Projectiles and exploding asteroids
 
diff --git a/Assets/Sprites/projectiles-spritesheet.png
new file mode 100755
index 0000000..49d7baf
--- /dev/null
+++ b/Assets/Sprites/projectiles-spritesheet.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b6124b8b01d601fa20b47f5be14e1be3ea7759838c1aac8f36df4859164e4cc
+size 21647

git log 命令从任何分支 (--all) 的提交中生成补丁 (-p),该分支添加或删除包含指定字符串 (Git LFS SHA-256 OID) 的行 (-S)。

该补丁显示了 LFS 对象的提交和路径,以及谁添加了它以及何时提交。您可以简单地签出提交,如果需要,Git LFS 会下载文件并将其放在您的工作副本中。

如果您怀疑某个特定的 Git LFS 对象在您当前的 HEAD 中,或者在特定的分支上,您可以使用 git grep 来查找引用它的文件路径:

# find a particular object by OID in HEAD
$ git grep 3b6124b8b01d601fa20b47f5be14e1be3ea7759838c1aac8f36df4859164e4cc HEAD
HEAD:Assets/Sprites/projectiles-spritesheet.png:oid sha256:3b6124b8b01d601fa20b47f5be14e1be3ea7759838c1aac8f36df4859164e4cc
  
# find a particular object by OID on the "power-ups" branch
$ git grep e88868213a5dc8533fc9031f558f2c0dc34d6936f380ff4ed12c2685040098d4 power-ups
power-ups:Assets/Sprites/shield2.png:oid sha256:e88868213a5dc8533fc9031f558f2c0dc34d6936f380ff4ed12c2685040098d4

您可以用任何包含 Git LFS 对象的引用、提交或树来替换 HEADpower-ups

包括/排除 Git LFS 文件

在某些情况下,您可能只想为特定提交下载可用的 Git LFS 内容的子集。例如,在配置 CI 版本以运行单元测试时,您可能只需要源代码,因此可能需要排除构建代码不需要的重量级文件。

您可以使用 git lfs fetch -X(或 --exclude)排除模式或子目录:

$ git lfs fetch -X "Assets/**" 

或者,您可能只想包含特定的模式或子目录。例如,音频工程师可以使用 git lfs fetch-I(或 --include)只获取 oggwav 文件:

$ git lfs fetch -I "*.ogg,*.wav" 

如果合并包含和排除,则只提取与包含模式匹配不匹配排除模式的文件。例如,您可以使用以下方式获取资产目录中 gifs 之外的所有内容:

$ git lfs fetch -I "Assets/**" -X "*.gif" 

排除和包含支持与 git lfs track.gitignore 相同的模式。您可以通过设置 lfs.fetchincludelfs.fetchexclude 配置属性来使这些模式在特定存储库中永久存在:

$ git config lfs.fetchinclude "Assets/**"
$ git config lfs.fetchexclude "*.gif"

通过附加 --global 选项,也可以将这些设置应用于系统上的每个存储库。

锁定 Git LFS 文件

不幸的是,没有解决二进制合并冲突的简单方法。使用 Git LFS 文件锁定功能,您可以按扩展名或文件名锁定文件,并防止二进制文件在合并期间被覆盖。

为了利用 LFS 的文件锁定功能,您首先需要告诉 Git 哪种类型的文件是可锁定的。在下方示例中,`--lockable` 标记被附加到 `git lfs track` 命令,该命令既将 PSD 文件存储在 LFS 中,又将其标记为可锁定。

$ git lfs track "*.psd" --lockable

然后将以下内容添加到您的 .gitattributes 文件中:

*.psd filter=lfs diff=lfs merge=lfs -text lockable

准备对 LFS 文件进行更改时,您将使用 lock 命令在 Git 服务器上将该文件登记为已锁定。

$ git lfs lock images/foo.psd
Locked images/foo.psd

一旦不再需要文件锁,就可以使用 Git LFS 的解锁命令将其删除。

$ git lfs unlock images/foo.psd

可以使用 --force 标记覆盖 Git LFS 文件锁,这与 git push 类似。除非您完全确定自己知道自己在做什么,否则不要使用 --force 标记。

$ git lfs unlock images/foo.psd --force

Git LFS 的工作原理

如果您有兴趣进一步了解干净和涂抹筛选器、预推送钩子以及 Git LFS 背后其他有趣的计算机科学,请查阅 Atlassian 在 LinuxCon 2016 上关于 Git LFS 的这篇演示: