[Git] Git整理(四) git rebase 的使用

FightFightFight發表於2018-06-28

概述

在之前總結分支相關內容時說道,合併兩個分支的提交可以使用git merge,然而除了這種方式之外,還有一種方式就是使用git rebase,這兩種方式的最終結果都相同,但是合併歷史卻不同;git merge是將兩個分支做一個三方合併(如果不是直接上游分支),這樣一來,檢視提交歷史記錄,可能會顯得非常凌亂。git rebase則會將當前分支相對於基低分支的所有提交生成一系列補丁,然後放到基底分支的頂端,從而使得提交記錄變稱一條直線,非常整潔。

git merge 和 git rebase 區別

假設現在本地倉庫中有兩個分支:master分支和branch1分支,提交歷史用圖來表示如下:
這裡寫圖片描述
現在要合併branch1到master分支,如果使用git merge則執行如下命令:

$ git checkout master
$ git merge branch1

合併後檢視提交歷史如下:

$ git log --graph --pretty="oneline"
*   fe8799e0aec30e388306883960b4cf438d3f1ec4 Merge branch 'branch1'
|\  
| * cf31255da6e84acc6f6840e3ceb0fd3129e2d73e UserA commit 3--branch1
| * 5c2d1c938f8e5f98dccaa0a5ab6222bd6b1cd75d UserA commit 2--branch1
* | 284aa3eb6c405411584d682a1387118fe92e4821 Usera commit master
* | 967fca58deb914ad1cda9ff84291fd946045207d Usera commit master
|/  
* d989fc50530918b3b7b0ed68b31d6751c2302875 UserA commit 1

使用圖來表示,本地倉庫提交歷史如下:
這裡寫圖片描述

現在我們使用git rebase合併原來的master分支和branch1分支,假設當前分支為branch1,基地分支為master:

$ git checkout branch1
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: UserA commit 2--branch1
Applying: UserA commit 3--branch1
Applying: Usera commit master
Applying: Usera commit master

合併後檢視提交歷史如下:

$ git log --graph --pretty="oneline"
* 6cf95c391ba7d43d0f5d95300130a43816af82c8 Usera commit master
* 63def8a8740b9b3c9f6c09ae49ba72faa9446cf6 Usera commit master
* 33049864f83a686bff9b2a2d8626427653a16f22 UserA commit 3--branch1
* 14ac1cac7357ccf35581c89e099793260264d3ea UserA commit 2--branch1
* d989fc50530918b3b7b0ed68b31d6751c2302875 UserA commit 1

使用圖來表示,本地倉庫提交歷史如下:
這裡寫圖片描述
可以看到,現在branch1分支上相對於master分支的提交,提交到了master分支的頂端,如此一來整個提交記錄保持在一條直線上。這就是git rebase

rebase原理

git rebase <branch>的原理是:找到兩個分支最近的共同祖先,根據當前分支(上例中branch1)的提交歷史生成一系列補丁檔案,然後以基地分支最後一個提交為新的提交起始點,應用之前生成的補丁檔案,最後形成一個新的合併提交。從而使得變基分支成為基地分支的直接下游。rebase一般被翻譯為變基。

branch1分支完成變基後,直接變成了master分支的下游了,這時切換到master分支,直接通過git merge即可將branch1分支的合併到master分支上:

$ git merge branch1 
Updating ff7658d..d6168dc
Fast-forward
 test.txt | 1 +
 1 file changed, 1 insertion(+)

掌握了rebase基本原理後,接下來對該命令一些引數進行下總結。

git rebase branchA branchB

首先會取出branchB,將branchB中的提交放在branchA的頂端,一般branchB為當前分支,可以不指定。

假設當前本地倉庫提交歷史如下,且處於topic分支:

     A---B---C topic
    /
D---E---F---G master

此時我們使用git rebase將兩個分支的提交合併到master分支的頂端:

$ git rebase master
# 或者
$ git rebase master topic

此時,提交歷史將變為:

             A'--B'--C' topic
            /
D---E---F---G master

git rebase --onto

如果要在合併兩分支時需要跳過某一分支的提交,這時就可以使用--onto引數了。比如,假設當前本地倉庫提交歷史如下:

A---B---E---F---G  master
    \
     C---D---H---I---J  next
                      \
                       K---L---M  topic

此時topic分支的上游分支是next分支,如果要將topic分支上的提交(K,M,L)跳過next分支,直接放到master分支上,就需要加上--onto引數:

$ git rebase --onto master next topic

上述命令的意思是:取出topic分支,找出topic和next分支的共同祖先之後的提交,然後放在master分支上,執行後提交歷史變為如下:

A---B---E---F---G  master
    \            \
     \            K'---L'---M'  topic 
      \     
       C---D---H---I---J  next
                      

git rebase --continue/abort/skip

這三個命令分別表示:繼續執行變基操作、終止變基、跳過某一檔案繼續進行。在rebase的過程中,有可能會出現檔案衝突,比如:

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: [Description]:test1
Using index info to reconstruct a base tree...
M	test.txt
Falling back to patching base and 3-way merge...
Auto-merging test.txt
CONFLICT (content): Merge conflict in test.txt
error: Failed to merge in the changes.
Patch failed at 0001 [Description]:test1
The copy of the patch that failed is found in: .git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
$ 

這種情況下,首先要解決衝突,解決衝突後可以選擇繼續執行rebase或者結束rebase,一般的做法為:

$ git add filename
$ git rebase --continue

或者可以選擇終止變基:

$ git rebase --abort

或者跳過該patch:

$ git rebase --skip

git rebase -i

該命令相比其他命令,使用頻率要高得多。git rebase -i <commitid>可以進行互動式變基,相比於git rebase <branch>用來變基,它經常用來操作當前分支的提交,git會將<commitid>-HEAD之間的提交列在一個變基指令碼中,每一筆提交根據使用者設定的命令,會進行不同的操作,如修改提交資訊、移除指定提交、合併兩個提交為一個(壓縮提交)、拆分提交等。

如,要對最近4次提交進行重新操作,則:

$ git rebase -i HEAD~4

此時將會彈出如下形式的變基指令碼:

  1 pick af98479 [Description]:test4
  2 pick 3cc9d66 test3
  3 pick a7e819e usera commit03 branch2
  4 pick efc5b15 usera commit04 branch2
  5 
  6 # Rebase de7b118..efc5b15 onto de7b118 (4 command(s))
  7 #
  8 # Commands:
  9 # p, pick = use commit
 10 # r, reword = use commit, but edit the commit message
 11 # e, edit = use commit, but stop for amending
 12 # s, squash = use commit, but meld into previous commit
 13 # f, fixup = like "squash", but discard this commit's log message
 14 # x, exec = run command (the rest of the line) using shell
 15 # d, drop = remove commit
 16 #
 17 # These lines can be re-ordered; they are executed from top to bottom.
 18 #
 19 # If you remove a line here THAT COMMIT WILL BE LOST.
 20 #
 21 # However, if you remove everything, the rebase will be aborted.
 22 #
 23 # Note that empty commits are commented out

這裡我們可以修改pick為下面給出的其他命令,比如如果要修改提交資訊,就使用r或reword,各指令的含義如下:

p,pick:直接使用該次提交
r,reword:使用該次提交,但重新編輯提交資訊
e,edit:停止到該次提交,通過`git commit --amend`追加提交,完畢之後不要忘記使用`git rebase --continue`完成這此rebase
s,squash,壓縮提交,將和上一次提交合併為一個提交
x,exec,執行命令
d,drop,移除這次提交

接下來我們看看他們如何使用。

1.修改多個提交資訊

使用git commit --amend在最近一次提交上追加提交,因此可以使用該命令來修改最後一次的提交資訊,如果要修改做個提交資訊,需要git rebase -i <commitid>開啟變基指令碼後在需要修改資訊的提交上執行reword操作,比如以下示例:

檢視最近四次提交記錄

$ git log --oneline -4
d0a80c2 02-b2
b6c6595 01-b2
ea2f366 b2
49afab9 commit branch3w

提交資訊非常不人性化,因此對以上幾個提交記錄修改提交資訊,將預設的pick改為r或者reword:

$ git rebase -i HEAD~4

  1 r 49afab9 commit branch3w
  2 r ea2f366 b2
  3 r b6c6595 01-b2
  4 r d0a80c2 02-b2
  5 # ......

儲存退出後,會彈出編譯器輸入提交資訊,輸入完畢後:

$ git rebase -i HEAD~4
[detached HEAD afeaed3] commit first
 Date: Wed Jun 27 20:26:33 2018 +0800
 1 file changed, 1 insertion(+), 1 deletion(-)
[detached HEAD 528910c] commit second
 Date: Wed Jun 27 20:29:07 2018 +0800
 1 file changed, 1 deletion(-)
[detached HEAD 0e09a0f] commit 3th
 Date: Wed Jun 27 20:29:25 2018 +0800
 1 file changed, 1 deletion(-)
[detached HEAD eaed13d] commit 4th
 Date: Wed Jun 27 20:29:35 2018 +0800
 1 file changed, 1 deletion(-)
Successfully rebased and updated detached HEAD.

再次檢視提交log:

$ git log --oneline -4
eaed13d commit 4th
0e09a0f commit 3th
528910c commit second
afeaed3 commit first

利用git rebase -i <commitid>reword就可以完成修改多次提交資訊了。

2.重新排序提交

改變變基指令碼中的順序就可以對之前的提交重新排序,如:
選擇最近4次提交進行處理:

$ git rebase -i HEAD~4

此時會開啟變基指令碼,在指令碼中將second這次提交放在最後一次提交中:

  1 pick ecd66f5 commit first
  2 pick 7dbfe25 commit 3th
  3 pick 82ba6a6 commit 4th
  4 pick a77e06e commit second
  5 # .....

儲存退出,檢視提交log,發現second變為最後一次提交:

$ git log --oneline -4
fe15bdb commit second
18fa9a9 commit 4th
d08c408 commit 3th
ecd66f5 commit first
$ 

3.壓縮提交

如果要壓縮兩個提交為一次,需要git rebase -i <commitid>開啟變基指令碼後在需要壓縮的提交上執行squash操作,當儲存退出後,會將該筆提交和上一筆提交壓縮為一個提交,比如:

先檢視當前提交記錄:

$ git log --oneline -4
fe15bdb commit second
18fa9a9 commit 4th
d08c408 commit 3th
ecd66f5 commit first

現在我們將4th和3th這兩筆提交壓縮為一筆提交,在執行git rebase -i HEAD~4後在變基指令碼中做如下修改:

  1 pick ecd66f5 commit first
  2 pick d08c408 commit 3th
  3 squash 18fa9a9 commit 4th
  4 pick fe15bdb commit second

儲存退出後,輸入一下提交資訊,成功後再檢視log:

$ git log --oneline -4
cf4159b commit second
9d73407 commit----compress 3th and 4th
ecd66f5 commit first

說明合併成功了,如果要對多個提交進行合併壓縮,則可以按照如下的方式:

  1 pick ecd66f5 commit first
  2 squash d08c408 commit 3th
  3 squash 18fa9a9 commit 4th
  4 pick fe15bdb commit second

這表示會將first、3th、4th進行合併。

4.拆分提交

如果要將一次提交拆分為多次提交,則可以將變基指令碼中對應提交的指令修改為edit。拆分一個提交會撤消這個提交,然後多次地、部分地、暫存與提交直到完成你所需次數的提交。比如下面示例:

首先檢視提交記錄:

$ git log --oneline -4
2a5c2aa commit 4th
f2ceb0f commit 3th
20fe2f9 commit second
c51adbe commit first

現在修改3th這次提交,將這次提交拆分為多次提交,首先執行git rebase -i HEAD~3,然後在變基指令碼中將該次提交指令改為edit

  1 pick 20fe2f9 commit second
  2 edit f2ceb0f commit 3th
  3 pick 2a5c2aa commit 4th
  # ......

儲存退出後,再次檢視提交記錄:

$ git log --oneline -3
f2ceb0f commit 3th
20fe2f9 commit second
c51adbe commit first

也就是說,3th這次提交是現在最近的一次提交了,我們要拆分這次提交,那就就要重置這次提交,讓HEAD指標指向上一次提交:

$ git reset HEAD~

現在進行多次提交:

$ git add .
$ git commit -m "split commit3th---1"
$ git add .
$ git commit -m "split commit3th---2"
......
$ git add .
$ git commit -m "split commit3th---n"

滿足自己拆分後,繼續完成這次rebase:

$ git rebase --continue

最後檢視提交記錄,原來的提交被移除,新增了三條:

$ git log --oneline -6
1df4a4d split 3th----3
1c22d70 split 3th----2
dbc7d91 split 3th----1
20fe2f9 commit second
c51adbe commit first
5.移除提交

如果要移除某次提交,可以在變基指令碼中將對應提交指令改為drop,或者直接乾脆刪除,比如要移除上例中新家的三個記錄:

  1 pick c51adbe commit first
  2 pick 20fe2f9 commit second
  3 drop dbc7d91 split 3th----1
  4 drop 1c22d70 split 3th----2
  5 #pick 1df4a4d split 3th----3

檢視提交記錄:

$ git log --oneline -6
20fe2f9 commit second
c51adbe commit first

rebase的風險

一旦分支中的提交物件釋出到公共倉庫,就千萬不要對該分支進行變基操作。
因為不管是git rebase <branch>還是git rebase <commitid>,都重置了提交的SHA-1校驗和,當你將本地變基後的提交推送到遠端後,別人從伺服器同步程式碼時,由於相同的內容卻有不同的SHA-1校驗值,因此會再此進行一次合併,於是就會有兩個作者、commit資訊完全相同的提交,但是SHA-1校驗值不同,這無疑會帶來麻煩。
因此,如果把變基當成一種在推送之前清理提交歷史的手段,而且僅僅變基那些尚未公開的提交物件,就沒問題。如果變基那些已經公開的提交物件,並且已經有人基於這些提交物件開展了後續開發工作的話,就會出現叫人沮喪的麻煩。

參考:
Git 官網

相關文章