今天早上照常 git fetch --prune
获取大家写的代码,发现需要好长时间,但没怎么在意。直到下午小伙伴们才发现居然 fetch
了一个多 GB!询问才发现小伙伴 JAKE(其实我是在推荐博客)误传了 1.47GB 的垃圾文件。关键是等发现时,develop
分支上已经有 20+ 个基于这个文件的新提交了。
小伙伴说“不要紧,现在我已经删除它了!”突然一阵后背发凉,我们才 900M 的仓库肯定一下子飙到了 2000+M,必须马上处理之。
如果你想知道到底发生了什么造成突然多出这么大的文件,请阅读:一个压缩包引发的血案 - niuyanjie’s blog。
问题的本质和解决思路
有的小伙伴问了,为何删了也会占用仓库空间?这是因为 Git 会记录历史的每一次提交,而且提交中包含了完整的数据。如果有一次提交中增加了一个大文件,即便后面删除了此文件再提交,之前增加文件的提交也在历史中。由于 Git 是分布式仓库,每个人都克隆了完整的 Git 仓库,包含完整的历史,于是这个大文件对空间的吞噬其实影响着每一个 Git 仓库的副本。
所以,解决问题的思路其实是——让整个 Git 历史中不存在这个文件!
一种方法是修改有问题的提交,使这个提交中不包含对此文件的修改记录;另一种方法就是将这个提交从整个提交历史记录中干掉。
后文介绍的方法中,“推荐的方法”属于前者,“不推荐的方法”属于后者。
推荐的方法
感谢小伙伴 林德熙 的帮助,帮我找到了一篇非常有价值的博客:Git如何永久删除文件(包括历史记录) - shines77 - 博客园。
强烈推荐只阅读上面那篇文章而不要阅读本文,因为本文真正用到的方法比上面的更 low。
看到 filter-branch
,突然想到前几天给 Git 仓库补提交一个文件用的是同一个方法:如何向整个 Git 仓库补提交一个文件,那既然如此,里面各种参数的含义也就读(si)得(dong)懂(fei)了(dong)……(/暗笑)。
命令如下:
1
$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch path-to-your-remove-file' --prune-empty --tag-name-filter cat -- --all
把 path-to-your-remove-file
改成那个误传的文件后,写下命令准备运行……运行……运行……
然后过了十多分钟还不到 5% 的进度……
算了,放弃吧……我们这种数万次提交,900M 的大仓库,这样的命令似乎玩不起啊……这个命令一定还能用,比如指定版本区间什么的,但是我不会……
不推荐的方法(但能解决问题)
我在本地把 develop
分支 reset
到那次误提交文件的提交之前,然后跳过有问题的提交,将 origin/develop
分支上其它新提交一个个 cherry-pick
到本地的 develop
分支上。需要注意不能 cherry-pick
那些合并的提交(就是那种有两个 parent 的提交)。
这样,本地的 develop
分支就刚好跳过那个误传文件的提交,而包含之后的所有提交了。
不管哪种方法,执行完之后都要做这些事情
上面两种方法执行完之后,都面临一个问题——本地 develop
和远端 origin/develop
分支将不再是快进式的,而且包含相同修改的不同提交。这是因为提交虽然还是之前那些提交,但提交的信息已经改变(SHA-1 和 parent)。
我必须让远程 Git 仓库应用我的最新分支才行。必须使用一个危险的命令:
1
git push -f
于是去远程服务器上取消了 develop
分支的保护,执行以上命令后重新保护它避免之后的危险操作。(前面不管那种方法都会面临这个危险操作,说它危险,是因为此操作会删除远端服务器上 develop
分支上的提交,如果此前的操作做得不对,可能造成严重的代码丢失的后果!)
另外,根据误传文件那个提交的 SHA-1 值,我们找到了远端还存在着包含此提交的其他人的分支,我们需要删除那些分支,确保服务器上任何分支都不包含此提交。使用的是这个命令:
1
git branch --contains <commit>
然后删除上面命令找到的远端分支(当然找小伙伴确认过可以删我才敢删,不然被打死了):
1
2
$ git push -d origin <branch_name>
$ git branch -d <branch_name>
这样,远端服务器上的任何分支都不存在包含误传文件的提交了。理论上新克隆的本地仓库将不再有 2000+M 的大小,实测也是如此。但已经克隆并且包含那次提交的小伙伴该怎么办?
小伙伴 林德熙 再次提供了一组命令,我和他一起简化后整理如下:
1
2
3
4
5
git fetch -f -p
git checkout develop
git reset origin/develop --hard
git reflog expire --expire=now --all
git gc --prune=now
命令的解释小伙伴 林德熙有详细介绍。大体为:
- 提取远端服务器上最新的提交(这样本地仓库才会包含我修复的那些提交)
- 切换到
develop
分支(避免影响到小伙伴当前的工作分支,防止丢失工作) - 将本地
develop
分支强制重置成远端的develop
分支(以便丢掉本地有问题的那些提交) - 立刻将所有无法跟踪到的提交标记为已过期(以便垃圾回收工具可以回收)
- 立刻进行垃圾回收(这样误传文件的那次提交包含的 1.47GB 空间就被回收啦)
需要注意:必须确保本地和远端没有任何分支可以跟踪到那次误传文件的提交。
如果本地没有基于之前的 develop
分支做新的修改,则以上命令足以将本地磁盘的空间收回。如下图:
但如果本地还有新的提交,以上命令敲完前三条后就要暂停了。需要将新的提交 cherry-pick
到新的 develop
分支上;随后删除之前提交的那个分支,确保本地也没有任何分支包含误传文件的提交。随后继续敲后面的两条命令,也可以将本地的磁盘空间回收。
至此,完结了吗?不!没有。
我们肯定还有小伙伴没有删干净,等哪一天再次把那个提交推送……今天算是白干了……
不过,好在我们必须经过代码审查才会将功能分支合并到 develop
。只要我们能及时发现,告诉大家在 fetch
发现花太长时间的时候立刻 Ctrl+C
取消 fetch
,那么我们还能拯救。而这次,只需要一个命令即可解决:
1
$ git push -d origin 死灰复燃的包含误传提交的某个分支名
另外再吐个槽,以上一切都做完了,还写完了这篇博客。结果 filter-branch
那个命令依然还跑着没有结束……
本文会经常更新,请阅读原文: https://blog.walterlv.com/git/2017/09/18/delete-a-file-from-whole-git-history.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。