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