章節 ▾ 第二版

7.13 Git 工具 - 替換

替換

正如我們之前強調過的,Git 物件資料庫中的物件是不可變的,但 Git 提供了一種有趣的方式來“假裝”用其他物件替換資料庫中的物件。

replace 命令允許你指定 Git 中的一個物件,並告訴 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-1 來說是 9c68fdc。所以,我們的基提交將基於那個樹。我們可以使用 commit-tree 命令建立我們的基提交,它只需要一個樹,然後會給我們一個全新的、無父提交的物件 SHA-1。

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

commit-tree 命令是一組通常被稱為“plumbing”命令的命令之一。這些命令通常不直接使用,而是由其他 Git 命令用於執行較小的任務。在進行像我們現在這樣更奇特的活動時,它們允許我們執行非常底層的操作,但並不適合日常使用。你可以在 Plumbing and Porcelain 中閱讀更多關於 plumbing 命令的資訊。

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

好的,現在我們有了一個基提交,我們可以使用 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. 將歷史記錄重新定位到基提交之上

好的,現在我們已經將我們的最近歷史記錄重寫到了一個一次性的基提交之上,該基提交現在包含了如何根據需要重構整個歷史記錄的說明。我們可以將這個新的歷史記錄推送到一個新的專案,現在當人們克隆該倉庫時,他們只會看到最近的兩個提交和一個帶有說明的基提交。

現在,讓我們切換到第一次克隆專案的協作者的角色,他想要完整的歷史記錄。要獲取克隆這個截斷後的倉庫後的歷史資料,需要新增第二個遠端倉庫指向歷史悠久的版本並進行獲取。

$ 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,指定要替換的提交,然後是用來替換它的提交。所以我們想用 project-history/master 分支中的“第四個”提交替換 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

這意味著我們可以輕鬆地與他人共享我們的替換資訊,因為我們可以將其推送到我們的伺服器,其他人可以輕鬆下載。這對於我們上面講到的歷史記錄嫁接場景來說並不是非常有幫助(因為每個人都會下載兩個歷史記錄,何必分開呢?),但在其他情況下可能有用。