如果不慎把如含有個資、密碼等機敏資訊的檔案放到 commit 裡,希望改寫過往歷史紀錄,把與機敏資訊有關的檔案通通刪除,可以使用 git filter-branch
指令。
但不是單純下 git filter-branch
就可以把歷史紀錄全部清乾淨,git 有多個備份機制,要徹底刪光與機敏資料有關的紀錄,就得把所有備份機制儲存的檔案都一併清空。
我們要準備三個 commit,前兩個 commit 都有自己的檔案,而這兩個 commit 連同第三個 commit,有都有共同指向的檔案 confidential.txt
。
先做出要給三個 commit 追蹤的檔案:
echo "Confidential file" > confidential.txt
接著做出只給第一個 commit 追蹤的檔案:
echo "First file" > file1.txt
接著讓這兩個檔案形成第一個 commit:
git add .
git commit -m "First commit with file1.txt"
我們希望下一個 commit 只保留 file.txt
,不要有 file1.txt
,於是輸入以下指令,刪除 file1.txt
:
git rm file1.txt
再建立第二個檔案給第二個 commit 追蹤:
echo "Second file" > file2.txt
把刪除 file1.txt
與新增 file2.txt
這變化也加到預存區、再建立第二筆 commit:
git add .
git commit -m "Second commit with file2.txt"
再把 file2.txt
刪除:
git rm file2.txt
並把這樣的變化變成第三個 commit:
git add .
git commit -m "Third commit with only confidential.txt"
這時 .git/
資料夾結構如下:
.git/
│
├── logs/
│ ├── ref/
│ │ └── heads/
│ │ └── main # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ │ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ │ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│ │
│ └── HEAD # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│
├── objects/
│ ├── 4c/
│ │ └── 5fd919d52e3c1b08f7924cfa05d6de100912fd
│ ├── 4d/
│ │ └── 9214fd32efd32da9ff60f83f3752bb40e6ee96
│ ├── 06/
│ │ └── e571d9fd9eae53cceb13aaf738564669f5f14c
│ ├── 6d/
│ │ └── 2c43686ab450fa3469472e38717192315e25a0
│ ├── 20/
│ │ └── d5b672a347112783818b3fc8cc7cd66ade3008
│ ├── 40/
│ │ └── da4dc2aff788a2afc9841d3c3e24007e468d1e
│ ├── 96/
│ │ └── 1175525f3c716b7dd77fcf14859399af05dee2
│ ├── aa/
│ │ └── 1397cfe3a674a81c845d1059bb33f1cb2c662d
│ ├── ff/
│ │ └── 12d1521a4cf351f04598cebee5226e99dce97f
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 指向ae64762d1b8d870db4c11fbfa470e492cfe93173
│ └── tags/
│
├── COMMIT_EDITMSG # 寫著Third commit with only confidential.txt
├── ...
此時物件結構圖示如下:
這時我們發現:confidential.txt
不應該被 git 追蹤的!於是使用 git filter-branch
把檔案刪掉:
git filter-branch -f --tree-filter "rm -f confidential.txt"
上述指令說明如下:
git filter-branch
:字面上的意思是「過濾分支」,而根據 git 官方文件,這道指令就是用來改寫分支。-f
:表示 force
強制。原 git filter-branch
在遇到特定情況(如上一次 git filter-branch
建立的備份點仍在)即不執行,加上這個 -f
可強制覆蓋掉之前的備份點。--tree-filter
:對於每一個 commit,git 會遍歷其對應的 tree 物件。"rm -f confidential.txt"
:如果一個 commit 中的 tree 有指向 confidential.txt
做出來的 blob 物件,就做出新的 tree 物件,而新的 tree 物件不再指向 confidential.txt
對應之 blob 物件。在改寫歷史的過程中,git 會生成一個名為 .git-rewrite/
的臨時資料夾,該資料夾結構如下:
.git-rewrite/
│
├── map/
├── t/
├── backup-refs
├── commit
├── heads
├── index
├── message
├── parse
├── raw-refs
├── revs
└── tree-state
這個 .git-rewrite/
資料夾在改寫完歷史之後就會消失,我們姑且不細看資料夾結構細節,先將其理解為「進行 git filter-branch
時的臨時工作區」。
輸入 git filter-branch
之後,終端機畫面如下:
出現警告訊息,說明這道指令會改寫歷史,另因 git filter-branch
相對複雜、跑的時間也比較久,現今 git 官方建議使用較新的指令 git filter-repo
,但這並非 git 內建指令,需要另外安裝,因此本文暫不介紹,若對該指令有興趣,可參考終端機裡出現的 GitHub 倉儲網址。
改寫歷史之後,.git/
資料夾變成:
.git/
│
├── logs/
│ ├── ref/
│ │ └── heads/
│ │ └── main # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ │ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ │ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│ │ # aa1397cfe3a674a81c845d1059bb33f1cb2c662d 2192da98f63a62229a5c1670f38292fe03f8a1cb Ralph <ralph@ralphmail.com> 1756714031 +0800 filter-branch: rewrite
│ │
│ └── HEAD # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│ # aa1397cfe3a674a81c845d1059bb33f1cb2c662d 2192da98f63a62229a5c1670f38292fe03f8a1cb Ralph <ralph@ralphmail.com> 1756714031 +0800 filter-branch: rewrite
│
├── objects/
│ ├── 4b/ # 新增的tree物件
│ │ └── 825dc642cb6eb9a060e54bf8d69288fbee4904
│ ├── 4c/
│ │ └── 5fd919d52e3c1b08f7924cfa05d6de100912fd
│ ├── 4d/
│ │ └── 9214fd32efd32da9ff60f83f3752bb40e6ee96
│ ├── 06/
│ │ └── e571d9fd9eae53cceb13aaf738564669f5f14c
│ ├── 6d/
│ │ └── 2c43686ab450fa3469472e38717192315e25a0
│ ├── 20/
│ │ └── d5b672a347112783818b3fc8cc7cd66ade3008
│ ├── 21/ # 新增的commit物件
│ │ └── 92da98f63a62229a5c1670f38292fe03f8a1cb
│ ├── 40/
│ │ └── da4dc2aff788a2afc9841d3c3e24007e468d1e
│ ├── 56/ # 新增的tree物件
│ │ └── 496a365c3db27f7276668e5d16719fb849c4e5
│ ├── 96/
│ │ └── 1175525f3c716b7dd77fcf14859399af05dee2
│ ├── 99/ # 新增的commit物件
│ │ └── a3e2ed873572cec030c34c29b1d2fd4b1beb52
│ ├── aa/
│ │ └── 1397cfe3a674a81c845d1059bb33f1cb2c662d
│ ├── b0/ # 新增的commit物件
│ │ └── 62cfa8109db9ddd9408116608ba8e1a0bf0614
│ ├── e4/ # 新增的tree物件
│ │ └── afcd11a121a038f52a6a0ad6d248644a10e0e9
│ ├── ff/
│ │ └── 12d1521a4cf351f04598cebee5226e99dce97f
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 指向2192da98f63a62229a5c1670f38292fe03f8a1cb
│ ├── original/
│ │ └── refs/
│ │ └── heads/
│ │ └── main # 指向aa1397cfe3a674a81c845d1059bb33f1cb2c662d
│ └── tags/
│
├── COMMIT_EDITMSG # 依然寫著Third commit with only confidential.txt
├── ...
└── index # 內容為空
不包含懸置物件的圖示如下:
不只 commit 是新的,連 tree 物件也通通都變成新的,且一律不再指向 06e571d...
這個 blob 物件,但值得留意的是:這些新 commit 物件建立的時間點與已成懸置物件的舊 commit 物件相同,不像 Day 25 用 git rebase
互動模式建立出的新 commit 也會有自己的 commit 時間。
不過,如果仔細觀察 .git/
資料夾,會發現當中有新的資料夾如下:
├── refs/
│ ├── original/
│ │ └── refs/
│ │ └── heads/
│ │ └── main # 指向aa1397cfe3a674a81c845d1059bb33f1cb2c662d
這個 refs/original/refs/heads/main
是備份,以便我們如果 git filter-branch
做錯的話,還能藉這個 main
寫的「前一個最新 commit」來復原,但如果要徹底清除這份歷史紀錄,就得連這個 main
的紀錄一起刪除:
rm .git/refs/original/refs/heads/main
刪除後,git 就不知道 git filter-branch
前,main
指向哪個分支,因此就無法復原了。
但還有一個地方存著備份:
├── logs/
│ ├── ref/
│ │ └── heads/
│ │ └── main # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ │ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ │ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│ │ # aa1397cfe3a674a81c845d1059bb33f1cb2c662d 2192da98f63a62229a5c1670f38292fe03f8a1cb Ralph <ralph@ralphmail.com> 1756714031 +0800 filter-branch: rewrite
│ │
│ └── HEAD # 0000000000000000000000000000000000000000 ff12d1521a4cf351f04598cebee5226e99dce97f Ralph <ralph@ralphmail.com> 1756713994 +0800 commit (initial): First commit with file1.txt
│ # ff12d1521a4cf351f04598cebee5226e99dce97f 40da4dc2aff788a2afc9841d3c3e24007e468d1e Ralph <ralph@ralphmail.com> 1756714013 +0800 commit: Second commit with file2.txt
│ # 40da4dc2aff788a2afc9841d3c3e24007e468d1e aa1397cfe3a674a81c845d1059bb33f1cb2c662d Ralph <ralph@ralphmail.com> 1756714031 +0800 commit: Third commit with only confidential.txt
│ # aa1397cfe3a674a81c845d1059bb33f1cb2c662d 2192da98f63a62229a5c1670f38292fe03f8a1cb Ralph <ralph@ralphmail.com> 1756714031 +0800 filter-branch: rewrite
因此如果現在下 git reflog
,還是能看到舊 commit 的歷史紀錄:
因此還得把這些紀錄也清空(嚴格來說是立刻過期):
git reflog expire --all --expire=now
這樣就把 logs/
裡面的紀錄清空了:
├── logs/
│ ├── ref/
│ │ └── heads/
│ │ └── main # 空
│ │
│ └── HEAD # 空
這時再下 git reflog
就看不到任何歷史:
但這些懸置物件目前仍在 .git/objects/
裡頭:
├── objects/
│ ├── 4b/
│ │ └── 825dc642cb6eb9a060e54bf8d69288fbee4904
│ ├── 4c/
│ │ └── 5fd919d52e3c1b08f7924cfa05d6de100912fd
│ ├── 4d/ # 懸置物件
│ │ └── 9214fd32efd32da9ff60f83f3752bb40e6ee96
│ ├── 06/ # 懸置物件
│ │ └── e571d9fd9eae53cceb13aaf738564669f5f14c
│ ├── 6d/ # 懸置物件
│ │ └── 2c43686ab450fa3469472e38717192315e25a0
│ ├── 20/
│ │ └── d5b672a347112783818b3fc8cc7cd66ade3008
│ ├── 21/
│ │ └── 92da98f63a62229a5c1670f38292fe03f8a1cb
│ ├── 40/ # 懸置物件
│ │ └── da4dc2aff788a2afc9841d3c3e24007e468d1e
│ ├── 56/
│ │ └── 496a365c3db27f7276668e5d16719fb849c4e5
│ ├── 96/ # 懸置物件
│ │ └── 1175525f3c716b7dd77fcf14859399af05dee2
│ ├── 99/
│ │ └── a3e2ed873572cec030c34c29b1d2fd4b1beb52
│ ├── aa/ # 懸置物件
│ │ └── 1397cfe3a674a81c845d1059bb33f1cb2c662d
│ ├── b0/
│ │ └── 62cfa8109db9ddd9408116608ba8e1a0bf0614
│ ├── e4/
│ │ └── afcd11a121a038f52a6a0ad6d248644a10e0e9
│ ├── ff/ # 懸置物件
│ │ └── 12d1521a4cf351f04598cebee5226e99dce97f
│ ├── info/
│ └── pack/
也就是進行 git filter-branch
前的這些物件:
我們要連同這些懸置物件通通清空,於是輸入以下指令:
git gc --prune=now
終端機結果如下,可用 git fsck --unreachable
指令來追蹤當下懸置物件:
這樣就把 .git/
資料夾所有有關 confidential.txt
的紀錄都清除了,如果還要把 GitHub 上的紀錄也都蓋掉,要再使用以下指令:
git push -f
此時 .git/
資料夾發生好多變化:
.git/
│
├── info/
│ ├── exlcude
│ └── refs # 新增檔案,內容為2192da98f63a62229a5c1670f38292fe03f8a1cb refs/heads/main
│
├── logs/
│ ├── ref/
│ │ └── heads/
│ │ └── main # 空
│ │
│ └── HEAD # 空
│
├── objects/
│ ├── info/
│ │ ├── commit-graph # 直接打開為亂碼
│ │ └── packs # P pack-976dc9017601bfa36d7bf3e10467a613033b7663.pack
│ └── pack/
│ ├── pack-976dc9017601bfa36d7bf3e10467a613033b7663.idx
│ └── pack-976dc9017601bfa36d7bf3e10467a613033b7663.pack
│
├── refs/
│ ├── heads/ # 空
│ ├── original/
│ │ └── refs/
│ │ └── heads/
│ │ └── main # 空
│ └── tags/
│
├── COMMIT_EDITMSG # 依然寫著Third commit with only confidential.txt
├── config
├── description
├── HEAD # ref: refs/heads/main
├── index # 內容為空
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# 2192da98f63a62229a5c1670f38292fe03f8a1cb refs/heads/main
咦?怎麼 objects/
裡都沒有東西,反倒出現 info/refs
、objects/info
、objects/pack
裡面的檔案,還有 pack-refs
,這些是什麼?
下篇文章,我們將探討 git 的「打包」機制,就能知道 git 是如何打包物件、形成這些檔案。
使用 git filter-branch
可以改寫歷史,做出新的 commit 與 tree 物件,不再指向不希望被 git 追蹤的檔案對應之 blob 物件。
但改寫完歷史後,還需要:
refs/original/refs/heads/main
備份點刪掉。logs/
裡的紀錄。objects/
裡的懸置物件刪掉。這樣才可以完全把與希望 git 不要再追蹤的檔案(與其對應 blob 物件)之所有歷史紀錄完全清乾淨。