把密碼、API 金鑰或一個幾百 MB 的檔案不小心 commit 進 Git、又推上遠端——這時候光 git rm 沒用,它還躺在歷史裡,任何人 clone 下來都翻得到。要真正清掉,得重寫 Git 歷史。
Git 內建的 filter-branch 能做,但它慢、容易出錯,官方文件自己都標示不建議用了。現在的標準工具是 git-filter-repo。
什麼是 git-filter-repo
用 Python 寫的開源工具,專門重寫 Git 歷史,是 filter-branch 的官方推薦替代品。比起 filter-branch,它快上一個量級、命令更直覺,還會自動做安全檢查(例如要求你在乾淨的 clone 上操作)。
安裝
1 | pip install git-filter-repo |
情境一:移除意外推送的敏感資料
1 | # 移除符合 glob 的檔案 |
--invert-paths 是「保留以外的全部」,配合 --path 就是把這些路徑從整個歷史刪掉。
但有件事要先講清楚:把檔案從歷史移除,不等於敏感資料安全了。 只要它曾經推上遠端,就要當作已經外洩——
- 立刻撤銷並更換所有外洩的密碼、金鑰
- 檢查其他分支、fork、CI 日誌是否也有
- 之後用
.gitignore+ pre-commit hook(如 git-secrets)擋住下一次
清歷史是止血,換金鑰才是真正解決。
情境二:移除大型檔案
1 | git filter-repo --path-glob '*.zip' --invert-paths |
情境三:修改作者 email
這裡是最多人踩的坑。git-filter-repo 的 callback 拿到的 email、name、message 全都是 bytes,不是字串——因為 Git 的內容本質是位元組。所以字串字面量一定要加 b 前綴:
1 | # ✅ 正確:email 是 bytes,用 b"..." |
改提交訊息的 --message-callback 同樣是 bytes:
1 | git filter-repo --message-callback 'return message.replace(b"[WIP]", b"[DONE]")' |
記住「全部加 b」就對了,這是 callback 最常見的報錯來源。
情境四:把子目錄變成根目錄
1 | git filter-repo --subdirectory-filter folder_name |
常用在從 monorepo 拆出一個獨立專案,連歷史一起搬走。
用之前要知道的幾件事
- 在複製出來的 repo 操作,原始 repo 留著當保險。
- 重寫不可逆,確定真的要做。
- 之後要
git push --force覆蓋遠端,先跟團隊講好,不然會把別人的本地搞亂。 - 不用手動下
git gc。git-filter-repo 跑完會自己 expire reflog、repack 清掉舊物件。
進階:用 callback 改檔案內容
要動的不只是路徑、還包括檔案內容時,用 --blob-callback,body 直接操作 blob(一樣是 bytes,而且是 in-place 修改、不用 return):
1 | git filter-repo --blob-callback ' |
整個 git-filter-repo 的世界都是位元組——路徑、email、訊息、檔案內容全是 bytes,這點一致。
跟 filter-branch 比
| 特性 | git-filter-repo | git filter-branch |
|---|---|---|
| 速度 | 快 | 慢 |
| 上手難度 | 簡單 | 複雜 |
| 安全檢查 | 有 | 無 |
| 官方態度 | 推薦 | 不建議使用 |
收尾
git-filter-repo 把「清掉 Git 歷史裡的東西」變得又快又相對安全。真正要記住的就兩點:callback 全用 bytes(加 b)、清完敏感資料一定要換金鑰。其他照情境套就行。




