
內容摘要
你無法把 SHA-1 倉庫抓進 SHA-256 倉庫——Git 會以 mismatched algorithms 錯誤拒絕。請改用 fast-export 與 fast-import 轉換,並把每個寫死的 40 字元雜湊當成 bug。
Git 會對每個物件——blob、tree、commit——計算雜湊,並以其雜湊值儲存。自 2005 年以來,這個雜湊一直是 SHA-1,而針對 SHA-1 發表的碰撞攻擊,正是 Git 新增第二種物件格式的原因。Git 3.0 計畫改以 SHA-256 為預設,這讓原本的理論問題,變成所有大型倉庫維護者都得排進時程的實際工作。
本文會在一座真實倉庫上完成轉換,示範你採用直覺 clone 做法時會遇到的確切錯誤,並列出當 40 字元雜湊變成 64 字元後會壞掉的所有寫死假設。
01物件格式改變時,什麼跟著改變
Git 是內容定址資料庫:物件的名稱就是其內容的雜湊,因此雜湊函數屬於倉庫身分的一部分。這立刻帶來兩個結果。
- 每個物件都會得到新名稱,所以每個 commit 雜湊都會改變。
- SHA-256 倉庫與 SHA-1 倉庫無法直接共享物件,因為相同內容在兩邊算出的雜湊不同。
第二點最常被低估。你無法把 SHA-256 的 clone 接上 SHA-1 歷史再推送;兩者的物件圖不相容。
請把這件事寫進計畫:這是重寫,不是設定
切換物件格式會改變每個 commit 的身分。任何綁在 SHA-1 commit 上的東西——CI action、發布標籤、submodule commit、部署紀錄——都會指向轉換後已不存在的物件。
02先確認你的倉庫實際用什麼格式
先量測,不要猜。以下指令都能在本機倉庫直接執行。
git --version
# git version 2.50.1 (Apple Git-155)
git rev-parse --show-object-format
# sha1
rev-parse --show-object-format 會印出你所在倉庫的格式。請對所有 checkout 執行一次,找出還在使用 SHA-1 的倉庫,再用物件數量估算工作量。
git count-objects -vH
git rev-list --all --count
想快速做正確性探測,就在 SHA-1 倉庫與 SHA-256 倉庫中對相同位元組計算雜湊。摘要長度與摘要本身都會改變:
cd sha1-repo && printf 'hello\n' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a
cd sha256-repo && printf 'hello\n' | git hash-object --stdin
# 2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4
建一個臨時的 SHA-256 倉庫,就能在不動到正式工作的情況下看到差異:
git init --object-format=sha256 scratch
cd scratch && git commit --allow-empty -m init
git rev-parse HEAD
# a7bd209de3540f82c9736ffd0422615b8d8ebb7009ee32a6d9303f93f2807342
commit id 是 64 個十六進位字元,不是 40 個。這一個差異會傳導到每個驗證或截斷雜湊的腳本。
03直覺做法會失敗——錯誤長這樣
建立 SHA-256 倉庫並把 SHA-1 倉庫加為遠端,是行不通的。抓取會直接報錯:
$ git init --object-format=sha256 converted
$ git -C converted remote add origin ../legacy.git
$ git -C converted fetch origin
fatal: mismatched algorithms: client sha256; server sha1
這不是權限問題,也不是少了參數;傳輸層拒絕在兩種格式之間搬移物件。這排除了多數人第一個想到的遷移策略。
不要以跨格式 clone 規劃遷移
git clone沒有 object-format 參數,而目前 Git 的跨格式 fetch 會失敗。若有教學叫你把倉庫 clone 成 SHA-256,那描述的版本或修補目前並未出貨。
04透過 fast-export 與 fast-import 轉換
可靠的做法是把歷史序列化為文字,再重播進新倉庫,讓新倉庫用 SHA-256 重新雜湊所有內容。這正是 fast-export 與 fast-import 的用途,以下已在一個兩次 commit 的倉庫上驗證。
- 建立 SHA-256 格式的目的地倉庫。
git init --object-format=sha256 converted
- 把來源倉庫的所有 ref 串流進新倉庫。 在來源 checkout 中執行,並把路徑指向目的地。
git fast-export --all --signed-tags=verbatim | \
(cd ../converted && git fast-import --quiet)
- 確認結果。 歷史完整保留,格式已經改變。
cd ../converted
git rev-parse --show-object-format
# sha256
git log --format='%H %s'
# 6b7d4f0bd580fe7030a11aecad57f6cbea14965fb64ea169e3d898f27c8fdae8 two
# 31b16d360e5e845a0895d08eafbcad0df340fb72cc1af4030bd02bf2b5e75e97 one
commit 訊息與順序都保留,雜湊則不然,因為它本來就不可能保留。
- 還原分支與標籤名稱。
fast-import會寫入它收到的 ref,但在宣告成功前,請確認預設分支、標籤與任何發布 ref 都存在。
git branch -a
git tag --list
git fsck --full
git fsck --full 應該回報沒有缺少物件。若沒有,代表匯出不完整——最常見的原因是 --all 未涵蓋你依賴的 ref 類型,例如你以為會保留的 notes 或 stash。
05規劃可演練的切換,而不是一次大爆炸
重寫一次,然後在鏡像上演練。對共享倉庫而言,可行的流程如下:
- 準備維護時段與一些停機時間。
- 凍結推送,把鏡像 clone 轉換成 SHA-256。
- 趁貢獻者暫停時,搜尋整個程式碼庫與 CI 設定中假設 40 字元雜湊的地方。
- 把轉換後的倉庫推成新的 origin,並強制推送所有分支。
- 讓每個 clone 重新 clone,而不是 pull,因為舊物件已無法對上新雜湊。
最後一步無法避免。既有的 SHA-1 clone 無法從轉換後的倉庫抓取,而在兩者之間合併會產生重複歷史。
06找出所有假設雜湊是 40 字元的地方
遷移真正失敗的地方在這裡,而且通常在轉換看似成功後數小時才發生。雜湊不只是 Git 的細節,它會滲進工具鏈。
| 位置 | 假設 | 修正 |
|---|---|---|
| CI 緩存鍵 | 40 個十六進位字元 | 改用分支加完整雜湊 |
| 固定版本的 GitHub Actions | uses: 裡的 commit SHA | 重寫後重新固定;舊 SHA 已不存在 |
| Submodule | 父倉庫固定子 commit | 從轉換後的子倉庫重新加入 |
| 發布說明 | 連到 commit URL 的連結 | 依新雜湊重新產生連結 |
| 腳本 | substr($sha, 0, 7) | 七字元仍可用,但碰撞風險提高 |
| 簽章 commit 與標籤 | 簽章涵蓋物件 | 重新簽章;簽章不會跟著重寫保留 |
請用搜尋找出所有位置,不要靠記憶。只要能比對 40 字元小寫十六進位字串的規則,就足以起步;在整套 monorepo 上跑之前,regex 測試工具可以快速調好它。
07讓兩種格式並存運作
過渡期間,同一台機器上會有 SHA-1 與 SHA-256 checkout,而 git rev-parse --show-object-format 會告訴你當下在哪一種。撰寫工具時請明確指定格式,不要讓腳本默默假設目前目錄是 SHA-1:
git rev-parse --show-object-format
git -C /path/to/repo rev-parse --show-object-format
若某個下游系統暫時無法更新,可以保留舊倉庫的唯讀 SHA-1 鏡像作為參照,但不要讓任何人對它提交——歷史分歧比延後遷移更糟。
08常見錯誤與修正
| 錯誤 | 原因 | 修正 |
|---|---|---|
mismatched algorithms | 在兩種物件格式之間抓取 | 改用 fast-export / fast-import 轉換 |
| 匯入後歷史缺少 | --all 未涵蓋想要的 ref | 匯出指定 ref,或一併包含 notes 與標籤 |
| CI 無法再 checkout 固定的 SHA | 固定的是遷移前的 commit | 重新固定到轉換後的 commit |
| 切換後推送被拒 | 既有 clone 帶有 SHA-1 物件 | 重新 clone 一份新的工作副本 |
git fsck 回報懸空 ref | 匯入留下的臨時 ref | 刪除過期 ref,再跑一次 git fsck |
這個遷移之所以困難,不是因為物件格式,而是因為雜湊是一種身分,會滲進從未提及 SHA-1 的緩存、固定版本與整合。用腳本轉換倉庫,然後在時段結束前,把每個寫死的雜湊都當成要追捕的 bug。
比對轉換前後的 ref 清單,確認沒有遺漏。
Diff 比對工具常見問題
可以就地轉換倉庫嗎?
沒有單一指令可做到。Git 不支援就地轉換物件格式;官方可行路徑是重建一個新倉庫,而這會重寫每個物件雜湊。
遷移會改動 commit 雜湊嗎?
會,而且這正是重點。每個 commit、tree 與 blob 都會得到新識別碼,因此標籤、簽章 commit、submodule 指標與外部參照都會改變。
SHA-256 倉庫可以從 SHA-1 遠端抓取嗎?
目前 Git 不行。它會以 mismatched algorithms 錯誤失敗,所以你必須透過 fast-export 與 fast-import 轉換,而不是 clone。
CI 會壞掉嗎?
只有當某處假設雜湊是 40 字元十六進位字串時才會:緩存鍵、產物路徑、以 commit 固定的 action、部署標記。切換前先搜尋 40 字元樣式。
SHA-256 比較快還是比較慢?
由於識別碼較長,SHA-256 物件較大,雜湊成本也不同,但實際影響比起遷移工作本身小得多。
相關文章
- 如何稽核 Git hooks 的憑證竊取風險
hook 會在 checkout 時執行任意程式碼,遷移前值得先檢查。
- 用 diff 工具比對兩段文字
逐行驗證遷移前後的 ref 清單。
相關工具
- Diff 比對工具
找出遷移前後的 ref 或設定差異。
- Regex 測試工具
找出腳本裡寫死的 40 字元雜湊。
- Base64 編碼/解碼
檢查嵌在權杖與載荷中的雜湊。