← 上一章:【狀況題】不小心把還沒合併的分支砍掉了,救得回來嗎? 下一章:合併發生衝突了,怎麼辦? →


另一種合併方式(使用 rebase)

前面介紹了使用 git merge 指令來合併分支,接下來介紹另一種合併分支的方式。假設我們現在的狀態是這樣:

rebase branch

catdog 以及 master 這三個分支,並且切換至 cat 分支上。這時候如果下這個指令:

$ git merge dog

則會產生一個額外的 Commit 來接合兩邊分支,這就是我們在「合併分支」章節曾經介紹過的方式。

Git 有另一個指令叫做 git rebase,也可以用來做跟 git merge 類似的事情。

從字面上來看,「rebase」是「re」加上「base」,翻成中文大概是「重新定義分支的參考基準」的意思。

所謂「base」就是指「你這分支是從哪裡生出來的」,以上面這個例子來說,catdog 這兩個分支的 base 都是 master。接著我們試著使用 git rebase 指令來「組合」catdog 這兩個分支:

$ 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」:

rebase branch

它會跳出一個對話框:

rebase branch

上面還寫著「Make sure your changes have not been pushed to anyone else」的貼心小提示,因為 Rebase 指令等於是修改歷史,不應該隨便對已經推出去給別人內容進行 rebase,因為這很容易造成其它人的困擾。這方面的「困擾」,可參閱「修改歷史紀錄」相關章節的介紹。

不管如何,按下 OK 鈕便會完成。完成之後,cat 分支將會接到 dog 分支上,像這樣:

rebase branch

如果使用 SourceTree 觀看歷史紀錄:

rebase branch

Rebase 合併分支跟一般的合併分支,第一個很明顯的差別,就是使用 Rebase 方式合併分支的話,Git 不會特別做出一個專門用來合併的 Commit。

是剪下、貼上嗎?

就以結果來看,感覺像是「把 cat 分支剪下來,然後貼在 dog 分支上面」,有點像插花時候「嫁接」的概念:

branch
photo by UGA College of Ag & Environmental Sciences

但其實不太一樣,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 的過程大概是這樣(請搭配下圖服用):

  1. 「我先拿 c68537 這個 Commit 接到 053fb2 這個 Commit 上」,因為 c68537 原本的上一層 Commit 是 e12d8e,現在要接到 053fb2 上,所以需要重新計算這個 Commit 的 SHA-1 值,重新做出一顆新的 Commit 物件 35bc96
  2. 「我再拿 b174a5 這個 Commit 接到剛剛那個新做出來的 Commit 物件 35bc96 上」,同理,因為 b174a5 這顆 Commit 要接到新的 Commit 的原因,所以它也會重新計算 SHA-1 值,得到一個新的 Commit 物件 28a76d
  3. 最後,原本的 cat 是指向 b174a5 這個 Commit,現在要改指向最後做出來的那顆新的 Commit 物件 28a76d
  4. HEAD 還是繼續指向 cat 分支。

所以,在 Rebase 的過程中你會看到 2 次的 “Applying” 的字樣,就是在做「重新計算」的事情。

那原本舊的那些…?

在上圖中,原本的那兩個 Commit(灰色),也就是 c68537b174a5 這兩個,他們的下場會是怎麼樣?

是也不會怎麼樣,反正他們就還是在 Git 的空間裡佔有一席之地,只是因為它已經沒有分支指著它,如果沒有特別去記他們這兩個 Commit 的 SHA-1 值,就會慢慢被邊緣化了吧。

但,他們並沒有馬上被刪除喔,他們只是默默的待在那邊,直到有一天被 Git 的資源回收車載走。關於 Git 的資源回收機制,請參閱「【冷知識】你知道 Git 有資源回收機制嗎? 」章節介紹。

誰 Rebase 誰有差嗎?

就以最後的檔案來說是沒什麼差別,但以歷史紀錄來說有差別,誰 Rebase 誰,會造成歷史紀錄上先後順序不同的差別。這是 cat 分支 Rebase dog 分支:

rebase branch

而這是 dog 分支 Rebase cat 分支:

rebase branch

這些 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