使用Git管理大型二进制文件

551
我正在寻求处理大型二进制文件的意见,这些文件是我源代码(Web应用程序)所依赖的。我们目前正在讨论几个选择:
  1. 手动复制所有二进制文件。
    • 优点:不确定。
    • 缺点:我强烈反对这种方法,因为在设置新站点/迁移旧站点时会增加错误的可能性。又多了一个障碍。
  2. 使用Git管理所有二进制文件。
    • 优点:消除“忘记”复制重要文件的可能性
    • 缺点:使存储库变得膨胀,并降低了管理代码库、检出、克隆等的灵活性,这将需要相当长的时间。
  3. 分离存储库。
    • 优点:仍然可以快速检出/克隆源代码,并且图像以其自己的存储库进行适当地归档。
    • 缺点:去掉了只有一个Git存储库的简便性。它肯定会引入一些我没有考虑过的其他问题。
您对此有何经验/想法?
还有:有人有使用多个Git存储库并在一个项目中管理它们的经验吗?
这些文件是一个程序的图像文件,该程序使用这些文件生成PDF。这些文件不会经常更改(可能长达数年),但是它们对于程序非常重要。如果没有这些文件,程序将无法正常工作。

29
当需要对二进制文件进行版本控制时怎么办?我在考虑艺术家团队合作处理资产时。 - Dan
3
如果必要的话,你需要权衡可用资源(磁盘、带宽、CPU时间)与所获得的利益之间的平衡。 - pi.
4
请注意,如果没有文件锁定,当多个人需要在同一个二进制文件上工作时,Git 的表现并不理想。 - yoyo
1
请参阅基于git的备份文件bup(https://dev59.com/qmMm5IYBdhLWcg3wFL04#19494211)。 - VonC
1
这里是 http://www.bestechvideos.com/tag/gitcasts - doughgle
显示剩余4条评论
13个回答

318

我最近发现了 git-annex 这个工具,觉得它非常棒。它专门用于高效管理大型文件。我将其用于管理我的照片、音乐等收藏品。git-annex 的开发活跃度很高。文件的内容可以从 Git 存储库中移除,Git 只会通过符号链接跟踪树形结构。然而,在拉取/推送后获取文件内容需要进行第二步操作,例如:

$ git annex add mybigfile
$ git commit -m'add mybigfile'
$ git push myremote
$ git annex copy --to myremote mybigfile ## This command copies the actual content to myremote
$ git annex drop mybigfile ## Remove content from local repo
...
$ git annex get mybigfile ## Retrieve the content
## or to specify the remote from which to get:
$ git annex copy --from myremote mybigfile

有许多可用的命令,并且网站上有很好的文档。在 Debian 上有一个软件包。


13
哇!赞同此精彩内容!这个实现了我最近提出的想法,而且更多功能。更不可思议的是,它是用 Haskell 写成的。顺便说一句,git-media 也是一个不错的选择。 - cdunn2001
36
但是,Annex不支持Windows系统。这对游戏开发者来说是一个问题。 - A.A. Grapsas
8
我听说Steam将不再支持Windows,并增加对Linux的支持... ;) 但是说真的,将其转移并不难吧?我猜一般的游戏开发者都能做到。 - Sam Watkins
7
关键问题在于,在正常配置下,Windows符号链接需要管理员权限才能创建。 - Laurens Holst
7
我刚发现了这个页面:https://git-annex.branchable.com/install/Windows/,上面写着现在 git annex 已经支持 Windows 了。如果有人在 Windows 上测试过它,我希望听听他或她的经验! - Kouichi C. Nakamura
显示剩余7条评论

179
如果程序没有这些文件就无法工作,将它们拆分到一个单独的仓库似乎不是一个好主意。我们有大型测试套件,我们会将它们拆分到一个单独的仓库中,但那些是真正的“辅助”文件。
然而,你可以在一个单独的仓库中管理这些文件,然后使用 git-submodule 以明智的方式将它们拉入到你的项目中。因此,你仍然拥有所有源代码的完整历史记录,但据我所知,你只会有图像子模块的一个相关修订版本。 git-submodule 工具应该能帮助你保持代码的正确版本与图像的正确版本一致。
这里有一篇来自 Git Book 的submodules介绍文章。

12
据我理解,您只会有一个相关的图像子模块版本。但我认为这是不正确的。 - Robin Green
29
没错,子模块是一个完整的 Git 仓库,只是恰好嵌套在父仓库内部。它知道自己的全部历史记录。你可以不那么频繁地在其中提交,但如果你存储了与父仓库相同的内容,它将会有与父仓库相同的问题。 - Cascabel
6
如果你的大型二进制文件会在一定时间间隔内发生变化,那么这个解决方案就不太好。我们有一个仓库非常臃肿,因为每次构建都会将新的二进制文件存储在其中。如果你不使用 Windows 系统,如下所述,Annex 是一个很好的解决方案。如果你正在使用 Windows,那么只能继续寻找其他解决方案了。 - A.A. Grapsas
5
在代码库中拥有大型二进制文件的另一个问题是性能。Git 并不适用于处理大型二进制文件,一旦仓库大小超过 3G,性能就会迅速下降。这意味着在代码库中存放大型二进制文件会限制您的托管选项。 - zoul
1
子模块可以通过创造性地滥用子模块来减少检出数据传输要求:当您想要更新子模块内容时,创建一个没有父级的新提交,然后将超级项目(主git存储库)指向新创建的没有父级的提交。逻辑上,这为子模块创建了一个断开的历史记录,但作为回报,任何版本的子模块都更容易传输,因为该版本没有历史记录。 - Mikko Rantalainen
显示剩余3条评论

60

3
“lfs-test-server”被声明为不适用于生产环境。实际上,我正在开发生产环境下的LFS服务器(https://github.com/artemkin/git-lfs-server)。它正在进行中,但已经可用,并且我们正在内部测试它。 - Stas
您是否可以使用git lfs来检出以前的二进制文件版本? - mucaho
1
@mucaho 你应该:git checkout 的语法没有改变,lfs smudge 脚本仍然需要被调用。 - VonC

34
请查看git bup,它是一个Git扩展程序,可智能存储大型二进制文件于Git存储库中。您可以将其作为子模块使用,但您不必担心存储库难以处理的问题。他们的样例用途之一是在Git中存储VM镜像。
我实际上没有看到更好的压缩比率,但我的存储库中没有真正的大型二进制文件。结果会因情况而异。

3
bup提供存储功能(内部使用奇偶校验归档来实现冗余,使用git进行压缩、去重和历史记录),但它不扩展git。git-annex是一个git扩展,提供了bup存储后端 - Tobu
@Tobu 当我发布这篇文章的时候,git annex(在主流版本中)还不存在。 - sehe
2
bup 绝对是管理大文件的有趣工具。我想指出一个 UI 上的区别:你在任何存储库上下文之外使用 bup 命令,而 git 只是一种实现细节。 - Tobu

29
你也可以使用 git-fat。我喜欢它只依赖于标准的Python和rsync。它还支持通常的Git工作流程,并提供以下不言自明的命令:
git fat init
git fat push
git fat pull

另外,您需要将.gitfat文件检入到您的代码库中,并修改您的.gitattributes以指定您想要用git fat管理的文件扩展名。

您可以使用普通的git add添加二进制文件,然后根据您的gitattributes规则调用git fat

最后,它具有一个优点,即实际存储二进制文件的位置可以在代码库和用户之间共享,并且支持rsync支持的任何内容。

更新:如果您正在使用Git-SVN桥接器,请勿使用git-fat。它会从Subversion代码库中删除二进制文件。但是,如果您使用的是纯Git代码库,则效果非常好。


26
我会使用子模块(像Pat Notz一样)或两个不同的代码库。如果您经常修改二进制文件,则应尝试最小化巨大代码库对清理历史的影响:
几个月前,我有一个非常类似的问题:约21GB的MP3文件,未分类(糟糕的名称,糟糕的id3标签,我不知道是否喜欢那个MP3文件...),并在三台计算机上复制。
我使用了一个带有主要Git代码库的外部硬盘驱动器,并将其克隆到每台计算机中。然后,我开始以惯常的方式对它们进行分类(推送、拉取、合并...多次删除和重命名)。
最后,我只剩下了约6GB的MP3文件,而.git目录中有约83GB。我使用git-write-tree和git-commit-tree创建了一个新的提交,没有先前的提交,然后启动了一个指向该提交的新分支。该分支的"git log"仅显示了一个提交。
然后,我删除了旧分支,仅保留了新分支,删除了引用日志,并运行了“git prune”:此后,我的.git文件夹仅重量为约6GB...
您可以以同样的方式定期“清除”巨大的代码库:您的“git clone”将更快速。

我曾经做过类似的事情,我不小心将一个仓库合并成了两个不同的仓库。不过这是一个有趣的用法模式。 :) - pi.
1
这是否与以下命令相同:rm -f .git; git init; git add . ; git commit -m "Trash the history." - Pat Notz
1
是的,在我的mp3案例中只是一样的。但有时您不想触及您的分支和标签(公共存储库中没有空间减少),但您希望加快仅对分支进行“git clone/fetch/pull”的速度(专用于该分支存储库的空间较少)。 - Daniel Fanjul

15
我想提出的解决方案基于孤立分支和轻微滥用标签机制,即 *孤立标签二进制存储(OTABS)。
如果您可以使用GitHub的LFS或其他第三方,请务必使用。如果不能,请继续阅读。请注意,此解决方案是一种黑客行为,应该作为这样处理。
OTABS的期望属性:
  • 它是一个纯粹的git解决方案,仅使用git -- 它可以在没有第三方软件(如git-annex)或第三方基础设施(如github的LFS)的情况下完成工作。
  • 它可以高效地存储二进制文件,即不会使您的存储库历史记录膨胀。
  • git pullgit fetch,包括 git fetch --all,仍然具有带宽效率,即默认情况下不会从远程拉取所有大型二进制文件。
  • 它可以在Windows上运行。
  • 它将所有内容存储在单个git存储库中。
  • 它允许删除过时的二进制文件(与bup不同)。

OTABS的不良属性

  • 这使得git clone可能效率低下(但根据您的使用情况不一定如此)。如果您部署此解决方案,您可能需要建议您的同事使用git clone -b master --single-branch <url>而不是git clone。这是因为默认情况下,git clone会复制整个存储库,包括您通常不想浪费带宽的未引用提交等内容。取自SO 4811434
  • 这使得git fetch <remote> --tags带宽效率低下,但不一定是存储效率低下。您可以建议您的同事不要使用它。
  • 您将不得不定期使用git gc技巧来从您不再需要的任何文件中清理您的存储库。
  • 它不像bupgit-bigfiles那样高效。但它分别更适合您尝试做的事情并且更易于使用。您可能会遇到数十万个小文件或几个GB范围内的文件的问题,但请继续阅读以获取解决方法。

添加二进制文件

在开始之前,请确保您已经提交了所有更改,您的工作树是最新的,并且您的索引不包含任何未提交的更改。如果发生任何灾难,将所有本地分支推送到远程(github等)可能是一个好主意。

  1. 创建一个新的孤儿分支。使用git checkout --orphan binaryStuff即可完成。这将产生一个完全与其他分支断开连接的分支,您在此分支中进行的第一次提交将没有父级,这将使它成为根提交。
  2. 使用git rm --cached * .gitignore清除索引。
  3. 深呼吸并使用rm -fr * .gitignore删除整个工作树。内部的.git目录将保持不变,因为通配符*不匹配它。
  4. 复制您的VeryBigBinary.exe或您的VeryHeavyDirectory/。
  5. 添加并提交。
  6. 现在变得棘手了——如果您将其作为分支推送到远程,则所有开发人员在下次调用git fetch时都会下载它,从而阻塞他们的连接。您可以通过推送标签而不是分支来避免这种情况。如果他们有一个输入git fetch <remote> --tags的习惯,这仍然可能影响您同事的带宽和文件系统存储,但请继续阅读以获取解决方法。现在,请执行git tag 1.0.0bin
  7. 推送您的孤儿标签git push <remote> 1.0.0bin
  8. 为了避免意外推送您的二进制分支,您可以删除它git branch -D binaryStuff。您的提交不会被标记为垃圾收集,因为指向它的孤儿标签1.0.0bin足以使其保持活动状态。

检查二进制文件

  1. 我(或我的同事)如何将VeryBigBinary.exe检出到当前工作树中?如果您当前的工作分支是master,您可以简单地使用git checkout 1.0.0bin -- VeryBigBinary.exe
  2. 如果您没有下载孤立标记1.0.0bin,则此操作将失败,在这种情况下,您需要先使用git fetch <remote> 1.0.0bin
  3. 您可以将VeryBigBinary.exe添加到主分支的.gitignore中,以便您的团队中没有人会意外地污染项目的主要历史记录。

完全删除二进制文件

如果您决定从本地存储库、远程存储库和同事的存储库中完全清除VeryBigBinary.exe,则可以执行以下操作:

  1. 删除远程孤立标签:git push <remote> :refs/tags/1.0.0bin
  2. 删除本地孤立标签(同时删除其他未引用的标签):git tag -l | xargs git tag -d && git fetch --tags。取自 SO 1841341,稍作修改。
  3. 使用 git gc 技巧在本地删除现在未被引用的提交:git -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 -c gc.rerereresolved=0 -c gc.rerereunresolved=0 -c gc.pruneExpire=now gc "$@"。它也会删除所有其他未被引用的提交。取自 SO 1904860
  4. 如果可能,在远程重复 git gc 技巧。如果你是自托管的仓库,则可能可行,但对于某些 Git 提供商(如 GitHub)或某些企业环境可能不可行。如果你的提供商的基础设施会在它们自己的时间内清理未引用的提交,请让其保持原样。如果你处于企业环境中,你可以建议 IT 每周运行一个 cron 作业来垃圾回收远程。无论他们是否这样做,都不会对你的团队带来任何带宽和存储方面的影响,只要你建议同事们始终使用 git clone -b master --single-branch <url> 而不是 git clone
  5. 所有想要摆脱过时孤立标签的同事只需要应用步骤 2-3。
  6. 然后重复添加二进制文件的步骤 1-8,以创建一个新的孤立标签 2.0.0bin。如果你担心同事会输入 git fetch <remote> --tags,实际上你可以再次将其命名为 1.0.0bin。这将确保下一次他们获取所有标签时,旧的 1.0.0bin 将未被引用并标记为随后的垃圾回收(使用步骤 3)。当你尝试在远程覆盖标签时,你必须使用 -f,如下所示:git push -f <remote> <tagname>

后记

  • OTABS不会触及您的主要或任何其他源代码/开发分支。提交哈希、所有历史记录和这些分支的小尺寸都不受影响。如果您已经用二进制文件膨胀了源代码历史记录,那么您需要将其清理为单独的工作(此脚本)可能会有用。

  • 在Windows上使用git-bash已确认可行。

  • 应用一组标准技巧以使二进制文件的存储更加高效是一个好主意。频繁运行git gc(不带任何额外参数)可以通过使用二进制增量来优化您的文件的底层存储。但是,如果您的文件不太可能在提交之间保持相似,则可以完全关闭二进制增量。此外,因为压缩已经压缩或加密的文件(如.zip、.jpg或.crypt)没有任何意义,所以git允许您关闭底层存储的压缩。不幸的是,这是一个全局设置,会影响您的源代码。

  • 您可能希望将OTABS的某些部分编写成脚本,以便更快地使用。特别是,将完全删除二进制文件的步骤2-3编写成update git钩子可能会给git fetch带来引人注目但危险的语义(“获取并删除所有过时的内容”)。

  • 您可能希望跳过完全删除二进制文件的第4步,以保留所有二进制更改的完整历史记录,并以中央存储库膨胀为代价。本地存储库随时间保持精简。

  • 在Java世界中,可以将此解决方案与maven --offline结合使用,以创建一个完全存储在版本控制中的可重现离线构建(与gradle相比,使用maven更容易)。在Golang世界中,可以在此解决方案的基础上构建,以管理您的GOPATH,而不是使用go get。在Python世界中,可以将其与virtualenv相结合,以生成一个自包含的开发环境,而不必依赖于每次从头开始构建的PyPi服务器。

  • 如果您的二进制文件经常更改,例如构建产物,则可能需要编写一个解决方案,该解决方案将最近5个版本的构建产物存储在孤立标签monday_bintuesday_bin等中,以及每个发布版本的孤立标签1.7.8bin2.0.0bin等。您可以每天旋转weekday_bin并删除旧的二进制文件。这样,您就可以获得两个世界的最佳优势:保留源代码的整个历史记录,但只保留二进制依赖项的相关历史记录。此外,非常容易获取给定标签的二进制文件,而无需获取具有完整历史记录的整个源代码:git init && git remote add <name> <url> && git fetch <name> <tag>应该适用于您


你必须定期使用 git gc —— 在那里停止阅读。为什么有人会放弃他们最后的安全带,而选择某些骇客呢? - user1643723
@user1643723 运行 git gc 不会有安全问题。所有未被引用的提交将默认安全地保存在硬盘上至少30天:https://git-scm.com/docs/git-gc - Adam Kurkiewicz
感谢详细的说明。我想尝试将一些二进制依赖项存储在我的GitHub仓库中,以便它们不会在某人克隆仓库时默认下载,但可以手动下载并更新本地仓库。然而,在这一步骤中,我遇到了一个错误:git push <remote> 1.0.0bin - remote: error: GH001: Large files detected. You may want to try Git Large File Storage。看起来GitHub可能不再支持这个?所涉及的二进制文件大小为100MB。 - user5359531
2
说实话,如果你被允许在工作中使用Github,那么为什么不使用LFS呢?Github的团队为创建这个产品付出了很多努力,他们甚至为你提供了托管服务,并且他们的基础设施已经针对使用它进行了优化。这个技巧是为那些真的无法使用LFS或其他第三方工具,并且需要一个纯Git解决方案的情况而设计的。 - Adam Kurkiewicz
我还更新了答案,以更清楚地说明这个解决方案实际上有多么的hacky。 - Adam Kurkiewicz

13

在我看来,如果你经常要修改那些大文件,或者你打算执行很多git clonegit checkout,那么你应该认真考虑使用另一个Git仓库(或者也许是另一种访问这些文件的方式)。

但是,如果你像我们这样工作,并且你的二进制文件不经常被修改,那么第一次克隆/检出会很慢,但之后应该就跟你想要的一样快了(假设你的用户继续使用他们最初克隆的仓库)。


14
而且,分开的仓库并不会缩短检出时间,因为您仍然需要检出两个仓库! - Emil Sit
@EmilSit 如果你稳定地清理“二进制仓库”的历史记录,那么单独的仓库可以使检出时间大大缩短。此外,开发人员不必每次都检出两个仓库。 - FabienAndre
为什么不让主模块的构建脚本从第二个仓库获取二进制文件,逐个提取它们(就像这里:https://dev59.com/xHNA5IYBdhLWcg3wBo5m)? - akauppi
1
即使您的二进制文件不经常更改,如果您经常将分支推送到存储库以进行协作,则大文件仍然可能会影响您的工作流程。 - Timo Reimann

11

SVN似乎比Git更有效地处理二进制差异。

我需要为文档(JPEG文件、PDF文件和.odt文件)选择一个版本控制系统。我刚刚测试了添加一个JPEG文件并将其旋转90度四次(以检查二进制差异的效果)。Git的仓库增长了400%。 SVN的仓库仅增长了11%。

因此,SVN在处理二进制文件方面效率更高。

所以,我的选择是用Git处理源代码,使用SVN处理类似文档这样的二进制文件。


34
只需要在添加这4个文件后运行"git gc"(重新打包和垃圾收集)即可。Git不会立即压缩所有添加的内容,以便您获得一组文件的压缩(在大小上更有效),并且不会减慢单独压缩每个添加的对象的速度。但即使没有运行"git gc",Git最终也会为您进行压缩(在注意到累积了足够的未打包对象后)。 - nightingale
24
我创建了一个空的 Git 存储库,并添加(并提交)一个完全白色的 BMP 图像,大小为 41MB,在此之后,该存储库的总大小为 328KB。执行 git gc 后,该存储库的总大小减小到了 184KB。然后,我将一个像素从白色变成黑色,并提交此更改,总 Git 存储库大小增加到了 388KB,再次执行 git gc 后,总 Git 存储库大小减小到了 184KB。这表明,Git 在压缩和查找二进制文件的差异方面表现得相当出色。 - Tader
6
@jpierson 顺便说一下:我刚刚评论了二进制增量。如果Git管理具有大型(GB大小)文件的存储库,则会耗尽所有内存并进行交换。为此,请使用[git-annex](http://git-annex.branchable.com/)(已在其他答案中提到)... - Tader
12
因为这是完全不真实的,所以没有人提到它。在这个页面的中间部分,Subversion Copies 是廉价的。具体内容请参考:http://svnbook.red-bean.com/en/1.7/svn.branchmerge.using.html - Joris Timmermans
13
@Tader:你的测试结果不好。从Git的角度来看,你所谓的二进制文件更像是文本文件——比特流是以字节对齐的,可以进行有意义的局部差异比较;毕竟,改变一个像素基本上等同于改变文本文件中的一个字符(而现在谁还用未压缩的位图文件?)。尝试使用小视频、压缩图像、虚拟机、zip文件或其他任何东西进行相同的实验,你会发现Git无法高效地处理数据增量;事实上,对于不可压缩数据,这是根本不可能的。 - Eamon Nerbonne
显示剩余10条评论

6

git clone --filter从Git 2.19 +浅克隆

如果Git和GitHub开发人员使其足够用户友好(例如他们对于子模块仍然没有实现),这个新选项最终可能成为解决二进制文件问题的最终方案。

它允许实际上只获取您想要的服务器文件和目录,并与远程协议扩展一起引入。

有了这个,我们可以先进行浅克隆,然后使用构建系统自动化获取每种构建类型的blob。

甚至已经有一个--filter=blob:limit<size>,允许限制要获取的最大blob大小。

我提供了一个极简详细的示例,演示该功能的外观:如何仅克隆Git存储库的子目录?


近四年后,git clone --filter已被Gitlab和Github支持,但没有实用程序来管理按需下载到您的工作副本中的无限历史增长。此外,原生的git仍然远远不能替代可以管理PB级数据的Perforce:Github将存储库限制为100 GB,Azure DevOps将其限制为250 GB(在此点开始出现打包错误)。通常给出的建议暗示着超过10 GB的服务器性能开始下降。甚至远远不足以满足视频游戏开发所需的要求。 - Gabriel Morin
@GabrielMorin 关于历史增长,除了 git clone --depth 1,您希望看到哪些功能? - Ciro Santilli OurBigBook.com
@ciro-santilli-ourbigbook-com 我希望通过清理机制不断地重新应用 --filter=blob:limit<size>,以便将按需下载但我在可配置的一段时间内没有检出的 blob 再次转换为 promisor blobs。 我希望在源文件方面本地拥有项目的完整历史记录,同时排除所有游戏资产 blob,这些 blob 的大小可以从几 MB 到几 GB 不等。这与浅克隆即 --depth 1 给你的完全不同。 Git 还必须能够处理无限大小的文件。 - Gabriel Morin
对于大型游戏项目,仅当前文件的修订版本就可能达到500 GB+,因此您不能在.git文件夹中拥有多个副本。但是部分克隆一旦按需下载了这些blob,就无法摆脱它们。 如果原生git要与Perforce或PlasticSCM竞争,它需要解决4个问题: 1.不要在部分克隆中累积不需要的本地历史记录 2.当存储库中存在大型二进制文件时,不要减慢速度 3.当总存储库大小达到250 GB时,不要出现打包错误 4.让我们将最大的二进制文件转移到更便宜的存储设备上 - Gabriel Morin
@GabrielMorin 我明白了。我想到了 git gc,但不确定它是否能完全满足您的需求。这是我开始寻找的地方。也许您需要一个 git gc --filter 选项来实现您的请求。 - Ciro Santilli OurBigBook.com

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接