Git分支

前面的話

  幾乎每一種版本控制系統都以某種形式支持分支。使用分支意味着能夠從開發主線上分離開來,而後在不影響主線的同時繼續工做。在不少版本控制系統中,這是個昂貴的過程,經常須要建立一個源代碼目錄的完整副本,對大型項目來講會花費很長時間javascript

  有人把Git的分支模型稱爲「必殺技特性」,而正是由於它,將Git從版本控制系統家族裏區分出來。Git有何特別之處呢?Git的分支可謂是難以置信的輕量級,它的新建操做幾乎能夠在瞬間完成,而且在不一樣分支間切換起來也差很少同樣快。和許多其餘版本控制系統不一樣,Git鼓勵在工做流程中頻繁使用分支與合併,哪怕一天以內進行許屢次都沒有關係。理解分支的概念並熟練運用後,纔會意識到爲何Git是一個如此強大而獨特的工具,並今後真正改變開發方式。本文將詳細介紹Git分支html

 

定義

  爲了理解Git分支的實現方式,咱們須要回顧一下Git是如何儲存數據的。Git保存的不是文件差別或者變化量,而只是一系列文件快照java

  在Git中提交時,會保存一個提交(commit)對象,該對象包含一個指向暫存內容快照的指針,包含本次提交的做者等相關附屬信息,包含零個或多個指向該提交對象的父對象指針:首次提交是沒有直接祖先的,普通提交有一個祖先,由兩個或多個分支合併產生的提交則有多個祖先git

  爲直觀起見,咱們假設工做目錄中有三個文件,準備將它們暫存後提交。暫存操做會對每個文件計算校驗和,而後把當前版本文件快照保存到Git倉庫(Git使用blob類型的對象存儲這些快照),並將校驗和加入暫存區域github

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'

  當使用git commit新建一個提交對象前,Git會先計算每個子目錄(本例中就是項目根目錄)的校驗和,而後在Git倉庫中將這些目錄保存爲樹(tree)對象。以後Git建立的提交對象,除了包含相關提交信息之外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就能夠在未來須要的時候,重現這次快照的內容了數據庫

  如今,Git倉庫中有五個對象:三個表示文件快照內容的blob對象;一個記錄着目錄樹內容及其中各個文件對應blob對象索引的tree對象;以及一個包含指向tree對象(根目錄)的索引和其餘提交信息元數據的commit對象。概念上來講,倉庫中的各個對象保存的數據和相互關係看起來以下圖所示vim

  作些修改後再次提交,那麼此次的提交對象會包含一個指向上次提交對象的指針(即下圖中的parent對象)。兩次提交後,倉庫歷史會變成下圖的樣子服務器

  如今來談分支。Git中的分支,其實本質上僅僅是個指向commit對象的可變指針。Git會使用master做爲分支的默認名字。在若干次提交後,其實已經有了一個指向最後一次提交對象的master分支,它在每次提交的時候都會自動向前移動網絡

  那麼,Git又是如何建立一個新的分支的呢?答案很簡單,建立一個新的分支指針。好比新建一個testing分支,可使用git branch命令:app

$ git branch testing

  這會在當前commit對象上新建一個分支指針

  那麼,Git是如何知道你當前在哪一個分支上工做的呢?其實答案也很簡單,它保存着一個名爲HEAD的特別指針。請注意它和你熟知的許多其餘版本控制系統(好比Subversion或CVS)裏的HEAD概念大不相同。在Git中,它是一個指向你正在工做中的本地分支的指針(能夠將HEAD想象爲當前分支的別名)。運行git branch命令,僅僅是創建了一個新的分支,但不會自動切換到這個分支中去,因此在這個例子中,咱們依然還在master分支裏工做

  要切換到其餘分支,能夠執行git checkout命令。咱們如今轉換到新建的testing分支

$ git checkout testing

  這樣HEAD就指向了testing分支

  這樣的實現方式會給咱們帶來什麼好處呢?好吧,如今不妨再提交一次:

$ git commit -a -m 'made a change'

  很是有趣,如今testing分支向前移動了一格,而master分支仍然指向原先git checkout時所在的commit對象。如今咱們回到master分支看看

$ git checkout master

  這條命令作了兩件事。它把HEAD指針移回到master分支,並把工做目錄中的文件換成了master分支所指向的快照內容。也就是說,如今開始所作的改動,將始於本項目中一個較老的版本。它的主要做用是將testing分支裏做出的修改暫時取消,這樣你就能夠向另外一個方向進行開發

$ git commit -a -m 'made other changes'

  如今咱們的項目提交歷史產生了分叉,由於剛纔咱們建立了一個分支,轉換到其中進行了一些工做,而後又回到原來的主分支進行了另一些工做。這些改變分別孤立在不一樣的分支裏:咱們能夠在不一樣分支裏反覆切換,並在時機成熟時把它們合併到一塊兒。而全部這些工做,僅僅須要branch和checkout這兩條命令就能夠完成

  因爲Git中的分支實際上僅是一個包含所指對象校驗和(40個字符長度SHA-1字串)的文件,因此建立和銷燬一個分支就變得很是廉價。說白了,新建一個分支就是向一個文件寫入41個字節(外加一個換行符)那麼簡單,固然也就很快了

  這和大多數版本控制系統造成了鮮明對比,它們管理分支大多采起備份全部項目文件到特定目錄的方式,因此根據項目文件數量和大小不一樣,可能花費的時間也會有至關大的差異,快則幾秒,慢則數分鐘。而Git的實現與項目複雜度無關,它永遠能夠在幾毫秒的時間內完成分支的建立和切換。同時,由於每次提交時都記錄了祖先信息(即parent對象),未來要合併分支時,尋找恰當的合併基礎(即共同祖先)的工做其實已經天然而然地擺在那裏了,因此實現起來很是容易。Git鼓勵開發者頻繁使用分支,正是由於有着這些特性做保障

  接下來看看,咱們爲何應該頻繁使用分支

 

新建與合併

  如今讓咱們來看一個簡單的分支與合併的例子,實際工做中大致也會用到這樣的工做流程:

  一、開發某個網站

  二、爲實現某個新的需求,建立一個分支

  三、在這個分支上開展工做

  假設此時,忽然接到一個電話說有個很嚴重的問題須要緊急修補,那麼能夠按照下面的方式處理:

  一、返回到原先已經發布到生產服務器上的分支

  二、爲此次緊急修補創建一個新分支,並在其中修復問題

  三、經過測試後,回到生產服務器所在的分支,將修補分支合併進來,而後再推送到生產服務器上。

  四、切換到以前實現新需求的分支,繼續工做

【新建與切換】

  假設你正在項目中愉快地工做,而且已經提交了幾回更新

  如今,決定要修補問題追蹤系統上的#53問題。順帶說明下,Git並不一樣任何特定的問題追蹤系統打交道。這裏爲了說明要解決的問題,才把新建的分支取名爲iss53。要新建並切換到該分支,運行git checkout並加上-b參數

$ git checkout -b iss53
Switched to a new branch 'iss53'
$ git branch iss53
$ git checkout iss53

  接着開始嘗試修復問題,在提交了若干次更新後,iss53分支的指針也會隨着向前推動,由於它就是當前分支(換句話說,當前的HEAD指針正指向iss53)

$ git commit -a -m 'added a new footer [issue 53]'

  如今你就接到了那個網站問題的緊急電話,須要立刻修補。有了Git,咱們就不須要同時發佈這個補丁和iss53裏做出的修改,也不須要在建立和發佈該補丁到服務器以前花費大力氣來複原這些修改。惟一須要的僅僅是切換回master分支

  不過在此以前,留心你的暫存區或者工做目錄裏,那些尚未提交的修改,它會和你即將檢出的分支產生衝突從而阻止Git爲你切換分支。切換分支的時候最好保持一個清潔的工做區域。稍後會介紹幾個繞過這種問題的辦法(分別叫作stashing和commitamending)。目前已經提交了全部的修改,因此接下來能夠正常轉換到master分支:

$ git checkout master
Switched to branch 'master'

  此時工做目錄中的內容和你在解決問題#53以前如出一轍,你能夠集中精力進行緊急修補。這一點值得牢記:Git會把工做目錄的內容恢復爲檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時徹底同樣

  接下來,你得進行緊急修補。咱們建立一個緊急修補分支hotfix來開展工做,直到搞定

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ git commit -a -m 'fixed the broken email address'
[hotfix 3a0874c] fixed the broken email address
 1 files changed, 1 deletion(-)

  有必要做些測試,確保修補是成功的,而後回到master分支並把它合併進來,而後發佈到生產服務器。用git merge命令來進行合併

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 README | 1 -
 1 file changed, 1 deletion(-)

  請注意,合併時出現了「Fast forward」的提示。因爲當前master分支所在的提交對象是要併入的hotfix分支的直接上游,Git只需把master分支指針直接右移。換句話說,若是順着一個分支走下去能夠到達另外一個分支的話,那麼Git在合併二者時,只會簡單地把指針右移,由於這種單線的歷史分支不存在任何須要解決的分歧,因此這種合併過程能夠稱爲快進(Fast forward)

  如今最新的修改已經在當前master分支所指向的提交對象中了,能夠部署到生產服務器上去了

  在那個超級重要的修補發佈之後,你想要回到被打擾以前的工做。因爲當前hotfix分支和master都指向相同的提交對象,因此hotfix已經完成了歷史使命,能夠刪掉了。使用git branch 的-d選項執行刪除操做

$ git branch -d hotfix
Deleted branch hotfix (was 3a0874c).

  如今回到以前未完成的#53問題修復分支上繼續工做

$ git checkout iss53
Switched to branch 'iss53'
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
 1 file changed, 1 insertion(+)

  值得注意的是以前hotfix分支的修改內容還沒有包含到iss53中來。若是須要歸入這次修補,能夠用git merge master把master分支合併到iss53;或者等iss53完成以後,再將iss53分支中的更新併入master

【分支的合併】

  在問題#53相關的工做完成以後,能夠合併回master分支。實際操做同前面合併hotfix分支差很少,只需回到master分支,運行git merge命令指定要合併進來的分支:

$ git checkout master
$ git merge iss53
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 1 +
 1 file changed, 1 insertion(+)

  請注意,此次合併操做的底層實現,並不一樣於以前hotfix的併入方式。由於此次你的開發歷史是從更早的地方開始分叉的。因爲當前master分支所指向的提交對象(C4)並非iss53分支的直接祖先,Git不得不進行一些額外處理。就此例而言,Git會用兩個分支的末端(C4和C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。下圖用紅框標出了Git用於合併的三個提交對象

  此次,Git沒有簡單地把分支指針右移,而是對三方合併後的結果從新作一個新的快照,並自動建立一個指向它的提交對象(C6)。這個提交對象比較特殊,它有兩個祖先(C4和C5)

  值得一提的是Git能夠本身裁決哪一個共同祖先纔是最佳合併基礎;這和CVS或Subversion(1.5之後的版本)不一樣,它們須要開發者手工指定合併基礎。因此此特性讓Git的合併操做比其餘系統都要簡單很多

  既然以前的工做成果已經合併到master了,那麼iss53也就沒用了。你能夠就此刪除它,並在問題追蹤系統裏關閉該問題

$ git branch -d iss53

【遇到衝突時的分支合併】

  有時候合併操做並不會如此順利。若是在不一樣的分支中都修改了同一個文件的同一部分,Git就沒法乾淨地把二者合到一塊兒(邏輯上說,這種問題只能由人來裁決)。若是你在解決問題#53的過程當中修改了hotfix中修改的部分,將獲得相似下面的結果

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

  Git做了合併,但沒有提交,它會停下來等你解決衝突。要看看哪些文件在合併時發生衝突,能夠用git status查閱:

$ 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")

  任何包含未解決衝突的文件都會以未合併(unmerged)的狀態列出。Git會在有衝突的文件里加入標準的衝突解決標記,能夠經過它們來手工定位並解決這些衝突。能夠看到此文件包含相似下面這樣的部分:

<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
  please contact us at support@github.com
</div>
>>>>>>> iss53

  能夠看到=======隔開的上半部分,是HEAD(即master分支,在運行merge命令時所切換到的分支)中的內容,下半部分是在iss53分支中的內容。解決衝突的辦法無非是兩者選其一或者由你親自整合到一塊兒。好比你能夠經過把這段內容替換爲下面這樣來解決:

<div id="footer">
please contact us at email.support@github.com
</div>

  這個解決方案各採納了兩個分支中的一部份內容,並且我還刪除了<<<<<<<,=======和>>>>>>>這些行。在解決了全部文件裏的全部衝突後,運行git add將把它們標記爲已解決狀態(實際上就是將一次快照保存到暫存區域)。由於一旦暫存,就表示衝突已經解決。若是你想用一個有圖形界面的工具來解決這些問題,不妨運行git mergetool,它會調用一個可視化的合併工具並引導你解決全部衝突:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

  若是不想用默認的合併工具,你能夠在上方"merge tool candidates"裏找到可用的合併工具列表,輸入你想用的工具名

  退出合併工具之後,Git會詢問你合併是否成功。若是回答是,它會爲你把相關文件暫存起來,以代表狀態爲已解決

  再運行一次git status來確認全部衝突都已解決:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   index.html

  若是以爲滿意了,而且確認全部衝突都已解決,也就是進入了暫存區,就能夠用git commit來完成此次合併提交。提交的記錄差很少是這樣:

Merge branch 'iss53'

Conflicts:
  index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#       .git/MERGE_HEAD
# and try again.
#

  若是想給未來看此次合併的人一些方便,能夠修改該信息,提供更多合併細節。好比你都做了哪些改動,以及這麼作的緣由。有時候裁決衝突的理由並不直接或明顯,有必要略加註解

 

分支管理

  git branch命令不只僅能建立和刪除分支,若是不加任何參數,它會給出當前全部分支的清單

$ git branch
  iss53
* master
  testing

  注意看master分支前的*字符:它表示當前所在的分支。也就是說,若是如今提交更新,master分支將隨着開發進度前移。若要查看各個分支最後一個提交對象的信息,運行git branch -v:

$ 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選項(Git1.5.6以上版本)。好比用git branch --merged查看哪些分支已被併入當前分支,也就是說哪些分支是當前分支的直接上游

$ git branch --merged
  iss53
* master

  以前咱們已經合併了iss53,因此在這裏會看到它。通常來講,列表中沒有*的分支一般均可以用git branch -d來刪掉。緣由很簡單,既然已經把它們所包含的工做整合到了其餘分支,刪掉也不會損失什麼

  另外能夠用git branch --no-merged查看還沒有合併的分支

$ git branch --no-merged
  testing

  它會顯示還未合併進來的分支。因爲這些分支中還包含着還沒有合併進來的工做成果,因此簡單地用 git branch -d刪除該分支會提示錯誤,由於那樣作會丟失數據:

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

  不過,若是你確實想要刪除該分支上的改動,能夠用大寫的刪除選項-D強制執行,就像上面提示信息中給出的那樣

 

工做流程

  因爲分支管理的便捷,衍生出了這類典型的工做模式,能夠根據項目的實際狀況進行選擇

【長期分支】

  因爲Git使用簡單的三方合併,因此就算在較長一段時間內,反覆屢次把某個分支合併到另外一分支,也不是什麼難事。也就是說,你能夠同時擁有多個開放的分支,每一個分支用於完成特定的任務,隨着開發的推動,你能夠隨時把某個特性分支的成果併到其餘分支中

  許多使用Git的開發者都喜歡用這種方式來開展工做,好比僅在master分支中保留徹底穩定的代碼,即已經發布或即將發佈的代碼。與此同時,他們還有一個名爲develop或next的平行分支,專門用於後續的開發,或僅用於穩定性測試—固然並非說必定要絕對穩定,不過一旦進入某種穩定狀態,即可以把它合併到master裏。這樣,在確保這些已完成的特性分支(短時間分支,好比以前的iss53分支)可以經過全部測試,而且不會引入更多錯誤以後,就能夠併到主幹分支中,等待下一次的發佈。

  本質上咱們剛纔談論的,是隨着提交對象不斷右移的指針。穩定分支的指針老是在提交歷史中落後一大截,而前沿分支老是比較靠前

  或者把它們想象成工做流水線,或許更好理解一些,通過測試的提交對象集合被遴選到更穩定的流水線

  你能夠用這招維護不一樣層次的穩定性。某些大項目還會有個proposed(建議)或pu(proposed updates,建議更新)分支,它包含着那些可能尚未成熟到進入next或master的內容。這麼作的目的是擁有不一樣層次的穩定性:當這些分支進入到更穩定的水平時,再把它們合併到更高層分支中去。再次說明下,使用多個長期分支的作法並不是必需,不過通常來講,對於特大型項目或特複雜的項目,這麼作確實更容易管理

【特性分支】

  在任何規模的項目中均可以使用特性(Topic)分支。一個特性分支是指一個短時間的,用來實現單一特性或與其相關工做的分支。可能你在之前的版本控制系統裏從未作過相似這樣的事情,由於一般建立與合併分支消耗太大。然而在Git中,一天以內創建、使用、合併再刪除多個分支是常見的事

  咱們在上節的例子裏已經見過這種用法了。咱們建立了iss53和hotfix這兩個特性分支,在提交了若干更新後,把它們合併到主幹分支,而後刪除。該技術容許你迅速且徹底的進行語境切換——由於你的工做分散在不一樣的流水線裏,每一個分支裏的改變都和它的目標特性相關,瀏覽代碼之類的事情於是變得更簡單了。你能夠把做出的改變保持在特性分支中幾分鐘,幾天甚至幾個月,等它們成熟之後再合併,而不用在意它們創建的順序或者進度

  如今咱們來看一個實際的例子,以下圖所示,由下往上,起先咱們在master工做到C1,而後開始一個新分支iss91嘗試修復91號缺陷,提交到C6的時候,又冒出一個解決該問題的新辦法,因而從以前C4的地方又分出一個分支iss91v2,幹到C8的時候,又回到主幹master中提交了C9和C10,再回到iss91v2繼續工做,提交C11,接着,又冒出個不太肯定的想法,從master的最新提交C10處開了個新的分支dumbidea作些試驗

  如今,假定兩件事情:咱們最終決定使用第二個解決方案,即iss91v2中的辦法;另外,咱們把dumbidea分支拿給同事們看了之後,發現它居然是個天才之做。因此接下來,咱們準備拋棄原來的iss91分支(實際上會丟棄C5和C6),直接在主幹中併入另外兩個分支。最終的提交歷史將變成以下圖所示

  請務必牢記這些分支所有都是本地分支,這一點很重要。當你在使用分支及合併的時候,一切都是在你本身的Git倉庫中進行的——徹底不涉及與服務器的交互

 

遠程分支

  遠程分支(remote branch)是對遠程倉庫中的分支的索引。它們是一些沒法移動的本地分支;只有在Git進行網絡交互時纔會更新。遠程分支就像是書籤,提醒着你上次鏈接遠程倉庫時上面各分支的位置

  咱們用(遠程倉庫名)/(分支名)這樣的形式表示遠程分支。好比咱們想看看上次同origin倉庫通信時master分支的樣子,就應該查看origin/master分支。若是你和同伴一塊兒修復某個問題,但他們先推送了一個iss53分支到遠程倉庫,雖然你可能也有一個本地的iss53分支,但指向服務器上最新更新的卻應該是origin/iss53分支

  可能有點亂,咱們不妨舉例說明。假設大家團隊有個地址爲git.ourcompany.com的Git服務器。若是你從這裏克隆,Git會自動爲你將此遠程倉庫命名爲origin,並下載其中全部的數據,創建一個指向它的master分支的指針,在本地命名爲origin/master,但你沒法在本地更改其數據。接着,Git創建一個屬於你本身的本地master分支,始於origin上master分支相同的位置,你能夠就此開始工做:

  若是你在本地master分支作了些改動,與此同時,其餘人向git.ourcompany.com推送了他們的更新,那麼服務器上的master分支就會向前推動,而與此同時,你在本地的提交歷史正朝向不一樣方向發展。不過只要你不和服務器通信,你的origin/master指針仍然保持原位不會移動

  能夠運行git fetch origin來同步遠程服務器上的數據到本地。該命令首先找到origin是哪一個服務器(本例爲git.ourcompany.com),從上面獲取你還沒有擁有的數據,更新你本地的數據庫,而後把origin/master的指針移到它最新的位置上

  爲了演示擁有多個遠程分支(在不一樣的遠程服務器上)的項目是如何工做的,咱們假設你還有另外一個僅供你的敏捷開發小組使用的內部服務器git.team1.ourcompany.com。能夠用git remote add命令把它加爲當前項目的遠程分支之一。咱們把它命名爲teamone,以便代替完整的Git URL以方便使用

  如今你能夠用git fetch teamone來獲取小組服務器上你尚未的數據了。因爲當前該服務器上的內容是你origin服務器上的子集,Git不會下載任何數據,而只是簡單地建立一個名爲teamone/master的遠程分支,指向teamone服務器上master分支所在的提交對象31b8e

【推送本地分支】

  要想和其餘人分享某個本地分支,你須要把它推送到一個你擁有寫權限的遠程倉庫。你建立的本地分支不會由於你的寫入操做而被自動同步到你引入的遠程服務器上,你須要明確地執行推送分支的操做。換句話說,對於無心分享的分支,你儘管保留爲私人分支好了,而只推送那些協同工做要用到的特性分支

  若是你有個叫serverfix的分支須要和他人一塊兒開發,能夠運行git push (遠程倉庫名) (分支名):

$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new branch]      serverfix -> serverfix

  這裏其實走了一點捷徑。Git自動把serverfix分支名擴展爲refs/heads/serverfix:refs/heads/serverfix,意爲「取出我在本地的serverfix分支,推送到遠程倉庫的serverfix分支中去」

  不過通常使用的時候均可以省略它。也能夠運行git push origin serverfix:serverfix來實現相同的效果,它的意思是「上傳我本地的serverfix分支到遠程倉庫中去,仍舊稱它爲serverfix分支」。經過此語法,你能夠把本地分支推送到某個命名不一樣的遠程分支:若想把遠程分支叫做awesomebranch,能夠用git push origin serverfix:awesomebranch來推送數據

  接下來,當你的協做者再次從服務器上獲取數據時,他們將獲得一個新的遠程分支origin/serverfix,並指向服務器上serverfix所指向的版本:

$ git fetch origin
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
From git@github.com:schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

  值得注意的是,在fetch操做下載好新的遠程分支以後,你仍然沒法在本地編輯該遠程倉庫中的分支。換句話說,在本例中,你不會有一個新的serverfix分支,有的只是一個你沒法移動的origin/serverfix指針

  若是要把該遠程分支的內容合併到當前分支,能夠運行git merge origin/serverfix。若是想要一份本身的serverfix來開發,能夠在遠程分支的基礎上分化出一個新的分支來:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

  這會切換到新建的serverfix本地分支,其內容同遠程分支origin/serverfix一致,這樣就能夠在裏面繼續開發了

【跟蹤遠程分支】

  從遠程分支checkout出來的本地分支,稱爲跟蹤分支(tracking branch)。跟蹤分支是一種和某個遠程分支有直接聯繫的本地分支。在跟蹤分支裏輸入git push,Git會自行推斷應該向哪一個服務器的哪一個分支推送數據。一樣,在這些分支裏運行git pull會獲取全部遠程索引,並把它們的數據都合併到本地分支中來。

  在克隆倉庫時,Git一般會自動建立一個名爲master的分支來跟蹤origin/master。這正是git push和git pull一開始就能正常工做的緣由。固然,你能夠爲所欲爲地設定爲其它跟蹤分支,好比origin上除了master以外的其它分支。剛纔咱們已經看到了這樣的一個例子:git checkout -b [分支名] [遠程名]/[分支名]。若是你有1.6.2以上版本的 Git,還能夠用--track選項簡化:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

  要爲本地分支設定不一樣於遠程分支的名字,只需在第一個版本的命令裏換個名字:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

  如今你的本地分支sf會自動將推送和抓取數據的位置定位到origin/serverfix了

【刪除遠程分支】

  若是再也不須要某個遠程分支了,好比搞定了某個特性並把它合併進了遠程的master分支(或任何其餘存放穩定代碼的分支),能夠用這個很是無厘頭的語法來刪除它:git push [遠程名] :[分支名]。若是想在服務器上刪除serverfix分支,運行下面的命令

$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
 - [deleted]         serverfix

  服務器上的分支沒了。有種方便記憶這條命令的方法:記住咱們不久前見過的 git push [遠程名] [本地分支]:[遠程分支] 語法,若是省略 [本地分支],那就等因而在說「在這裏提取空白而後把它變成[遠程分支]」

 

分支衍合

  把一個分支中的修改整合到另外一個分支的辦法有兩種:merge 和 rebase(rebase的翻譯爲「衍合」或變基)

【基本的衍合操做】

  在分支合併一節中,開發進程分叉到兩個不一樣分支,又各自提交了更新

  最容易的整合分支的方法是merge命令,它會把兩個分支最新的快照(C3和C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)

  其實,還有另一個選擇:你能夠把在C3裏產生的變化補丁在C4的基礎上從新打一遍。在Git裏,這種操做叫作衍合(rebase)。有了rebase命令,就能夠把在一個分支裏提交的改變移到另外一個分支裏重放一遍

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

  它的原理是回到兩個分支最近的共同祖先,根據當前分支(也就是要進行衍合的分支experiment)後續的歷次提交對象(這裏只有一個C3),生成一系列文件補丁,而後以基底分支(也就是主幹分支master)最後一個提交對象(C4)爲新的出發點,逐個應用以前準備好的補丁文件,最後會生成一個新的合併提交對象(C3'),從而改寫experiment的提交歷史,使它成爲master分支的直接下游

  如今回到master分支,進行一次快進合併

  如今的C3'對應的快照,其實和普通的三方合併,即上個例子中的C5對應的快照內容如出一轍了。雖然最後整合獲得的結果沒有任何區別,但衍合能產生一個更爲整潔的提交歷史。若是視察一個衍合過的分支的歷史記錄,看起來會更清楚:彷彿全部修改都是在一根線上前後進行的,儘管實際上它們本來是同時並行發生的

  通常咱們使用衍合的目的,是想要獲得一個能在遠程分支上乾淨應用的補丁——好比某些項目你不是維護者,但想幫點忙的話,最好用衍合:先在本身的一個分支裏進行開發,當準備向主項目提交補丁的時候,根據最新的origin/master進行一次衍合操做而後再提交,這樣維護者就不須要作任何整合工做(其實是把解決分支補丁同最新主幹代碼之間衝突的責任,化轉爲由提交補丁的人來解決),只需根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的補丁

  [注意]合併結果中最後一次提交所指向的快照,不管是經過衍合,仍是三方合併,都會獲得相同的快照內容,只不過提交歷史不一樣罷了。衍合是按照每行的修改次序重演一遍修改,而合併是把最終結果合在一塊兒

【有趣的衍合】

  衍合也能夠放到其餘分支進行,並不必定非得根據分化以前的分支。如下圖爲例,咱們爲了給服務器端代碼添加一些功能而建立了特性分支server,而後提交C3和C4。而後又從C3的地方再增長一個client分支來對客戶端代碼進行一些相應修改,因此提交了C8和C9。最後,又回到server分支提交了C10

  假設在接下來的一次軟件發佈中,咱們決定先把客戶端的修改併到主線中,而暫緩併入服務端軟件的修改(由於還須要進一步測試)。這個時候,咱們就能夠把基於client分支而非server分支的改變(即C8和C9),跳過server直接放到master分支中重演一遍,但這須要用git rebase的--onto選項指定新的基底分支master

$ git rebase --onto master server client

  這比如在說:「取出client分支,找出client分支和server分支的共同祖先以後的變化,而後把它們在master上重演一遍」

  雖然client裏的C8,C9在C3以後,但這僅代表時間上的前後,而非在C3修改的基礎上進一步改動,由於server和client這兩個分支對應的代碼應該是兩套文件,雖然這麼說不是很嚴格,但應理解爲在C3時間點以後,對另外的文件所作的C8,C9修改,放到主幹重演

  如今能夠快進master分支了

$ git checkout master
$ git merge client

  如今咱們決定把server分支的變化也包含進來。咱們能夠直接把server分支衍合到master,而不用手工切換到server分支後再執行衍合操做——git rebase [主分支] [特性分支]命令會先取出特性分支server,而後在主分支master上重演

$ git rebase master server

  因而,server的進度應用到master的基礎上,以下圖所示

  而後就能夠快進主幹分支master了

$ git checkout master
$ git merge server

  如今client和server分支的變化都已經集成到主幹分支來了,能夠刪掉它們了。最終咱們的提交歷史以下圖所示

【衍合的風險】

  要用衍合得遵照一條準則:一旦分支中的提交對象發佈到公共倉庫,就千萬不要對該分支進行衍合操做

  在進行衍合的時候,實際上拋棄了一些現存的提交對象而創造了一些相似但不一樣的新的提交對象。若是你把原來分支中的提交對象發佈出去,而且其餘人更新下載後在其基礎上開展工做,而稍後你又用git rebase拋棄這些提交對象,把新的重演後的提交對象發佈出去的話,你的合做者就不得不從新合併他們的工做,這樣當你再次從他們那裏獲取內容時,提交歷史就會變得一團糟

  下面咱們用一個實際例子來講明爲何公開的衍合會帶來問題。假設你從一箇中央服務器克隆而後在它的基礎上搞了一些開發,提交歷史以下圖所示

  如今,某人在C1的基礎上作了些改變,併合並他本身的分支獲得結果C6,推送到中央服務器。當你抓取併合並這些數據到你本地的開發分支中後,會獲得合併結果C7

  接下來,那個推送C6上來的人決定用衍合取代以前的合併操做;繼而又用git push --force覆蓋了服務器上的歷史,獲得C4'。而以後當你再從服務器上下載最新提交後,會獲得

  下載更新後須要合併,但此時衍合產生的提交對象C4'的SHA-1校驗值和以前C4徹底不一樣,因此Git會把它們看成新的提交對象處理,而實際上此刻你的提交歷史C7中早已經包含了C4的修改內容,因而合併操做會把C7和C4'合併爲C8

  C8這一步的合併是早晚會發生的,由於只有這樣你才能和其餘協做者提交的內容保持同步。而在C8以後,你的提交歷史裏就會同時包含C4和C4',二者有着不一樣的SHA-1校驗值,若是用git log查看歷史,會看到兩個提交擁有相同的做者日期與說明,使人費解。而更糟的是,當你把這樣的歷史推送到服務器後,會再次把這些衍合後的提交引入到中央服務器,進一步困擾其餘人

  這個例子中,出問題的責任方是那個發佈了C6後又用衍合發布C4'的人,其餘人會所以反饋雙重歷史到共享主幹,從而混淆你們的視聽

  若是把衍合當成一種在推送以前清理提交歷史的手段,並且僅僅衍合那些還沒有公開的提交對象,就沒問題。若是衍合那些已經公開的提交對象,而且已經有人基於這些提交對象開展了後續開發工做的話,就會出現叫人沮喪的麻煩

 

保存現場

  軟件開發中,bug就像屢見不鮮同樣。有了bug就須要修復,在Git中,因爲分支是如此的強大,因此,每一個bug均可以經過一個新的臨時分支來修復,修復後,合併分支,而後將臨時分支刪除。

  當你接到一個修復一個代號101的bug的任務時,很天然地,你想建立一個分支issue-101來修復它,可是,等等,當前正在dev上進行的工做尚未提交:

$ git status
# On branch dev
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file:   hello.py
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   readme.txt
#

  並非不想提交,而是工做只進行到一半,還無法提交,預計完成還需1天時間。可是,必須在兩個小時內修復該bug,怎麼辦?

  幸虧,Git還提供了一個stash功能,能夠把當前工做現場「儲藏」起來,等之後恢復現場後繼續工做

$ git stash
Saved working directory and index state WIP on dev: 6224937 add merge
HEAD is now at 6224937 add merge

  如今,用git status查看工做區,就是乾淨的(除非有沒有被Git管理的文件),所以能夠放心地建立分支來修復bug。

  首先肯定要在哪一個分支上修復bug,假定須要在master分支上修復,就從master建立臨時分支

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

  如今修復bug,須要把「Git is free software ...」改成「Git is a free software ...」,而後提交

$ git add readme.txt 
$ git commit -m "fix bug 101"
[issue-101 cc17032] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)

  修復完成後,切換到master分支,並完成合並,最後刪除issue-101分支

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 2 commits.
$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
 readme.txt |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git branch -d issue-101
Deleted branch issue-101 (was cc17032).

  如今,是時候接着回到dev分支

$ git checkout dev
Switched to branch 'dev'
$ git status
# On branch dev
nothing to commit (working directory clean)

  工做區是乾淨的,剛纔的工做現場存到哪去了?用git stash list命令看看:

$ git stash list
stash@{0}: WIP on dev: 6224937 add merge

  一是用git stash apply恢復,可是恢復後,stash內容並不刪除,你須要用git stash drop來刪除;

  另外一種方式是用git stash pop,恢復的同時把stash內容也刪了

$ git stash pop
# On branch dev
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file:   hello.py
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   readme.txt
#
Dropped refs/stash@{0} (f624f8e5f082f2df2bed8a4e09c12fd2943bdd40)

  再用git stash list查看,就看不到任何stash內容了

$ git stash list

  能夠屢次stash,恢復的時候,先用git stash list查看,而後恢復指定的stash,用命令

$ git stash apply stash@{0}

  總結一下,修復bug時,咱們會經過建立新的bug分支進行修復,而後合併,最後刪除;當手頭工做沒有完成時,先把工做現場git stash一下,而後去修復bug,修復後,再git stash pop,回到工做現場

相關文章
相關標籤/搜索