iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Software Development

深入一點點認識 Git系列 第 26

Day 26-深入一點點認識 Git:git filter-branch 可以改寫歷史,但還不足以完全清除檔案紀錄

  • 分享至 

  • xImage
  •  

如果不慎把如含有個資、密碼等機敏資訊的檔案放到 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
├── ...

此時物件結構圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513f6NzKQoiO3.png

刪除 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 之後,終端機畫面如下:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513dyyHawSwDu.png

出現警告訊息,說明這道指令會改寫歷史,另因 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           # 內容為空

不包含懸置物件的圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513bvYwEJMyMq.png

不只 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 的歷史紀錄:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513gdTlAUG0eE.png

因此還得把這些紀錄也清空(嚴格來說是立刻過期):

git reflog expire --all --expire=now

這樣就把 logs/ 裡面的紀錄清空了:

├── logs/
│   ├── ref/
│   │   └── heads/
│   │         └── main  # 空
│   │
│   └── HEAD            # 空

這時再下 git reflog 就看不到任何歷史:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513SlO16qfvCL.png

但這些懸置物件目前仍在 .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 前的這些物件:
https://ithelp.ithome.com.tw/upload/images/20250926/20178513OaxxgaHB9e.png

我們要連同這些懸置物件通通清空,於是輸入以下指令:

git gc --prune=now

終端機結果如下,可用 git fsck --unreachable 指令來追蹤當下懸置物件:
https://ithelp.ithome.com.tw/upload/images/20250926/2017851347kDreycFB.png

這樣就把 .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/refsobjects/infoobjects/pack 裡面的檔案,還有 pack-refs,這些是什麼?

下篇文章,我們將探討 git 的「打包」機制,就能知道 git 是如何打包物件、形成這些檔案。

小結

使用 git filter-branch 可以改寫歷史,做出新的 commit 與 tree 物件,不再指向不希望被 git 追蹤的檔案對應之 blob 物件。

但改寫完歷史後,還需要:

  1. refs/original/refs/heads/main 備份點刪掉。
  2. 清除 logs/ 裡的紀錄。
  3. objects/ 裡的懸置物件刪掉。

這樣才可以完全把與希望 git 不要再追蹤的檔案(與其對應 blob 物件)之所有歷史紀錄完全清乾淨。

參考資料

  1. git filter-branch
  2. Day26|【Git】 從 Git 中移除重要個資或徹底清除檔案 - git filter-branch
  3. 【冷知識】怎麼樣把檔案真正的從 Git 裡移掉?
  4. git-reflog
  5. git-gc

上一篇
Day 25-深入一點點認識 Git:對 commit 的任何更動其實都是在做新 commit
下一篇
Day 27-深入一點點認識 Git:透過 git gc 指令觀察 git 的打包機制
系列文
深入一點點認識 Git27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言