
用 git includeIf 與 filter-repo --mailmap 清掉洩漏的公司信箱 — 27 個個人儲存庫全中招
Git identity 屬於目錄,不是儲存庫。includeIf gitdir 修正日後的設定,git filter-repo --mailmap 修正已經 push 的提交。
本頁目錄
引言
我架設了一個個人部落格,push 了第一個 commit,然後看了一眼 author 那一行:
Author: my-work-name <me@company.example>公司信箱,出現在一個個人專案裡,而且是一個我打算公開的儲存庫。沒有任何東西壞掉。Git 只是忠實地做了我幾年前設定好、之後就再也沒想過的事。等我去確認這件事到底擴散了多遠,發現不只一個儲存庫。是 27 個。
最後的修法是一個我從沒用過的 git 功能:includeIf "gitdir:",它會依儲存庫所在的位置決定要用哪個 identity,讓答案跟著目錄走,而不必每個儲存庫各自設定。至於已經 push 出去的 commit,需要另一套處理方式:git filter-repo --mailmap。
本文整理了讓我重新看待這個問題的計數過程、最先嘗試的一行指令修法以及它為什麼修錯了層、條件式 include 的設定與兩個會悄悄讓它失效的細節,以及改寫已經在 GitHub 上的歷史時出的差錯。
一行指令能修,但修錯了層
明顯的修法只要一行指令:
git config user.email personal@example.com這樣能修好這個儲存庫。但問題並沒有解決,因為問題從來就不是這個儲存庫。我是數過之後才看清這一點的。
先數清楚
在寫任何設定之前,我先數了一遍:
for d in ~/Developer/personal/*/; do [ -d "$d/.git" ] || continue printf '%-30s %s\n' "$(basename $d)" "$(git -C "$d" config user.email)"done27 個儲存庫,每一個都是公司信箱。不是因為我犯了 27 次錯,而是因為我根本沒做過任何決定:每個儲存庫都繼承了同一個全域預設值,而那個預設值只在一台工作用的機器上、為了工作,設定過一次。
這重新框定了修法。對單一儲存庫下的git config user.email,是打在錯誤層上的補丁。它留下另外 26 個儲存庫繼續錯下去,也保證了第 28 個儲存庫一樣會出錯,因為建立新儲存庫這件事根本不會提醒我。
條件式 include
Git 可以依儲存庫所在的位置決定 identity。這個功能從 git 2.13 就存在,而我從沒用過:
; Default: work[user] name = my-work-name email = me@company.example
; Anything under ~/Developer/personal/ overrides the above[includeIf "gitdir:~/Developer/personal/"] path = ~/.gitconfig-personal[user] name = my-personal-handle email = personal@example.com現在 identity 變成了程式碼放在哪裡的屬性,而這才是真正決定哪個 identity 才對的東西。新的個人儲存庫從建立的那一刻起就被涵蓋,不必逐一設定,也沒什麼需要記住的。
兩個目錄讀的都是同一份~/.gitconfig。只有個人用的那個目錄同時符合includeIf的條件,所以只有它會額外讀入第二份檔案。
有兩個細節會悄悄讓這套設定失效,而且都不會報錯:
結尾的斜線很關鍵。 gitdir:~/Developer/personal/比對的是那個目錄及其底下的所有內容。沒有結尾斜線的話,比對的只是單一路徑,該路徑底下的每個儲存庫都會被漏掉。
順序也很關鍵。 Git 的設定採後寫覆蓋前寫,所以includeIf必須寫在它要覆蓋的[user]區塊之後。把它放在檔案開頭,下面預設的[user]就會直接覆蓋掉 include 剛設定好的內容。檔案看起來沒問題,但行為完全沒變。
不要假設,直接兩邊都驗證:
git -C ~/Developer/personal/some-repo config user.email # personalgit -C ~/Developer/work/some-repo config user.email # work如果第二個指令的結果不知不覺變成了個人信箱,代表你的比對範圍設得太寬了。
用哪個信箱
直覺是用自己真正的個人信箱。但可以考慮改用 GitHub 的 noreply 信箱:
26102772+oharu121@users.noreply.github.com格式是<user-id>+<username>@users.noreply.github.com,id 可以用gh api user --jq .id取得。用這個信箱 author 的 commit,一樣會連結到你的帳號,一樣算進 contribution 圖,但不會有任何真實的收件匣暴露在公開儲存庫裡。
用真實信箱有一個值得知道的失敗模式。 如果那個信箱在 GitHub 帳號上還沒驗證,commit 就會顯示成未連結的狀態,沒有頭像、沒有個人檔案連結,contribution 也不會算數。而且如果你開啟了Block command line pushes that expose my email,用那個信箱 push 會直接失敗。
修正已經 push 的部分
往後的部分解決了,但已經存在的 commit 沒有。
這件事值得現在立刻做,或者永遠不要做。如果只有兩個 commit,五分鐘就搞定;如果是兩年的歷史,加上別人手上的 clone,那就是一場遷移了。成本只會愈拖愈高。
git commit --amend --reset-author是很誘人的一行指令,但除了最上面那個 commit 之外,用在其他地方都是錯的。它會把碰到的每個 commit 的 authorship 都改成你現在的 identity,包括那些根本不是你的 commit。應該改用 mailmap,它只會改寫比對得上的項目:
New Name <new@example.com> Old Name <old@company.example>git filter-repo --mailmap mailmap.txt --force因為它比對的是舊的 identity,bot 和協作者的 commit 都會原封不動地通過。以我的情況來說,有一個 Dependabot 的 commit 疊在我自己的 commit 上面;如果是一律改寫,它就會被重新歸到我頭上。
踩到的三個坑
標籤不會跟著分支走。 我的v0.1.0標籤原本指向被改寫的那個 commit,改寫之後它仍然指向舊的物件。分支和標籤是兩個獨立的 ref,兩個都需要 force push:
filter-repo把main移到改寫後的 commit 上。v0.1.0標籤是另一個獨立的 ref,filter-repo完全不會碰它,所以它繼續指向那個孤立的原始物件。
git push --force-with-lease=main:<old-sha> origin maingit push --force origin refs/tags/v0.1.0要用帶有明確預期 SHA 的--force-with-lease,而不是單純的--force。如果你作業期間遠端多了新東西(以我的情況來說,CI 在幾分鐘前自動合併了一個依賴套件的 PR),push 會直接中止,而不是把它蓋掉。
git filter-repo會刪掉你的origin遠端。 這是刻意的行為:它假設你是在一個用完即丟的 clone 上操作,想擋下你反射性地把改寫後的歷史 push 回去。它會告訴你已經刪掉了,之後你再自己加回來:
git remote add origin <url>確認 release 有沒有事。 GitHub 的 release 是綁定在標籤的名稱上的,所以強制更新標籤仍然會讓 release 保持連結,但不要假設,要去確認:
gh release view v0.1.0 --json tagName,body --jq '.tagName, (.body | length)'gh api repos/<owner>/<repo>/git/ref/tags/v0.1.0 --jq '.object.sha'有一點值得老實說清楚:舊的物件仍然留在 GitHub 上,沒有任何東西參照它,但仍能用 SHA 存取,直到被垃圾回收為止。如果外洩本身真的是問題,而不只是不美觀,就需要另外提出支援請求;光靠 force push 並不會把它們清除。
總結
這個 bug 藏在比任何設定檔都高一層的地方:identity 被設成全域的,但它其實是你現在人在哪個專案裡的屬性。
- 在決定要在哪裡套用修法之前,先數清楚受影響的儲存庫有幾個。
- 使用
includeIf "gitdir:…/",讓答案跟著目錄走。注意結尾的斜線,並且把它放在[user]之後。 - 比起真實的收件匣,優先使用 GitHub 的 noreply 信箱。
- 趁現在只有兩個 commit 的時候改寫既有歷史。用 mailmap 只改寫自己,並且記得標籤是獨立的 ref。
原本可以早幾年就抓到這個問題的檢查,只要一行:
git log -1 --format='%an <%ae>'在新儲存庫的第一個 commit 之後執行一次。



