源码管理系统不是永远不出问题的黑盒子。如果你没有合理地维护好你的仓库,对于要使用它们的开发人员来说,这些仓库会变成一个重大的负担。有些工具能帮你维护它们,Roberto Tyley的BFG Repo-Cleaner for Git就是这样一个工具。这个工具可以用来删除那些你不小心提交到git分支上的二进制大文件。
InfoQ:给那些不熟悉git的读者解释一下,为什么二进制大文件是个问题?
Roberto:在git中,所有和你共用一个库的人,当他们把库下载下来时,他们会得到你这个项目的完整历史记录。一般情况下这没什么问题——真正的代码只占很少体积,但拥有完整的历史记录使你能够实施分布式的合作、快速得到diff结果以及更多的便利——但项目的历史记录会慢慢膨胀。如果你提交了大文件——比如100MB以上——你仓库的体积会飞速增长,并且当其他人clone这个仓库的时候,他们要下载的数据就更多了。最终你会意识到你犯了一个错误,但是就算你决定要通过一个commit来删除这些文件,它们其实还在那儿,在你的历史数据中,下载整个仓库所花的时间不会有任何变化。
除了大文件以外,不小心commit进去的密码或其他隐私数据也有这个问题——就算你删掉它们,它们实际上还在历史数据中。你只是犯了个错,但是git却不让你纠正。现在我们该怎么办?
InfoQ:要从git库中移除二进制或其他不合适的文件,最基本的方法是使用git-filter-branch命令。你能不能解释一下这个命令到底做了什么,以及为什么你认为它还不够好。
Roberto:没错,git-filter-branch命令是一个强大的工具——就像一把万能的电锯一样——它调用脚本来重写git的历史记录,从头开始写。你给它一个bash脚本,它会从头到尾针对每个commit运行这个脚本,一个接一个,逐步重建历史记录。你的脚本可以做你想做的任何事——删除文件、移除不想要的文本(比如密码),或者把每个jpg文件都替换为一张烤土豆的照片。一旦git-filter-branch结束后,你的git历史记录可能从表面上看起来完全一样——一样的日期,一样的注解等等——但是这个新的历史记录实际上和以前完全不一样了——你已经动过它了。
从2007年开始,git-filter-branch就成为标准git的一部分,当时git发布才两年。所以很久以来git-filter-branch一直是解决git历史记录问题的权威方法。但它有两个问题:
- 很难用
- 太慢了
难用的原因有几个——git-filter-branch不是要达到一个固定的目的,它本身并不给你解决方案。它很灵活,理论上可以做任何事,但是你必须清楚自己在干什么。你想做的一件最简单的事情可能是——从每个commit中删除一个文件——以下是你的做法:
git filter-branch --index-filter 'git rm --cached --ignore-unmatch big.mp4' --tag-name-filter cat -- --all这命令行太不友好了。而且如果你想做点更复杂事——比如删掉某个密码所有出现过的地方,会怎么样?或者删掉所有你不小心放进去的大文件?git-filter-branch本身不支持找出这些东西,你必须写一个复杂的脚本,才能告诉git-filter-branch去删除什么。网上有无数为filter-branch编写的脚本——我在写BFG之前就写过一些。几乎同一时间,我的一些在Guardian的同事想要从我们最大的代码库中删除密码,几天后他们搞出一个filter-branch脚本,这脚本看起来要跑数个小时——他们只能小心翼翼地把它放到计划任务中,让它在晚上跑,以免影响到团队中的其他开发人员,而且他们费了好大劲,预先测试了这个脚本好多次,确保排除所有的bug。
整件事情干下来太痛苦了。虽然git-filter-branch是一个高级工具,但git已经越来越普及(这种趋势持续至今),大量的人会遇到这个问题,这会给他们带来无尽的困惑和痛苦。所以这看起来是一个值得去解决的问题。
我已经比较精通git了,写过Agit,一个Android下的git客户端,并且对git数据存储结构的基本原理有很好的理解。当我思考git-filter-branch为什么这么慢的时候,我意识到只要你愿意对此问题的需求稍作调整,你就能得到一个快很多的解决方案。
InfoQ:你的项目网站说,BFG Repo-Cleaner比git-filter-branch“快10到720倍”。这是如何做到的?
Roberto:准确地说,如果跟git-filter-branch能处理的最简单任务(比如删除一个已知的文件)做比较,这个数据是10到1000倍。再举个独立人士的评测结果,Elliot Glaysher(开发Chrome的Google软件工程师)在把Chromium代码库从svn迁移到git的时候,测试了BFG的性能——BFG做这件事情只要十分钟,而git-filter-branch需要三天——大约快乐430倍。
这里有一个视频,对比了git-filter-branch(跑在四核Mac电脑上)和BFG(跑在Raspberry Pi上)速度:https://www.youtube.com/watch?v=Ir4IHzPhJuI
BFG之所以能在性能上有如此大的改进,原因在于它本来就和git-filter-branch不一样。
BFG做到了,因为我们充分考虑了最常见的场景,以及git本身存储数据的方式。这个场景就是删除不想要的文件——至少我想说这是最常见的使用git-filter-branch的场景,并且也是我感兴趣的场景。针对这个特定的问题,git的实现方式决定了我们可以采取完全不同的方案:在git中,所有文件和文件夹的数据都只存储一次,并且每个都会有一个独一无二的id——git-id。如果大量的commit都没有修改某个文件,那么这个文件只会被保存一次。如果这个文件有两个版本,并且需要在两个版本间来回切换,这个文件只会存储两次,每个版本各一次。所以,如果一个文件只存储一次,因为它出现在100个commit中,就要对它清理100次,这肯定是不现实的。BFG只对git库中的每个对象清理一次,然后记住它的git-id,以后遇到它,不把它计算在内就行了。我们对功能做了限制——git-filter-branch能让你做任何事,而BFG不行——以此换来性能上的巨大改进。
速度的提升还受益于没有进程间的切换(JVM会搞定一切,不需要在C代码和bash代码间来回切),以及能充分利用你的多核电脑。我很高兴看到BFG会使你所有的处理器核发烫,每个核都在尽其所能地删除文件和目录——但是很不幸git-filter-branch只能串行处理工作,一个接一个地处理commit——前一个处理完之前,无法开始下一个。
BFG的速度——明显是——一大优势。它可以让人们更高效地对改写代码库历史记录进行安全的试验,确定无误后再开始真正动手清理代码库,把结果push到服务器,通知所以开发人员删掉旧库,拉一份全新的——这种沟通的事情对于一个大型团队来说,工作量很大。
InfoQ:你为什么选择用Scala写BFG Repo-Cleaner而不是直接用Java?
Roberto: Guardian公司热衷于使用Scala,并且对于绝大多数工作让我在Java和Scala之间选,我每次都会选Scala。事实上这个项目用Scala非常合适:基于JVM,我就可以利用高性能的JGit库。作为函数式编程语言,Scala很适合去处理git的不可变数据结构,并且能利用并行开发的优势。用Scala编程是一种乐趣。在我为ScalaDays准备的演讲《Scala帮助git运行得更快》中,我对Scala的优势做了更深入的剖析。
InfoQ:你有没有想过提供一个GUI版本的BFG Repo-Cleaner,作为独立的工具也好,作为其他git工具的插件也好?
Roberto:虽然我觉得作为命令行工具BFG工作得很好,我也做了一些有意思的可视化方面的尝试。六月份我在泰特现代艺术馆(Tate Modern)举行的“玩转空间(Hack The Space)”活动上,把git-filter-branch和BFG操作git库历史记录的过程用实验性的3D过程展示了出来。除了视觉效果非常漂亮以外,它也非常清晰地显示了两者在速度方面差别这么大!此举的另一个目的是更清楚地解释BFG对你的仓库到底做了些什么——我可以理解人们很担心把删除旧文件的任务交给BFG(哪怕它不会碰你当前的文件,因为我们假设用户过去犯过的错误不会再犯,当前库中的文件都是他们要的)。有时候在需求方面,用户想要的和他们真正需要的东西有一定的差距,图形化的展示对于这两者的统一很有帮助。
关于插件,我真的很想简化一下在BFG中用脚本自定义处理动作的方式——接近git-filter-branch的灵活性,但依然保持快速。Christian Hoffmeister最近把BFG集成进了一个自定义工具(git-timeshift),但离达到我满意的程度还有差距。
按照现在的情况,命令行版本的BFG被广泛采用——twitter上有很多很棒的帖子在讨论如何使用它,BFG文档网站的引用者统计记录显示,BFG在被形形色色的用户使用,从主流的投资银行,到研究型实验室、手机生产商,甚至那些为军用飞机编写航天软件的神秘公司。基于下载量来做个估算,据此我猜测,BFG自发布以来,已经为大家节省了大约30人年的工作量。
BFG是开源、完全免费的,并且我希望你们的读者下次需要清理git历史记录的时候,BFG能给他们带来良好的体验。
Roberto Tyley是Guardian公司的开发人员、Guardian会员项目的技术经理,BFG、gu-who、Agit的作者,也为其他很多开源项目做贡献。他以前在GitHub工作过,创建了"animated diff"项目,他热衷于解释事物的原理。
参考英文原文:Removing Binary Files from git using Roberto Tyley’s BFG Repo-Cleaner