用 git replace 改善 Git 本地版本庫的開發線圖

使用 Git 做版本控制時,有些開發歷史可能不便公開,它們通常會成為零散的本地分支。我們可能會希望這些本地分支能和公開的開發線接在一起,以利查閱及比對,此時可利用 git replace 修改本地版本庫的開發線,這樣做不須改寫既有歷史,不會影響公開版本庫,相關資訊也可以輕鬆在私人版本庫之間轉移。

例 1:把廢棄的舊開發線接上主開發線

如下圖,本來在 master-old 開發,開放原始碼時基於某些因素不公開舊歷史(比如舊歷史有些程式碼來自非開放授權的軟體,之後才逐漸修改移除),於是將版本 A、B、C 改寫成 D(D 內容與 C 相同,或有少許修改),然後公開為 master 開發線。如何把舊歷史和新歷史接在一起?

例 1 處理前圖

答:執行以下指令,即可把 master 和 master-old 接在一起:

git replace master~2 --graft master-old
例 1 處理後圖

如果 D 和 C 內容相同,也可以考慮用不呈現 D 的做法:

git replace master~2 master-old
例 1 處理後圖 2

例 2:把壓縮合併的開發線模擬成真正的合併

在 master 分支建立一 topic 特性分支做開發,基於某些因素(比如有些版本無法編譯但團隊要求所有提交必須通過編譯)以壓縮方式發布。如何把 topic 開發線呈現為 merge 到主開發線?

例 2 處理前圖

答:執行以下指令,即可把 topic 開發線合併到主開發線:

git replace master~1 --graft master~1^ topic
例 2 處理後圖

例 3:把修改過的特性分支換成真正的特性分支,或兩者並存

在 master 分支建立一 topic-old 特性分支做開發,後來基於種種考考重新改寫為 topic 分支而後合併到主開發線及公開。如何讓開發線看起來像是把 topic-old 合併到主開發線?

例 3 處理前圖

答:執行以下指令,可以讓主線看起來是基於 topic-old 開發:

git replace topic topic-old
例 3 處理後圖

如果希望能同時查閱 topic 及 topic-old 兩種開發線,可考慮這種做法:

git replace master~1 --graft master~1^ topic topic-old
例 3 處理後圖 2

例 4:隱藏不想看到的爛歷史

在 master 分支建立一 topic 特性分支做開發,topic 運作良好但開發過程過於雜亂且缺乏查閱價值。如何隱藏雜亂無章的 topic 開發線?

例 4 處理前圖

答:一般可考慮用 git log --first-parent master 讓查閱主開發線時不顯示支線,如果除了這段歷史外還是想看到其他支線,可執行以下指令讓 topic 分支消失:

git replace master --graft master^
例 4 處理後圖

git replace 功能說明

git replace X Y 可以把 Git 物件 X 替換為另一個 Git 物件 Y,之後執行幾乎所有的 Git 指令在需要取用 X 的內容時,都會重導向為取用 Y 的內容。git replace 可替換任何類型的 Git 物件,不過實務上比較常用於替換 commit。

如果本來的版本線是這樣:

A --- B --- C --- D (topic)

E --- F --- G --- H (master)

執行 git replace E D 以後,git log master 看到的版本線會是這樣:

A --- B --- C --- E --- F --- G --- H (master)

這是因為 E 被替換成 D,因此 git log 嘗試讀取 E 的 parents 時會改為讀取 D 的 parents,於是看起來 C 會接在 E 前面;此外 git log 標示 E 的 ref 時會多一項 replaced 提示這個 commit 被替換過。同理,執行 git diff C E 的輸出會改為 git diff C D 的輸出。

注意替換後有些東西是「不變」的:git log 中 E 的 hash 值仍是本來的 hash 值,而非 D 的 hash 值;topic 分支在替換後仍附著於 D 而不會變成附著於 E,因此 git log master 不會在 E 的 refs 列出 topic,而 git log topic 仍是看到原來的 A --- B --- C --- D。

替換後如果執行 git push master,Git 會抓 master (H)「本來的」開發線 E --- F --- G --- H 推送;git push E 也是抓「本來的」開發線 E 推送;相對地,執行 git push C 就會把 A --- B --- C 推送出去。由此可見替換後推送原先 master 線上的 commit 都不會把 master 線以外的 commit 推送出去,但要特別小心「看起來像在 master 分支但其實在其他本地分支」的 commit(此例為 A、B、C),別把它們不小心推送出去。

執行 git replace E D 指令後 Git 後台會產生一個新的 ref refs/replace/<E 的 hash> 並參照到 D。替換後由於 D 被這個 ref 參照,因此即使把 topic 分支刪除 D 也不會被當垃圾回收(至於 E?那就看 master 線有沒有繼續參照到 E)。

為 git replace 加上 --graft 參數可模擬稼接,--graft 參數可指定一或多個 parents。比如執行 git replace E --graft C X,Git 會建立一個新提交 E',E' 的 parents 會設為 C 和 X,其他如提交訊息、時間、作者、包含的檔案等都和 E 相同,然後把 E 替換為 E'。因此執行後用 git log 看起來是 C 和 X 合併成 E。

要列出所有被替換的物件,可用 git replace -l 或不加參數直接用 git replace。要刪除對 E 的替換,可用 git replace -d E。要讓任何 Git 指令忽略替換效果可指定 Git 主參數 git --no-replace-objects <command> ... 或 Git 環境變數 GIT_NO_REPLACE_OBJECTS=1 git <command> ...

由於 git replace 實際上是操作 ref,因此備份也很簡單──透過 git pushgit fetch 等指令就能把這些替換操作分享到其他版本庫。不過 Git 預設不會處理分支(及標籤)以外的命名空間,因此必須在指令中明確指定(比如 git push origin refs/replace/*git fetch +refs/replace/*:refs/replace/*),或在 config 中設定額外處理這些 refs。如果只是要做個完整備份,有個簡單無腦的方法是用鏡像:git clone --mirror ...git push --mirror ...

除此之外 git replace 還有許多有趣特性,限於篇幅就不多提,想深入瞭解就讀讀手冊及實地用用吧。

參考資料

  1. Pro Git 2 對 git-replace 的介紹
  2. git-replace 官方說明文件

留言

這個網誌中的熱門文章

Windows 批次檔令人崩潰的特殊字元處理

中文與英文的比較

為什麼 Mercurial 沒有比 Git 更好