前言
這一節主要介紹git cherry-pick與git rebase的原理及使用。
一、Git cherry-pick
Git cherry-pick的作用為移植提交。比如在dev分支錯誤地進行了兩次提交2nd和3rd,如果想要將這兩次提交移植到master分支上。采用先刪除再添加的方法將會很繁瑣,而使用cherry-pick就能輕松實現這一需求。
首先在版本庫中創建了兩個分支master和dev,並模擬上述場景:

可以看到,在dev分支上進行了兩次提交,在master分支上只進行了一次提交。現在想要將這兩次提交移植到master分支上。整體分為兩步:
- 第一步:將
dev分支上多余的兩次提交移植到master分支上; - 第二步:刪除
dev分支上多余的兩次提交;
1.第一步
git cherry-pick commit_id
首先切換到master分支,然后使用如下命令將dev分支上的兩次提交移植到master分支上:
//移植2nd提交
git cherry-pick 009dd
//移植3rd提交
git cherry-pick aec8c
009dd和aec8c分別表示需要移植的提交2nd和3rd的SHA1值:

移植過程為:

-
如上圖所示,執行了兩次
cherry-pick指令,創建了兩個內容與2nd、3rd一致的提交對象50477和f05a0。所以,cherry-pick指令移植提交的實質是:先將需要移植的提交復制一份,再拼接到master分支上,簡稱先復制,再拼接; -
上面按照順序先移植了提交
2nd再移植提交3rd,不會發生沖突; -
不按順序移植,如先移植提交
3rd會發生合並沖突,需要手動解決:

通過vi test.txt查看發生合並沖突的test.txt文件:

可以發現master分支上initial commit提交中的文件test.txt直觀上並不與提交3rd中的test.txt沖突,如下圖所示:

但是為什么會發生合並沖突呢?原因在於三方合並原則:

如上圖所示,當想要將dev中的提交E與master分支的提交B合並時,首先要找到B和E的公共父節點A,在A的基礎上根據B和E進行三方合並;
了解了三方合並原則后就能解釋上面發生合並沖突的原因了:
-
由於提交
3rd是基於提交2nd創建的,因此3rd中保留了2rd中對文件的操作記錄; -
如果直接將
3rd拼接到initial commit后面,就會失去提交2nd的記錄; -
由此提交
3rd就不能通過提交2nd找到公共提交節點init,這就會導致合並失敗;
所以,無論內容是否沖突,合並過程都會出現沖突:

解決方法:手動合並三步曲:
- 首先,選擇要保留的內容,解決沖突:

- 然后,通過
git add將修改信息納入暫存區:

- 最后,通過
git commit提交修改信息:

完成后查看master分支的提交歷史:

可以看到解決沖突,手動合並后,成功完成了整個cherry-pick過程。並且新增的提交是手動合並時進行的提交,而不是直接復制的提交3rd:

2.第二步
此時兩分支的狀態為:

接下來就要刪除dev分支上錯誤的兩次提交2nd和3rd,相當於版本回退;可以使用三種方法:revert、reset和checkout,這里演示checkout和reset兩種方法。
使用checkout
首先切換到dev分支,然后通過以下指令切換到提交initial commit:
//dd703是提交initial_commit的SHA1值
git checkout dd703
此時該節點處於游離狀態:

然后再刪除dev分支:

由於之前修改的dev分支沒有與master進行合並,所以刪除時需要使用參數-D強制刪除。
刪除后,剩下master分支與游離提交。此時再通過以下指令將游離的節點設置為dev分支即可:
git checkout -b dev

由此通過"偷天換日"的方式使dev分支回到了錯誤提交前的狀態;
使用reset
由於使用checkout只是移動了HEAD指針,沒移動dev分支指針,所以會出現游離提交節點;而reset會同步移動HEAD和dev分支指針,不會造成這樣的問題。所以這里使用reset進行版本回退會簡單很多:
git reset --hard dd703

二、git rebase簡介
首先,rebase有兩個意思:變基、衍合,即變換分支的參考基點。默認情況下,分支會以分支上的第一次提交作為基點,如下圖所示master分支默認以提交1st作為基點:

如果以提交4th作為master分支的基點,master分支就會變為:

這個變化基點的過程就稱之為變基(rebase);
rebase與merge十分相似,不過二者的工作方式有着顯著的差異。比如:將A和B兩分支進行合並:
- 在
A分支上執行git merge B,表示的是將B分支合並到A分支上; - 而在
A分支上執行git rebase B,則表示將A分支通過變基合並到B分支上;
三、merge與 rebase
1.采用merge合並分支

現在有兩個分支origin和mywork,如果想要將origin分支合並到mywork分支上。根據三方合並原則,需要在c4、c6和它們的公共父提交節點c2的基礎上進行合並:

合並后產生一次新的提交c7,該提交有兩個父節點c4和c6。具體的合並方式為:如果沒有沖突git就會自動采用Fast-forward方式進行合並,有沖突就解決沖突再進行手動合並。
2.采用rebase合並分支
由於是mywork分支需要變基合並到origin分支上,所以首先切換到mywork分支(注意這里與采用merge方法時所在的分支相反):
git checkout mywork
再進行合並:
git rebase origin
合並后的結果為:

注意:被合並的分支origin保持不動,而合並它的分支mywork將自己的提交作為補丁(patch)一個個應用(applying)到分支origin指向的提交后面;
在這個過程中git會自動創建c5'和c6'。原來的c5和c6就沒用了,會被git gc回收。合並后分支mywork的提交記錄變成了一條直線:

也就是說:
rebase會將被合並分支(mywork)上的提交應用到合並分支(origin)上,並且修改被合並分支(mywork)的提交記錄。
四、rebase原理分析
如圖所示,master和dev分支都以提交節點A為基准點:

如果dev分支想要變換A這個基准點,那么:
第一步:切換到dev分支上;
第二步:執行git rebase master,過程如下;
上述命令中
rebase參數后面指定的就是變更后的基准點:
- 如果是分支,如
master,基准點為該分支的最新提交節點,也就是C;- 如果是一個
commit_id,基准點為該commit_id對應的提交節點;
1.基准點為分支
沿用以上模型:

- 首先,將
dev分支上除了基准點A外的所有節點復制一份,即D'和E',作為補丁備用,並將分支dev指向新基准點C:

- 然后,按原來
dev上的節點順序(D->E)將補丁應用(Patch Applying)到新基准點C后面,並同時改變分支dev指向:
追加補丁D':

每次向新基准點應用補丁時,都會出現三個選項:

git rebase --continue
該選項表示:解決了合並沖突后,繼續應用剩余補丁E':

git rebase --skip
該選項表示:跳過當前補丁,繼續應用下一個補丁:

如果一直執行該選項,直到應用完分支dev上的補丁,結束rebase后,兩分支的狀態為:

git rebase --abort
該選項表示:終止rebase操作,回到執行rebase指令前的狀態:

2.基准點為提交
過程詳解

如圖所示,若將提交節點B作為基准點,在當前test分支上執行:
git rebase 3ccc8
會直接將原來的節點C和D應用到新基准點B后,相當於沒有發生變化,這個變基的過程為:
- 首先,將基准點和
test分支指向改變為節點B,並將test分支上基准點往后的提交節點作為補丁:

- 然后,按順序將補丁
C和D應用到新基准點B后面:

- 最后,
test分支的狀態為:

所以,直接執行git rebase 678e0不會有任何變化:

但是,我們可以通過在rebase中添加參數-i,進入rebase交互模式,這樣就能在rebase操作過程中對特定的補丁進行一系列操作;
實戰演示
首先在test分支上進行了四次提交:

執行以下指令將test分支的基准點變為提交節點B(678e0),並進行變基:
git rebase -i 678e0
執行該指令后,會進入vim編輯器:

可以根據需要將pick參數,改變為下面代表不同作用的參數;這樣就可以對節點C和D進行不同的操作了。比如:
pick:默認參數,表示不對提交節點進行任何操作,直接應用原提交節點。不創建新提交;reword:應用復制過后的原提交節點,但是可以編輯該節點的提交信息。通過這個參數,可以修改特定提交的提交信息。會創建新的提交;edit:應用復制過后的原提交節點,會在設置了該參數的補丁上停止rebase操作。待修改完該補丁后,調用git rebase --continue繼續進行rebase。會創建新的提交;squash:將新基點后面的全部提交節點進行合並,也就是將這里的C和D兩個節點進行合並。會創建新的提交;- 還有其他參數這里就不一一介紹了。
這次直接使用默認的pick參數,通過:wq保存並退出vim編輯器,完成rebase操作:

執行rebase操作前:

可以看到當新基准點為特定提交時:
- 在
rebase的過程中使用默認參數pick,並不會像當新基准點為分支時那樣創建新的提交; - 而一旦使用其他參數(如
reword)對補丁進行了修改,就會創建新的提交;
五、rebase注意事項
-
不要對
master分支執行rebase,否則會引起很多的問題(master一定是遠程共享的分支); -
一般來說,執行
rebase的分支都是自己的本地分支,千萬不要在與其他人共享的遠程分支上使用rebase;這不難理解,遠程分支上的代碼可能已經被其他人克隆到本地了,如果通過
rebase修改了遠程分支的提交歷史,這樣其他人每次拉取代碼到本地時,就都需要進行復雜的合並。 -
所以,本地的非
master分支合並時推薦使用git rebase,其他分支的合並推薦使用git merge;
注意:git merge和git rebase的顯著區別是,前者不會修改git的提交記錄,而后者會!
六、rebase應用場合
1.合並分支
由於git merge采用的是三方合並的原則,沒有公共提交節點就無法進行合並,此時可以采用rebase進行合並。如下圖所示:

本地master與遠程master分支沒有公共提交節點,無法采用git merge合並。可采用rebase進行合並:
//origin/master代表着遠程master分支
git rebase origin/master
合並后本地master分支的狀態為:

2.修改特定提交
以下情況就適合使用rebase來解決,當回退版本並進行修改時:
比如在master分支上進行了3次提交:

回退到第二次提交2nd,並對提交信息進行修改:

當我們回到原來的第三次提交3rd時,會發現之前的修改並沒有被保存:

此時可以使用rebase,將提交1st作為新的提交節點(正如第四大點講解的)。首先執行:
git rebase -i 5ab3f
通過添加參數-i進入交互模式,將提交2nd默認的pick參數修改為reword參數:

保存並退出后,進入修改提交信息界面:

保存並退出,由此完成修改:

七、rebase實戰
為了演示,額外創建兩個分支dev和test,分別在兩個分支上進行兩次提交:

它們有一個共同的父節點提交節點init,此時本地倉庫的狀態如下:

-
由於要對
test分支進行變基,從而合並到dev分支上,所以需要先切換到test分支上,這與merge操作是相反的; -
隨后在
test分支上執行如下命令對該分支進行變基:
git rebase dev
該指令翻譯過來就是:我test 分支,現在要重新定義我的基准點,即使用 dev 分支指向的提交作為我新的基准點。過程如下:
-
首先,將
test分支上的提交(補丁)tes1應用到新基准點dev2尾部,出現了合並沖突:
查看狀態,發現
test分支變基過程中的新基准點正是dev分支指向的提交361be,即提交節點dev2:
如圖所示,此時有三個選項:
-
選項一:
git rebase --abort:表示終止rebase操作,恢復到操作前; -
選項二:
git rebase --skip:表示丟棄當前test分支的補丁,如果一直執行該選項,變基完成后,兩分支的狀態如下所示:
即此時
test分支與dev分支上具有相同的文件:
並且
test分支上的提交記錄被改變為了dev分支上的提交記錄:
這就是一直執行選項
git rebase --skip,丟棄全部test分支補丁的結果: -
選項三:
git rebase --continue:解決沖突,手動合並后,繼續變基;在
dev分支上新增兩次提交dev3和dev4:
切換回
test分支同樣新增兩次提交tes3和tes4:
此時兩分支的狀態為:

隨后在
test分支上執行git rebase dev,在處理test分支上的第一個補丁tes3時出現沖突:
打開沖突文件
test.txt,手動解決沖突:
刪除
4、7、9行:
解決沖突后,執行
git add將對文件``test.txt`的修改操作納入暫存區,標識已解決沖突:注意:這里並不需要進行一次提交,繼續執行
rebase操作即可;
隨后再執行
git rebase --continue,繼續處理test分支的下一個補丁(變基):
rebase結束后,查看test分支的提交記錄:
可以發現修改了
test分支的提交歷史,達到了預期的合並效果。並且,此時
test分支上的tes3與tes4兩次提交的SHA1值與執行rebase前這兩次提交的SHA1值是不一樣的:
這也就驗證了,
git在rebase過程中會自動創建提交節點的結論。此時dev分支與test分支的狀態如下所示:
如果在
dev分支上執行git merge test,采用的應當是Fast-forward方式:
使用
gitk可以更加直觀地表示這一狀態:
細心的你可能已經發現了,
rebase與cherry-pick十分類似。只不過cherry-pick不會修改分支提交記錄,而rebase會。
八、merge與rebase的選擇
使用rebase時要遵循rebase的黃金法則:永遠不要在公共分支上使用rebase。公共分支可以理解為master分支。由於rebase會重寫分支提交記錄,因此會給項目的回溯帶來危險。以下為它與merge的區別:
-
merge是一個合並操作,使用git merge提交歷史會出現分叉,顯得不是那么簡潔。但是,它的好處在於不會修改任何一次提交,會完整地將所有的提交都保存下來,方便回溯。並且只能合並有公共提交節點的分支; -
rebase是沒有合並操作的,它只是將當前分支所做的修改復制到了目標分支的最后一次提交上。所以可以不受三方合並原則約束,合並沒有公共提交節點的分支;使用
rebase會修改提交歷史,得到的分支提交歷史更加整潔。就好像寫書,只會出版最終版本,之前的書稿並不會出版。但是,一定要注意不能在共享的分支上使用rebase。
二者都是很強大的分支整合命令,使用哪個由具體情境決定。
九、rebase、reset、revert
這三個指令的名字很像,容易混淆,下表對比了它們的用途以及區別:
| 指令 | 改變提 交歷史 | 用途 |
|---|---|---|
| Reset | 是 | 把目前分支的狀態設定成某個指定的Commit狀態,通常適用於尚未推送的Commit |
| Rebase | 是 | 不管是新增、修改、刪除Commit都相當方便。可用來整理、編輯還未推送的Commit,通常也只適用於尚未推送的Commit |
| Revert | 否 | 新增一個Commit來反轉(取消)另一個Commit內容,原本的Commit依舊會保留在提交歷史中。雖然會因此而增加Commit數,但通常比較適用於已經推送的Commit,或者不允許使用Reset或Rebase指令修改提交歷史的場合 |
十、git最佳實踐
學到這里就可以完全理解使用git將本地倉庫文件推送到遠程倉庫的一般步驟了:
-
第一步:創建本地倉庫:
git init -
第二步:添加用戶信息:
git config --global user.name '張三' git config --global user.email 'zhangsan@git.com' -
第三步:添加遠程倉庫地址:
git remote add origin https://www.github.com/example -
第四步:修改文件;
-
第五步:將工作區中的文件納入暫存區:
git add . -
第六步:將暫存區中的文件提交到版本庫:
git commit -m '注釋' -
第七步:與遠程倉庫進行同步:
git pull --rebase origin master -
第八步:建立本地分支與遠程分支的聯系,並進行推送:
git push -u origin master
通過這一節的學習,相信你已經熟練掌握了
cherry-pick和rebase的原理及使用方法了。下一節將會介紹Git子庫:submodule與subtree。期待與你再次相見!
