幾乎所有的版本控制系統(tǒng)都以分支的方式進(jìn)行操作,分支是獨(dú)立于項目主線的一條支線,我們可以在不影響主線代碼的情況下,在分支下進(jìn)行工作。對于傳統(tǒng)的一些版本控制工具來說,我們通常需要花費(fèi)比較多的時間拷貝主線代碼,創(chuàng)建一個分支,并且對分支的管理效率也越來越不令人滿意,而如今備受推崇的Git確實名副其實,Git中的分支非常輕量,我們可以隨時隨意創(chuàng)建任意數(shù)量的新分支,幾乎感覺不到什么延時,而且對分支的操作也很高效,如,切換分支,暫存內(nèi)容,分支合并,分支提交等。
Git分支的與眾不同
上面我們提到相對于其他大多數(shù)版本控制系統(tǒng),Git分支是輕量且高效的,為什么呢?答案是:傳統(tǒng)的版本控制系統(tǒng)存儲的數(shù)據(jù)是文件的變更,而Git則是存儲一系列的文件快照(snapshot)。
Git分支的這些特性,使得分支對我們幾乎沒有什么限制,一般針對每一個功能或需求都可以隨意創(chuàng)建分支,而在傳統(tǒng)的版本控制系統(tǒng),這樣幾乎是不現(xiàn)實的。
當(dāng)我們向服務(wù)器提交數(shù)據(jù)時,Git會存儲一個提交對象(commit object),這個存儲對象包括一系列有用信息,詳見另一篇文章中提交對象。
Git主干分支(master)
master,有主人,大師的意思,在Git是通常作為主干分支,Git初始化倉庫時,默認(rèn)創(chuàng)建的分支名就是master,就像默認(rèn)的遠(yuǎn)端主機(jī)別名是origin一樣,大多數(shù)人不會修改它,這并不說明它與別的分支有什么區(qū)別,你可以隨意修改名稱。
分支類型
在Git中,除了默認(rèn)的master主干分支,我們創(chuàng)建的每一個分支,一般可分為兩種:
- 長運(yùn)行分支(Long-Running branch):與master并行,長期存在使用的分支,如用以測試項目穩(wěn)定性或作為主分支;
- 主題分支(topic branch):針對每一個需求或功能或bug而暫時創(chuàng)建的分支,一旦任務(wù)完成,即可能回收。
分支指針(HEAD)
Git中有一個HEAD指針,始終指向當(dāng)前分支,如圖可見,項目當(dāng)前處在master分支,之前一共有三次提交:
上圖可見,第一行顯示了當(dāng)前項目所有分支,HEAD -> master表明當(dāng)前所處分支為master,我們可以總結(jié)如下圖:
我們可以在項目根目錄.git文件下找到一個HEAD文件:vi .git/HEAD,其內(nèi)保存了指向當(dāng)前分支最新提交的指針:
該指針指向refs/heads/分支名文件,我們進(jìn)入.git/refs/heads/目錄,其下以分支名為文件名列出了所有分支:
我們查看當(dāng)前分支文件,執(zhí)行vi master:
可以看到,其內(nèi)存儲的就是當(dāng)前分支的最新一次提交對象ID。
創(chuàng)建分支(git branch, git checkout -b)
接下來,假設(shè)有一個需求A,我們創(chuàng)建一個分支work-a:
git checkout -b 分支名
-b參數(shù)聲明為創(chuàng)建新分支。
等價于以下兩條指令:
git branch 分支名
git checkout 分支名
切換分支(git checkout)
git checkout 分支名表示切換到該分支,上文提到指定-b配置即說明創(chuàng)建新分支。
注:在切換分支前,一定確保當(dāng)前分支的修改已經(jīng)提交或者緩存。
多分支并行
我們經(jīng)常會遇到同時需要開發(fā)多個功能和需求,或者突然發(fā)現(xiàn)線上bug需要緊急處理,我們只需要提交當(dāng)前分支修改,然后切換到主干分支,從其基礎(chǔ)上再切出一個新分支fix-bug1:
可以看到,在work-a分支上我們新增了一次提交:
b287b8e22470b20cc98e6224a8023708b4cc6989。
現(xiàn)在我們在fix-bug1分支上修復(fù)bug后,進(jìn)行提交:
可以看到,在fix-bug1分支上多了一個提交:ca270e6,現(xiàn)在整個結(jié)構(gòu)就變成如下圖:
合并分支(git merge)
我們已經(jīng)修復(fù)了某bug或完成了功能開發(fā),這時要做的是把代碼并入主干,,當(dāng)然一般公司或團(tuán)隊都需要經(jīng)過代碼審查,才能并入主干,在此略過不談,分支合并相關(guān)指令:
git merge 分支名
該指令告訴Git將指定分支合并到當(dāng)前分支,當(dāng)然是可能出現(xiàn)沖突的,我們按照指示解決沖突,即可。
現(xiàn)在我們先切換到master分支,然后把fix-bug1分支并入主干:
可以看到執(zhí)行g(shù)it merge指令后,狀態(tài)信息顯示:
- 第一行Updating,告訴我們提交記錄更新至ca270e6;
- 第二行Fast-forward,即快速推進(jìn),說明Git直接將當(dāng)前分支推進(jìn)到指向新提交對象;
- 后面是merge的內(nèi)容信息。
非快速推進(jìn)合并(NO FAST-FORWARD)
現(xiàn)在,我們再次創(chuàng)建一個分支fix-bug2,并進(jìn)行幾次修改提交:
多次提交后,狀態(tài)如下:
我們通過非快速推進(jìn)方式合并分支進(jìn)主干分支:
如上圖,指定–no-ff即聲明進(jìn)行非快速推進(jìn)合并,第二行的Merge made by the ‘recursive’ strategy表明通過非快速推進(jìn)方式合并,我們發(fā)現(xiàn)除了分支上進(jìn)行的提交記錄外,Git創(chuàng)建了一個新的提交對象:7a657a,使用
git log –graph指令查看其信息:
如圖,快速推進(jìn)方式合并入主干的fix-bug1分支的提交記錄直接并入主線,且不會創(chuàng)建新的提交對象;而對于非快速推進(jìn)方式合并的fix-bug2分支,其提交歷史也都保存,但是并未進(jìn)入主線,而是保存了一條支線,同時,在主線上創(chuàng)建一個新的提交對象。
最后描述其結(jié)構(gòu)如圖:
非快速推進(jìn)與快速推進(jìn)合并(FAST-FORWARD & NO FAST-FORWARD)
從上例,對比一下兩種方式合并分支的異同:
- 提交對象都會保存;
- 報存提交對象方式不同:快速推進(jìn)方式是直接在主線(合并主分支)上,添加這些提交對象,即直接移動HEAD指針;而非快速推進(jìn)方式是將提交對象保存在支線,然后在主線新建一個提交對象,修改HEAD指針及新建提交對象的指針,而且此新建提交對象有兩個父提交對象(即有兩個parent指針)。
- 合并后分支指向不同:快速推進(jìn)合并后,兩個分支將同時指向最新提交對象,而非快速推進(jìn)合并后,合并主分支指向新建的提交對象,另一分支指向不變。
我們查看一下新創(chuàng)建提交對象:
可以看到該提交對象中有兩個指針指向父提交對象,一個指向主線中的父提交對象,一個指向fix-bug2分支合并而來的支線父提交對象。
三路合并(THREE-WAY MERGE)
除了之前提到的兩種合并的情況,其實還存在這樣一種情況,就是現(xiàn)在假如我完成了work-a分支的開發(fā),需要將其并入主干,我們能看到當(dāng)前master主干分支已經(jīng)推進(jìn)到7a6576了,而work-a分支指向b287b8,兩者有共同祖先提交對象6d50f6,我們將其合并:
上圖第二行表明此次是通過非快速推進(jìn)方式合并,我們查看提交對象記錄圖:
結(jié)構(gòu)如圖:
我們發(fā)現(xiàn),三路合并結(jié)構(gòu)是在需要合并的兩個分支的最新提交對象的基礎(chǔ)上,創(chuàng)建一個新提交對象(4ae14b),將合并主分支(即執(zhí)行合并指令時,當(dāng)前所處分支)的HEAD指針前移指向該提交對象,該提交對象有兩個父提交對象,分別為合并前待合并分支的最新提交對象(即b287b8和7a657a)。
關(guān)于三路合并需要明確:
- 三路合并其實是一種非快速推進(jìn)合并方式;
- 三路合并的前提是兩個分支有共同祖先提交對象;
分支沖突(conflict)
在合并分支,不可避免會發(fā)生沖突,當(dāng)我們在兩個分支對同一文件同一部分進(jìn)行不同修改后,發(fā)起合并時就會提示有沖突,假設(shè)我們有work-b分支,在其基礎(chǔ)上切出新分支work-b-1,然后在兩分支上分別對README.md文件同一部分進(jìn)行不同修改并提交,然后將work-b-1分支合并到work-b分支:
發(fā)現(xiàn)README.md文件有沖突,查看該文件:
如上圖,列出了兩個分支的不同修改,HEAD表明當(dāng)前分支的修改內(nèi)容,下面是work-b-1分支的修改,我們選擇需要保留的內(nèi)容,刪除其他無關(guān)信息和內(nèi)容,然后保存該文件,查看當(dāng)前狀態(tài):
根據(jù)提示,解決沖突后提交:
查看分支
對于創(chuàng)建過但并未刪除的分支,我們可以查看分支列表,依然使用git branch指令,不傳入任何參數(shù):
圖中列出了所有分支,前面帶星號的表示當(dāng)前分支,當(dāng)然我們還可以查看指明最新提交信息的分支列表,可以添加-v參數(shù):
篩選分支
除了可以查看所有分支列表,Git還支持篩選已合并或未合并至當(dāng)前分支的所有分支:
- –merged參數(shù)指明篩選已合并分支;
- –no-merged參數(shù)指明篩選未合并分支。
刪除分支(git branch -d)
當(dāng)分支合并入主干后,也許我們不再需要那個分支了,我們需要將其刪除,使用指令:
git branch -d 分支名
之前介紹到使用git branch是創(chuàng)建新分支,而指定-d參數(shù),說明需要刪除該分支:
遠(yuǎn)程分支(remote branch)
我們注意到,前文所講述的分支都是存在本地的,即本地分支,還需要了解遠(yuǎn)程分支,如[remote]/[branch]這種形式,表示是遠(yuǎn)端主機(jī)的某分支,關(guān)于遠(yuǎn)端主機(jī)詳情請查看,其實遠(yuǎn)程分支和本地分支基本理論概念還是相同的,區(qū)別是有些指令不同而已:
git checkout -b test origin/develop
以上指令即從遠(yuǎn)程分支(遠(yuǎn)端主機(jī)origin上的develop分支)切出新的本地分支test分支。
跟蹤分支(TRACKING BRANCH)
前文已經(jīng)介紹了本地分支和遠(yuǎn)程分支的概念及操作,那么這兩類分支之間應(yīng)該有某種關(guān)系將他們關(guān)聯(lián)起來,本地項目都需要與遠(yuǎn)端主機(jī)倉庫同步(pull & push),當(dāng)我們從一個遠(yuǎn)程分支切出(創(chuàng)建)一個本地分支時,這個分支就叫跟蹤分支(tracking branch),而遠(yuǎn)程分支叫上游分支(upstream branch)。
當(dāng)我們克隆一個遠(yuǎn)端倉庫時,會默認(rèn)創(chuàng)建一個跟蹤分支master,其上游分支就是遠(yuǎn)端主機(jī)別名/master。
創(chuàng)建跟蹤分支
創(chuàng)建跟蹤分支指令如下:
git checkout -b 本地分支名 遠(yuǎn)端主機(jī)別名/遠(yuǎn)程分支名
當(dāng)然也可以不指定分支名,使用遠(yuǎn)程分支同名:
git checkout --track 遠(yuǎn)端主機(jī)別名/遠(yuǎn)程分支名
修改跟蹤關(guān)系
有時候,可能需要為本地分支設(shè)置其上游分支,添加-u參數(shù):
git branch -u 遠(yuǎn)端主機(jī)別名/遠(yuǎn)程分支名
以上指令就指明當(dāng)前分支跟蹤某遠(yuǎn)端主機(jī)的遠(yuǎn)程分支。
查看跟蹤分支(git branch -vv)
使用以下指令查看分支的上游分支:
git branch -vv
上圖輸出信息第二行表明master分支跟蹤遠(yuǎn)程origin/master分支,ahead 7表明本地有7個提交未推到服務(wù)器,其他分支不是跟蹤分支,沒有上游分支。
刪除遠(yuǎn)程分支
對于不再需要的遠(yuǎn)程分支,是可以刪除的:
git push origin --delete test
以上指令刪除遠(yuǎn)端主機(jī)origin的test分支,但是在垃圾回收之前,Git服務(wù)器仍然會保留分支數(shù)據(jù),我們可以很方便的恢復(fù)數(shù)據(jù),之后會詳細(xì)介紹。
變基(rebase)
Git中有兩種方式整合不同分支的修改:第一種是前文介紹的合并(merge),另一種就是本節(jié)的主題變基(rebase)。
變基其實與前文提到的三路合并(three-way merge)頗有淵源:
如圖work-a分支與主干master分支合并后,創(chuàng)建一個新提交對象,我們還可以通過變基完成兩個分支的修改整合,由于work-a分支已合并到master分支,我們在work-a分支再提交一次修改e0ae7dc,然后我們將work-a分支對master分支進(jìn)行變基:
執(zhí)行變基時,由于兩個分支對同一文件同一部分進(jìn)行了不同修改,會提示沖突,需要解決沖突,我們修改文件解決沖突,然后查看狀態(tài):
上圖,第一行rebase in progress; onto 4ae14b3說明當(dāng)前分支針對4ae14b3快照進(jìn)行變基,第三到第五行分別說明:
- 第三行:解決沖突然后執(zhí)行g(shù)it rebase –continue指令繼續(xù)變基;
- 第四行:執(zhí)行g(shù)it rebase –skip指令,跳過解決沖突;
- 第五行:執(zhí)行g(shù)it rebase –abort指令,終止變基,回到分支變基前狀態(tài)。
下面第6到第八行說明:
- 第七行:使用git reset HEAD 指令撤銷某文件變更;
- 第八行:使用git add 指令標(biāo)記沖突為已解決狀態(tài)。
最后一行no changes added to commit (use “git add” and/or “git commit -a”),說明尚未標(biāo)記沖突,需要使用指令標(biāo)記變更,在繼續(xù)執(zhí)行變基:
如上圖,變基后,在主線上創(chuàng)建新提交對象640b83,并修改work-a分支指針指向該提交對象:
之后我們可以正常的合并:
變基后合并
如圖,主線分支更新提交對象到640b83a,第二行Fast-forward說明此次合并屬于快速推進(jìn)合并方式,結(jié)構(gòu)如下:
三路合并與變基
基于上例,三路合并,整合修改變更后會保留分支的原始提交記錄,新創(chuàng)建提交對象有兩個父提交對象,一個在主線上,一個在待合并分支上;而變基則不能保留待合并分支的原始提交記錄,主線上新建的提交對象只有一個位于主線上的父提交對象。更多變基相關(guān)內(nèi)容計劃單獨(dú)出文介紹。
至于到底選用哪種方式整合變更,變基還是合并,這個一直有爭論,沒有哪一種方式絕對合理,我們只需要把握一個原則:無論變基還是合并,你應(yīng)該只操作本地歷史記錄,任何已經(jīng)推到服務(wù)器并入主干的內(nèi)容和提交歷史不應(yīng)該更改。