返回博客

git filter-repo 在生产机上跑完,nginx 开始 404——记一次仓库瘦身踩坑

某个部署在线上的项目,.git 目录膨胀到了 2.8 GB,原因是历史上有人把 public/assets/ 里的视频文件直接 git add 进了版本库,之后也没有从历史中移除。每次 push/clone 都要传几百兆,CI 拉代码慢得让人抓狂。

决定用 git filter-repo 重写历史、把大 blob 从所有 commit 里抹掉。照着网上的教程,SSH 登上生产机,在工作目录里直接执行——命令跑完,仓库从 2.8 GB 缩到了 500 MB,效果拔群。然后打开浏览器,静态资源全部 404。

现象

  • git filter-repo 命令正常退出,无报错
  • 仓库体积大幅缩小,符合预期
  • nginx 服务的静态文件(图片、视频、字体)全部返回 404
  • ls public/assets/ 发现目录为空——文件被物理删除了

根因

git filter-repo 默认会将工作树同步到重写后的新 HEAD。你从历史里删掉了那个 799 MB 的视频,新 HEAD 的 tree 里自然没有这个文件,filter-repo 执行完毕时就把工作树里对应的物理文件一并删掉了。

这和 git checkout / git reset --hard 的行为逻辑相同——只是教程里没人提,因为在本地开发机上根本不在乎工作树里的几个测试文件;而生产服务器上,工作树里跑着 nginx 实时服务的静态资源,删了就是线上事故。

坑:直接在生产工作仓库运行 git filter-repo 会删工作树文件

git filter-repo 执行完成后,会把工作树重置为重写后的新 HEAD。如果你把某个目录从历史中剔除,该目录下的所有物理文件都会从工作树里消失——无论它们是否还在被 nginx/apache 服务。在生产服务器上照做,等于亲手删掉线上静态资源。

正解

核心思路:在一个 bare clone(无工作树)上跑 filter-repo,再 force-push 到远端;生产机的工作树和 .git 原样保留。

# 1. 在生产机本地 clone 一个 bare mirror(纯 .git,无工作树)
TS=$(date +%Y%m%d-%H%M)
git clone --mirror /www/wwwroot/myapp /root/myapp-cleanup-${TS}.git
 
# 2. 安装 git-filter-repo(若未安装)
pip3 install --user git-filter-repo
 
# 3. 在 bare mirror 上执行 filter-repo(没有工作树,不会删任何业务文件)
cd /root/myapp-cleanup-${TS}.git
~/.local/bin/git-filter-repo --force \
  --path public/assets/ \
  --path-glob '*.mp4' \
  --invert-paths
 
# 4. GC 压缩,确认体积
git reflog expire --expire=now --all
git gc --prune=now --aggressive
du -sh .
 
# 5. 修正 remote(mirror clone 默认指向本地路径,不是远端)
git remote set-url origin git@github.com:your-org/your-repo.git
 
# 6. force-push 到远端(用显式分支名,不要 --mirror)
git push --force origin master

几个细节需要注意:

  • 不要用 git push --mirror origin:mirror push 会把 refs/remotes/origin/* 也推到远端,污染远端的 ref 命名空间。
  • 生产机的 .git 保留原样,当天然备份:不要在 force-push 完之后立刻在生产机 git fetch && git reset --hard origin/master——这等于把大文件再从工作树里删掉一次,走回老路。生产本地 repo 与远端分叉是可以接受的,生产机本来就不应该 push。
  • 事前查 .env 历史git log --all --oneline -- .env,如果曾经入库过凭据,要先走凭据轮换流程,不能只清历史了事。
正解:bare mirror 上跑 filter-repo,工作树零影响

git clone --mirror <prod-path> 得到的是纯 .git,没有工作树。在这个副本上跑 filter-repo,不管删多少历史 blob,nginx 正在服务的物理文件一个字节都不会少。清完再 git push --force origin <branch> 推远端即可。

顺手检查:.gitignore 盲区

.git 膨胀的根本原因往往是 .gitignore 漏写了某个目录。常见高危路径:

public/assets/
static/
uploads/
media/
dist/

清完历史记得补 .gitignore,否则下次有人 git add . 又会把大文件喂进去,历史再次膨胀。

一句话外卖

永远不在生产工作仓库直跑 git filter-repo——用 --mirror clone 出一个 bare 副本,那里没有工作树,filter-repo 没有文件可删。