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


前言

前情提要: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。期待與你再次相見!


免責聲明!

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



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