對于初學(xué)者來說git rebase命令就如同Git當(dāng)中的巫術(shù)一樣,應(yīng)該被敬而遠(yuǎn)之。但是它在小心使用情況下,確實(shí)可以讓開發(fā)團(tuán)隊(duì)更加方便。這篇文章中,將對git rebase和git merge兩個(gè)命令進(jìn)行比對,并辨別所有潛在的基于rebasing的Git工作流。
概念簡述
首先我們要知道git rebase和git merge解決的問題是類似的。它們都被設(shè)計(jì)出來用于將一個(gè)分支的變更合到另一個(gè)分支上——只是它們的方式不一樣。
想想當(dāng)你新開一個(gè)分支用來開發(fā)新特性,而另一個(gè)程序員在master分支上提交了新的commit會(huì)發(fā)生什么。這個(gè)結(jié)果就是大家把Git作為一個(gè)合作開發(fā)工具時(shí)經(jīng)常遇到的分叉歷史(fork history)情形。
[圖片上傳失敗...(image-67c7ab-1533606429903)]
現(xiàn)在,假設(shè)master分支上的新commit和你開發(fā)中的新功能是有相關(guān)性的。為了將新的commit歸入你的feature分支中,你面臨兩個(gè)選項(xiàng):merging或rebasing。
Merge選項(xiàng)
最簡單的選擇就是將master分支合并(merge)到新功能分支(feature)中,命令如下:
git checkout feature
git merge master
或者你可以直接寫個(gè)單行命令
git merge master feature
這個(gè)操作會(huì)在feature分支中產(chǎn)生一個(gè)將兩個(gè)分支纏在一起的新的“merge commit”,你的分支結(jié)構(gòu)將會(huì)變成這個(gè)樣子:
[圖片上傳失敗...(image-81137e-1533606429903)]
Merging操作因?yàn)槠錈o危害行,所以是一個(gè)非常不錯(cuò)的操作。已經(jīng)存在的所有分支都不會(huì)改變。這避免了所有rebasing操作會(huì)產(chǎn)生的隱患(下面會(huì)討論)。
另一方面,這也表示你的”feature“分支在你每次需要?dú)w并新的跟改后,會(huì)出現(xiàn)一個(gè)無關(guān)開發(fā)的”merge commit“。當(dāng)”master“非?;钴S時(shí),這相當(dāng)會(huì)污染你的feature分支的歷史。雖然這可能可以用過使用高級(jí)的git log選項(xiàng)緩解這個(gè)問題,但它依舊會(huì)導(dǎo)致其他開發(fā)者難以理解項(xiàng)目的開發(fā)歷史。
Rebase操作
作為一種merging的替代品,你可以用一下命令將feature分支rebase到master分支上:
git checkout feature
git rebase master
這將會(huì)移動(dòng)整個(gè)feature分支到master分支的尖端上,使得master分支的所有新commit都有效的歸并進(jìn)來。但是,和使用merge commit不同,rebasing會(huì)通過為原來分支的每個(gè)commit創(chuàng)建新的含有標(biāo)志的commit重寫項(xiàng)目的歷史。
[圖片上傳失敗...(image-989052-1533606429903)]
Rebasing最大的有點(diǎn)就是你會(huì)得到一個(gè)更干凈的項(xiàng)目歷史。首先,它沒有git merge必須有卻不需要有的merge commit。第二,就想你看到的示意圖那樣,rebasing的結(jié)果是一個(gè)完美的線性項(xiàng)目歷史——你可以一直保持從項(xiàng)目的尖端開始開發(fā)新功能而不會(huì)出現(xiàn)分叉。這將更容易的讓你的項(xiàng)目使用像git log、git bisect和gitk這寫命令。
但是,對于這個(gè)新的commit歷史,有兩個(gè)需要權(quán)衡的事項(xiàng):安全和可追溯性。如果你不遵守Golden Rule of Rebasing,重寫項(xiàng)目歷史會(huì)成為你合作開發(fā)中潛在的災(zāi)難性隱患。所以,除非不重要,rebasing會(huì)失去merge commit提供的上下文關(guān)系——你看不到上游更改什么時(shí)候歸入你的feature分支。
Interactive Rebasing
當(dāng)將commits移動(dòng)到新的分支時(shí),Interactive rebasing會(huì)給你一個(gè)改變commits信息的機(jī)會(huì)。這個(gè)功能比自動(dòng)rebase更加有用,因?yàn)樗峁┠阃耆瓶胤种У腸ommit歷史的能力。最典型的用法就是在將一個(gè)feature合并到`master前,用來清理feature分支混亂的歷史提交。
開始一個(gè)interactive rebase操作需要加上一個(gè)i選項(xiàng)在git rebase命令中:
git checkout feature
git rebase -i master
然后會(huì)出現(xiàn)一個(gè)羅列所有需要移動(dòng)commit列表的編輯器:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
這個(gè)列表明確定義了這個(gè)分支在rebase操作確認(rèn)后的樣子。通過改變pick命令或重編入口,你可以隨心所欲的改變這個(gè)分支的歷史。舉個(gè)栗子,如果第二個(gè)commit修改了第一個(gè)commit中的一個(gè)小問題,你可以通過fixup命令將它單獨(dú)固話成一個(gè)commit:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
當(dāng)你保存并關(guān)閉文件,Git會(huì)根據(jù)你的說明做rebase操作,項(xiàng)目歷史的結(jié)果將會(huì)如下:
[圖片上傳失敗...(image-53d4bf-1533606429903)]
排除了無意義的commit后,你的feature分支的歷史變的更容易理解了。這種活git merge是做不到的。
The Golden Rule of Rebasing
一旦你理解了說明是rebasing,接下來最重要的事情就是知道什么時(shí)候不應(yīng)該使用它。git rebase黃金規(guī)則是絕對不要在公共分支使用它。
舉個(gè)栗子,想想當(dāng)你將master分支rebase到你的分支會(huì)發(fā)生什么:
[圖片上傳失敗...(image-f15516-1533606429903)]
rebase操作將所有在master上的commit移到了feature的尖端。問題是,這個(gè)只發(fā)生在你的repository中。其他程序員依舊在原來的master分支上工作呢!在rebasing出現(xiàn)的新commit出現(xiàn)后,Git會(huì)認(rèn)為你的master分支的歷史和其他人的發(fā)生的分叉。
唯一將兩個(gè)master分支同步到一起的方法只有將它們合并(merge)回來,結(jié)果就是出現(xiàn)一個(gè)額外的merge commit和兩個(gè)擁有相同更改的commit集合(一個(gè)是原來存在的,另一個(gè)是從你的rebased分支來的)。毫無疑問的說,這個(gè)一個(gè)非常讓人迷惑的境地。
所以,在你運(yùn)行git rebase命令前,你要總是反省自己,”其他人會(huì)看到這個(gè)分支嗎?“如果會(huì),那么把你的咸豬手從鍵盤上移開,然后想一個(gè)沒有破壞性的方式做你的更改(比如,git revert命令)。如果不會(huì),那么你安全的重寫歷史吧,怎么開心怎么寫(:
Force-Pushing
如果你試圖將rebase過的master分支push到遠(yuǎn)程倉庫上,Git將會(huì)阻止你這樣做,因?yàn)檫@和遠(yuǎn)程master分支時(shí)沖突的。但是,你可以通過強(qiáng)制push通過加上--force標(biāo)識(shí),像這樣:
# Be very careful with this command!
git push --force
這樣會(huì)用你的本地倉庫被rebase的master分支覆蓋遠(yuǎn)程master分支,這樣會(huì)使團(tuán)隊(duì)中的其他人感到迷惑(所有人都會(huì)遇到?jīng)_突!)。所以,一定要非常小心的使用這個(gè)命令,除非你非常明確的知道你在做什么。
唯一你可以這樣force-pushing的情況是當(dāng)你完成一個(gè)本地清理后,你想將這個(gè)私有的feature分支push到遠(yuǎn)程倉庫中(例如,備份目標(biāo))。這就像是在說,”媽的,我真的不想將這個(gè)原來版本的分支push上去。將當(dāng)前的版本替換上去?!霸俅温暶?,重要的是要保證沒有其他人在原來的版本的分支上進(jìn)行新功能分支的開發(fā)。
工作流程演練
Rebasing操作根據(jù)你們團(tuán)隊(duì)當(dāng)前存在的Git工作流程適當(dāng)?shù)募尤胧褂谩T谶@個(gè)部分,我們來看看rebasing在新功能開發(fā)中可以提供哪些好處。
任何工作流中要支配rebasing操作的第一步就是為每個(gè)新功能創(chuàng)建一個(gè)獻(xiàn)祭分支。這給予了你分支結(jié)構(gòu)安全使用rebasing的基礎(chǔ)條件:
[圖片上傳失敗...(image-7b02b8-1533606429903)]
本地清理
將rebasing操作歸入你的工作流中的其中一種最優(yōu)方式是一邊清理本地、一邊開發(fā)新功能。周期性使用interactive rebase,你可以保證你的每一個(gè)commit在你的feature分支中是清晰且有意義的。這可以讓你在寫代碼是不需要去擔(dān)心那些跳脫的commit——你可以在之后修復(fù)它們。
當(dāng)使用git rebase時(shí),你有兩個(gè)選項(xiàng)可以用來base:新功能分支的父分支(例如,master),或者只你的新功能分支的之前的commit。我們在Interactive Rebasing 章節(jié)看過第一個(gè)選項(xiàng)的例子了。后一個(gè)選項(xiàng)在你只要修復(fù)最近幾個(gè)commit時(shí)顯得更優(yōu)秀。舉個(gè)例子,以下命令表示對最近的3個(gè)commit進(jìn)行interactive rebase操作。
git checkout feature
git rebase -i HEAD~3
通過具體的HEAD~3作為新的基點(diǎn),你不需要真的移動(dòng)分支——你只要interactively重寫3個(gè)基于它的commit。注意這將不會(huì)歸入feature分支的上游更改。
[圖片上傳失敗...(image-5e1042-1533606429903)]
如果你想用這個(gè)方法重寫feature分支的全部commit,git merge-base命令可以方便的找到feature分支的原來基點(diǎn)。使用后接下來會(huì)返回原來基點(diǎn)的commit ID,然后你可以用來進(jìn)行git rebase操作了:
git merge-base feature master
這種interactive rebasing的使用方式由于它只影響本地分支,所以它也是一個(gè)非常好的在你工作流中使用git rebase的理由。其他程序員只會(huì)看到你已經(jīng)完成的開發(fā)功能擁有一個(gè)干凈、簡單引出新功能分支的歷史。
但是在此聲明,這只能用于私有的新功能分支。如果你和其他程序員在相同的新功能分支合作開發(fā),這個(gè)分支就是公開的,公開的分支就不允許你重寫歷史。
沒有任何git merge操作可以替代用interactive rebase清理本地commit。
將上游更改歸入feature分支
在Conceptual Overview 章節(jié)中,我們看到如何用git merge或git rebase將上游分支歸入master分支。Merging是一個(gè)保護(hù)你倉庫中全部歷史的方式,rebasing則是用將feature分支移動(dòng)到master分支尖端的方式建立一個(gè)線性的歷史。
這個(gè)使用git rebase的方式和本地清理差不多(兩者可以一起使用),但是過程中,它是將master上的上游commit歸入其中。
要牢記的是,rebase到遠(yuǎn)程分支不能是master這一鐵律。這可以發(fā)生在和其他程序員共同維護(hù)同一個(gè)feature分支時(shí),然后你必須將他們的更改全部歸并到你的倉庫中。
舉個(gè)例子,讓你和另一個(gè)叫John的程序員一起增加commit到feature分支,你的倉庫可能看上去像如下情形,在feaching遠(yuǎn)程John的倉庫的feature分支之后:
[圖片上傳失敗...(image-bedec7-1533606429903)]
你可以解決這個(gè)分叉就像你合并master分支的上游更改一樣:不論你merge本地feature和john/feature,還是rebase你的本地feature到john/feature尖端。
[圖片上傳失敗...(image-babae2-1533606429903)]
[圖片上傳失敗...(image-e8bf7d-1533606429903)]
注意,這個(gè)rebase不違反Golden Rule of Rebasing,因?yàn)橹挥心愕谋镜?code>featurecommit被移動(dòng)了——所有早于它的都沒有被碰。這就像是說,”把握的更改加到John已經(jīng)做好的那部分上?!按蠖鄶?shù)情況下,這比通過合并與遠(yuǎn)程分支同步更直觀。
默認(rèn)的,git pull 命令會(huì)執(zhí)行一次merge,但你可以強(qiáng)制用rebase合并遠(yuǎn)程分支,只要你加一個(gè)--rebase選項(xiàng)即可。
用Pull Request審查Feature分支
如果你用pull request作為你審查代碼過程的一部分,那么在發(fā)起pull request后,你需要避免使用git base。在你發(fā)起pull request后,其他程序員將可以看到你的commits,這就意味著你的分支是一個(gè)公共分支了。重寫它的歷史對Git來說是行不通的,而你的隊(duì)友會(huì)跟蹤后續(xù)的提交增加新功能。
任何其他程序員的更改都應(yīng)該用git rebase合并,而不是git rebase。
以此,一般在提交pull request前使用interactive rebase 清理你的代碼是一個(gè)好的想法。
合并一個(gè)認(rèn)可的Feature
當(dāng)一個(gè)feature已經(jīng)被你的團(tuán)隊(duì)認(rèn)可,在使用merge將feature合并到主工程代碼之前,你擁有使用rebasing將feature放到master分支尖端的選擇。
這個(gè)場景和將上游更改歸入feature分支類似,但是你不被循序重寫master分支的commit歷史,你最終就只能用git merge來合并feature分支。然而,在merge只用使用rebase,你可以保證merge可以在完美的線性歷史中,快速進(jìn)行。這同時(shí)也給了你壓制在你發(fā)起pull request啟發(fā)發(fā)生后續(xù)commit的機(jī)會(huì)。
[圖片上傳失敗...(image-fd1ebf-1533606429903)]
如果你依舊無法完全對git rebase感到舒服,你可以總是通過rebase一個(gè)臨時(shí)分支的方式。通過這種方式,當(dāng)你意外的弄亂了一個(gè)feature的歷史,你可以checkout原分支,然后再來一次。舉個(gè)栗子:
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch
總結(jié)
這就是所有你準(zhǔn)備對你的分支使用rebasing需要知道的東西。當(dāng)你希望一個(gè)干凈的、線性的歷史,沒有不需要的merge commit,你就應(yīng)該在從其他分支合并更改時(shí)嘗試git rebase替代git merge。
話說回來,如果你想要保證你項(xiàng)目有一個(gè)完整的歷史,避免任何重寫公共commit的風(fēng)險(xiǎn),你就繼續(xù)使用git merge吧。兩個(gè)選擇都是完全有效的,但至少,現(xiàn)在你可以選擇利用git rebase的好處。