分支簡(jiǎn)介
Git 保存的不是文件的變化或者差異,而是一系列不同時(shí)刻的文件快照。
當(dāng)使用git commit
進(jìn)行提交操作時(shí),Git會(huì)先計(jì)算每一個(gè)子目錄(本例中只有項(xiàng)目根目錄)的校驗(yàn)和,然后在 Git 倉(cāng)庫(kù)中這些校驗(yàn)和保存為樹對(duì)象。 隨后,Git 便會(huì)創(chuàng)建一個(gè)提交對(duì)象,它除了包含上面提到的那些信息外, 還包含指向這個(gè)樹對(duì)象(項(xiàng)目根目錄)的指針。如此一來(lái),Git 就可以在需要的時(shí)候重現(xiàn)此次保存的快照。
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
現(xiàn)在,Git 倉(cāng)庫(kù)中有五個(gè)對(duì)象:三個(gè)blob 對(duì)象
保存著文件快照)、一個(gè)樹對(duì)象
(記錄著目錄結(jié)構(gòu)和 blob 對(duì)象 索引)以及一個(gè)提交對(duì)象
(包含著指向前述樹對(duì)象的指針和所有提交信息)。
Git 的分支,其實(shí)本質(zhì)上僅僅是指向提交對(duì)象的可變指針。 Git 的默認(rèn)分支名字是master
。 在多次提交操作之
后,你其實(shí)已經(jīng)有一個(gè)指向最后那個(gè)提交對(duì)象的 master 分支。 它會(huì)在每次的提交操作中自動(dòng)向前移動(dòng)。
分支創(chuàng)建
Git 是怎么創(chuàng)建新分支的呢?
Git只是為你創(chuàng)建了一個(gè)可以移動(dòng)的新的指針。 比如,創(chuàng)建一個(gè) testing
分 支,使用git branch
命令:
# 這會(huì)在當(dāng)前所在的提交對(duì)象上創(chuàng)建一個(gè)指針
$ git branch testing
那么,Git 又是怎么知道當(dāng)前在哪一個(gè)分支上呢?
Git有一個(gè)名為 HEAD
的特殊指針,指向當(dāng)前所在的本地分支(將 HEAD 想象為當(dāng)前分支的別名)。 在本例中,你仍然在master
分支 上。 因?yàn)?code>git branch命令僅僅創(chuàng)建一個(gè)新分支,并不會(huì)自動(dòng)切換到新分支中去。
可以使用git log
命令查看各個(gè)分支當(dāng)前所指的對(duì)象。提供這一功能的參數(shù)是--decorate
$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project
分支切換
要切換到一個(gè)已存在的分支,你需要使用 git checkout
命令。 我們現(xiàn)在切換到新創(chuàng)建的testing
分支去:
# 這樣 HEAD 就指向 testing 分支了
$ git checkout testing
那么,這樣的實(shí)現(xiàn)方式會(huì)給我們帶來(lái)什么好處呢? 現(xiàn)在不妨再提交一次:
$ vim test.rb
$ git commit -a -m 'made a change'
如圖所示,testing
分支向前移動(dòng)了,但是master
分支卻沒(méi)有,它仍然指向運(yùn)行git checkou
t時(shí)所指的對(duì)象。 現(xiàn)在我們切換回 master
分支看看:
$ git checkout master
這條命令做了兩件事。 一是使 HEAD 指回 master
分支,二是將工作目錄恢復(fù)成 master
分支所指向的快照內(nèi)容。 也就是說(shuō),你現(xiàn)在做修改的話,項(xiàng)目將始于一個(gè)較舊的版本。 本質(zhì)上來(lái)講,這就是忽略testing
分支所做的修改,以便于向另一個(gè)方向進(jìn)行開發(fā)。
我們不妨再稍微做些修改并提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
現(xiàn)在,這個(gè)項(xiàng)目的提交歷史已經(jīng)產(chǎn)生了分叉(如圖)。 因?yàn)閯偛拍銊?chuàng)建了一個(gè)新分支,并切換過(guò)去進(jìn)行了一些工作,隨后又切換回 master
分支進(jìn)行了另外一些工作。 上述兩次改動(dòng)針對(duì)的是不同分支:你可以在不同分支間不斷地來(lái)回切換和工作,并在時(shí)機(jī)成熟時(shí)將它們合并起來(lái)。 而所有這些工作,你需要的命令只有branch
、checkout
和 commit
。
你可以簡(jiǎn)單地使用git log
命令查看分叉歷史。運(yùn)行git log --oneline --decorate --graph --all
,它會(huì)輸出你的提交歷史、各個(gè)分支的指向以及項(xiàng)目的分支分叉情況。
$ git log --oneline --decorate --graph --all
創(chuàng)建并切換分支
$ git checkout -b iss53
# 它是下面兩條命令的簡(jiǎn)寫:
$ git branch iss53
$ git checkout iss53
合并分支
你可以運(yùn)行你的測(cè)試,確保你的代碼修改是正確的,然后將其合并回你的 master
分支來(lái)部署到線上:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在合并的時(shí)候,“fast-forward”
是指,當(dāng)你試圖合并兩個(gè)分支時(shí),如果順著一個(gè)分支走下去能夠到達(dá)另一個(gè)分支,那么 Git 在合并兩者的時(shí)候,只會(huì)簡(jiǎn)單的將指針向前推 進(jìn)(指針右移),因?yàn)檫@種情況下的合并操作沒(méi)有需要解決的分歧——這就叫做 “fast-forward”
。
因?yàn)椋?code>master分支所在提交并不是原本 iss53
分支所在提交的直接祖先,而是合并 hotfix
分支,Git 不得不做一些額外的工作。 出現(xiàn)這種情況的時(shí)候,Git 會(huì)使用兩個(gè)分支的末端所指的快照(C4
和 C5
)以及這兩個(gè)分支的工作祖先(C2
),做一個(gè)簡(jiǎn)單的三方合并。
和之前將分支指針向前推進(jìn)所不同的是,Git 將此次三方合并的結(jié)果做了一個(gè)新的快照并且自動(dòng)創(chuàng)建一個(gè)新的提交指向它。 這個(gè)被稱作一次合并提交,它的特別之處在于他有不止一個(gè)父提交。
刪除分支
$ git branch -d hotfix
遇到?jīng)_突時(shí)的分支合并
有時(shí)候合并操作不會(huì)如此順利。 如果你在兩個(gè)不同的分支中,對(duì)同一個(gè)文件的同一個(gè)部分進(jìn)行了不同的修改,Git 就沒(méi)法干凈的合并它們,在合并它們的時(shí)候就會(huì)產(chǎn)生合并沖突:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
此時(shí) Git 做了合并,但是沒(méi)有自動(dòng)地創(chuàng)建一個(gè)新的合并提交。 Git 會(huì)暫停下來(lái),等待你去解決合并產(chǎn)生的沖突。 你可以在合并沖突后的任意時(shí)刻使用git status
命令來(lái)查看那些因包含合并沖突而處于未合并狀態(tài)的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并沖突而有待解決的文件,都會(huì)以未合并狀態(tài)標(biāo)識(shí)出來(lái)。 Git 會(huì)在有沖突的文件中加入標(biāo)準(zhǔn)的沖突解決標(biāo)記,這樣你可以打開這些包含沖突的文件然后手動(dòng)解決沖突。 出現(xiàn)沖突的文件會(huì)包含一些特殊區(qū)段,看起來(lái)像下面這個(gè)樣子:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
這表示HEAD
所指示的版本(也就是你的master
分支所在的位置,因?yàn)槟阍谶\(yùn)行merge
命令的時(shí)候已經(jīng)檢出到了這個(gè)分支)在這個(gè)區(qū)段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 下半部分。 為了解決沖突,你必須選擇使用由 ======= 分割的兩部分中的一個(gè),或者你也可以自行合并這些內(nèi)容。
在你解決了所有文件里的沖突之后,對(duì)每個(gè)文件使用 git add 命令來(lái)將其標(biāo)記為沖突已解決。 一旦暫存這 些原本有沖突的文件,Git 就會(huì)將它們標(biāo)記為沖突已解決。
如果你想使用圖形化工具來(lái)解決沖突,你可以運(yùn)行git mergetool
,該命令會(huì)為你啟動(dòng)一個(gè)合適的可視化合并工具(在這里 Git 使用opendiff
做為默認(rèn)的合并工具),并帶領(lǐng)你一步一步解決這些沖突:
$ git mergetool
# This message is displayed because 'merge.tool' is not configured.
等你退出合并工具之后,Git 會(huì)詢問(wèn)剛才的合并是否成功。 如果你回答是,Git 會(huì)暫存那些文件以表明沖突已解決。你可以再次運(yùn)行g(shù)it status來(lái)確認(rèn)所有的合并沖突都已被解決。如果確定之前有沖突的的文件都已經(jīng)暫存了,這時(shí)你可以輸入git commit
來(lái)完成合并提交。
分支管理
查看當(dāng)前所有分支的一個(gè)列表:
$ git branch
iss53
* master
testing
注意 master
分支前的*
字符:它代表現(xiàn)在檢出的那一個(gè)分支(也就是當(dāng)前 HEAD
指針?biāo)赶虻姆种?。 這意味著如果在這時(shí)候提交,master
分支將會(huì)隨著新的工作向前移動(dòng)。
要查看每一個(gè)分支的最后一次提交:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
--merged
與 --no-merged
這兩個(gè)選項(xiàng)可以過(guò)濾這個(gè)列表中已經(jīng)合并或尚未合并到當(dāng)前分支的分支。
$ git branch --merged
iss53
* master
# 在這個(gè)列表中分支名字前沒(méi)有 * 號(hào)的分支通常可以使用 git branch -d刪除掉;
# 因?yàn)槟阋呀?jīng)將它們的工作整合到了另一個(gè)分支,所以并不會(huì)失去任何東西。
$ git branch --no-merged
testing
# 這里顯示的分支,因?yàn)樗诉€未合并的工作,使用 git branch -d 命令刪除它時(shí)會(huì)失敗;
# 如果真的想要?jiǎng)h除分支并丟掉那些工作,可以使用 -D 選項(xiàng)強(qiáng)制刪除它。
$ git branch -D
分支開發(fā)工作流
長(zhǎng)期分支
Git 使用簡(jiǎn)單的三方合并,在整個(gè)項(xiàng)目開發(fā)周期的不同階段,你可以同時(shí)擁有多個(gè)開放的分支,你可以定期地把某些特性分支合并入其他分支中。
比如只在 master
分支上保留完全穩(wěn)定的代碼,還有一些名為 develop
或者next
的平行分支,被用來(lái)做后續(xù)開發(fā)或者測(cè)試穩(wěn)定性——這些分支不必保持絕對(duì)穩(wěn)定,但是一旦達(dá)到穩(wěn)定狀態(tài),它們就可以被合并入 master
分支了。 這樣,在確保這些已完成的特性分支(短期分支,比如之前的 iss53
分支)能夠通過(guò)所有測(cè)試,并且不會(huì)引入更多 bug 之后,就可以合并入主干分支中,等待下一次的發(fā)布。
可以用這種方法維護(hù)不同層次的穩(wěn)定性。 一些大型項(xiàng)目還有一個(gè) proposed
(建議) 或 pu: proposed updates
(建議更新)分支,它可能因包含一些不成熟的內(nèi)容而不能進(jìn)入 next
或者 master
分支。
這么做的目的是使你的分支具有不同級(jí)別的穩(wěn)定性。當(dāng)它們具有一定程度的穩(wěn)定性后,再把它們合并入具有更高級(jí)別穩(wěn)定性的分支中。
使用多個(gè)長(zhǎng)期分支的方法并非必要,但是這么做通常很有幫助,尤其是當(dāng)你在一個(gè)非常龐大或者復(fù)雜的項(xiàng)目中工作時(shí)。
特性分支
特性分支是一種短期分支,它被用來(lái)實(shí)現(xiàn)單一特性或其相關(guān)工作。
當(dāng)你新建和合并分支的時(shí)候,所有這一切都只發(fā)生在你本地的 Git 版本庫(kù)中 —— 沒(méi)有與服務(wù)器發(fā)生交互。
遠(yuǎn)程分支
遠(yuǎn)程引用是對(duì)遠(yuǎn)程倉(cāng)庫(kù)的引用(指針),包括分支、標(biāo)簽等等。
可以通過(guò)git ls-remote (remote)
來(lái)顯式地獲得遠(yuǎn)程引用的完整列表,或者通過(guò)git remote show (remote)
獲得遠(yuǎn)程分支的更多信息。 然而,一個(gè)更常見(jiàn)的做法是利用遠(yuǎn)程跟蹤分支。
遠(yuǎn)程跟蹤分支是遠(yuǎn)程分支狀態(tài)的引用。它們是你不能移動(dòng)的本地引用,當(dāng)你做任何網(wǎng)絡(luò)通信操作時(shí),它們會(huì)自動(dòng)移動(dòng)。 遠(yuǎn)程跟蹤分支像是你上次連接到遠(yuǎn)程倉(cāng)庫(kù)時(shí),那些分支所處狀態(tài)的書簽。
它們以(remote)/(branch)
形式命名。 例如,如果你想要看你最后一次與遠(yuǎn)程倉(cāng)庫(kù)origin
通信時(shí) maste
分支的狀態(tài),你可以查看 origin/master
分支。 你與同事合作解決一個(gè)問(wèn)題并且他們推送了一個(gè) iss53
分支,你可能有自己的本地 iss53
分支,但是在服務(wù)器上的分支會(huì)指向 origin/iss53
的提交。
假設(shè)你的網(wǎng)絡(luò)里有一個(gè)在 git.ourcompany.com
的 Git 服務(wù)器。 如果你從這里克隆,Git 的 clone
命令會(huì)為你自動(dòng)將其命名為 origin
,拉取它的所有數(shù)據(jù),創(chuàng)建一個(gè)指向它的 master 分支的指針,并且在本地將其命名為 origin/master
。 Git 也會(huì)給你一個(gè)與 origin
的 master
分支在指向同一個(gè)地方的本地 master
分支,這樣你就有工作的基礎(chǔ)。
(“origin”
是 git clone 時(shí)默認(rèn)的遠(yuǎn)程倉(cāng)庫(kù)名字。 如果你運(yùn)行 git clone -o booyah
,那么你默認(rèn)的遠(yuǎn)程分支名字將會(huì)是 booyah/master
。)
為什么創(chuàng)建分支之后,別人更新了代碼而本地不更新?
如果你在本地的 master
分支做了一些工作,然而在同一時(shí)間,其他人推送提交到 git.ourcompany.com
并 更新了它的 master 分支,那么你的提交歷史將向不同的方向前進(jìn)。 也許,只要你不與 origin
服務(wù)器連接,你 的 origin/master
指針就不會(huì)移動(dòng)。
如果要同步你的工作,運(yùn)行git fetch origin
命令。 這個(gè)命令查找 “origin” 是哪一個(gè)服務(wù)器(在本例中是 git.ourcompany.com
),從中抓取本地沒(méi)有的數(shù)據(jù),并且更新本地?cái)?shù)據(jù)庫(kù),移動(dòng) origin/master
指針指向新的、更新后的位置。
推送
當(dāng)你想要公開分享一個(gè)分支時(shí),需要將其推送到有寫入權(quán)限的遠(yuǎn)程倉(cāng)庫(kù)上。 本地的分支并不會(huì)自動(dòng)與遠(yuǎn)程倉(cāng)庫(kù)同步 - 你必須顯式地推送想要分享的分支。 這樣,你就可以把不愿意分享的內(nèi)容放到私人分支上,而將需要和別人協(xié)作的內(nèi)容推送到公開分支。
如果希望和別人一起在名為serverfix的分支上工作,你可以像推送第一個(gè)分支那樣推送它。運(yùn)行git push (remote) (branch)
:
$ git push origin serverfix
這里有些工作被簡(jiǎn)化了。 Git 自動(dòng)將 serverfix
分支名字展開為refs/heads/serverfix:refs/heads/serverfix
,那意味著,“推送本地的 serverfix
分支來(lái)更新遠(yuǎn)程倉(cāng)庫(kù)上的 serverfix
分支。”
你也可以運(yùn)行git push origin serverfix:serverfix
,它會(huì)做同樣的事-相當(dāng)于它說(shuō),“推送本地的 serverfix 分支,將其作為遠(yuǎn)程倉(cāng)庫(kù)的 serverfix 分支” 可以通過(guò)這種格式來(lái)推送本地分支到一個(gè)命名不相同的遠(yuǎn)程分支。 如果并不想讓遠(yuǎn)程倉(cāng)庫(kù)上的分支叫做 serverfix
,可以運(yùn)行 git push origin serverfix:awesomebranch
來(lái)將本地的serverfix
分支推送到遠(yuǎn)程倉(cāng)庫(kù)上的awesomebranch
分支。
下一次其他協(xié)作者從服務(wù)器上抓取數(shù)據(jù)時(shí),他們會(huì)在本地生成一個(gè)遠(yuǎn)程分支 origin/serverfix
,指向服務(wù)器的 serverfix
分支的引用:
$ git fetch origin
要特別注意的一點(diǎn)是當(dāng)抓取到新的遠(yuǎn)程跟蹤分支時(shí),本地不會(huì)自動(dòng)生成一份可編輯的副本(拷貝)。不會(huì)有一個(gè)新的 serverfix
分支 - 只有一個(gè)不可以修改的 origin/serverfix
指針。
可以運(yùn)行 git merge origin/serverfix
將這些工作合并到當(dāng)前所在的分支。 如果想要在自己的 serverfix
分支上工作,可以將其建立在遠(yuǎn)程跟蹤分支之上:
$ git checkout -b serverfix origin/serverfix
這會(huì)給你一個(gè)用于工作的本地分支,并且起點(diǎn)位于 origin/serverfix
。
跟蹤分支
從一個(gè)遠(yuǎn)程跟蹤分支檢出一個(gè)本地分支會(huì)自動(dòng)創(chuàng)建一個(gè) 跟蹤分支(上游分支)。
跟蹤分支是與遠(yuǎn)程分支有直接關(guān)系的本地分支。 如果在一個(gè)跟蹤分支上輸入git pull
,Git 能自動(dòng)地識(shí)別去哪個(gè)服務(wù)器上抓取、合并到哪個(gè)分支。
當(dāng)克隆一個(gè)倉(cāng)庫(kù)時(shí),它通常會(huì)自動(dòng)地創(chuàng)建一個(gè)跟蹤 origin/master
的 master
分支。 然而,你也可以設(shè)置其他的跟蹤分支 - 其他遠(yuǎn)程倉(cāng)庫(kù)上的跟蹤分支,或者不跟蹤 master 分支。 最簡(jiǎn)單的就是之前看到的例子,運(yùn)行 git checkout -b [branch] [remotename]/[branch]
。 這是一個(gè)十分常用的操作所以 Git 提供了 --track 快捷方式:
$ git checkout --track origin/serverfix
如果想要將本地分支與遠(yuǎn)程分支設(shè)置為不同名字,你可以輕松地增加一個(gè)不同名字的本地分支的上一個(gè)命令:
$ git checkout -b sf origin/serverfix
# 現(xiàn)在,本地分支 sf 會(huì)自動(dòng)從 origin/serverfix 拉取
設(shè)置已有的本地分支跟蹤一個(gè)剛剛拉取下來(lái)的遠(yuǎn)程分支,或者想要修改正在跟蹤的上游分支,你可以在任意時(shí)間使用-u
或--set-upstream-to
選項(xiàng)運(yùn)行git branch
來(lái)顯式地設(shè)置。
$ git branch -u origin/serverfix
- 上游快捷方式
當(dāng)設(shè)置好跟蹤分支后,可以通過(guò)@{upstream}
或@{u}
快捷方式來(lái)引用它。 所以在 master 分支時(shí)并且它正在跟蹤origin/master
時(shí),如果愿意的話可以使用git merge @{u}
來(lái)取 代git merge origin/master
。
查看設(shè)置的所有跟蹤分支:
$ git branch -vv
- 注意:命令返回的值來(lái)自于你從每個(gè)服務(wù)器上最后一次抓取的數(shù)據(jù)。
這個(gè)命令并沒(méi)有連接服務(wù)器,它只會(huì)告訴你關(guān)于本地緩存的服務(wù)器數(shù)據(jù)。 如果想要統(tǒng)計(jì)最新的領(lǐng)先與落后數(shù)字,需要在運(yùn)行此命令前抓 取所有的遠(yuǎn)程倉(cāng)庫(kù)。可以像這樣做:
$ git fetch --all; git branch -vv
拉取
當(dāng) git fetch
命令從服務(wù)器上抓取本地沒(méi)有的數(shù)據(jù)時(shí),它并不會(huì)修改工作目錄中的內(nèi)容。 它只會(huì)獲取數(shù)據(jù)然后讓你自己合并。 然而,有一個(gè)命令叫作 git pull
在大多數(shù)情況下它的含義是一個(gè)git fetch
緊接著一個(gè) git merge
命令。
如果有一個(gè)設(shè)置好的跟蹤分支,不管它是顯式地設(shè)置還是通過(guò) clone
或checkout
命令為你創(chuàng)建的,git pull
都會(huì)查找當(dāng)前分支所跟蹤的服務(wù)器與分支,從服務(wù)器上抓取數(shù)據(jù)然后嘗試合并入那個(gè)遠(yuǎn)程分支。
刪除遠(yuǎn)程分支
$ git push origin --delete serverfix
變基
在 Git 中整合來(lái)自不同分支的修改主要有兩種方法merge
以及 rebase
。
- 整合分支最容易的方法是
merge
命令。 它會(huì)把兩個(gè)分支的最新快照(C3
和C4
)以及二者最近的共同祖先(C2
)進(jìn)行三方合并,合并的結(jié)果是生成一個(gè)新的快照(并提交)。
- 還有一種方法:
rebase
。你可以提取在C4
中引入的補(bǔ)丁和修改,然后在C3
的基礎(chǔ)上應(yīng)用一次。 在 Git 中,這種操作就叫做變基
。
$ git checkout experiment
$ git rebase master
假設(shè)你希望將 client
中的修改合并到主分支并發(fā)布,但暫時(shí)并不想合并 server
中的修改,可以使用 git rebase
命令的 --onto
選項(xiàng),選中在client
分支里但不在server
分支里的修改(即 C8
和C9
),將它們?cè)?master
分支上重放:
$ git rebase --onto master server client
# 取出client分支,找出處于client分支和server分支的共同祖先之后的修改,
# 然后把它們?cè)趍aster分支上重放一遍
現(xiàn)在可以快進(jìn)合并master分支了:
$ git checkout master
$ git merge client
接下來(lái)你決定將 server
分支中的修改也整合進(jìn)來(lái)。 使用git rebase [basebranch] [topicbranch]
命令可以直接將特性分支(即本例中的 server
)變基到目標(biāo)分支(即 master
)上。這樣做能省去你先切換到 server
分支。
$ git rebase master server
$ git checkout master
$ git merge server
參考文獻(xiàn):https://www.git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere