UrsaKit 將 Git 倉庫從 SHA-1 遷移到 SHA-256 的指南封面

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 的倉庫上驗證。

  1. 建立 SHA-256 格式的目的地倉庫。
git init --object-format=sha256 converted
  1. 把來源倉庫的所有 ref 串流進新倉庫。 在來源 checkout 中執行,並把路徑指向目的地。
git fast-export --all --signed-tags=verbatim | \
  (cd ../converted && git fast-import --quiet)
  1. 確認結果。 歷史完整保留,格式已經改變。
cd ../converted
git rev-parse --show-object-format
# sha256
git log --format='%H %s'
# 6b7d4f0bd580fe7030a11aecad57f6cbea14965fb64ea169e3d898f27c8fdae8 two
# 31b16d360e5e845a0895d08eafbcad0df340fb72cc1af4030bd02bf2b5e75e97 one

commit 訊息與順序都保留,雜湊則不然,因為它本來就不可能保留。

  1. 還原分支與標籤名稱。 fast-import 會寫入它收到的 ref,但在宣告成功前,請確認預設分支、標籤與任何發布 ref 都存在。
git branch -a
git tag --list
git fsck --full

git fsck --full 應該回報沒有缺少物件。若沒有,代表匯出不完整——最常見的原因是 --all 未涵蓋你依賴的 ref 類型,例如你以為會保留的 notes 或 stash。

05規劃可演練的切換,而不是一次大爆炸

重寫一次,然後在鏡像上演練。對共享倉庫而言,可行的流程如下:

  1. 準備維護時段與一些停機時間。
  2. 凍結推送,把鏡像 clone 轉換成 SHA-256。
  3. 趁貢獻者暫停時,搜尋整個程式碼庫與 CI 設定中假設 40 字元雜湊的地方。
  4. 把轉換後的倉庫推成新的 origin,並強制推送所有分支。
  5. 讓每個 clone 重新 clone,而不是 pull,因為舊物件已無法對上新雜湊。

最後一步無法避免。既有的 SHA-1 clone 無法從轉換後的倉庫抓取,而在兩者之間合併會產生重複歷史。

06找出所有假設雜湊是 40 字元的地方

遷移真正失敗的地方在這裡,而且通常在轉換看似成功後數小時才發生。雜湊不只是 Git 的細節,它會滲進工具鏈。

位置假設修正
CI 緩存鍵40 個十六進位字元改用分支加完整雜湊
固定版本的 GitHub Actionsuses: 裡的 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。

常見問題

可以就地轉換倉庫嗎?

沒有單一指令可做到。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 物件較大,雜湊成本也不同,但實際影響比起遷移工作本身小得多。

返回文章列表

相關文章

相關工具