用 git replace 改善 Git 本地版本庫的開發線圖
使用 Git 做版本控制時,有些開發歷史可能不便公開,它們通常會成為零散的本地分支。我們可能會希望這些本地分支能和公開的開發線接在一起,以利查閱及比對,此時可利用 git replace
修改本地版本庫的開發線,這樣做不須改寫既有歷史,不會影響公開版本庫,相關資訊也可以輕鬆在私人版本庫之間轉移。
例 1:把廢棄的舊開發線接上主開發線
如下圖,本來在 master-old 開發,開放原始碼時基於某些因素不公開舊歷史(比如舊歷史有些程式碼來自非開放授權的軟體,之後才逐漸修改移除),於是將版本 A、B、C 改寫成 D(D 內容與 C 相同,或有少許修改),然後公開為 master 開發線。如何把舊歷史和新歷史接在一起?
答:執行以下指令,即可把 master 和 master-old 接在一起:
git replace master~2 --graft master-old
如果 D 和 C 內容相同,也可以考慮用不呈現 D 的做法:
git replace master~2 master-old
例 2:把壓縮合併的開發線模擬成真正的合併
在 master 分支建立一 topic 特性分支做開發,基於某些因素(比如有些版本無法編譯但團隊要求所有提交必須通過編譯)以壓縮方式發布。如何把 topic 開發線呈現為 merge 到主開發線?
答:執行以下指令,即可把 topic 開發線合併到主開發線:
git replace master~1 --graft master~1^ topic
例 3:把修改過的特性分支換成真正的特性分支,或兩者並存
在 master 分支建立一 topic-old 特性分支做開發,後來基於種種考考重新改寫為 topic 分支而後合併到主開發線及公開。如何讓開發線看起來像是把 topic-old 合併到主開發線?
答:執行以下指令,可以讓主線看起來是基於 topic-old 開發:
git replace topic topic-old
如果希望能同時查閱 topic 及 topic-old 兩種開發線,可考慮這種做法:
git replace master~1 --graft master~1^ topic topic-old
例 4:隱藏不想看到的爛歷史
在 master 分支建立一 topic 特性分支做開發,topic 運作良好但開發過程過於雜亂且缺乏查閱價值。如何隱藏雜亂無章的 topic 開發線?
答:一般可考慮用 git log --first-parent master
讓查閱主開發線時不顯示支線,如果除了這段歷史外還是想看到其他支線,可執行以下指令讓 topic 分支消失:
git replace master --graft master^
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 push
、git fetch
等指令就能把這些替換操作分享到其他版本庫。不過 Git 預設不會處理分支(及標籤)以外的命名空間,因此必須在指令中明確指定(比如 git push origin refs/replace/*
、git fetch +refs/replace/*:refs/replace/*
),或在 config 中設定額外處理這些 refs。如果只是要做個完整備份,有個簡單無腦的方法是用鏡像:git clone --mirror ...
、git push --mirror ...
。
除此之外 git replace 還有許多有趣特性,限於篇幅就不多提,想深入瞭解就讀讀手冊及實地用用吧。
留言
張貼留言
1.本格歡迎任何留言,只有廣告和垃圾留言會刪。
2.希望您盡量留下代稱,以方便大家討論、回覆。
3.如果您打算長篇大論,建議在您自己的部落格貼文,然後留下連結和摘要。