跳至主要内容

【Git】Git rebase 介紹:竟然可以合併多個 commit!

一般在合併分支時,比較常見的方式是使用 git merge,在看 git graph 的時候也比較容易看出分支的合併情況。不過當分支越來越多時,git graph 會變得越來越複雜,有些人反而喜歡分支乾淨一點,因此有另外一種合併分支的方式,那就是使用 git rebase。

git rebase 是什麼?​

rebase 的意思是重新定位基礎,也就是說,當你使用 git rebase 時,你會把你的分支重新定位到另一個 commit 上。

來看一下 git merge 和 git rebase 的差異:

git merge


git rebase

可以看到,使用 git merge 時,會在分支上產生一個新的 commit,而使用 git rebase 時,則是把分支上的 commit 移動到另一個 commit 上。

那 git rebase 之所以可以這麼做,就是因為他不僅改變了分支的基礎,也改變了分支上的 commit 時間順序及 commit hash。

所以在使用 git rebase 時,要特別注意,盡量不要在已經 push 到遠端的分支上使用 git rebase(除非確定那個分支只有你在用),因為這樣會改變遠端分支的 commit 歷史記錄,大部分的情況都是在本地端分支上使用 git rebase。

git rebase 誰接誰?​

在使用 git merge 的時候,要把 features 分支合併到 master 分支,會使用以下指令:

git checkout master
# 切換到 master 分支
git merge features

用中文翻起來會比較不直覺,會覺得 A merge B 應該是從 A 合併到 B,但實際上是從 B 合併到 A。

之前聽過一個解釋的方法,是把 merge 當作是 pull 的意思,也就是說,git merge features 可以理解為,從 master 分支拉 features 分支進來。(不知道有沒有幫助理解,我自己是覺得好理解很多)

而在使用 git rebase 的時候,順序跟 git merge 是相反的,要把 features 分支 rebase 到 master 分支,會使用以下指令:

git checkout features
# 切換到 features 分支
git rebase master
小技巧

剛開始在使用的時候,常常忘記是要切到哪個分支作操作,後來發現一個小技巧,不管是 git merge 還是 git rebase,只要哪一個分支會被更動,就切到哪一個分支,舉例來說:

  • A merge B:A 會新增一個 commit,所以切到 A 分支
  • B rebase A:B 會被更改歷史記錄,所以切到 B 分支

git rebase 錯了怎麼辦?​

在使用 git rebase 的時候,如果想要取消 rebase,絕對不要使用 git reset,因為 git reset 是把 HEAD 從現在的 commit hash 退回到上一個 commit hash,而 git rebase 會修改歷史記錄,所以用 git reset 沒辦法回復到 rebase 之前的狀態。

這時候可以使用 git reset ORIG_HEAD --hard 來退回 rebase,ORIG_HEAD 是當你操作 git 時,如果是比較重要的改變 (Ex: git merge/git rebase),git 會幫你記錄你的 HEAD,這樣就可以用來回復到改變之前的狀態。

補充

ORIG_HEAD 的內容可以在 .git/ORIG_HEAD 檔案中找到,打開可以看到一個 commit 的 hash 值,這個 hash 值就是你上一次重大操作 git 時的 HEAD。

而在下 git reset ORIG_HEAD --hard 時,因為他本身也是一個重大操作,所以也會再次更新 ORIG_HEAD 的內容,也就是說 git reset ORIG_HEAD 只能使用一次且為最後一次的重大操作,如果要回復到更早的狀態,可以使用 git reflog 來查看歷史記錄,再使用 git reset 來退回。

git rebase interactive 互動模式​

git rebase 有提供一個 Interactive (互動) 模式,也是我最常用的功能,讓你能夠修改 commit 的順序及內容,這個模式可以幫你修改及合併多個 commit,讓你的歷史記錄更加乾淨,是個超級好用一定要學起來的功能!

使用方式如下:

# 顯示從指定的 commit 到目前的 commit(不包含指定的 commit)
git rebase -i <commit hash>
# 顯示最近的 n 個 commit
git rebase -i HEAD~n
提示

git rebase -i <commit hash> 會顯示該 commit hash 之後的 commit,如果是第一個 commit,可以使用 git rebase -i --root 來顯示所有的 commit。

當你進入互動模式後,預設會使用 vim 來進行編輯,假設我輸入 git rebase -i HEAD~2,會看到以下畫面:



由上往下是從舊到新的 commit,而在每一行的開頭會有一個動作,這邊列出常見的動作分別是:

  • pick:保留該 commit
  • reword:保留該 commit,並開啟 vim 讓你修改 commit message
  • edit:保留該 commit,但在 rebase 過程中會暫停,可以輸入 git commit --amend 來修改 commit 內容,或是 git rebase --continue 來繼續 rebase。
  • squash:合併該 commit 到前一個 commit,會進入 vim 讓你選擇要保留的 commit message
  • fixup:合併該 commit 到前一個 commit,但不保留 commit message,直接使用前一個 commit 的 message
  • drop:刪除該 commit
Vim 基本操作
  • i:可以進入編輯模式,就是一般的文字編輯
  • esc:離開編輯模式,回到一般模式才可以進行下面指令操作
  • :wq:儲存並離開
  • :q!:不儲存並離開
  • :cq:關閉並退出,在 reflog 中不會留下記錄

pick & drop​

當你進入互動模式後,預設所有的 commit 都是 pick,也就是保留該 commit,如果你想要刪除某個 commit,可以把該行的 pick 改成 drop,或是直接刪除該行。

  • 使用 drop 及刪除整行:


vim 指令
  • dw:刪除一個單字
  • dd:刪除一整行

reword & edit​

想要修改 commit message,可以把 pick 改成 reword,這樣在 rebase 過程中會進入 vim 讓你修改 commit message。

  • 使用 reword 修改 commit message:


如果你在修改 commit 內容時,想要暫停 rebase 去做其他事情 (Ex: 新增檔案) ,可以把 pick 改成 edit,這樣在 rebase 過程中會暫停,做完事情後,可以輸入 git commit --amend 來修改最近的 commit 訊息,或是 git rebase --continue 來繼續 rebase。

  • 使用 edit 暫停 rebase 來新增檔案:


資訊

如果只是要修改最近一次的 commit message,可以使用 git commit --amend 來修改,會比較方便。

fixup & squash​

squash 和 fixup 都是合併 commit 的動作,不過 fixup 會直接使用前一個 commit 的 message。

  • 使用 fixup 合併 commit:


而 squash 會進入 vim 列出所有的 commit message,讓你選擇要保留的 message。

  • 使用 squash 合併 commit:


包含 merge commit 的 rebase​

在 rebase 的過程中,如果遇到 merge commit,預設是會被忽略掉,所以歷史紀錄就會包含 merge 的所有 commit,有時候會容易造成混淆。

例如下面是 merge-branch rebase 到 master:



原本的 merge-branch 有包含 test-branch 的 commit 跟 merge,但是 rebase 後,merge 的 commit 就消失了,取而代之的是 test-branch 的 commit。

在 git 2.22 版本之後,新增了一個參數 --rebase-merges,可以讓你在 rebase 時,保留 merge commit。

git rebase -r <commit hash>

這樣就可以保留 merge commit,讓你的歷史記錄更加完整。



這個參數也可以搭配 interactive 模式使用,讓你可以更加自由的修改 commit。

git rebase -i -r <commit hash>

加上 -r 的輸出比起一般的 -i 更加複雜,以下面這張圖為例



當我們執行 git rebase -i -r 8104fda2 時會像這樣:



  • # Branch : 通常每一個 merge 會是一個區塊,# Branch 是用來辨識每一個區塊的標籤,預設會抓取該分支的名稱並用 - 連接 (Ex: feature/branch -> feature-branch,如果同樣名稱會在後面 -數字來做辨別)。
  • label : 用來標記 commit,供後續 reset 或是 merge 使用(有需要的才會做標記),命名模式同 # Branch。
  • reset : 這是用來重新定位基礎的 commit,後面會接著 label 或是 commit hash。
  • merge : 也就是 merge 的動作,-C 代表使用原本的 commit message,如果改成 -c 則會進入 vim 讓你修改 commit message。

我們一步一步的來看:

  1. label onto : rebase 的初始定位點,預設會叫 onto
  2. # Branch merge-branch : 這是一個區塊的開始,名稱為該分支名稱
  3. reset onto : 定位在基礎的 commit
  4. pick c888119 add test : pick 這個 commit
  5. label branch-point : 因後續需要用到這個 commit 的定位點,所以進行標記,而屬於自己分支的 commit 會用 branch-point 來做區分
  6. pick 7b13f14 add merge branch : pick 這個 commit
  7. label merge-branch : 標記這個 commit,因屬於 merge-branch 的分支,所以用 merge-branch 來做區分
  8. reset branch-point : 重新定位到 branch-point 的 commit
  9. pick 89eb363 add master test : pick 這個 commit
  10. merge -C b9ddbd0 merge-branch : 進行 merge 的動作,並使用原本的 commit message
提示

如果今天要刪除 merge commit,要直接把整行刪除,用 drop 會無法執行。

參考資料​