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

前言

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

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

1、Git cherry-pick

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

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

image-20200418213440673

能夠看到,在dev分支上進行了兩次提交,在master分支上只進行了一次提交。如今想要將這兩次提交移植master分支上。總體分爲兩步:app

  • **第一步:**將dev分支上多餘的兩次提交移植到master分支上;
  • **第二步:**刪除dev分支上多餘的兩次提交;

1.第一步

git cherry-pick commit_id

首先切換到master分支,而後使用以下命令將dev分支上的兩次提交移植到master分支上:編輯器

//移植2nd提交
git cherry-pick 009dd
//移植3rd提交
git cherry-pick aec8c
複製代碼

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

image-20200418215229274

移植過程爲:學習

image-20200418220353735

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

  • 上面按照順序先移植了提交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

2、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分支上;

3、mergerebase

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)的提交記錄。

4、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)對補丁進行了修改,就會建立新的提交;

5、rebase注意事項

  • 不要對master分支執行rebase,不然會引發不少的問題(master必定是遠程共享的分支);

  • 通常來講,執行rebase的分支都是本身的本地分支,千萬不要在與其餘人共享的遠程分支上使用rebase

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

  • 因此,本地的非master分支合併時推薦使用git rebase,其餘分支的合併推薦使用git merge

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

6、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

7、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

    刪除四、七、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會。

8、mergerebase的選擇

使用rebase時要遵循rebase的黃金法則:永遠不要在公共分支上使用rebase。公共分支能夠理解爲master分支。因爲rebase會重寫分支提交記錄,所以會給項目的回溯帶來危險。如下爲它與merge的區別:

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

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

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

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

9、rebaseresetrevert

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

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

10、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。期待與你再次相見!

相關文章
相關標籤/搜索