前情提要:Git應用詳解第八講:Git標籤、別名與Git gcgit
這一節主要介紹git cherry-pick
與git rebase
的原理及使用。github
Git cherry-pick
Git cherry-pick
的做用爲移植提交。好比在dev
分支錯誤地進行了兩次提交2nd
和3rd
,若是想要將這兩次提交移植到master
分支上。採用先刪除再添加的方法將會很繁瑣,而使用cherry-pick
就能輕鬆實現這一需求。vim
首先在版本庫中建立了兩個分支master
和dev
,並模擬上述場景:bash
能夠看到,在dev
分支上進行了兩次提交,在master
分支上只進行了一次提交。如今想要將這兩次提交移植到master
分支上。總體分爲兩步:app
dev
分支上多餘的兩次提交移植到master
分支上;dev
分支上多餘的兩次提交;git cherry-pick commit_id
首先切換到master
分支,而後使用以下命令將dev
分支上的兩次提交移植到master
分支上:編輯器
//移植2nd提交
git cherry-pick 009dd
//移植3rd提交
git cherry-pick aec8c
複製代碼
009dd
和aec8c
分別表示須要移植的提交2nd
和3rd
的SHA1
值:post
移植過程爲:學習
如上圖所示,執行了兩次cherry-pick
指令,建立了兩個內容與2nd、3rd
一致的提交對象50477
和f05a0
。因此,cherry-pick
指令移植提交的實質是:先將須要移植的提交複製一份,再拼接到master
分支上,簡稱先複製,再拼接;spa
上面按照順序先移植了提交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
:
此時兩分支的狀態爲:
接下來就要刪除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
merge
合併分支如今有兩個分支origin
和mywork
,若是想要將origin
分支合併到mywork
分支上。根據三方合併原則,須要在c4
、c6
和它們的公共父提交節點c2
的基礎上進行合併:
合併後產生一次新的提交c7
,該提交有兩個父節點c4
和c6
。具體的合併方式爲:若是沒有衝突git
就會自動採用Fast-forward
方式進行合併,有衝突就解決衝突再進行手動合併。
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
對應的提交節點;
沿用以上模型:
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
指令前的狀態:
如圖所示,若將提交節點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
應用場合因爲git merge
採用的是三方合併的原則,沒有公共提交節點就沒法進行合併,此時能夠採用rebase
進行合併。以下圖所示:
本地master
與遠程master
分支沒有公共提交節點,沒法採用git merge
合併。可採用rebase
進行合併:
//origin/master表明着遠程master分支
git rebase origin/master
複製代碼
合併後本地master
分支的狀態爲:
如下狀況就適合使用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
,手動解決衝突:
刪除四、七、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
。期待與你再次相見!