git rebase和git merge的用法


http://softlab.sdut.edu.cn/blog/subaochen/2016/01/git-rebase%E5%92%8Cgit-merge%E7%9A%84%E7%94%A8%E6%B3%95%E5%8C%BA%E5%88%AB/

git rebase和git merge常令人迷惑,都是合並分支,什么時候用rebase,什么時候用merge呢?下面通過兩個實驗徹底搞清楚這兩個命令的區別。

hello-git是一個已經有一些提交(C0-C5)的示例項目,我們下面的兩個實驗都基於hello-git,分別通過merge和rebase兩種方法將topoic分支合並到master分支。首先做一點點准備工作:

 

$ git clone https://github.com/subaochen/hello-git # 獲取演示文件
$ git fetch origin topic # 獲取topic分支。默認git clone下來的是master分支
此時hello-git的狀態如圖 1所示,也就是說,master分支指向C3,topic分支指向C5,當前分支是master(HEAD指向master)。
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/0_home_subaochen_git_blog_imgs_git_rebase_1-1.png
圖 1:hello-git的初始狀態

git merge用法簡單,要把topic分支合並到master分支,首先確保當前處於master分支:

 

$ git branch
* master
topic
然后直接執行merge指令即可:

 

$ git merge topic
git merge的過程是首先找到master分支和topic分支的起點C2,然后把C2提交之后的所有提交,即C3、C4、C5合並起來形成一個新的提交C6。
執行命令提交合並的結果:

 

$ git commit

將創建一個新的提交C6,如圖2所示:

image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/1_home_subaochen_git_blog_imgs_git_merge_1-1.png
圖 2:git merge后的狀態
此時,master分支指向新的提交C6,並且C6有兩個父節點,即C6是C3、C4、C5合並后的結果。
如果通過git log命令可以看到如下的結果:

 

$ git log –oneline –decorate –graph –all
* 1679337 (HEAD -> master) C6:Merge branch ‘topic’
|\
| * 4b874d6 (origin/topic, topic) C5
| * 6e2550c C4
* | 96bba70 (origin/master, origin/HEAD) C3
|/
* 5aadaa6 C2
* 273c00a C1
* d50589b C0
* fb68e6f Initial commit
重點:git merge合並后只創建一個新的提交,即這個新的提交包含了所有的合並結果,合並的過程在合並后就丟失了。

在實驗rebase方法合並分支之前,首先刪除上一步的項目目錄hello-git,即重新clone一個新的項目出來:

 

$ rm -rf hello-git
$ git clone https://github.com/subaochen/hello-git
rebase的基本思路是,將topic分支的每一個提交(C4、C5)相對於C2的變更(diff,即補丁)臨時保存起來,然后逐一在C3上面重放(replay,即逐一在C3上面打補丁),每打一個補丁即創建一個新的提交,直到所有topic分支的補丁全部打完,這樣就完成了在master分支上合並topic分支的任務。rebase的本意也是這樣:將當前分支(topic)重新基於(rebase)指定的分支構建。下面是rebase方法合並topic分支到master分支的基本流程:

4.1 獲取topic分支

 

$ git fetch topic
來自 https://github.com/subaochen/hello-git
* branch topic -> FETCH_HEAD
此時hello-git的狀態和圖 1一樣。

4.2 切換到topic分支

 

$ git checkout topic
分支 topic 設置為跟蹤來自 origin 的遠程分支 topic。
切換到一個新分支 ‘topic’

4.3 rebase onto master分支

 

$ git rebase master
首先,重置頭指針以便在上面重放您的工作…
正應用:C4
正應用:C5
此時hello-git的狀態如圖 3所示:
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/2_home_subaochen_git_blog_imgs_git_after_rebase_1-1.png
圖 3:將C4、C5的diff在C3上面重放后的狀態
通過git log命令可以看出此時HEAD指向topic(當前分支)和C5,但是master分支仍然指向C3。

 

$ git log –oneline –decorate –graph –all
* 2de5ca6 (HEAD -> topic) C5
* 9eed5cb C4
* 96bba70 (origin/master, origin/HEAD, master) C3
| * 4b874d6 (origin/topic) C5
| * 6e2550c C4
|/
* 5aadaa6 C2
* 273c00a C1
* d50589b C0
* fb68e6f Initial commit
注意:如果topic分支是最新的,即topic分支是從master分支的最新節點開始,並且此后master分支沒有新的進展,如圖 4所示的情形,則get rebase master的結果只是顯示:topic是最新的。
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/3_home_subaochen_git_blog_imgs_git_before_rebase_0-1.png
圖 4:topic分支是最新時的情形

4.4 fast forward merge

我們的目標是將topic分支合並到master分支,因此現在切換到master分支執行:

 

$ git checkout master
$ git merge topic
更新 96bba70..2de5ca6
Fast-forward
C4 | 1 +
C5 | 1 +
2 files changed, 2 insertions(+)
create mode 100644 C4
create mode 100644 C5
由於在第三步的rebase操作的時候已經將topic分支合並到了master分支,這里的git merge topic僅僅是移動HEAD指針而已,因此被稱為快進(fast forward)模式。此時HEAD指向了C5,如圖 5所示:
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/4_home_subaochen_git_blog_imgs_git_after_rebase_ff-1.png
圖 5:在master分支fast forword merge之后的狀態
如果使用git log命令查看,此時HEAD已經指向了C5。

 

* 2de5ca6 (HEAD -> master, topic) C5
* 9eed5cb C4
* 96bba70 (origin/master, origin/HEAD) C3
| * 4b874d6 (origin/topic) C5
| * 6e2550c C4
|/
* 5aadaa6 C2
* 273c00a C1
* d50589b C0
* fb68e6f Initial commit

4.5 rebase的進一步解釋

注意到以下兩個命令的效果是一樣的:

 

$ git rebase master
$ git rebase master topic # 首先執行git rebase master,然后執行git checkout topic。但是實際上git rebase master也可以達到同樣的效果。另外,這條命令不管當前在哪個分支上都有效,而且語義更加明確,推薦。

4.6 神奇的rebase –onto

假設有如圖 6的情形(請git clone https://github.com/subaochen/hello-git-rebase獲取圖中的示例代碼):
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/5_home_subaochen_git_blog_imgs_git_rebase_off_topic_1-1.png
圖 6:在server分支上存在client分支的情形
通過git log –oneline –decorate –graph –all觀察的結果是:

 

* 5c38282 (server) C9
* 0b8cc9e C4
| * d2ba3bd (client) C8
| * aecaedb C7
|/
* 1e3855d C3
| * 894eb0e (HEAD -> master) C6
| * a1cdb1e C5
|/
* 20b3774 (origin/master, origin/HEAD) C2
* e99b06f C1
* 255c292 Initial commit
現在假設client分支的開發穩定了,我們希望把client分支合並到master分支上。但是此時server分支的開發尚在繼續,怎么辦呢?git rebase –onto提供了神奇的效果:

 

$ git rebase –onto master server client
首先,回退分支以便在上面重放您的工作…
應用:C7
應用:C8
這個命令的意思是說:“檢查client分支,獲得client和server的分歧點(這里是C3)以來的所有補丁(這里是C7、C8)並打在master分支上”,基本上,你可以把這句命令倒着念回去。執行后的結果會是圖所示的那樣。
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/6_home_subaochen_git_blog_imgs_git_rebase_off_topic_2-1.png
圖 7:client分支合並到master后的情形
然后再在master分支執行fast forword merge即可:

 

$ git checkout master
$ git merge client
更新 894eb0e..d64c83b
Fast-forward
C7 | 1 +
C8 | 1 +
2 files changed, 2 insertions(+)
create mode 100644 C7
create mode 100644 C8
最終的結果正如我們期望的:
image: http://softlab.sdut.edu.cn/blog/subaochen/wp-content/uploads/sites/4/2016/02/7_home_subaochen_git_blog_imgs_git_rebase_off_topic_3-1.png
圖 8:rebase –onto的最終結果
git log –oneline –decorate –graph –all的執行結果也佐證了這一點:

 

$ git log –oneline –decorate –graph –all
* d64c83b (HEAD -> master, client) C8
* 729c0bb C7
* 894eb0e (origin/master, origin/HEAD) C6
* a1cdb1e C5
| * 5c38282 (origin/server, server) C9
| * 0b8cc9e C4
| | * d2ba3bd (origin/client) C8
| | * aecaedb C7
| |/
| * 1e3855d C3
|/
* 20b3774 C2
* e99b06f C1
* 255c292 Initial commit

雖然,兩種合並分支方式的最終結果並沒有什么差別,但是rebase模式的最大優點是可以保留分支的變動情況,或者說,topic分支的歷史在master分支上被完整的復現了出來。merge模式的合並則不然,merge在合並點像和面一樣將兩個分支融合在一,只是記錄了合並的結果,在master分支上看不出topic分支的變動情況。因此,在大多數情況下,建議使用rebase來合並分支。如果是采用pull更新遠程分支,建議使用如下的命令:

 

$ git pull –rebase upstream branch
為了方便起見,建議設置別名:

 

$ git alias pull “pull –rebase”
通常,在一些開源項目中要求使用rebase合並分支。如果你要為一個開源項目開發一個新特性,通常會創建一個分支,然后在這個分支上開發新特性,開發完成后再合並到master分支上來。如果采用rebase方式合並分支,則開源項目的維護人只需要執進行fast forward merge即可。比如: https://developer.jboss.org/wiki/HackingOnWildFly#

重復“准備工作”獲得一個新鮮的hello-git,我們故意制造一個沖突看看:在master分支中執行:

 

$ echo “C4 from master” >> C4
然后按照rebase的合並方法試圖將topic分支合並到master的時候會提示:

 

首先,回退分支以便在上面重放您的工作…
應用:C4
使用索引來重建一個(三方合並的)基礎目錄樹…
回落到基礎版本上打補丁及進行三方合並…
自動合並 C4
沖突(添加/添加):合並沖突於 C4
error: 無法合並變更。
打補丁失敗於 0001 C4
失敗的補丁文件副本位於:.git/rebase-apply/patch
當您解決了此問題后,執行 “git rebase –continue”。
如果您想跳過此補丁,則執行 “git rebase –skip”。
要恢復原分支並停止變基,執行 “git rebase –abort”。
非常人性化,這告訴我們rebase在給C4打補丁時發現沖突因此停了下來(C5還沒有處理)並且給出三個解決方案:

 

  • 解決沖突后執行git rebase –continue繼續rebase的流程。
  • 如果跳過這個補丁(通常這是不允許的)則可以執行git rebase –skip。
  • 如果后悔了,可以通過執行git rebase –abort回復rebase之前的狀態。
通常的做法是,打開C4這個文件解決沖突后執行git add C4,然后git rebase –continue。

這部分還需要加強學習和理解!
TBD,see revert a faulty merge howto

如果更新還沒有push到遠程倉庫,那么使用rebase,盡量保留更新的歷史;如果更新已經push到了遠程倉庫,那么永遠不要rebase這部分代碼!

 

    • 《pro git》,分支一章講的非常精彩,這篇日志的部分內容搬運自pro git外加自己的理解。
    • git help rebase也是不錯的學習材料!
    • 本文的pdf版本和dot源文件可到https://github.com/subaochen/blog 下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM