Git
章節 ▾ 第二版

7.13 Git 工具 - 取代

取代

如同我們之前強調過的,Git 物件資料庫中的物件是不可變的,但 Git 確實提供了一種有趣的方式來假裝使用其他物件來取代資料庫中的物件。

replace 命令可讓您在 Git 中指定一個物件,並說「每次您參考這個物件時,假裝它是個不同的物件」。這在用另一個提交取代歷史中的一個提交時最有用,而無需使用例如 git filter-branch 來重建整個歷史。

舉例來說,假設您有一個龐大的程式碼歷史,並且想要將您的儲存庫分割成一個給新開發人員的簡短歷史,以及一個給對資料探勘感興趣的人的更長且更大的歷史。您可以將一個歷史嫁接到另一個歷史上,方法是「取代」新行中的最早提交,並使用舊行上的最新提交。這樣做的好處是,您實際上不必重寫新歷史中的每個提交,因為您通常需要重寫這些提交才能將它們連接在一起(因為父系會影響 SHA-1)。

讓我們試試看。讓我們取得現有的儲存庫,將其分割成兩個儲存庫,一個是最近的,另一個是歷史的,然後我們將看看如何透過 replace 重新組合它們,而無需修改最近儲存庫的 SHA-1 值。

我們將使用一個包含五個簡單提交的簡單儲存庫

$ git log --oneline
ef989d8 Fifth commit
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

我們想要將其分割成兩個歷史線。一條線從提交一到提交四 - 那將是歷史的一條。第二條線僅包含提交四和五 - 那將是最近的歷史。

Example Git history
圖 163. Git 歷史範例

嗯,建立歷史歷史很容易,我們只需要在歷史中放入一個分支,然後將該分支推送至新遠端儲存庫的 master 分支。

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit
Creating a new `history` branch
圖 164. 建立新的 history 分支

現在我們可以將新的 history 分支推送至新儲存庫的 master 分支

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

好的,所以我們的歷史已發佈。現在比較困難的部分是縮短我們最近的歷史,使其更小。我們需要一個重疊,以便我們可以使用另一個中的等效提交來取代一個提交,因此我們將其截斷為僅提交四和五(因此提交四會重疊)。

$ git log --oneline --decorate
ef989d8 (HEAD, master) Fifth commit
c6e1e95 (history) Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

在這種情況下,建立一個包含如何擴充歷史的說明的基本提交會很有用,以便其他開發人員知道,如果他們遇到截斷歷史中的第一個提交並且需要更多歷史記錄時該怎麼做。因此,我們要做的是建立一個初始提交物件作為我們的基準點,其中包含說明,然後在其上變更剩餘的提交(四和五)的基底。

要執行此操作,我們需要選擇一個分割點,對於我們而言,它是第三個提交,在 SHA 詞彙中為 9c68fdc。因此,我們的基本提交將基於該樹狀結構。我們可以使用 commit-tree 命令建立我們的基本提交,該命令僅採用樹狀結構,並將回傳一個全新的、無父系的提交物件 SHA-1。

$ echo 'Get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
注意

commit-tree 命令是一組通常稱為「底層」命令的命令之一。這些命令通常不打算直接使用,而是由其他 Git 命令使用來執行較小的工作。在我們做這種比較奇怪的事情時,它們允許我們執行非常低階的事情,但並非用於日常使用。您可以在底層和瓷器中閱讀更多關於底層命令的資訊。

Creating a base commit using `commit-tree`
圖 165. 使用 commit-tree 建立基本提交

好的,既然我們現在有了一個基礎提交(base commit),我們可以使用 git rebase --onto 將其餘的歷史記錄重新設定基底於此。--onto 參數將會是我們剛從 commit-tree 取得的 SHA-1 值,而 rebase 的起點將會是第三個提交(也就是我們想要保留的第一個提交 9c68fdc 的父提交)

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
Rebasing the history on top of the base commit
圖 166. 將歷史記錄重新設定基底於基礎提交

好的,現在我們已經將最近的歷史記錄重新寫在一個拋棄式的基礎提交之上,這個基礎提交現在包含了關於如何在我們想要時重新建立整個歷史記錄的指令。我們可以將這個新的歷史記錄推送到一個新的專案,這樣當人們複製該儲存庫時,他們只會看到最近的兩個提交,然後是一個帶有指令的基礎提交。

現在讓我們切換角色,想像成某個第一次複製專案,並且想要取得完整歷史記錄的人。要在複製這個截斷的儲存庫後取得歷史資料,必須為歷史儲存庫新增一個第二個遠端來源並執行 fetch

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

現在協作者的 master 分支會有他們最近的提交,而 project-history/master 分支會有歷史提交。

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
622e88e Get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

要將它們合併,您可以簡單地使用 git replace,並指定您想要替換的提交以及您想要用來替換的提交。因此,我們想要將 master 分支中的「第四個」提交替換為 project-history/master 分支中的「第四個」提交

$ git replace 81a708d c6e1e95

現在,如果您查看 master 分支的歷史記錄,它看起來會像這樣

$ git log --oneline master
e146b5f Fifth commit
81a708d Fourth commit
9c68fdc Third commit
945704c Second commit
c1822cf First commit

很酷吧?在不必更改上游所有 SHA-1 值的情況下,我們能夠將歷史記錄中的一個提交替換為完全不同的提交,並且所有正常的工具(bisectblame 等)都會如我們預期的方式運作。

Combining the commits with `git replace`
圖 167. 使用 git replace 合併提交

有趣的是,它仍然顯示 81a708d 為 SHA-1 值,儘管它實際上使用的是我們用來替換它的 c6e1e95 提交資料。即使您執行像 cat-file 這樣的指令,它也會顯示已替換的資料

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

請記住,81a708d 的實際父提交是我們的佔位符提交(622e88e),而不是這裡所說的 9c68fdce

另一個有趣的地方是,此資料會保留在我們的引用中

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

這表示我們可以很容易地與其他人分享我們的替換,因為我們可以將其推送到我們的伺服器,其他人也可以輕鬆下載。在我們這裡討論的歷史嫁接情境中,這並不是很實用(因為無論如何每個人都會下載這兩個歷史記錄,那又何必將它們分開?),但在其他情況下可能會很有用。

scroll-to-top