Git應用詳解第九講:Git cherry-pick與Git rebase
前言
前情提要:Git應用詳解第八講:Git標籤、別名與Git gc
這一節主要介紹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 '[email protected]'
-
第三步:新增遠端倉庫地址:
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
。期待與你再次相見!