0%

探究git如何从历史提交记录中移除大文件

起因

项目中使用到了IP离线数据库 GeoLite2-City.mmdb,结果在git提交时就直接把该文件给提交了。

然后 commit 命令弹出了下面的警告信息:

1
2
3
4
5
6
7
Compressing objects: 100% (125/125), done.
Writing objects: 100% (152/152), 32.12 MiB | 378.00 KiB/s, done.
Total 152 (delta 62), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (62/62), completed with 20 local objects.
remote: warning: See http://git.io/iEPt8g for more information.
remote: warning: File files/GeoLite2-City.mmdb is 66.64 MB; this is larger than GitHub's recommended maximum file size of 50.00 MB
remote: warning: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.

后来想想其实这个文件并不需要通过git管理,只需要在项目部署时将其拷贝到服务器上的相应目录就可以了。

最重要的一点就是可以减少git仓库的大小。

但想要回滚回去还面临一个问题:也就是我在这次 commit 之后又进行多次 commit

主要是当时看到上面的警告提示并没有太在意,后来提交过几次后发现整个项目的代码才几百KB,而这一个文件就60多MB,而且拉取仓库还很慢。于是就后知后觉的想要把它删除。


如何解决

可以通过 count-objects 命令查看当前git项目占用的大小:

1
2
3
4
5
6
7
8
9
➜ git count-objects -v
count: 1047
size: 39168
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0

其中,size 大小是 KB,所以整个项目大约有 38MB 左右。

移除文件跟踪

发现提交问题后,我就首先通过 git rm 命令移除了对该大文件的跟踪:

1
2
➜ git rm --cached -r files/
rm 'files/GeoLite2-City.mmdb'

然后在 .gitignore 文件中添加目录排除:

1
2
3
# .gitingore

files/

因为该大文件直接存放在项目根目录下的 files 目录下,考虑到该目录仅用作存放类似的外部引用文件,所以这里就直接对该目录做了排除处理。


移除历史记录

虽然上面已经将该大文件移除了git跟踪,但是由于之前已经执行过 commit 命令,该大文件也就存在于git的历史提交记录中,那么其所占的空间大小并不会随之减少。

后来搜索到git中有一个 filter-branch 命令,可以用来更改提交记录。

找出要清理的大文件

通过如下命令找出大小排名前5的提交记录:

1
git verify-pack -v .git/objects/pack/pack-*.idx | sort -k 3 -g | tail -5

执行结果:

1
2
3
4
5
6
git verify-pack -v .git/objects/pack/pack-*.idx | sort -k 3 -g | tail -5
f1fa258c4de231d60dd34bfe086cb1fd9bdfeffe blob 6729 1973 168915
a3b8064f46bfc68a8cd4a23c9e4d07e8512f498e blob 9196 1837 153011
fb6959274ac0df12ba5ca20efacb06ba902b37d8 blob 22021 3110 147510
e585637c26cb4b5bb1b21f95045e5f2ce845848b blob 96767 36508 108212
0482b283b80efeb1ab0549132fead647f7b9b9e7 blob 69875685 33652834 178184

查看该提交包含的文件:

1
2
git rev-list --objects --all | grep 0482b283b80efeb1ab0549132fead647f7b9b9e7
0482b283b80efeb1ab0549132fead647f7b9b9e7 files/GeoLite2-City.mmdb

可见提交记录中的对应文件就是 files/GeoLite2-City.mmdb

其中:

  • verify-pack 表示用于显示已打包的内容
  • rev-list 表示用来列出Git仓库中的提交
  • --objects 表示列出该提交涉及的所有文件ID
  • --all 表示所有分支的提交,相当于指定了位于/refs下的所有引用

重写commit,删除大文件

通过 filter-branch 命令,来删除上面找到的大文件:

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 换成具体的文件路径。

其中:

  • filter-branch 表示:用来重写Git仓库中的提交
  • -f/--force 表示:git filter-branch拒绝从现有的临时目录开始过滤,或者当已经有ref时 refs/original/ ,使用该参数强制执行过滤
  • --index-filter 表示:重写索引的过滤器。用来指定一条Bash命令,然后Git会检出(checkout)所有的提交, 执行该命令,然后重新提交
  • --ignore-unmatch 表示:
  • --prune-empty 表示:过滤常常生成空的提交,从而使树保持不变。该参数会删除空提交
  • --tag-name-filter 表示:重写标签名称,原始标签不会被删除,但可以被覆盖;使用 --tag-name-filter cat 来简单地更新标签
  • -- 表示:分隔符
  • --all 表示:需要重写所有分支(或引用)

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➜ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch files/GeoLite2-City.mmdb' --prune-empty --tag-name-filter cat -- --all
WARNING: git-filter-branch has a glut of gotchas generating mangled history
rewrites. Hit Ctrl-C before proceeding to abort, then use an
alternative filtering tool such as 'git filter-repo'
(https://github.com/newren/git-filter-repo/) instead. See the
filter-branch manual page for more details; to squelch this warning,
set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite f4767ff924b90157e19397af404a2ba38c5378fe (41/54) (3 seconds passed, remaining 0 predicted) rm 'files/GeoLite2-City.mmdb'
Rewrite d095e133f47958a41a9f6bf09e9c3f8472f269b9 (41/54) (3 seconds passed, remaining 0 predicted) rm 'files/GeoLite2-City.mmdb'
Rewrite b70f3fc69bb87fa63964a5536ee4ad3a7a4c3b87 (41/54) (3 seconds passed, remaining 0 predicted) rm 'files/GeoLite2-City.mmdb'
Rewrite 507ba2652efabbd907d6aaad2d459ac1397cb54f (41/54) (3 seconds passed, remaining 0 predicted) rm 'files/GeoLite2-City.mmdb'
Rewrite cc8706560aac102304ab16dd1aa1b4ee24186558 (41/54) (3 seconds passed, remaining 0 predicted) rm 'files/GeoLite2-City.mmdb'
Rewrite 5e2088ef3f256ba3c9ebfc73af36c7f2c5f19b09 (41/54) (3 seconds passed, remaining 0 predicted)
Ref 'refs/heads/dev' was rewritten
WARNING: Ref 'refs/heads/main' is unchanged
Ref 'refs/remotes/origin/dev' was rewritten
WARNING: Ref 'refs/remotes/origin/main' is unchanged
WARNING: Ref 'refs/tags/v0.1.0' is unchanged
WARNING: Ref 'refs/tags/v0.1.2' is unchanged
WARNING: Ref 'refs/tags/v0.1.3' is unchanged
v0.1.0 -> v0.1.0 (05258f376c40e2ce3db184957e8ecff94a12a41a -> 05258f376c40e2ce3db184957e8ecff94a12a41a)
v0.1.2 -> v0.1.2 (d8d2681645ce807069c7a1f1c2865f8d74862a20 -> d8d2681645ce807069c7a1f1c2865f8d74862a20)
v0.1.3 -> v0.1.3 (39f36e09ba24badcae6360686f1dc433690b185d -> 39f36e09ba24badcae6360686f1dc433690b185d)

通过 Sourcetree 工具查看一下所做的更改:

21108092206|500

其中,红色框选部分是原来的提交记录,绿色框选部分是移除大文件后被更改的提交记录。


清理和回收本地空间

此时如果通过命令 git count-objects -v 去查看git仓库占用大小,会发现并没有什么变化:

1
2
3
4
5
6
7
8
9
➜ git count-objects -v
count: 1058
size: 39212
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0

这是因为,虽然我们已经删除了大文件,但在 .git 目录下仍然保留了这些 objects,等待垃圾回收。我们可以手动将其立即删除。

执行如下命令:

1
2
3
rm -rf .git/refs/original
git reflog expire --expire=now --all
git gc --prune=now

其中:

  • rm .git/refs/original 表示:删除git的备份文件
  • git reflog expire --expire=now --all 表示:使所有散落的object立即失效
  • git gc --prune=now 表示:立即执行垃圾回收gc

再次查看文件占用大小,为了便于查看,可以通过增加 -H 选项,自动将其转换为 MB 大小:

1
2
3
4
5
6
7
8
9
➜ git count-objects -vH
count: 0
size: 0 bytes
in-pack: 1022
packs: 1
size-pack: 32.30 MiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytes

推送修改

通过强制覆盖的方式 -f,--force 将修改后的代码提交到远端:

1
2
3
git checkout dev

git push origin dev:dev -f

总结

虽然通过上面的操作我们可以将已经 commit 的大文件完整移除,但我发现其中还是有一些需要注意的问题。

总结下来就是:我不建议使用 filter-branch 命令来解决此种情况

主要有两点:

原因一:不推荐

可以看到在执行 git filter-branch 命令时给出了下面的警告信息:

WARNING: git-filter-branch has a glut of gotchas generating mangled history
rewrites. Hit Ctrl-C before proceeding to abort, then use an
alternative filtering tool such as ‘git filter-repo’
(https://github.com/newren/git-filter-repo/) instead. See the
filter-branch manual page for more details; to squelch this warning,
set FILTER_BRANCH_SQUELCH_WARNING=1.

filter-branch 会完全改写你的提交历史记录,git官方推荐使用 git filter-repo 命令来代替。

至于 filter-repo 命令的具体操作,后续再补充说明。

原因二:不安全

因为我也是第一次遇到需要对提交过的大文件进行移除的场景,所以整个的操作流程也是在网上搜索了好多的资料才敢下手操作。整个过程也是非常的 战战兢兢 生怕一步错导致整个项目出错而无法恢复。

毕竟其中不乏各种 --force 命令。

所以说,虽然这种方式能够解决,但我并不推荐这样做。


更好的方法

除了上面官方推荐的filter-repo 命令外,后来想想,其实当我在发现大文件被 commit 后的几次提交时,我就可以通过 git reset 命令将本地版本回退到没有大文件之前的版本,重新提交这些更改。

具体的操作这里暂时先不展开。

(ps. 主要是目前项目中的大文件情况已经解决,只能后续再次遇到时或者专门针对这种情况做一个深入探讨了😋)


如有疑问或需要技术讨论,请留言或发邮件到 service@itfanr.cc