01 分支的實(shí)現(xiàn)原理
Git的分支特性常常被稱(chēng)為“必殺技特性”,因?yàn)榉种Ыo團(tuán)隊(duì)開(kāi)發(fā)提供了很大的便利,而且在Git中的分支實(shí)現(xiàn)非常輕量,創(chuàng)建分支,切換分支等復(fù)雜操作在Git中只是改變指針指向而已。
在學(xué)習(xí)分支之前,我們先來(lái)回顧一下一次提交的形成過(guò)程。Git中有三個(gè)存放文件的區(qū)域,工作區(qū),暫存區(qū),上一次的提交對(duì)象,使用Git的過(guò)程其實(shí)是操作這三個(gè)區(qū)域的過(guò)程。工作區(qū)就是我們?cè)陔娔X上看到的目錄(不包含.git目錄,.git目錄默認(rèn)是隱藏的),我們的新建,刪除,修改操作都在這里進(jìn)行。
我們現(xiàn)在的工作流程是這樣的。在對(duì)工作區(qū)改動(dòng)過(guò)后,Git發(fā)現(xiàn)工作區(qū)里的內(nèi)容和暫存區(qū)里的內(nèi)容不一樣了,就會(huì)發(fā)出提示說(shuō)有未暫存的改動(dòng)(即git status命令顯示出的Changes unstaged for commit 和 Untracked files),然后我們把需要的改動(dòng)更新到暫存區(qū)里,Git又發(fā)現(xiàn)暫存區(qū)里的內(nèi)容和上一次提交的內(nèi)容不一樣了,就發(fā)出提示說(shuō)有待提交的改動(dòng)(即git status命令顯示出的Changes to be commited中的內(nèi)容),最后我們用git commit進(jìn)行提交,Git會(huì)把暫存區(qū)保存成一個(gè)文件,然后把指向這個(gè)文件的指針與提交者的用戶名和郵箱,提交時(shí)間等內(nèi)容,放到一個(gè)新建的commit對(duì)象里,并讓這個(gè)commit對(duì)象鏈接到上一個(gè)對(duì)象,當(dāng)然commit對(duì)象也會(huì)被保存起來(lái),而且會(huì)計(jì)算出一個(gè)SHA1校驗(yàn)和讓我們來(lái)找到這個(gè)commit對(duì)象,最后把上一個(gè)提交對(duì)象替換為剛生成的commit對(duì)象(其實(shí)就是把HEAD指針的指向從上一個(gè)commit對(duì)象改為了新建的commit對(duì)象),一次提交就這么結(jié)束了。按上述步驟一直工作下去,我們的commit對(duì)象可能會(huì)鏈接成下面這樣子。

然后我們發(fā)現(xiàn)現(xiàn)在這個(gè)功能的實(shí)現(xiàn)方式并不好,我們要回退到這個(gè)功能開(kāi)發(fā)前的那一個(gè)版本重新開(kāi)發(fā),于是我們用git reset --hard 提交2指令跳回去了,隨著這個(gè)指令的執(zhí)行,Git把HEAD指針的指向改成了提交2,并且把暫存區(qū)內(nèi)容更新為了提交2中的內(nèi)容,因?yàn)榧恿?code>--hard參數(shù),Git會(huì)接著將工作區(qū)的內(nèi)容更新為暫存區(qū)中的內(nèi)容。 注意Git并不會(huì)把提交刪掉,有同學(xué)會(huì)想,既然沒(méi)刪掉,那為什么我用git log看不到他們了,因?yàn)?code>git log是從HEAD指向的那個(gè)提交開(kāi)始,依次打log的。

然后我們接著開(kāi)發(fā),中途提交了兩次,commit對(duì)象就變成了這樣。

之后,你可能又覺(jué)得不好,又從頭來(lái)一遍,或者你覺(jué)得還是第一次那種好,就用git reflog查出了那次提交4的commitID,用git reset --hard commitID跳回去了。其實(shí)這樣我們已經(jīng)創(chuàng)建了兩個(gè)分支了,在這個(gè)例子中他們分別代表了我們功能的兩種實(shí)現(xiàn)方法。之后你可能又會(huì)開(kāi)發(fā)很多功能,每個(gè)功能里,可能還會(huì)有很多小功能,……最后你的commit對(duì)象就連接成了這個(gè)樣子。

額??傊妥兂闪艘豢脴?shù)的形狀,有很多的分支。Commit對(duì)象的樹(shù)形結(jié)構(gòu)為分支特性提供了天然的支持,剩下要做的就只是把分支標(biāo)志出來(lái)而已,于是Git給了每個(gè)分支一個(gè)指向提交的指針,從這個(gè)指針一直遍歷到結(jié)尾就是這個(gè)分支的提交歷史。而且對(duì)分支的操作,都只是改變指針指向而已。
以前我們說(shuō)過(guò),HEAD表示了當(dāng)前版本,其實(shí)HEAD是指向分支的,它標(biāo)志了當(dāng)前分支,而當(dāng)前分支的分支指針確定了當(dāng)前的版本。在每次提交的時(shí)候,Git只是在新建commit對(duì)象的基礎(chǔ)上,把當(dāng)前分支的分支指針指向新的提交而已。而以前我們所說(shuō)的git reset操作只是把當(dāng)前分支的分支指針指向?qū)?yīng)提交,然后替換暫存區(qū)而已,如果有--hard參數(shù)那么再替換工作區(qū)。分支間的切換也非常簡(jiǎn)單,如果要從分支1切換到分支2上去開(kāi)發(fā),只要把HEAD指針指向分支2,然后把暫存區(qū)和工作區(qū)的內(nèi)容用分支2的分支指針對(duì)應(yīng)的Commit對(duì)象中的內(nèi)容替換即可。
像我們之前看到過(guò)的master,是Git倉(cāng)庫(kù)初始化時(shí)默認(rèn)生成的分支,我們之前的操作都是在master分支上進(jìn)行的。
上面的例子用Git中的分支模型表示的話就像下面這樣。

在實(shí)際開(kāi)發(fā)中,通常會(huì)在主分支外,給各個(gè)功能的開(kāi)發(fā)新建一個(gè)分支,并在功能開(kāi)發(fā)完了之后用分支的合并操作,將分支合并到主分支上去。就像是一個(gè)汽車(chē)工廠,分開(kāi)建造各種零件,在零件造好了之后合并到汽車(chē)架子上去。
02 分支的作用
分支間互不影響的特點(diǎn)可以在團(tuán)隊(duì)開(kāi)發(fā)中發(fā)揮很大的作用。兩個(gè)人可以各建一條分支來(lái)開(kāi)發(fā)新功能,只要在開(kāi)發(fā)完后將代碼合并到主分支即可,而不必頻繁地合并代碼,提高了效率,同時(shí)也保持了主分支的整潔。
在開(kāi)發(fā)中,經(jīng)常會(huì)遇到線上版本出現(xiàn)Bug,不得不放下手頭新功能的開(kāi)發(fā)去修復(fù)bug的情況。在Git中,我們通常會(huì)建兩條分支,一條用來(lái)發(fā)布新版本,一條用來(lái)開(kāi)發(fā)新版本,在新版本開(kāi)發(fā)完后再把這個(gè)版本合并回主分支。如果出現(xiàn)了bug,只要在主分支上修改即可,不會(huì)和新功能的開(kāi)發(fā)混合起來(lái)。有的同學(xué)可能會(huì)說(shuō),那我一條分支時(shí),出現(xiàn)了bug,也只要修復(fù)代碼就好了呀!注意,向外發(fā)布的是穩(wěn)定的版本,肯定不能把還沒(méi)開(kāi)發(fā)好的新功能混在一起發(fā)布出去啊,那你還得把新功能的代碼刪掉,在發(fā)布后又要把新功能的代碼恢復(fù)回來(lái)繼續(xù)開(kāi)發(fā)。
另外,用好分支可以保持主分支的整潔,也能讓整個(gè)項(xiàng)目的開(kāi)發(fā)歷史清晰明了。為了更好的利用分支,還演化出了各種各樣的工作流,這里不多介紹,大家可以通過(guò)文章頂部的鏈接自行了解。
03 分支操作
常用的分支操作有創(chuàng)建分支,分支間切換,分支的合并(merge)和變基(rebase)。接下來(lái)會(huì)依次介紹。
分支操作一:創(chuàng)建分支
分支的創(chuàng)建可以用git checkout -b <branch_name>來(lái)完成,branch_name為分支名。如果該分支不存在,就會(huì)創(chuàng)建這個(gè)分支并切換到該分支上。其實(shí),Git只是創(chuàng)建了一個(gè)指針,并且把HEAD指向了這個(gè)指針而已。
當(dāng)前我們的提交歷史如下,只有一個(gè)master分支,origin/master是遠(yuǎn)程分支,對(duì)應(yīng)遠(yuǎn)程庫(kù)中的master分支。

用以下指令在當(dāng)前位置創(chuàng)建兩個(gè)新的分支,名為produceWheel和produceEngine。
$ git checkout -b produceWheel
$ git checkout -b produceEngine
可以看到多了兩個(gè)標(biāo)簽,它代表了我們剛創(chuàng)建的兩個(gè)分支。

現(xiàn)在我們?cè)诋?dāng)前分支上新建一個(gè)提交試試,我們修改activity_main.xml文件,在最后添加一行produceEngine,然后提交。
$ git commit -am "EngineCompleted"
可以看到,HEAD標(biāo)簽和produceEngine標(biāo)簽前進(jìn)了一個(gè)提交。Git在新建提交對(duì)象之后,把produceEngine分支的分支指針指向了新的提交對(duì)象,而HEAD標(biāo)簽隨著當(dāng)前分支的移動(dòng)而移動(dòng)。

可以用git branch -D <branch_name>來(lái)刪除對(duì)應(yīng)分支。
Android Studio中的相應(yīng)操作
分支的創(chuàng)建操作如下,選中對(duì)應(yīng)的提交,鼠標(biāo)右鍵,在彈出的菜單中按下New Branch,在彈出的對(duì)話框中填寫(xiě)分支名即可。如果要?jiǎng)h除分支,也是選中分支所在的提交,然后鼠標(biāo)右鍵,在彈出的菜單中按如下路徑找到刪除操作,刪除即可(注意不能刪除當(dāng)前分支,刪除當(dāng)前分支前,先切換到別的分支)。
分支操作二:分支間切換
分支間的切換可以用git checkout <branch_name>來(lái)完成。要注意它和git reset [--hard] <commitID>的區(qū)別。他們的本質(zhì)區(qū)別是git checkout <branch_name>切換的是HEAD指針的指向來(lái)改變當(dāng)前版本的,準(zhǔn)確的說(shuō)是切換當(dāng)前分支,而git reset [--hard] <commitID>是改變當(dāng)前分支的分支指針的只想來(lái)改變當(dāng)前版本的。
這里演示一下他們的區(qū)別。當(dāng)前提交歷史如下,當(dāng)前分支為produceEngine。

我們切換分支到produceWheel上。
$ git checkout produceWheel
再查看提交歷史,只是HEAD標(biāo)簽換了個(gè)指向,指向了produceWheel,從而改變了當(dāng)前版本。

將分支切換回去,我們用git reset [--hard] <commitID>回退到同一個(gè)版本看一下效果。
$ git reset --hard HEAD^
再查看提交歷史,發(fā)現(xiàn)除了HEAD以外,produceEngine也一起回到了上一個(gè)版本。其實(shí)HEAD的內(nèi)容沒(méi)變,變的是produceEngine,只是因?yàn)镠EAD是指向produceEngine的,所以連帶的回到了上一個(gè)版本。

當(dāng)然,除了指針指向的切換以外,git checkout <branch_name>還會(huì)把暫存區(qū)和工作區(qū)的內(nèi)容替換為切換后的提交中的內(nèi)容。這一點(diǎn)和git reset --hard <commitID>操作一樣,不過(guò)git checkout <branch_name>在發(fā)現(xiàn)有還未提交的改動(dòng)時(shí),它會(huì)報(bào)錯(cuò)并提醒用戶將這些改動(dòng)貯藏起來(lái)(用git stash操作可以保存當(dāng)前所有改動(dòng),到需要的時(shí)候再恢復(fù)進(jìn)工作區(qū)),或者刪掉(git reset --hard HEAD)他們,等到清理完了這些改動(dòng)后才會(huì)允許用戶進(jìn)行git checkout <branch_name>操作,但其實(shí)Git是很機(jī)智的,他會(huì)先嘗試著將改動(dòng)合并到目標(biāo)分支的工作區(qū)里,如果有沖突才會(huì)報(bào)錯(cuò)。
Android Studio中的相應(yīng)操作
先選中分支所在提交,鼠標(biāo)右鍵,按如下路徑進(jìn)行操作即可。image
分支操作三:分支的合并
可以用git merge <branch_name>來(lái)合并分支,它能將branch_name所指的分支和并到當(dāng)前分支上來(lái),合并的時(shí)候注意一下是誰(shuí)合并到誰(shuí)上。合并其實(shí)可以看成是一種特殊的提交,因?yàn)樗褍蓷l分支上的改動(dòng)合并到一起,在當(dāng)前分支上生成一個(gè)新的提交,與普通提交不同的是,合并的改動(dòng)來(lái)自兩個(gè)提交,所有它會(huì)連接到兩個(gè)父提交對(duì)象上。
Git中的合并通常被叫做三方合并。合并時(shí)會(huì)確定三個(gè)提交,當(dāng)前分支對(duì)應(yīng)的提交,被合并分支對(duì)應(yīng)的提交,和兩條分支的共同祖先提交。Git會(huì)把其他兩個(gè)提交相對(duì)于共同祖先提交的改動(dòng)提取出來(lái),如果這兩份改動(dòng)里對(duì)同一個(gè)文件進(jìn)行了改動(dòng),那么Git就會(huì)提示自動(dòng)合并失敗,讓我們手動(dòng)修改沖突的文件,在修改完后,使用git commit把所有改動(dòng)提交,如果沒(méi)有對(duì)同一個(gè)文件進(jìn)行改動(dòng),Git就會(huì)自動(dòng)幫我們添加所有改動(dòng)到新提交中。
下面合并操作的演示。我們當(dāng)前的提交歷史如下。

我們?cè)囍鴮⒎种?code>produceEngine合并到主分支上。
$ git checkout master // 先切換到master分支上
$ git merge produceEngine -m "引擎生產(chǎn)完成" // 把produceEngine分支合并到master分支上
結(jié)果如圖所示。當(dāng)前分支是master(因?yàn)檫@個(gè)從圖里看不出來(lái),再截一張圖又太累贅,我就直接跟你們講了),合并之后,master從添加了忽略規(guī)則前進(jìn)到了EngineCompleted,這時(shí)同學(xué)們又要問(wèn)了,前面不是說(shuō)master分支會(huì)新建一個(gè)提交,然后這個(gè)提交會(huì)同時(shí)連接兩個(gè)分支的嗎!這是個(gè)特殊情況,從兩個(gè)分支的共同祖先出發(fā),master分支壓根就沒(méi)有改動(dòng),只有produceEngine分支上有改動(dòng),那還合并什么,produceEngine分支的當(dāng)前提交就是我master分支要的結(jié)果呀!于是Git就偷了個(gè)懶,直接把master分支的分支指針指向produceEngine分支的當(dāng)前提交了。這種簡(jiǎn)化的合并模式叫做——快進(jìn)(fast-forward)。

有的時(shí)候我們不想要用快進(jìn)模式來(lái)合并,我們想看保留我們的工作軌跡——在哪開(kāi)始開(kāi)發(fā)這個(gè)功能,在哪這個(gè)工作完成,中間有哪幾步。我們可以用--no-ff來(lái)關(guān)閉快進(jìn)模式。讓我們用--no-ff重新來(lái)一遍。
$ git reset --hard HEAD^ // 撤銷(xiāo)上次合并
$ git merge --no-ff produceEngine -m "引擎生產(chǎn)完成"
不用快進(jìn)模式時(shí)的提交歷史如下。跟我們之前設(shè)想的一樣。

現(xiàn)在我們轉(zhuǎn)去生產(chǎn)輪胎。先切換到produceWheel分支上。
$ git checkout produceWheel
再在produceWheel分支上進(jìn)行開(kāi)發(fā),這邊我們?cè)?code>activity_main.xml的最后加上一行ProduceWheel,然后提交。
$ git commit -am "WheelCompleted"
開(kāi)發(fā)完后,我們切換回master分支把改動(dòng)合并回來(lái)。
$ git merge produceWheel -m "輪子生產(chǎn)完成"
結(jié)果,額。報(bào)錯(cuò)了。

Git提示說(shuō)它嘗試了自動(dòng)合并,但是失敗了。因?yàn)樵?code>master分支上我們改了activity_main.xml文件,而在produceWheel分支上我們也改了activity_main.xml文件,Git不知道要怎么處理這些兩個(gè)改動(dòng)。于是Git提示我們,讓我們解決沖突后,提交結(jié)果。
我們打開(kāi)沖突文件,Git已經(jīng)幫我們標(biāo)記出了各分支的修改。

我們手動(dòng)修改這個(gè)文件,修改結(jié)果如下。

然后提交。
$ git commit -m "輪胎生產(chǎn)完成"
提交之后,合并就完成了,現(xiàn)在我們的提交歷史是下面這個(gè)樣子的。

Android Studio中的相應(yīng)操作
選擇要合并進(jìn)當(dāng)前分支的分支,鼠標(biāo)右鍵,按如圖所示路徑操作。如果有沖突存在,Android Studio會(huì)彈出
Files Merged with Confilcts對(duì)話框,右邊有三個(gè)選項(xiàng),分別是,采用自己的改動(dòng),采用其他分支的改動(dòng),合并改動(dòng),我們選擇合并改動(dòng)。然后會(huì)彈出合并改動(dòng)的窗口,左邊是當(dāng)前分支的改動(dòng),右邊是其他分支的改動(dòng)(要合并進(jìn)來(lái)的那個(gè)分支) ,中間是沖突合并的結(jié)果。選擇箭頭可以采用改動(dòng)到結(jié)果中,而選擇叉號(hào)會(huì)忽略這個(gè)修改。
這里我們兩邊的改動(dòng)都采用了。
沖突解決完成后,點(diǎn)擊
Apply按鈕即可完成合并。上述方式雖然方便,但是沒(méi)有辦法自己寫(xiě)提交信息。如果要寫(xiě)提交信息,可以用
VCS->Git->Merge Changes下的合并操作,窗口如下,在Commit Message欄里可以填寫(xiě)提交信息。用這種方法合并,在解決沖突后需要自己提交一下。
分支操作四:變基
除了合并之外,變基也可以將兩條分支的內(nèi)容整合到一起。使用變基操作時(shí),Git會(huì)先確定兩條分支的共同祖先,然后會(huì)依次當(dāng)前分支中各個(gè)提交的修改提取出來(lái),并且結(jié)合另一個(gè)分支上的修改,形成新的提交,一個(gè)一個(gè)拼接到指定分支之后。

變基可以使提交歷史整潔,但是它會(huì)修改已有的提交歷史。
對(duì)已經(jīng)推送到中央倉(cāng)庫(kù)的分支,要慎重考慮能不能用變基。變基會(huì)修改已有的提交歷史,而且會(huì)刪去原提交歷史上的提交(內(nèi)容沒(méi)刪,只是不再連接到原有的樹(shù)形結(jié)構(gòu)上了), 但是對(duì)中央倉(cāng)庫(kù)的fetch操作只能拿取本地分支沒(méi)有的提交,而不會(huì)刪除中央倉(cāng)庫(kù)沒(méi)有但本地存在的提交。如果其他人拉取過(guò)變基前的分支,那他可能要手動(dòng)把它刪除,如果他不僅拉取過(guò),而且在之上進(jìn)行了一些開(kāi)發(fā),進(jìn)行過(guò)提交,合并等等操作,那他可能會(huì)打你一頓。
變基可以用命令git rebase <branch_name>完成,它會(huì)將當(dāng)前分支整合到指定分支上。舉個(gè)栗子,我們當(dāng)前的提交歷史如下。

我們將produceWheel分支rebase到master上。
$ git rebase master
運(yùn)行后,Git提示我們它正在把WheelCompleted這個(gè)提交應(yīng)用到master分支上,但是在合并activity_main.xml文件時(shí),發(fā)生了沖突,讓我們解決沖突后用git rebase --continue繼續(xù)rebase。

用沖突解決工具,顯示如下,左邊是提交WheelCompleted的內(nèi)容,右邊是master分支上提交引擎生產(chǎn)完成的內(nèi)容,它們均在祖先提交添加了忽略規(guī)則上,添加了一行。

我們把改動(dòng)都添加上去。沖突解決后輸入命令git rebase --continue繼續(xù)變基。在這之后master分支上就生成了一個(gè)新的WheelCompleted提交。

接著,Git又告訴我們它正在把BetterWheelCompleted這個(gè)提交應(yīng)用到master分支上,但是在合并activity_main.xml文件時(shí),發(fā)生了沖突,讓我們解決沖突后用git rebase --continue繼續(xù)rebase。

再次打開(kāi)沖突解決工具,顯示如下,左邊是提交BetterWheelCompleted的內(nèi)容,右邊是master分支上新WheelCompleted提交的內(nèi)容,他們均在老WheelCompleted提交的基礎(chǔ)上添加了一行。

老樣子,我們把改動(dòng)都添加上,然后輸入命令git rebase --continue繼續(xù)變基。然后,master分支上就又生成了新的BetterWheelCompleted提交,至此提交已經(jīng)全部轉(zhuǎn)移完成了,Git將produceWheel分支的分支指針指向新的BetterWheelCompleted提交,變基至此就結(jié)束了。完成后的提交歷史如下,這時(shí)只要再進(jìn)行一次簡(jiǎn)單地快進(jìn)合并就把produceWheel分支的內(nèi)容整合到master分支上了。

大家可能會(huì)對(duì)rebase過(guò)程中的各個(gè)新節(jié)點(diǎn)的產(chǎn)生過(guò)程感到迷惑。其實(shí)新節(jié)點(diǎn)可以看做是由舊節(jié)點(diǎn)和上一個(gè)提交的新節(jié)點(diǎn)以上一個(gè)提交的舊節(jié)點(diǎn)為祖先節(jié)點(diǎn)合并產(chǎn)生,舉個(gè)栗子,新BetterWheelCompleted節(jié)點(diǎn)可以看做是由舊BetterWheelCompleted節(jié)點(diǎn)和新WheelCompleted節(jié)點(diǎn)以舊WheelCompleted節(jié)點(diǎn)為祖先合并產(chǎn)生的。
Android Studio中的相應(yīng)操作
首先,切換到要變基的分支,然后在如下路徑中進(jìn)行操作,因?yàn)榻鉀Q沖突等操作與合并中相同,就不再多說(shuō)了。也可以使用
VCS->Git->Rebase路徑中的操作。
下一篇:Git的點(diǎn)點(diǎn)滴滴,附帶Android Studio中的操作(四):用Git進(jìn)行協(xié)同開(kāi)發(fā)









