Git詳解之三 Git分支(第二部分)

3.3  分支的管理

到目前爲止,你已經學會了如何建立、合併和刪除分支。除此以外,咱們還須要學習如何管理分支,在往後的常規工做中會常常用到下面介紹的管理命令。javascript

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

$ git branch 
iss53 * master testing

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

$ 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

$git branch --merged iss53 * master

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

另外能夠用 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 強制執行,就像上面提示信息中給出的那樣。網絡

3.4  利用分支進行開發的工做流程

如今咱們已經學會了新建分支和合並分支,能夠(或應該)用它來作點什麼呢?在本節,咱們會介紹一些利用分支進行開發的工做流程。而正是因爲分支管理的便捷,才衍生出了這類典型的工做模式,你能夠根據項目的實際狀況選擇一種用用看。ide

長期分支學習

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

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

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

圖 3-18. 穩定分支老是比較老舊。

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

圖 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 作些試驗。


圖 3-20. 擁有多個特性分支的提交歷史。

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

圖 3-21. 合併了 dumbidea 和 iss91v2 後的分支歷史。

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

3.5  遠程分支

遠程分支(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):


圖 3-22. 一次 Git 克隆會創建你本身的本地分支 master 和遠程分支 origin/master,它們都指向 origin/master 分支的最後一次提交。

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

圖 3-23. 在本地工做的同時有人向遠程倉庫推送內容會讓提交歷史開始分流。

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

圖 3-24. git fetch 命令會更新 remote 索引。

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

圖 3-25. 把另外一個服務器加爲遠程倉庫

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

圖 3-26. 你在本地有了一個指向 teamone 服務器上 master 分支的索引。

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

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

$ git push origin serverfix 
Counting objects: 20, done. Compressing objects: 100% (14/14), done. Writing objects: 100% (15/1
這其實有點像條捷徑。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 (de值得注意的是,在 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 newbranch "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 newbranch "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 [遠程名] [本地分支]:[遠程分支] 語法,若是省略[本地分支],那就等因而在說「在這裏提取空白而後把它變成[遠程分支]」。

3.6  分支的衍合

把一個分支整合到另外一個分支的辦法有兩種:merge 和 rebase(譯註:rebase 的翻譯暫定爲「衍合」,你們知道就能夠了。)。在本章咱們會學習什麼是衍合,如何使用衍合,爲何衍合操做如此富有魅力,以及咱們應該在什麼狀況下使用衍合。

基本的衍合操做

請回顧以前有關合並的一節(見圖 3-27),你會看到開發進程分叉到兩個不一樣分支,又各自提交了更新。

圖 3-27. 最初分叉的提交歷史。

以前介紹過,最容易的整合分支的方法是 merge 命令,它會把兩個分支最新的快照(C3 和 C4)以及兩者最新的共同祖先(C2)進行三方合併,合併的結果是產生一個新的提交對象(C5)。如圖 3-28 所示:

圖 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 stag

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

圖 3-29. 把 C3 裏產生的改變到 C4 上重演一遍。

如今回到 master 分支,進行一次快進合併(見圖 3-30):

圖 3-30. master 分支的快進。

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

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

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


有趣的衍合

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

了特性分支 server,而後提交 C3 和 C4。而後又從 C3 的地方再增長一個client 分支來對客戶端代碼進行一些相應修改,因此提交了 C8 和 C9。最後,又回到 server 分支提交了 C10。

圖 3-31. 從一個特性分支裏再分出一個特性分支的歷史。

假設在接下來的一次軟件發佈中,咱們決定先把客戶端的修改併到主線中,而暫緩併入服務端軟件的修改(由於還須要進一步測試)。這個時候,咱們就能夠把基於 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 修改,放到主幹重演。):

圖 3-32. 將特性分支上的另外一個特性分支衍合到其餘分支。

如今能夠快進 master 分支了(見圖 3-33):

$ git checkout master 
$ git merge client

圖 3-33. 快進 master 分支,使之包含 client 分支的變化。

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

$ git rebase master server

因而,server 的進度應用到 master 的基礎上,如圖 3-34 所示:

因而,server 的進度應用到 master 的基礎上,如圖 3-34 所示:


圖 3-34. 在 master 分支上衍合 server 分支。

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

$ git checkout master 
$ git merge server

如今 client 和 server 分支的變化都已經集成到主幹分支來了,能夠刪掉它們了。最終咱們的提交歷史會變成圖 3-35 的樣子:

$ git branch -d client $ git branch -d server

圖 3-35. 最終的提交歷史

衍合的風險

呃,奇妙的衍合也並不是天衣無縫,要用它得遵照一條準則:

一旦分支中的提交對象發佈到公共倉庫,就千萬不要對該分支進行衍合操做。

若是你遵循這條金科玉律,就不會出差錯。不然,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。

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

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


圖 3-36. 克隆一個倉庫,在其基礎上工做一番。

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

圖 3-37. 抓取他人提交,併入本身主幹。

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

圖 3-38. 有人推送了衍合後獲得的 C4’,丟棄了你做爲開發基礎的 C4 和 C6。

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

圖 3-39. 你把相同的內容又合併了一遍,生成一個新的提交 C8。

C8 這一步的合併是早晚會發生的,由於只有這樣你才能和其餘協做者提交的內容保持同步。而在 C8 以後,你的提交歷史裏就會同時包含 C4 和 C4’,二者有着不一樣的 SHA-1 校驗值,若是用git log 查看歷史,會看到兩個提交擁有相同的做者日期與說明,使人費解。而更糟的是,當你把這樣的歷史推送到服務器後,會再次把這些衍合後的提交引入到中央服務 器,進一步困擾其餘人(譯註:這個例子中,出問題的責任方是那個發佈了 C6 後又用衍合發布 C4’ 的人,其餘人會所以反饋雙重歷史到共享主幹,從而混淆你們的視聽。)。

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

3.7  小結

讀到這裏,你應該已經學會了如何建立分支並切換到新分支,在不一樣分支間轉換,合併本地分支,把分支推送到共享服務器上,使用共享分支與他人協做,以及在分享以前進行衍合。

相關文章
相關標籤/搜索