Git應用詳解第九講:Git cherry-pick與Git rebase

AhuntSun發表於2020-04-19

前言

前情提要:Git應用詳解第八講:Git標籤、別名與Git gc

這一節主要介紹git cherry-pickgit rebase的原理及使用。

一、Git cherry-pick

Git cherry-pick的作用為移植提交。比如在dev分支錯誤地進行了兩次提交2nd3rd,如果想要將這兩次提交移植到master分支上。採用先刪除再新增的方法將會很繁瑣,而使用cherry-pick就能輕鬆實現這一需求。

首先在版本庫中建立了兩個分支masterdev,並模擬上述場景:

image-20200418213440673

可以看到,在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

009ddaec8c分別表示需要移植的提交2nd3rdSHA1值:

image-20200418215229274

移植過程為:

image-20200418220353735

  • 如上圖所示,執行了兩次cherry-pick指令,建立了兩個內容與2nd、3rd一致的提交物件50477f05a0。所以,cherry-pick指令移植提交的實質是:先將需要移植的提交複製一份,再拼接到master分支上,簡稱先複製,再拼接

  • 上面按照順序先移植了提交2nd再移植提交3rd,不會發生衝突;

  • 不按順序移植,如先移植提交3rd會發生合併衝突,需要手動解決:

image-20200418220823727

通過vi test.txt檢視發生合併衝突的test.txt檔案:

image-20200408123432173

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

image-20200408123754034

但是為什麼會發生合併衝突呢?原因在於三方合併原則

image-20200408143344853

如上圖所示,當想要將dev中的提交Emaster分支的提交B合併時,首先要找到BE的公共父節點A,在A的基礎上根據BE進行三方合併;

瞭解了三方合併原則後就能解釋上面發生合併衝突的原因了:

  • 由於提交3rd是基於提交2nd建立的,因此3rd中保留了2rd中對檔案的操作記錄;

  • 如果直接將3rd拼接到initial commit後面,就會失去提交2nd的記錄;

  • 由此提交3rd就不能通過提交2nd找到公共提交節點init,這就會導致合併失敗;

所以,無論內容是否衝突,合併過程都會出現衝突:

image-20200418222100291

解決方法:手動合併三步曲:

  • 首先,選擇要保留的內容,解決衝突:

image-20200408133308462

  • 然後,通過git add將修改資訊納入暫存區:

image-20200408133412891

  • 最後,通過git commit提交修改資訊:

image-20200418222349351

完成後檢視master分支的提交歷史:

image-20200418222512780

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

image-20200418222844236

2.第二步

此時兩分支的狀態為:

image-20200418223143850

接下來就要刪除dev分支上錯誤的兩次提交2nd3rd,相當於版本回退;可以使用三種方法:revertresetcheckout,這裡演示checkoutreset兩種方法。

使用checkout

首先切換到dev分支,然後通過以下指令切換到提交initial commit

//dd703是提交initial_commit的SHA1值
git checkout dd703

此時該節點處於遊離狀態:

image-20200418223451519

然後再刪除dev分支:

image-20200418223548734

由於之前修改的dev分支沒有與master進行合併,所以刪除時需要使用引數-D強制刪除。

刪除後,剩下master分支與遊離提交。此時再通過以下指令將遊離的節點設定為dev分支即可:

git checkout -b dev

image-20200418223939367

由此通過"偷天換日"的方式使dev分支回到了錯誤提交前的狀態;

使用reset

由於使用checkout只是移動了HEAD指標,沒移動dev分支指標,所以會出現遊離提交節點;而reset會同步移動HEADdev分支指標,不會造成這樣的問題。所以這裡使用reset進行版本回退會簡單很多:

git reset --hard dd703

image-20200418224610750

二、git rebase簡介

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

image-20200409151236167

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

image-20200409151428243

這個變化基點的過程就稱之為變基(rebase);

rebasemerge十分相似,不過二者的工作方式有著顯著的差異。比如:將AB兩分支進行合併:

  • A分支上執行git merge B ,表示的是將B分支合併到A分支上;
  • 而在A分支上執行git rebase B,則表示將A分支通過變基合併到B分支上;

三、merge rebase

1.採用merge合併分支

image-20200408232708342

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

image-20200408232523880

合併後產生一次新的提交c7,該提交有兩個父節點c4c6。具體的合併方式為:如果沒有衝突git就會自動採用Fast-forward方式進行合併,有衝突就解決衝突再進行手動合併。

2.採用rebase合併分支

由於是mywork分支需要變基合併到origin分支上,所以首先切換到mywork分支(注意這裡與採用merge方法時所在的分支相反):

git checkout mywork

再進行合併:

git rebase origin

合併後的結果為:

image-20200408232225944

注意:被合併的分支origin保持不動,而合併它的分支mywork將自己的提交作為補丁(patch)一個個應用(applying)到分支origin指向的提交後面;

在這個過程中git會自動建立c5'c6'。原來的c5c6就沒用了,會被git gc回收。合併後分支mywork的提交記錄變成了一條直線:

image-20200408231936193

也就是說:rebase會將被合併分支(mywork)上的提交應用到合併分支(origin)上,並且修改被合併分支(mywork)的提交記錄。

四、rebase原理分析

如圖所示,masterdev分支都以提交節點A為基準點:

image-20200418232253571

如果dev分支想要變換A這個基準點,那麼:

第一步:切換到dev分支上;

第二步:執行git rebase master,過程如下;

上述命令中rebase引數後面指定的就是變更後的基準點:

  • 如果是分支,如master,基準點為該分支的最新提交節點,也就是C
  • 如果是一個commit_id,基準點為該commit_id對應的提交節點;

1.基準點為分支

沿用以上模型:

image-20200418232806243

  • 首先,將dev分支上除了基準點A外的所有節點複製一份,即D'E',作為補丁備用,並將分支dev指向新基準點C

image-20200418232419176

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

追加補丁D'

image-20200418232650653

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

image-20200418232951097

git rebase --continue

該選項表示:解決了合併衝突後,繼續應用剩餘補丁E'

image-20200418233223765

git rebase --skip

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

image-20200418233400640

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

image-20200418233514562

git rebase --abort

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

image-20200418233837513

2.基準點為提交

過程詳解

image-20200409184756113

如圖所示,若將提交節點B作為基準點,在當前test分支上執行:

git rebase 3ccc8

會直接將原來的節點CD應用到新基準點B後,相當於沒有發生變化,這個變基的過程為:

  • 首先,將基準點和test分支指向改變為節點B,並將test分支上基準點往後的提交節點作為補丁:

image-20200409195531185

  • 然後,按順序將補丁CD應用到新基準點B後面:

image-20200409202803624

  • 最後,test分支的狀態為:

image-20200409202843582

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

image-20200409203900098

但是,我們可以通過在rebase中新增引數-i,進入rebase互動模式,這樣就能在rebase操作過程中對特定的補丁進行一系列操作;

實戰演示

首先在test分支上進行了四次提交:

image-20200409191637780

執行以下指令將test分支的基準點變為提交節點B678e0),並進行變基:

git rebase -i 678e0

執行該指令後,會進入vim編輯器:

image-20200409192056322

可以根據需要將pick引數,改變為下面代表不同作用的引數;這樣就可以對節點CD進行不同的操作了。比如:

  • pick:預設引數,表示不對提交節點進行任何操作,直接應用原提交節點。不建立新提交;
  • reword:應用複製過後的原提交節點,但是可以編輯該節點的提交資訊。通過這個引數,可以修改特定提交的提交資訊。會建立新的提交;
  • edit:應用複製過後的原提交節點,會在設定了該引數的補丁上停止rebase操作。待修改完該補丁後,呼叫git rebase --continue繼續進行rebase。會建立新的提交;
  • squash:將新基點後面的全部提交節點進行合併,也就是將這裡的CD兩個節點進行合併。會建立新的提交;
  • 還有其他引數這裡就不一一介紹了。

這次直接使用預設的pick引數,通過:wq儲存並退出vim編輯器,完成rebase操作:

image-20200409194956051

執行rebase操作前:

image-20200409191637780

可以看到當新基準點為特定提交時:

  • rebase的過程中使用預設引數pick,並不會像當新基準點為分支時那樣建立新的提交;
  • 而一旦使用其他引數(如reword)對補丁進行了修改,就會建立新的提交;

五、rebase注意事項

  • 不要對master分支執行rebase,否則會引起很多的問題(master一定是遠端共享的分支);

  • 一般來說,執行rebase的分支都是自己的本地分支,千萬不要在與其他人共享的遠端分支上使用rebase

    這不難理解,遠端分支上的程式碼可能已經被其他人克隆到本地了,如果通過rebase修改了遠端分支的提交歷史,這樣其他人每次拉取程式碼到本地時,就都需要進行復雜的合併。

  • 所以,本地的非master分支合併時推薦使用git rebase,其他分支的合併推薦使用git merge

注意:git mergegit rebase的顯著區別是,前者不會修改git的提交記錄,而後者會!

六、rebase應用場合

1.合併分支

由於git merge採用的是三方合併的原則,沒有公共提交節點就無法進行合併,此時可以採用rebase進行合併。如下圖所示:

image-20200411205020369

本地master與遠端master分支沒有公共提交節點,無法採用git merge合併。可採用rebase進行合併:

//origin/master代表著遠端master分支
git rebase origin/master

合併後本地master分支的狀態為:

image-20200411205034662

2.修改特定提交

以下情況就適合使用rebase來解決,當回退版本並進行修改時:

比如在master分支上進行了3次提交:

image-20200419174116301

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

image-20200419174313522

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

image-20200419174404816

此時可以使用rebase,將提交1st作為新的提交節點(正如第四大點講解的)。首先執行:

git rebase -i 5ab3f

通過新增引數-i進入互動模式,將提交2nd預設的pick引數修改為reword引數:

image-20200419174618553

儲存並退出後,進入修改提交資訊介面:

image-20200419174829838

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

image-20200419174935598

七、rebase實戰

為了演示,額外建立兩個分支devtest,分別在兩個分支上進行兩次提交:

image-20200419150528809

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

image-20200419154602095

  • 由於要對test分支進行變基,從而合併到dev分支上,所以需要先切換到test分支上,這與merge操作是相反的;

  • 隨後在test分支上執行如下命令對該分支進行變基:

git rebase dev

該指令翻譯過來就是:我test 分支,現在要重新定義我的基準點,即使用 dev 分支指向的提交作為我新的基準點。過程如下:

  • 首先,將test分支上的提交(補丁)tes1應用到新基準點dev2尾部,出現了合併衝突:

    image-20200419151735485

    檢視狀態,發現test分支變基過程中的新基準點正是dev分支指向的提交361be,即提交節點dev2

    image-20200419152146120

如圖所示,此時有三個選項:

  • 選項一:git rebase --abort:表示終止rebase操作,恢復到操作前;

  • 選項二:git rebase --skip:表示丟棄當前test分支的補丁,如果一直執行該選項,變基完成後,兩分支的狀態如下所示:

    image-20200419154352758

    即此時test分支與dev分支上具有相同的檔案:

    image-20200419155017163

    並且test分支上的提交記錄被改變為了dev分支上的提交記錄:

    image-20200419153242071

    這就是一直執行選項git rebase --skip,丟棄全部test分支補丁的結果:

  • 選項三:git rebase --continue:解決衝突,手動合併後,繼續變基;

    dev分支上新增兩次提交dev3dev4

    image-20200419153831132

    切換回test分支同樣新增兩次提交tes3tes4

    image-20200419154032932

    此時兩分支的狀態為:

    image-20200419184609655

    隨後在test分支上執行git rebase dev,在處理test分支上的第一個補丁tes3時出現衝突:

    image-20200419155615321

    開啟衝突檔案test.txt,手動解決衝突:

    image-20200419155711866

    刪除4、7、9行:

    image-20200419155812608

    解決衝突後,執行git add將對檔案``test.txt`的修改操作納入暫存區,標識已解決衝突:

    注意:這裡並不需要進行一次提交,繼續執行rebase操作即可;

    image-20200419160220448

    隨後再執行git rebase --continue,繼續處理test分支的下一個補丁(變基):

    image-20200419160305488

    rebase結束後,檢視test分支的提交記錄:

    image-20200419160412284

    可以發現修改了test分支的提交歷史,達到了預期的合併效果。

    並且,此時test分支上的tes3tes4兩次提交的SHA1值與執行rebase前這兩次提交的SHA1值是不一樣的:

    image-20200419160741986

    這也就驗證了,gitrebase過程中會自動建立提交節點的結論。此時dev分支與test分支的狀態如下所示:

    image-20200419161342071

    如果在dev分支上執行git merge test ,採用的應當是Fast-forward方式:

    image-20200419161456806

    使用gitk可以更加直觀地表示這一狀態:

    image-20200419161529226

細心的你可能已經發現了,rebasecherry-pick十分類似。只不過cherry-pick不會修改分支提交記錄,而rebase會。

八、mergerebase的選擇

使用rebase時要遵循rebase的黃金法則:永遠不要在公共分支上使用rebase。公共分支可以理解為master分支。由於rebase會重寫分支提交記錄,因此會給專案的回溯帶來危險。以下為它與merge的區別:

  • merge是一個合併操作,使用git merge提交歷史會出現分叉,顯得不是那麼簡潔。但是,它的好處在於不會修改任何一次提交,會完整地將所有的提交都儲存下來,方便回溯。並且只能合併有公共提交節點的分支;

  • rebase是沒有合併操作的,它只是將當前分支所做的修改複製到了目標分支的最後一次提交上。所以可以不受三方合併原則約束,合併沒有公共提交節點的分支;

    使用rebase會修改提交歷史,得到的分支提交歷史更加整潔。就好像寫書,只會出版最終版本,之前的書稿並不會出版。但是,一定要注意不能在共享的分支上使用rebase

二者都是很強大的分支整合命令,使用哪個由具體情境決定。

九、rebaseresetrevert

這三個指令的名字很像,容易混淆,下表對比了它們的用途以及區別:

指令 改變提 交歷史 用途
Reset 把目前分支的狀態設定成某個指定的Commit狀態,通常適用於尚未推送的Commit
Rebase 不管是新增、修改、刪除Commit都相當方便。可用來整理、編輯還未推送的Commit,通常也只適用於尚未推送的Commit
Revert 新增一個Commit來反轉(取消)另一個Commit內容,原本的Commit依舊會保留在提交歷史中。雖然會因此而增加Commit數,但通常比較適用於已經推送的Commit,或者不允許使用ResetRebase指令修改提交歷史的場合

十、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-pickrebase的原理及使用方法了。下一節將會介紹Git子庫:submodulesubtree。期待與你再次相見!

相關文章