幾乎每一種版本控制系統都以某種形式支持分支。使用分支意味着你能夠從開發主線上分離開來,而後在不影響主線的同時繼續工做。在不少版本控制系統中,這是個昂貴的過程,經常須要建立一個源代碼目錄的完整副本,對大型項目來講會花費很長時間。javascript
有人把 Git 的分支模型稱爲「必殺技特性」,而正是由於它,將 Git 從版本控制系統家族裏區分出來。Git 有何特別之處呢?Git 的分支可謂是難以置信的輕量級,它的新建操做幾乎能夠在瞬間完成,而且在不一樣分支間切換起來也差很少同樣快。和許多其餘版本控制系統不一樣,Git 鼓勵在工做流程中頻繁使用分支與合併,哪怕一天以內進行許屢次都沒有關係。理解分支的概念並熟練運用後,你纔會意識到爲何 Git 是一個如此強大而獨特的工具,並今後真正改變你的開發方式。html
爲了理解 Git 分支的實現方式,咱們須要回顧一下 Git 是如何儲存數據的。或許你還記得第一章的內容,Git 保存的不是文件差別或者變化量,而只是一系列文件快照。java
在 Git 中提交時,會保存一個提交(commit)對象,該對象包含一個指向暫存內容快照的指針,包含本次提交的做者等相關附屬信息,包含零個或多個指向該提交對 象的父對象指針:首次提交是沒有直接祖先的,普通提交有一個祖先,由兩個或多個分支合併產生的提交則有多個祖先。git
爲直觀起見,咱們假設在工做目錄中有三個文件,準備將它們暫存後提交。暫存操做會對每個文件計算校驗和(即第一章中提到的 SHA-1 哈希字串),而後把當前版本的文件快照保存到 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 對象。概念上來講,倉庫中的各個對象保存的數據和相互關係看起來如圖 3-1 所示:vim
做些修改後再次提交,那麼此次的提交對象會包含一個指向上次提交對象的指針(譯註:即下圖中的 parent 對象)。兩次提交後,倉庫歷史會變成圖 3-2 的樣子:服務器
如今來談分支。Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 做爲分支的默認名字。在若干次提交後,你其實已經有了一個指向最後一次提交對象的 master 分支,它在每次提交的時候都會自動向前移動。網絡
那麼,Git 又是如何建立一個新的分支的呢?答案很簡單,建立一個新的分支指針。好比新建一個 testing 分支,可使用git branch
命令:數據結構
$ git branch testing
這會在當前 commit 對象上新建一個分支指針(見圖 3-4)。
那麼,Git 是如何知道你當前在哪一個分支上工做的呢?其實答案也很簡單,它保存着一個名爲 HEAD 的特別指針。請注意它和你熟知的許多其餘版本控制系統(好比 Subversion 或 CVS)裏的 HEAD 概念大不相同。在 Git 中,它是一個指向你正在工做中的本地分支的指針(譯註:將 HEAD 想象爲當前分支的別名。)。運行git branch
命令,僅僅是創建了一個新的分支,但不會自動切換到這個分支中去,因此在這個例子中,咱們依然還在 master 分支裏工做(參考圖 3-5)。
要切換到其餘分支,能夠執行 git checkout
命令。咱們如今轉換到新建的 testing 分支:
$ git checkout testing
這樣 HEAD 就指向了 testing 分支(見圖3-6)。
這樣的實現方式會給咱們帶來什麼好處呢?好吧,如今不妨再提交一次:
$ vim test.rb $ git commit -a -m 'made a change'
圖 3-7 展現了提交後的結果。
很是有趣,如今 testing 分支向前移動了一格,而 master 分支仍然指向原先 git checkout
時所在的 commit 對象。如今咱們回到 master 分支看看:
$ git checkout master
圖 3-8 顯示告終果。
這條命令作了兩件事。它把 HEAD 指針移回到 master 分支,並把工做目錄中的文件換成了 master 分支所指向的快照內容。也就是說,如今開始所作的改動,將始於本項目中一個較老的版本。它的主要做用是將 testing 分支裏做出的修改暫時取消,這樣你就能夠向另外一個方向進行開發。
咱們做些修改後再次提交:
$ vim test.rb $ git commit -a -m 'made other changes'
如今咱們的項目提交歷史產生了分叉(如圖 3-9 所示),由於剛纔咱們建立了一個分支,轉換到其中進行了一些工做,而後又回到原來的主分支進行了另一些工做。這些改變分別孤立在不一樣的分支裏:咱們能夠 在不一樣分支裏反覆切換,並在時機成熟時把它們合併到一塊兒。而全部這些工做,僅僅須要branch
和 checkout
這兩條命令就能夠完成。
因爲 Git 中的分支實際上僅是一個包含所指對象校驗和(40 個字符長度 SHA-1 字串)的文件,因此建立和銷燬一個分支就變得很是廉價。說白了,新建一個分支就是向一個文件寫入 41 個字節(外加一個換行符)那麼簡單,固然也就很快了。
這和大多數版本控制系統造成了鮮明對比,它們管理分支大多采起備份全部項目文件到特定目錄的方式,因此根據項目文件數量和大小不一樣,可能花費的時間 也會有至關大的差異,快則幾秒,慢則數分鐘。而 Git 的實現與項目複雜度無關,它永遠能夠在幾毫秒的時間內完成分支的建立和切換。同時,由於每次提交時都記錄了祖先信息(譯註:即parent
對象),未來要合併分支時,尋找恰當的合併基礎(譯註:即共同祖先)的工做其實已經天然而然地擺在那裏了,因此實現起來很是容易。Git 鼓勵開發者頻繁使用分支,正是由於有着這些特性做保障。
接下來看看,咱們爲何應該頻繁使用分支。
如今讓咱們來看一個簡單的分支與合併的例子,實際工做中大致也會用到這樣的工做流程:
1. 開發某個網站。 2. 爲實現某個新的需求,建立一個分支。 3. 在這個分支上開展工做。
假設此時,你忽然接到一個電話說有個很嚴重的問題須要緊急修補,那麼能夠按照下面的方式處理:
1. 返回到原先已經發布到生產服務器上的分支。 2. 爲此次緊急修補創建一個新分支,並在其中修復問題。 3. 經過測試後,回到生產服務器所在的分支,將修補分支合併進來,而後再推送到生產服務器上。 4. 切換到以前實現新需求的分支,繼續工做。
首先,咱們假設你正在項目中愉快地工做,而且已經提交了幾回更新(見圖 3-10)。
如今,你決定要修補問題追蹤系統上的 #53 問題。順帶說明下,Git 並不一樣任何特定的問題追蹤系統打交道。這裏爲了說明要解決的問題,才把新建的分支取名爲 iss53。要新建並切換到該分支,運行git checkout
並加上 -b
參數:
$ git checkout -b iss53 Switched to a new branch "iss53"
這至關於執行下面這兩條命令:
$ git branch iss53 $ git checkout iss53
圖 3-11 示意該命令的執行結果。
接着你開始嘗試修復問題,在提交了若干次更新後,iss53
分支的指針也會隨着向前推動,由於它就是當前分支(換句話說,當前的 HEAD
指針正指向 iss53
,見圖 3-12):
$ vim index.html $ git commit -a -m 'added a new footer [issue 53]'
如今你就接到了那個網站問題的緊急電話,須要立刻修補。有了 Git ,咱們就不須要同時發佈這個補丁和 iss53
裏做出的修改,也不須要在建立和發佈該補丁到服務器以前花費大力氣來複原這些修改。惟一須要的僅僅是切換回master
分支。
不過在此以前,留心你的暫存區或者工做目錄裏,那些尚未提交的修改,它會和你即將檢出的分支產生衝突從而阻止 Git 爲你切換分支。切換分支的時候最好保持一個清潔的工做區域。稍後會介紹幾個繞過這種問題的辦法(分別叫作 stashing 和 commit amending)。目前已經提交了全部的修改,因此接下來能夠正常轉換到master
分支:
$ git checkout master Switched to branch "master"
此時工做目錄中的內容和你在解決問題 #53 以前如出一轍,你能夠集中精力進行緊急修補。這一點值得牢記:Git 會把工做目錄的內容恢復爲檢出某分支時它所指向的那個提交對象的快照。它會自動添加、刪除和修改文件以確保目錄的內容和你當時提交時徹底同樣。
接下來,你得進行緊急修補。咱們建立一個緊急修補分支 hotfix
來開展工做,直到搞定(見圖 3-13):
$ git checkout -b 'hotfix' Switched to a new branch "hotfix" $ vim index.html $ git commit -a -m 'fixed the broken email address' [hotfix]: created 3a0874c: "fixed the broken email address" 1 files changed, 0 insertions(+), 1 deletions(-)
有必要做些測試,確保修補是成功的,而後回到 master
分支並把它合併進來,而後發佈到生產服務器。用 git merge
命令來進行合併:
$ git checkout master $ git merge hotfix Updating f42c576..3a0874c Fast forward README | 1 - 1 files changed, 0 insertions(+), 1 deletions(-)
請注意,合併時出現了「Fast forward」的提示。因爲當前 master
分支所在的提交對象是要併入的 hotfix
分支的直接上游,Git 只需把master
分支指針直接右移。換句話說,若是順着一個分支走下去能夠到達另外一個分支的話,那麼 Git 在合併二者時,只會簡單地把指針右移,由於這種單線的歷史分支不存在任何須要解決的分歧,因此這種合併過程能夠稱爲快進(Fast forward)。
如今最新的修改已經在當前 master
分支所指向的提交對象中了,能夠部署到生產服務器上去了(見圖 3-14)。
在那個超級重要的修補發佈之後,你想要回到被打擾以前的工做。因爲當前 hotfix
分支和 master
都指向相同的提交對象,因此hotfix
已經完成了歷史使命,能夠刪掉了。使用 git branch
的 -d
選項執行刪除操做:
$ git branch -d hotfix Deleted branch hotfix (3a0874c).
如今回到以前未完成的 #53 問題修復分支上繼續工做(圖 3-15):
$ git checkout iss53 Switched to branch "iss53" $ vim index.html $ git commit -a -m 'finished the new footer [issue 53]' [iss53]: created ad82d7a: "finished the new footer [issue 53]" 1 files changed, 1 insertions(+), 0 deletions(-)
不用擔憂以前 hotfix
分支的修改內容還沒有包含到 iss53
中來。若是確實須要歸入這次修補,能夠用git merge master
把 master 分支合併到 iss53
;或者等 iss53
完成以後,再將iss53
分支中的更新併入 master
。
在問題 #53 相關的工做完成以後,能夠合併回 master
分支。實際操做同前面合併 hotfix
分支差很少,只需回到master
分支,運行 git merge
命令指定要合併進來的分支:
$ git checkout master $ git merge iss53 Merge made by recursive. README | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
請注意,此次合併操做的底層實現,並不一樣於以前 hotfix
的併入方式。由於此次你的開發歷史是從更早的地方開始分叉的。因爲當前 master
分支所指向的提交對象(C4)並非 iss53
分支的直接祖先,Git 不得不進行一些額外處理。就此例而言,Git 會用兩個分支的末端(C4 和 C5)以及它們的共同祖先(C2)進行一次簡單的三方合併計算。圖 3-16 用紅框標出了 Git 用於合併的三個提交對象:
此次,Git 沒有簡單地把分支指針右移,而是對三方合併後的結果從新作一個新的快照,並自動建立一個指向它的提交對象(C6)(見圖 3-17)。這個提交對象比較特殊,它有兩個祖先(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
查閱:
[master*]$ git status index.html: needs merge # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # (use "git checkout -- ..." to discard changes in working directory) # # unmerged: index.html #
任何包含未解決衝突的文件都會以未合併(unmerged)的狀態列出。Git 會在有衝突的文件里加入標準的衝突解決標記,能夠經過它們來手工定位並解決這些衝突。能夠看到此文件包含相似下面這樣的部分:
contact : email.support@github.com=======please contact us at support@github.com>>>>>>> iss53:index.html<<<<<<< HEAD:index.html
能夠看到 =======
隔開的上半部分,是 HEAD
(即 master
分支,在運行merge
命令時所切換到的分支)中的內容,下半部分是在 iss53
分支中的內容。解決衝突的辦法無非是兩者選其一或者由你親自整合到一塊兒。好比你能夠經過把這段內容替換爲下面這樣來解決:
please contact us at email.support@github.com
這個解決方案各採納了兩個分支中的一部份內容,並且我還刪除了 <<<<<<<
,=======
和 >>>>>>>
這些行。在解決了全部文件裏的全部衝突後,運行 git add
將把它們標記爲已解決狀態(譯註:實際上就是來一次快照保存到暫存區域。)。由於一旦暫存,就表示衝突已經解決。若是你想用一個有圖形界面的工具來解決這些問題,不妨運行git mergetool
,它會調用一個可視化的合併工具並引導你解決全部衝突:
$ git mergetool merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff Merging the files: index.html Normal merge conflict for 'index.html': {local}: modified {remote}: modified Hit return to start merge resolution tool (opendiff):
若是不想用默認的合併工具(Git 爲我默認選擇了 opendiff
,由於我在 Mac 上運行了該命令),你能夠在上方」merge tool candidates」裏找到可用的合併工具列表,輸入你想用的工具名。咱們將在第七章討論怎樣改變環境中的默認值。
退出合併工具之後,Git 會詢問你合併是否成功。若是回答是,它會爲你把相關文件暫存起來,以代表狀態爲已解決。
再運行一次 git status
來確認全部衝突都已解決:
$ git status # On branch master # Changes to be committed: # (use "git reset HEAD ..." 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
要從該清單中篩選出你已經(或還沒有)與當前分支合併的分支,能夠用 --merge
和 --no-merged
選項(Git 1.5.6 以上版本)。好比用git branch --merge
查看哪些分支已被併入當前分支(譯註:也就是說哪些分支是當前分支的直接上游。):
$ 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 an ancestor of your current HEAD. If you are sure you want to delete it, run 'git branch -D testing'.
不過,若是你確實想要刪除該分支上的改動,能夠用大寫的刪除選項 -D
強制執行,就像上面提示信息中給出的那樣。
如今咱們已經學會了新建分支和合並分支,能夠(或應該)用它來作點什麼呢?在本節,咱們會介紹一些利用分支進行開發的工做流程。而正是因爲分支管理的便捷,才衍生出了這類典型的工做模式,你能夠根據項目的實際狀況選擇一種用用看。
因爲 Git 使用簡單的三方合併,因此就算在較長一段時間內,反覆屢次把某個分支合併到另外一分支,也不是什麼難事。也就是說,你能夠同時擁有多個開放的分支,每一個分支用於完成特定的任務,隨着開發的推動,你能夠隨時把某個特性分支的成果併到其餘分支中。
許多使用 Git 的開發者都喜歡用這種方式來開展工做,好比僅在 master
分支中保留徹底穩定的代碼,即已經發布或即將發佈的代碼。與此同時,他們還有一個名爲develop
或 next
的平行分支,專門用於後續的開發,或僅用於穩定性測試 — 固然並非說必定要絕對穩定,不過一旦進入某種穩定狀態,即可以把它合併到master
裏。這樣,在確保這些已完成的特性分支(短時間分支,好比以前的 iss53
分支)可以經過全部測試,而且不會引入更多錯誤以後,就能夠併到主幹分支中,等待下一次的發佈。
本質上咱們剛纔談論的,是隨着提交對象不斷右移的指針。穩定分支的指針老是在提交歷史中落後一大截,而前沿分支老是比較靠前(見圖 3-18)。
或者把它們想象成工做流水線,或許更好理解一些,通過測試的提交對象集合被遴選到更穩定的流水線(見圖 3-19)。
你能夠用這招維護不一樣層次的穩定性。某些大項目還會有個 proposed
(建議)或 pu
(proposed updates,建議更新)分支,它包含着那些可能尚未成熟到進入next
或 master
的內容。這麼作的目的是擁有不一樣層次的穩定性:當這些分支進入到更穩定的水平時,再把它們合併到更高層分支中去。再次說明下,使用多個長期分支的作法並不是必需,不過通常來講,對於特大型項目或特複雜的項目,這麼作確實更容易管理。
在任何規模的項目中均可以使用特性(Topic)分支。一個特性分支是指一個短時間的,用來實現單一特性或與其相關工做的分支。可能你在之前的版本控 制系統裏從未作過相似這樣的事情,由於一般建立與合併分支消耗太大。然而在 Git 中,一天以內創建、使用、合併再刪除多個分支是常見的事。
咱們在上節的例子裏已經見過這種用法了。咱們建立了 iss53
和 hotfix
這兩個特性分支,在提交了若干更新後,把它們合併到主幹分支,而後刪除。該技術容許你迅速且徹底的進行語境切換 — 由於你的工做分散在不一樣的流水線裏,每一個分支裏的改變都和它的目標特性相關,瀏覽代碼之類的事情於是變得更簡單了。你能夠把做出的改變保持在特性分支中幾 分鐘,幾天甚至幾個月,等它們成熟之後再合併,而不用在意它們創建的順序或者進度。
如今咱們來看一個實際的例子。請看圖 3-20,由下往上,起先咱們在 master
工做到 C1,而後開始一個新分支 iss91
嘗試修復 91 號缺陷,提交到 C6 的時候,又冒出一個解決該問題的新辦法,因而從以前 C4 的地方又分出一個分支iss91v2
,幹到 C8 的時候,又回到主幹 master
中提交了 C9 和 C10,再回到 iss91v2
繼續工做,提交 C11,接着,又冒出個不太肯定的想法,從 master
的最新提交 C10 處開了個新的分支dumbidea
作些試驗。
如今,假定兩件事情:咱們最終決定使用第二個解決方案,即 iss91v2
中的辦法;另外,咱們把 dumbidea
分支拿給同事們看了之後,發現它居然是個天才之做。因此接下來,咱們準備拋棄原來的iss91
分支(實際上會丟棄 C5 和 C6),直接在主幹中併入另外兩個分支。最終的提交歷史將變成圖 3-21 這樣:
請務必牢記這些分支所有都是本地分支,這一點很重要。當你在使用分支及合併的時候,一切都是在你本身的 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
分支相同的位置,你能夠就此開始工做(見圖 3-22):
若是你在本地 master
分支作了些改動,與此同時,其餘人向 git.ourcompany.com
推送了他們的更新,那麼服務器上的master
分支就會向前推動,而於此同時,你在本地的提交歷史正朝向不一樣方向發展。不過只要你不和服務器通信,你的 origin/master
指針仍然保持原位不會移動(見圖 3-23)。
能夠運行 git fetch origin
來同步遠程服務器上的數據到本地。該命令首先找到 origin
是哪一個服務器(本例爲git.ourcompany.com
),從上面獲取你還沒有擁有的數據,更新你本地的數據庫,而後把 origin/master
的指針移到它最新的位置上(見圖 3-24)。
爲了演示擁有多個遠程分支(在不一樣的遠程服務器上)的項目是如何工做的,咱們假設你還有另外一個僅供你的敏捷開發小組使用的內部服務器 git.team1.ourcompany.com
。能夠用第二章中提到的git remote add
命令把它加爲當前項目的遠程分支之一。咱們把它命名爲 teamone
,以便代替原始的 Git 地址(見圖 3-25)。
如今你能夠用 git fetch teamone
來獲取小組服務器上你尚未的數據了。因爲當前該服務器上的內容是你 origin
服務器上的子集,Git 不會下載任何數據,而只是簡單地建立一個名爲teamone/master
的分支,指向 teamone
服務器上 master
分支所在的提交對象31b8e
(見圖 3-26)。
要想和其餘人分享某個本地分支,你須要把它推送到一個你擁有寫權限的遠程倉庫。你的本地分支不會被自動同步到你引入的遠程服務器上,除非你明確執行推送操做。換句話說,對於無心分享的分支,你儘管保留爲私人分支好了,而只推送那些協同工做要用到的特性分支。
若是你有個叫 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 分支中去」。咱們將在第九章進一步介紹refs/heads/
部分的細節,不過通常使用的時候均可以省略它。也能夠運行 git push origin serverfix:serferfix
來實現相同的效果,它的意思是「上傳我本地的 serverfix 分支到遠程倉庫中去,仍舊稱它爲 serverfix 分支」。經過此語法,你能夠把本地分支推送到某個命名不一樣的遠程分支:若想把遠程分支叫做awesomebranch
,能夠用 git push origin serverfix:awesomebranch
來推送數據。
接下來,當你的協做者再次從服務器上獲取數據時,他們將獲得一個新的遠程分支 origin/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 refs/remotes/origin/serverfix. 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 refs/remotes/origin/serverfix. Switched to a new branch "serverfix"
要爲本地分支設定不一樣於遠程分支的名字,只需在前個版本的命令裏換個名字:
$ git checkout -b sf origin/serverfix Branch sf set up to track remote branch refs/remotes/origin/serverfix. 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
的翻譯暫定爲「衍合」,你們知道就能夠了。)。在本章咱們會學習什麼是衍合,如何使用衍合,爲何衍合操做如此富有魅力,以及咱們應該在什麼狀況下使用衍合。
請回顧以前有關合並的一節(見圖 3-27),你會看到開發進程分叉到兩個不一樣分支,又各自提交了更新。
以前介紹過,最容易的整合分支的方法是 merge
命令,它會把兩個分支最新的快照(C3 和 C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)。如圖 3-28 所示:
其實,還有另一個選擇:你能夠把在 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
分支的直接下游,如圖 3-29 所示:
如今回到 master
分支,進行一次快進合併(見圖 3-30):
如今的 C3’ 對應的快照,其實和普通的三方合併,即上個例子中的 C5 對應的快照內容如出一轍了。雖然最後整合獲得的結果沒有任何區別,但衍合能產生一個更爲整潔的提交歷史。若是視察一個衍合過的分支的歷史記錄,看起來會更 清楚:彷彿全部修改都是在一根線上前後進行的,儘管實際上它們本來是同時並行發生的。
通常咱們使用衍合的目的,是想要獲得一個能在遠程分支上乾淨應用的補丁 — 好比某些項目你不是維護者,但想幫點忙的話,最好用衍合:先在本身的一個分支裏進行開發,當準備向主項目提交補丁的時候,根據最新的origin/master
進行一次衍合操做而後再提交,這樣維護者就不須要作任何整合工做(譯註:其實是把解決分支補丁同最新主幹代碼之間衝突的責任,化轉爲由提交補丁的人來解決。),只需根據你提供的倉庫地址做一次快進合併,或者直接採納你提交的補丁。
請注意,合併結果中最後一次提交所指向的快照,不管是經過衍合,仍是三方合併,都會獲得相同的快照內容,只不過提交歷史不一樣罷了。衍合是按照每行的修改次序重演一遍修改,而合併是把最終結果合在一塊兒。
衍合也能夠放到其餘分支進行,並不必定非得根據分化以前的分支。以圖 3-31 的歷史爲例,咱們爲了給服務器端代碼添加一些功能而建立了特性分支 server
,而後提交 C3 和 C4。而後又從 C3 的地方再增長一個client
分支來對客戶端代碼進行一些相應修改,因此提交了 C8 和 C9。最後,又回到 server
分支提交了 C10。
假設在接下來的一次軟件發佈中,咱們決定先把客戶端的修改併到主線中,而暫緩併入服務端軟件的修改(由於還須要進一步測試)。這個時候,咱們就能夠把基於 server
分支而非 master
分支的改變(即 C8 和 C9),跳過 server
直接放到master
分支中重演一遍,但這須要用 git rebase
的 --onto
選項指定新的基底分支master
:
$ git rebase --onto master server client
這比如在說:「取出 client
分支,找出 client
分支和 server
分支的共同祖先以後的變化,而後把它們在master
上重演一遍」。是否是有點複雜?不過它的結果如圖 3-32 所示,很是酷(譯註:雖然 client
裏的 C8, C9 在 C3 以後,但這僅代表時間上的前後,而非在 C3 修改的基礎上進一步改動,由於server
和 client
這兩個分支對應的代碼應該是兩套文件,雖然這麼說不是很嚴格,但應理解爲在 C3 時間點以後,對另外的文件所作的 C8,C9 修改,放到主幹重演。):
如今能夠快進 master
分支了(見圖 3-33):
$ git checkout master $ git merge client
如今咱們決定把 server
分支的變化也包含進來。咱們能夠直接把 server
分支衍合到 master
,而不用手工切換到 server
分支後再執行衍合操做 — git rebase [主分支] [特性分支]
命令會先取出特性分支server
,而後在主分支 master
上重演:
$ git rebase master server
因而,server
的進度應用到 master
的基礎上,如圖 3-34 所示:
而後就能夠快進主幹分支 master
了:
$ git checkout master $ git merge server
如今 client
和 server
分支的變化都已經集成到主幹分支來了,能夠刪掉它們了。最終咱們的提交歷史會變成圖 3-35 的樣子:
$ git branch -d client $ git branch -d server
呃,奇妙的衍合也並不是天衣無縫,要用它得遵照一條準則:
一旦分支中的提交對象發佈到公共倉庫,就千萬不要對該分支進行衍合操做。
若是你遵循這條金科玉律,就不會出差錯。不然,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。
在進行衍合的時候,實際上拋棄了一些現存的提交對象而創造了一些相似但不一樣的新的提交對象。若是你把原來分支中的提交對象發佈出去,而且其餘人更新下載後在其基礎上開展工做,而稍後你又用git rebase
拋棄這些提交對象,把新的重演後的提交對象發佈出去的話,你的合做者就不得不從新合併他們的工做,這樣當你再次從他們那裏獲取內容時,提交歷史就會變得一團糟。
下面咱們用一個實際例子來講明爲何公開的衍合會帶來問題。假設你從一箇中央服務器克隆而後在它的基礎上搞了一些開發,提交歷史相似圖 3-36 所示:
如今,某人在 C1 的基礎上作了些改變,併合並他本身的分支獲得結果 C6,推送到中央服務器。當你抓取併合並這些數據到你本地的開發分支中後,會獲得合併結果 C7,歷史提交會變成圖 3-37 這樣:
接下來,那個推送 C6 上來的人決定用衍合取代以前的合併操做;繼而又用 git push --force
覆蓋了服務器上的歷史,獲得 C4’。而以後當你再從服務器上下載最新提交後,會獲得:
下載更新後須要合併,但此時衍合產生的提交對象 C4’ 的 SHA-1 校驗值和以前 C4 徹底不一樣,因此 Git 會把它們看成新的提交對象處理,而實際上此刻你的提交歷史 C7 中早已經包含了 C4 的修改內容,因而合併操做會把 C7 和 C4’ 合併爲 C8(見圖 3-39):
C8 這一步的合併是早晚會發生的,由於只有這樣你才能和其餘協做者提交的內容保持同步。而在 C8 以後,你的提交歷史裏就會同時包含 C4 和 C4’,二者有着不一樣的 SHA-1 校驗值,若是用git log
查看歷史,會看到兩個提交擁有相同的做者日期與說明,使人費解。而更糟的是,當你把這樣的歷史推送到服務器後,會再次把這些衍合後的提交引入到中央服務 器,進一步困擾其餘人(譯註:這個例子中,出問題的責任方是那個發佈了 C6 後又用衍合發布 C4’ 的人,其餘人會所以反饋雙重歷史到共享主幹,從而混淆你們的視聽。)。
若是把衍合當成一種在推送以前清理提交歷史的手段,並且僅僅衍合那些還沒有公開的提交對象,就沒問題。若是衍合那些已經公開的提交對象,而且已經有人基於這些提交對象開展了後續開發工做的話,就會出現叫人沮喪的麻煩。
讀到這裏,你應該已經學會了如何建立分支並切換到新分支,在不一樣分支間轉換,合併本地分支,把分支推送到共享服務器上,使用共享分支與他人協做,以及在分享以前進行衍合。