← 上一章:【狀況題】不小心把還沒合併的分支砍掉了,救得回來嗎? 下一章:合併發生衝突了,怎麼辦? →
另一種合併方式(使用 rebase)
前面介紹了使用 git merge
指令來合併分支,接下來介紹另一種合併分支的方式。假設我們現在的狀態是這樣:
有 cat
、dog
以及 master
這三個分支,並且切換至 cat
分支上。這時候如果下這個指令:
$ git merge dog
則會產生一個額外的 Commit 來接合兩邊分支,這就是我們在「合併分支」章節曾經介紹過的方式。
Git 有另一個指令叫做 git rebase
,也可以用來做跟 git merge
類似的事情。
從字面上來看,「rebase」是「re」加上「base」,翻成中文大概是「重新定義分支的參考基準」的意思。
所謂「base」就是指「你這分支是從哪裡生出來的」,以上面這個例子來說,cat
跟 dog
這兩個分支的 base 都是 master
。接著我們試著使用 git rebase
指令來「組合」cat
跟 dog
這兩個分支:
$ git rebase dog
這個指令翻成白話文,大概就是「我,就是 cat
分支,我現在要重新定義我的參考基準,並且將使用 dog
分支當做我新的參考基準」的意思。這個指令執行的訊息如下:
$ git rebase dog
First, rewinding head to replay your work on top of it...
Applying: add cat 1
Applying: add cat 2
如果要使用 SourceTree 來進行 Rebase,可在左邊選單找到想要 Rebase 的對象,按滑鼠右鍵並選擇「Rebase current branch onto dog」:
它會跳出一個對話框:
上面還寫著「Make sure your changes have not been pushed to anyone else」的貼心小提示,因為 Rebase 指令等於是修改歷史,不應該隨便對已經推出去給別人內容進行 rebase,因為這很容易造成其它人的困擾。這方面的「困擾」,可參閱「修改歷史紀錄」相關章節的介紹。
不管如何,按下 OK 鈕便會完成。完成之後,cat
分支將會接到 dog
分支上,像這樣:
如果使用 SourceTree 觀看歷史紀錄:
Rebase 合併分支跟一般的合併分支,第一個很明顯的差別,就是使用 Rebase 方式合併分支的話,Git 不會特別做出一個專門用來合併的 Commit。
是剪下、貼上嗎?
就以結果來看,感覺像是「把 cat
分支剪下來,然後貼在 dog
分支上面」,有點像插花時候「嫁接」的概念:
photo by Reka Biro-Horvath
但其實不太一樣,Rebase 不是「剪下、貼上」這麼單純。有注意到剛剛在 rebase 的時候的那段訊息嗎?
$ git rebase dog
First, rewinding head to replay your work on top of it...
Applying: add cat 1
Applying: add cat 2
以我們這個例子來說,Rebase 的過程大概是這樣(請搭配下圖服用):
|
所以,在 Rebase 的過程中你會看到 2 次的 “Applying” 的字樣,就是在做「重新計算」的事情。
那原本舊的那些…?
在上圖中,原本的那兩個 Commit(灰色),也就是 c68537
跟 b174a5
這兩個,他們的下場會是怎麼樣?
是也不會怎麼樣,反正他們就還是在 Git 的空間裡佔有一席之地,只是因為它已經沒有分支指著它,如果沒有特別去記他們這兩個 Commit 的 SHA-1 值,就會慢慢被邊緣化了吧。
但,他們並沒有馬上被刪除喔,他們只是默默的待在那邊,直到有一天被 Git 的資源回收車載走。關於 Git 的資源回收機制,請參閱「【冷知識】你知道 Git 有資源回收機制嗎? 」章節介紹。
誰 Rebase 誰有差嗎?
就以最後的檔案來說是沒什麼差別,但以歷史紀錄來說有差別,誰 Rebase 誰,會造成歷史紀錄上先後順序不同的差別。這是 cat
分支 Rebase dog
分支:
而這是 dog
分支 Rebase cat
分支:
這些 Commit 的歷史先後順序就明顯不同了。
【狀況題】怎麼取消 rebase?
如果是一般的合併,也許只要 git reset HEAD^ --hard
一行指令,拆掉這個合併的 Commit 大家就會退回合併前的狀態。但是,從上面的結果可得知,Rebase 並沒有做出那個合併專用的 Commit,而是整串都串在一起了,就跟一般的 Commit 差不多。所以這時候如果執行 git reset HEAD^ --hard
,只會拆掉最後一個 Commit,但並不會回到 Rebase 前的狀態。
使用 Reflog
第一個方法是使用 Reflog。
沒錯,又是它,其實 Reflog 會紀錄很多好用的東西。舉個例子來說,我剛剛把 cat
分支 Rebase 到 dog
分支上,所以目前的樣子長這樣:
$ git log --oneline
28a76dc (HEAD -> cat) add cat 2
35bc96e add cat 1
053fb21 (dog) add dog 2
b69eb62 add dog 1
e12d8ef (master) add database.yml in config folder
85e7e30 add hello
657fce7 add container
abb4f43 update index page
cef6e40 create index page
cc797cd init commit
翻一下現在的 Reflog:
$ git reflog
28a76dc (HEAD -> cat) HEAD@{0}: rebase finished: returning to refs/heads/cat
28a76dc (HEAD -> cat) HEAD@{1}: rebase: add cat 2
35bc96e HEAD@{2}: rebase: add cat 1
053fb21 (dog) HEAD@{3}: rebase: checkout dog
b174a5a HEAD@{4}: checkout: moving from master to cat
e12d8ef (master) HEAD@{5}: checkout: moving from new_cat to master
b174a5a HEAD@{6}: checkout: moving from master to new_cat
e12d8ef (master) HEAD@{7}: checkout: moving from new_cat to master
b174a5a HEAD@{8}: checkout: moving from master to new_cat
...[略]...
看得出來最新的幾次紀錄都是在做 Rebase,但我看到這行:
b174a5a HEAD@{4}: checkout: moving from master to cat
看起來這應該是在開始做 Rebase 前的最後動作,所以就是它了!我使用 reset
指令硬切回去:
$ git reset b174a5a --hard
HEAD is now at b174a5a add cat 2
這就一來就會回到 Rebase 前的狀態了。
使用 ORIG_HEAD
在 Git 有另一個特別的紀錄點叫做 ORIG_HEAD
,這個 ORIG_HEAD 會記錄「危險操作」之前 HEAD 的位置。例如分支合併或是 Reset 之類的都算是所謂的「危險操作」。透過這個紀錄點來取消這次 Rebase 相對的更簡單:
$ git rebase dog
First, rewinding head to replay your work on top of it...
Applying: add cat 1
Applying: add cat 2
成功重新計算 2 個 Commit 並接到 dog
分支上了,這時候可使用這個指令輕鬆的跳回 Rebase 前的狀態:
$ git reset ORIG_HEAD --hard
HEAD is now at b174a5a add cat 2
一切又都回來了!
使用 Rebase 時機?
小結一下,使用 Rebase 來合併分支的好處,就是它不像一般合併可能會產生額外的合併專用的 Commit,而且歷史順序可以依照誰 Rebase 誰而決定;但缺點就是它相對的比一般的合併來得沒那麼直覺,一個不小心可能會弄壞掉而且還不知道怎麼 Reset 回來,或是發生衝突的時候就會停在一半,對不熟悉 Rebase 的人來說是個困擾。關於發生衝突,將在「合併發生衝突了,怎麼辦?」章節再另外介紹。
通常在還沒有推(Push)出去但感覺得有點亂(或太瑣碎)的 Commit,我會先使用 Rebase 分支來整理完再推出去。但如同前面提到的,Rebase 等於是修改歷史,修改已經推出去的歷史可能會對其它人帶來困擾,所以對於已經推出去的內容,非必要的話請盡量不要使用 Rebase。
Comments