偶然发现Git文件夹非常大,使用BGF来处理Git历史Blob文件

我们使用Git来管理项目的时候,可能会提交一些Blob的二进制文件,这些文件并不能像文本文件一样采用diff delta的形式进行版本控制。如果这些文件一直跟随master的主版本,那么就是属于有效的文件。

然而很多时候这些二进制文件会被删除重建,那么由于Git的特性,这些文件会一直留在Git的历史记录中,这样会导致Git仓库变得庞大,不利于版本控制和迁移。最直观的就是clone的时候会很慢,而使用--depth=1则无法看到历史提交的代码。

查找历史文件

历史提交的二进制文件通常我们可以认为是不需要的,然而在多人协作的时候这个事情我们并不能非常确定。因此我们需要主动查找较大的二进制文件来处理,最简单的办法就是直接扫描大文件。

bash 复制代码
git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}')"

通过这个命令我们可以看到历史提交中最大的10个文件,然后我们可以根据这个文件的hash值来查找这个文件的提交记录。如果需要看到文件的大小,也可以再补充下输出。

bash 复制代码
git verify-pack -v .git/objects/pack/*.idx \
  | grep blob \
  | sort -k 3 -n \
  | tail -10 \
  | awk '{print $1, $3}' \
  | while read -r hash size; do
      file=$(git rev-list --objects --all | grep "$hash")
      echo "$file $size"
    done

BFG Repo-Cleaner

虽然可以使用git-filter-branch来处理历史提交,但是这个命令的效率比较低,且容易出现错误。BFG是一种更简单、更快捷的替代方法,主要是用来处理历史提交的文件,可以删除指定的文件,也可以替换文件内容。

BFG需要使用Java来运行,因此需要先确保运行环境JDK >= 8。之后使用git clone --mirror来克隆仓库,然后使用BFG来处理历史提交。

bash 复制代码
git clone --mirror git://example.com/some-big-repo.git

需要注意的是,使用--mirror标识会将整个仓库的完整副本克隆下来,包括所有的提交历史,并且不会存在任何普通文件。同样的,当处理完成之后这个仓库所有的分支都会被修改,因此需要谨慎操作。

而如果仅处理单个分支,那就是使用常规的git clone即可。但是需要注意的是,由于其他分支仍然有可能持有旧的分支内容,因此就必须要将所有涉及到的分支依次处理,这样就非常麻烦了。

接下来就是使用BFG来处理历史提交,BFG的使用非常简单,只需要指定需要处理的文件即可。下载BFGjar包可以在https://rtyley.github.io/bfg-repo-cleaner/中找到。

bash 复制代码
java -jar bfg.jar --delete-files path/to/file.txt some-big-repo.git

BFG还提供了一些其他的功能,比如删除指定大小的文件、替换文件内容、删除文件夹、删除文件通用匹配符。

bash 复制代码
java -jar bfg.jar --strip-blobs-bigger-than 1M some-big-repo.git
java -jar bfg.jar --replace-text pwd.txt some-big-repo.git
java -jar bfg.jar --delete-folders path some-big-repo.git
java -jar bfg.jar --delete-files '*.png' some-big-repo.git

但是这里的匹配模式也存在局限,例如无法同时指定文件和大小,例如需要移除> 1Mpng文件是做不到的,经过测试其匹配模式总是倾向于后设置的模式。不过这里并没有阅读源码,只是简单的测试判断。

此外,不需要担心BFG会删除HEAD的提交,BFG不会处理HEAD的提交,即使BFG会从早期的历史记录中删除文件,当然也可以通过--no-blob-protection来关闭保护。

复制代码
WARNING: The dirty content above may be removed from other commits, but as
the *protected* commits still use it, it will STILL exist in your repository.

在处理完成后,在同级目录下会生成report文件夹,其中包含了处理的文件信息,可以查看处理的文件数量、大小等信息,以及HASH变更的信息。在检查无误后,要将历史记录的过期时间设置为现在,且需要使用git来清理无引用的数据,这样就可以将BFG处理的数据真正删除。

bash 复制代码
cd some-big-repo.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive

此时就可以通过du命令检查文件夹的大小了,通常我们只需要关注.git文件夹即可。

bash 复制代码
du -sh .git

最后,直接使用git push来推送到远程仓库即可,这样就完成了历史提交的处理。注意如果使用分支模式的话,就需要加入-force选项来强制推送。

bash 复制代码
git push

这里还需要有一个额外的步骤,需要让每个参与者将本地的仓库删除,然后完整重新clone最新的仓库,防止持有旧数据的仓库重新推回仓库。此外,也可以使用git-filter-repo来实现类似的处理。

GitHub

从历史记录中删除文件并不是简单的事情,如果需要我们手动来执行操作的话,就很像我们从某一次提交开始,不断向后rebase。那么在这个过程中自然就会导致commithash值发生变化,从而出现一些问题,这里我们主要关注在GitHub的表现。

  1. 如果二进制文件是在很久前的提交,例如5年前的提交,而假设我们仅会删除此提交的某个文件,对于其他的提交并没有处理。但是由于需要重写历史提交记录,这就会导致从5年前引入的id开始到最新的提交全部被重写,这部分可以在BFGCommit Tree-Dirt History中关注到。
  2. 前边我们也提到了BFG会将hash变化信息写入report文件夹,实际上在重写的commit描述信息中,也会发现Former-commit-id: xxx的数据,用以标识这个commit重写前的引用。
  3. GitHubcontributions面板中,也就是绿色的瓷砖部分,会出现重写commit的历史提交出现重复贡献的现象,也就是说原来可能仅有1个提交,现在变成了2个,对于这个问题是可以减小影响面的。
  4. 虽然contributions面板中会出现重复的提交,但是通过api获取的提交记录总数中并不会出现重复的数量增量,也就是说GitHub并没有将重写的commit计入历史提交记录中。
  5. 对于通过分支模式而不是mirror模式清理的单独分支,虽然通过BFG可以将历史提交的二进制文件删除,但是其commit数量的计算会出现问题,其会切断fork之前的联系,也就是说原本fork分支的提交记录会被重新计算。
  6. 虽然对于主分支的提交数量不会影响,但是此时如果我们打开重写过后的commit描述,可以发现仍然可以找到原本的commit。这就意味着这个object实际上并没有被删除,只是不再被引用,而是处于游离指针的状态。

实际上这里的影响面还是挺大的,特别是对于早些时间引入的二进制文件,会导致大范围的历史提交记录重写。

对于contributions面板的影响,也是看起来比较大的问题,不过我们可以先fork主分支,然后再将主分支设置为fork分支,然后再更改名称的形式来解决这个问题。但是这个方式并没有完全解决问题,还是会因为提交日期的重写而导致contributions面板的数据不准确,不过影响面小了很多。

而对于其他问题,通常都是无法避免的问题,因为Git的特性决定了这个问题的存在。如果需要处理历史提交中私有文件的泄漏,则通常认为是不可靠的,此时必须要立即修正密钥。因此无论是当前提交还是在处理历史提交的时候,需要谨慎操作,不要泄漏私密文件,尽量避免对历史提交的处理。

Blog

参考