前言
上篇我就Git協定講了兩大點,Git的起步,Git的基礎操作,其中詳細的小夥伴可以先去閱讀我上一篇文章
https://my.oschina.net/qiangdada/blog/800093(一入前端深似海,從此紅塵是路人系列第十彈之如何合理利用Git進行團隊協作(一))。接下來直接和大家分享有關Git分支以及如何操作遠端倉庫進行團隊協作。
一、遠端倉庫的使用
1、檢視目前的遠端倉庫
要檢視目前配置有哪些遠端倉庫,可以用
git remote
指令,它會列出每個遠端庫的簡短名字。在克隆完某個項目後,至少可以看到一個名為 origin 的遠端庫,Git 預設使用這個名字來辨別你所克隆的原始倉庫
# step 1
$ git clone https://github.com/xuqiang521/data-visualization.git
#Cloning into 'data-visualization'...
#remote: Counting objects: 38, done.
#remote: Compressing objects: 100% (29/29), done.
#remote: Total 38 (delta 7), reused 38 (delta 7), pack-reused 0
#Unpacking objects: 100% (38/38), done.
# step 2
$ cd data-visualization
# step 3
$ git remote
#origin
也可以加上
-v
選項(譯注:此為
--verbose
的簡寫,取首字母),顯示對應的克隆位址:
$ git remote -v
#origin https://github.com/xuqiang521/data-visualization.git (fetch)
#origin https://github.com/xuqiang521/data-visualization.git (push)
如果有多個遠端倉庫,此指令也可以将其全部列出。這樣一來,我就可以非常輕松地從這些使用者的倉庫中,拉取他們的送出到本地。
2、添加遠端倉庫
要添加一個新的遠端倉庫,可以指定一個簡單的名字,以便将來引用,運作
git remote add [shortname] [url]
:
# step 1
$ git remote
#origin
# step 2
$ git remote add pb git://github.com/paulboone/ticgit.git
# step 3
$ git remote -v
#origin git://github.com/schacon/ticgit.git
#pb git://github.com/paulboone/ticgit.git
現在可以用字元串
pb
指代對應的倉庫位址了。比如說,要抓取所有 Paul 有的,但本地倉庫沒有的資訊,可以運作
git fetch pb
$ git fetch pb
#remote: Counting objects: 58, done.
#remote: Compressing objects: 100% (41/41), done.
#remote: Total 44 (delta 24), reused 1 (delta 0)
#Unpacking objects: 100% (44/44), done.
#From git://github.com/paulboone/ticgit
#* [new branch] master -> pb/master
#* [new branch] ticgit -> pb/ticgit
現在,Paul 的主幹分支(master)已經完全可以在本地通路了,對應的名字是
pb/master
,你可以将它合并到自己的某個分支,或者切換到這個分支,看看有些什麼有趣的更新。
3、從遠端倉庫抓取資料
正如之前所看到的,可以用下面的指令從遠端倉庫抓取資料到本地:
$ git fetch [remote-name]
此指令會到遠端倉庫中拉取所有你本地倉庫中還沒有的資料。運作完成後,你就可以在本地通路該遠端倉庫中的所有分支,将其中某個分支合并到本地,或者隻是取出某個分支,一探究竟。
如果是克隆了一個倉庫,此指令會自動将遠端倉庫歸于 origin 名下。是以,
git fetch origin
會抓取從你上次克隆以來别人上傳到此遠端倉庫中的所有更新(或是上次 fetch 以來别人送出的更新)。有一點很重要,需要記住,fetch 指令隻是将遠端的資料拉到本地倉庫,并不自動合并到目前工作分支,隻有當你确實準備好了,才能手工合并。
如果設定了某個分支用于跟蹤某個遠端倉庫的分支,可以使用
git pull
指令自動抓取資料下來,然後将遠端分支自動合并到本地倉庫中目前分支。在日常工作中我們經常這麼用,既快且好。實際上,預設情況下
git clone
指令本質上就是自動建立了本地的 master 分支用于跟蹤遠端倉庫中的 master 分支(假設遠端倉庫确實有 master 分支)。是以一般我們運作
git pull
,目的都是要從原始克隆的遠端倉庫中抓取資料後,合并到工作目錄中的目前分支。
4、推送資料到遠端倉庫
項目進行到一個階段,要同别人分享目前的成果,可以将本地倉庫中的資料推送到遠端倉庫。實作這個任務的指令很簡單:
git push [remote-name] [branch-name]
。如果要把本地的 master 分支推送到
origin
伺服器上(再次說明下,克隆操作會自動使用預設的 master 和 origin 名字),可以運作下面的指令:
$ git push origin master
隻有在所克隆的伺服器上有寫權限,或者同一時刻沒有其他人在推資料,這條指令才會如期完成任務。如果在你推資料前,已經有其他人推送了若幹更新,那你的推送操作就會被駁回。你必須先把他們的更新抓取到本地,合并到自己的項目中,然後才可以再次推送。
5、檢視遠端倉庫資訊
我們可以通過指令
git remote show [remote-name]
檢視某個遠端倉庫的詳細資訊,比如要看所克隆的
origin
倉庫,可以運作:
$ git remote show origin
#* remote origin
#Fetch URL: https://github.com/xuqiang521/data-visualization.git
#Push URL: https://github.com/xuqiang521/data-visualization.git
#HEAD branch: master
#Remote branch:
# master tracked
#Local branch configured for 'git pull':
# master merges with remote master
#Local ref configured for 'git push':
# master pushes to master (up to date)
除了對應的克隆位址外,它還給出了許多額外的資訊。它友善地告訴你如果是在 master 分支,就可以用
git pull
指令抓取資料合并到本地。另外還列出了所有處于跟蹤狀态中的遠端分支。
上面的例子非常簡單,而随着使用 Git 的深入,
git remote show
給出的資訊可能會像這樣:
$ git remote show origin
#* remote origin
#URL: [email protected]:defunkt/github.git
#Remote branch merged with 'git pull' while on branch issues
#issues
#Remote branch merged with 'git pull' while on branch master
#master
#New remote branches (next fetch will store in remotes/origin)
#caching
#Stale tracking branches (use 'git remote prune')
#libwalker
#walker2
#Tracked remote branches
#acl
#apiv2
#dashboard2
#issues
#master
#postgres
#Local branch pushed with 'git push'
#master:master
它告訴我們,運作
git push
時預設推送的分支是什麼(譯注:最後兩行)。它還顯示了有哪些遠端分支還沒有同步到本地(譯注:第六行的
caching
分支),哪些已同步到本地的遠端分支在遠端伺服器上已被删除(譯注:
Stale tracking branches
下面的兩個分支),以及運作
git pull
時将自動合并哪些分支(譯注:前四行中列出的
issues
和
master
分支)。
6、遠端倉庫的删除和重命名
在新版 Git 中可以用
git remote rename
指令修改某個遠端倉庫在本地的簡稱,比如想把
pb
改成
paul
,可以這麼運作:
# step 1
$ git remote rename pb paul
# step 2
$ git remote
#origin
#paul
注意,對遠端倉庫的重命名,也會使對應的分支名稱發生變化,原來的
pb/master
分支現在成了
paul/master
。
碰到遠端倉庫伺服器遷移,或者原來的克隆鏡像不再使用,又或者某個參與者不再貢獻代碼,那麼需要移除對應的遠端倉庫,可以運作
git remote rm
指令:
# step 1
$ git remote rm paul
# step 2
$ git remote
#origin
二、Git分支
1、何為分支
為了了解 Git 分支的實作方式,我們需要回顧一下 Git 是如何儲存資料的。看過我上一篇部落格的朋友應該知道,Git 儲存的不是檔案差異或者變化量,而隻是一系列檔案快照。
在 Git 中送出時,會儲存一個送出(commit)對象,該對象包含一個指向暫存内容快照的指針,包含本次送出的作者等相關附屬資訊,包含零個或多個指向該送出對象的父對象指針:首次送出是沒有直接祖先的,普通送出有一個祖先,由兩個或多個分支合并産生的送出則有多個祖先。
為直覺起見,我們假設在工作目錄中有三個檔案,準備将它們暫存後送出。暫存操作會對每一個檔案計算校驗和,然後把目前版本的檔案快照儲存到 Git 倉庫中(Git 使用 blob 類型的對象存儲這些快照),并将校驗和加入暫存區域:
# step 1
$ git add README test.rb LICENSE
# step 2
$ git commit -m 'initial commit of my project'
當使用
git commit
建立一個送出對象前,Git 會先計算每一個子目錄的校驗和,然後在 Git 倉庫中将這些目錄儲存為樹(tree)對象。之後 Git 建立的送出對象,除了包含相關送出資訊以外,還包含着指向這個樹對象(項目根目錄)的指針,如此它就可以在将來需要的時候,重制此次快照的内容了。
現在,Git 倉庫中有五個對象:三個表示檔案快照内容的 blob 對象;一個記錄着目錄樹内容及其中各個檔案對應 blob 對象索引的 tree 對象;以及一個包含指向 tree 對象(根目錄)的索引和其他送出資訊中繼資料的 commit 對象。概念上來說,倉庫中的各個對象儲存的資料和互相關系看起來如圖2-1所示:
圖 2-1. 單個送出對象在倉庫中的資料結構
作些修改後再次送出,那麼這次的送出對象會包含一個指向上次送出對象的指針(譯注:即下圖中的 parent 對象)。兩次送出後,倉庫曆史會變成圖2-2的樣子:
圖 2-2. 多個送出對象之間的連結關系
現在來談分支。Git 中的分支,其實本質上僅僅是個指向 commit 對象的可變指針。Git 會使用 master 作為分支的預設名字。在若幹次送出後,你其實已經有了一個指向最後一次送出對象的 master 分支,它在每次送出的時候都會自動向前移動。
圖2-3.分支其實就是從某個送出對象往回看的曆史
那麼,Git 又是如何建立一個新的分支的呢?答案很簡單,建立一個新的分支指針。比如建立一個 testing 分支,可以使用
git branch
$ git branch testing
這會在目前 commit 對象上建立一個分支指針(見圖2-4)。
圖 2-4. 多個分支指向送出資料的曆史
那麼,Git 是如何知道你目前在哪個分支上工作的呢?其實答案也很簡單,它儲存着一個名為 HEAD 的特别指針。請注意它和你熟知的許多其他版本控制系統(比如 Subversion 或 CVS)裡的 HEAD 概念大不相同。在 Git 中,它是一個指向你正在工作中的本地分支的指針。運作
git branch
指令,僅僅是建立了一個新的分支,但不會自動切換到這個分支中去,是以在這個例子中,我們依然還在 master 分支裡工作(參考圖 2-5)
圖 2-5. HEAD 指向目前所在的分支
要切換到其他分支,可以執行
git checkout
指令。我們現在轉換到建立的 testing 分支:
$ git checkout testing
這樣 HEAD 就指向了 testing 分支(見圖2-6)。
圖 2-6. HEAD 在你轉換分支時指向新的分支
這樣的實作方式會給我們帶來什麼好處呢?好吧,現在不妨再送出一次:
# step 1
$ vim test.rb
# step 2
$ git commit -a -m 'made a change'
圖 2-7 展示了送出後的結果
圖 2-7. 每次送出後 HEAD 随着分支一起向前移動
非常有趣,現在 testing 分支向前移動了一格,而 master 分支仍然指向原先
git checkout
時所在的 commit 對象。現在我們回到 master 分支看看:
$ git checkout master
圖 2-8 顯示了結果。
圖 2-8. HEAD 在一次 checkout 之後移動到了另一個分支
這條指令做了兩件事。它把 HEAD 指針移回到 master 分支,并把工作目錄中的檔案換成了 master 分支所指向的快照内容。也就是說,現在開始所做的改動,将始于本項目中一個較老的版本。它的主要作用是将 testing 分支裡作出的修改暫時取消,這樣你就可以向另一個方向進行開發。
我們作些修改後再次送出:
# step 1
$ vim test.js
# step 2
$ git commit -a -m 'made other changes'
現在我們的項目送出曆史産生了分叉(如圖 2-9 所示),因為剛才我們建立了一個分支,轉換到其中進行了一些工作,然後又回到原來的主分支進行了另外一些工作。這些改變分别孤立在不同的分支裡:我們可以在不同分支裡反複切換,并在時機成熟時把它們合并到一起。而所有這些工作,僅僅需要
branch
checkout
這兩條指令就可以完成。
圖 2-9. 不同流向的分支曆史
2、分支的建立與合并
現在讓我們來看一個簡單的分支與合并的例子,實際工作中大體也會用到這樣的工作流程:
- 開發某個網站。
- 為實作某個新的需求,建立一個分支。
- 在這個分支上開展工作。
假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那麼可以按照下面的方式處理:
- 傳回到原先已經釋出到生産伺服器上的分支。
- 為這次緊急修補建立一個新分支,并在其中修複問題。
- 通過測試後,回到生産伺服器所在的分支,将修補分支合并進來,然後再推送到生産伺服器上。
- 切換到之前實作新需求的分支,繼續工作。
a.分支的建立與切換
首先,我們假設你正在項目中愉快地工作,并且已經送出了幾次更新(見圖 2-10)。
圖 2-10. 一個簡短的送出曆史
現在,你決定要修補問題追蹤系統上的 #53 問題。順帶說明下,Git 并不同任何特定的問題追蹤系統打交道。這裡為了說明要解決的問題,才把建立的分支取名為 iss53。要建立并切換到該分支,運作
git checkout
并加上
-b
參數:
$ git checkout -b iss53
#Switched to a new branch "iss53"
這相當于執行下面這兩條指令:
# step 1
$ git branch iss53
# step 2
$ git checkout iss53
圖 2-11 示意該指令的執行結果。
圖 2-11. 建立了一個新分支的指針
接着你開始嘗試修複問題,在送出了若幹次更新後,
iss53
分支的指針也會随着向前推進,因為它就是目前分支(換句話說,目前的
HEAD
指針正指向
iss53
,見圖 2-12):
# step 1
$ vim index.html
# step 2
$ git commit -a -m 'added a new footer [issue 53]'
圖 2-12. iss53 分支随工作進展向前推進
現在你就接到了那個網站問題的緊急電話,需要馬上修補。有了 Git ,我們就不需要同時釋出這個更新檔和
iss53
裡作出的修改,也不需要在建立和釋出該更新檔到伺服器之前花費大力氣來複原這些修改。唯一需要的僅僅是切換回
master
分支。
不過在此之前,留心你的暫存區或者工作目錄裡,那些還沒有送出的修改,它會和你即将檢出的分支産生沖突進而阻止 Git 為你切換分支。切換分支的時候最好保持一個清潔的工作區域。稍後會介紹幾個繞過這種問題的辦法(分别叫做 stashing 和 commit amending)。目前已經送出了所有的修改,是以接下來可以正常轉換到
master
分支:
$ git checkout master
#Switched to branch "master"
此時工作目錄中的内容和你在解決問題 #53 之前一模一樣,你可以集中精力進行緊急修補。這一點值得牢記:Git 會把工作目錄的内容恢複為檢出某分支時它所指向的那個送出對象的快照。它會自動添加、删除和修改檔案以確定目錄的内容和你當時送出時完全一樣。
接下來,你得進行緊急修補。我們建立一個緊急修補分支
hotfix
來開展工作,直到搞定(見圖 2-13):
# step 1
$ git checkout -b 'hotfix'
#Switched to a new branch "hotfix"
# step 2
$ vim index.html
# step 3
$ git commit -a -m 'fixed the broken email address'
#[hotfix]: created 3a0874c: "fixed the broken email address"
#1 files changed, 0 insertions(+), 1 deletions(-)
圖 2-13. hotfix 分支是從 master 分支所在點分化出來的
有必要作些測試,確定修補是成功的,然後回到
master
分支并把它合并進來,然後釋出到生産伺服器。用
git merge
指令來進行合并:
# step 1
$ git checkout master
# step 2
$ 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
分支所指向的送出對象中了,可以部署到生産伺服器上去了(見圖 2-14)。
圖 2-14. 合并之後,master 分支和 hotfix 分支指向同一位置。
在那個超級重要的修補釋出以後,你想要回到被打擾之前的工作。由于目前
hotfix
分支和
master
都指向相同的送出對象,是以
hotfix
已經完成了曆史使命,可以删掉了。使用
git branch
的
-d
選項執行删除操作:
$ git branch -d hotfix
#Deleted branch hotfix (3a0874c).
現在回到之前未完成的 #53 問題修複分支上繼續工作(圖 2-15):
# step 1
$ git checkout iss53
#Switched to branch "iss53"
# step 2
$ vim index.html
# step 3
$ 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(-)
圖 2-15. iss53 分支可以不受影響繼續推進。
不用擔心之前
hotfix
分支的修改内容尚未包含到
iss53
中來。如果确實需要納入此次修補,可以用
git merge master
把 master 分支合并到
iss53
;或者等
iss53
完成之後,再将
iss53
分支中的更新并入
master
b.分支的合并
在問題 #53 相關的工作完成之後,可以合并回
master
分支。實際操作同前面合并
hotfix
分支差不多,隻需回到
master
分支,運作
git merge
指令指定要合并進來的分支:
# step 1
$ git checkout master
# step 2
$ 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)進行一次簡單的三方合并計算。圖 2-16 用紅框标出了 Git 用于合并的三個送出對象:
圖 2-16. Git 為分支合并自動識别出最佳的同源合并點。
這次,Git 沒有簡單地把分支指針右移,而是對三方合并後的結果重新做一個新的快照,并自動建立一個指向它的送出對象(C6)(見圖 2-17)。這個送出對象比較特殊,它有兩個祖先(C4 和 C5)。
值得一提的是 Git 可以自己裁決哪個共同祖先才是最佳合并基礎;這和 CVS 或 Subversion(1.5 以後的版本)不同,它們需要開發者手工指定合并基礎。是以此特性讓 Git 的合并操作比其他系統都要簡單不少。
圖 2-17. Git 自動建立了一個包含了合并結果的送出對象。
既然之前的工作成果已經合并到
master
了,那麼
iss53
也就沒用了。你可以就此删除它,并在問題追蹤系統裡關閉該問題。
$ git branch -d iss53
c.遇到沖突時的分支合并
有時候合并操作并不會如此順利。如果在不同的分支中都修改了同一個檔案的同一部分,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
# 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)
#
# unmerged: index.html
#
任何包含未解決沖突的檔案都會以未合并(unmerged)的狀态列出。Git 會在有沖突的檔案裡加入标準的沖突解決标記,可以通過它們來手工定位并解決這些沖突。可以看到此檔案包含類似下面這樣的部分:
<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div>
>>>>>>> iss53:index.html
可以看到
=======
隔開的上半部分,是
HEAD
(即
master
分支,在運作
merge
指令時所切換到的分支)中的内容,下半部分是在
iss53
分支中的内容。解決沖突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段内容替換為下面這樣來解決:
<div id="footer">
please contact us at [email protected]
</div>
這個解決方案各采納了兩個分支中的一部分内容,而且我還删除了
<<<<<<<
,
=======
>>>>>>>
這些行。在解決了所有檔案裡的所有沖突後,運作
git add
将把它們标記為已解決狀态(實際上就是來一次快照儲存到暫存區域)。因為一旦暫存,就表示沖突已經解決。
再運作一次
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.
#
如果想給将來看這次合并的人一些友善,可以修改該資訊,提供更多合并細節。比如你都作了哪些改動,以及這麼做的原因。有時候裁決沖突的理由并不直接或明顯,有必要略加注解。
3、分支的管理
到目前為止,你已經學會了如何建立、合并和删除分支。除此之外,我們還需要學習如何管理分支,在日後的正常工作中會經常用到下面介紹的管理指令。
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 qiangdada 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
強制執行,就像上面提示資訊中給出的那樣。
4、利用分支進行開發的工作流程
a.長期分支
由于 Git 使用簡單的三方合并,是以就算在較長一段時間内,反複多次把某個分支合并到另一分支,也不是什麼難事。也就是說,你可以同時擁有多個開放的分支,每個分支用于完成特定的任務,随着開發的推進,你可以随時把某個特性分支的成果并到其他分支中。
許多使用 Git 的開發者都喜歡用這種方式來開展工作,比如僅在
master
分支中保留完全穩定的代碼,即已經釋出或即将釋出的代碼。與此同時,他們還有一個名為
develop
或
next
的平行分支,專門用于後續的開發,或僅用于穩定性測試 — 當然并不是說一定要絕對穩定,不過一旦進入某種穩定狀态,便可以把它合并到
master
裡。這樣,在確定這些已完成的特性分支(短期分支,比如之前的
iss53
分支)能夠通過所有測試,并且不會引入更多錯誤之後,就可以并到主幹分支中,等待下一次的釋出。
本質上我們剛才談論的,是随着送出對象不斷右移的指針。穩定分支的指針總是在送出曆史中落後一大截,而前沿分支總是比較靠前(見圖 2-18)。
圖 2-18. 穩定分支總是比較老舊。
或者把它們想象成工作流水線,或許更好了解一些,經過測試的送出對象集合被遴選到更穩定的流水線(見圖 2-19)。
圖 2-19. 想象成流水線可能會容易點。
你可以用這招維護不同層次的穩定性。某些大項目還會有個
proposed
(建議)或
pu
(proposed updates,建議更新)分支,它包含着那些可能還沒有成熟到進入
next
master
的内容。這麼做的目的是擁有不同層次的穩定性:當這些分支進入到更穩定的水準時,再把它們合并到更高層分支中去。再次說明下,使用多個長期分支的做法并非必需,不過一般來說,對于特大型項目或特複雜的項目,這麼做确實更容易管理。
b.特性分支
在任何規模的項目中都可以使用特性(Topic)分支。一個特性分支是指一個短期的,用來實作單一特性或與其相關工作的分支。可能你在以前的版本控制系統裡從未做過類似這樣的事情,因為通常建立與合并分支消耗太大。然而在 Git 中,一天之内建立、使用、合并再删除多個分支是常見的事。
現在我們來看一個實際的例子。請看圖 2-20,由下往上,起先我們在
master
工作到 C1,然後開始一個新分支
iss91
嘗試修複 91 号缺陷,送出到 C6 的時候,又冒出一個解決該問題的新辦法,于是從之前 C4 的地方又分出一個分支
iss91v2
,幹到 C8 的時候,又回到主幹
master
中送出了 C9 和 C10,再回到
iss91v2
繼續工作,送出 C11,接着,又冒出個不太确定的想法,從
master
的最新送出 C10 處開了個新的分支
dumbidea
做些試驗。
圖 2-20. 擁有多個特性分支的送出曆史。
現在,假定兩件事情:我們最終決定使用第二個解決方案,即
iss91v2
中的辦法;另外,我們把
dumbidea
分支拿給同僚們看了以後,發現它竟然是個天才之作。是以接下來,我們準備抛棄原來的
iss91
分支(實際上會丢棄 C5 和 C6),直接在主幹中并入另外兩個分支。最終的送出曆史将變成圖 2-21 這樣:
圖 2-21. 合并了 dumbidea 和 iss91v2 後的分支曆史。
請務必牢記這些分支全部都是本地分支,這一點很重要。當你在使用分支及合并的時候,一切都是在你自己的 Git 倉庫中進行的 — 完全不涉及與伺服器的互動。
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
分支相同的位置,你可以就此開始工作(見圖 2-22):
圖 2-22. 一次 Git 克隆會建立你自己的本地分支 master 和遠端分支 origin/master,并且将它們都指向
origin
上的
master
如果你在本地
master
分支做了些改動,與此同時,其他人向
git.ourcompany.com
推送了他們的更新,那麼伺服器上的
master
分支就會向前推進,而于此同時,你在本地的送出曆史正朝向不同方向發展。不過隻要你不和伺服器通訊,你的
origin/master
指針仍然保持原位不會移動(見圖 2-23)。
圖 2-23. 在本地工作的同時有人向遠端倉庫推送内容會讓送出曆史開始分流。
可以運作
git fetch origin
來同步遠端伺服器上的資料到本地。該指令首先找到
origin
是哪個伺服器,從上面擷取你尚未擁有的資料,更新你本地的資料庫,然後把
origin/master
的指針移到它最新的位置上(見圖 2-24)
圖 2-24. git fetch 指令會更新 remote 索引。
為了示範擁有多個遠端分支(在不同的遠端伺服器上)的項目是如何工作的,我們假設你還有另一個僅供你的靈活開發小組使用的内部伺服器
git.team1.ourcompany.com
。可以用我上面提到的
git remote add
指令把它加為目前項目的遠端分支之一。我們把它命名為
teamone
,以便代替完整的 Git URL 以友善使用(見圖 2-25)
圖 2-25. 把另一個伺服器加為遠端倉庫
現在你可以用
git fetch teamone
來擷取小組伺服器上你還沒有的資料了。由于目前該伺服器上的内容是你
origin
伺服器上的子集,Git 不會下載下傳任何資料,而隻是簡單地建立一個名為
teamone/master
的遠端分支,指向
teamone
伺服器上
master
分支所在的送出對象
31b8e
(見圖 2-26)。
圖 2-26. 你在本地有了一個指向 teamone 伺服器上 master 分支的索引。
6、推送本地分支
要想和其他人分享某個本地分支,你需要把它推送到一個你擁有寫權限的遠端倉庫。你建立的本地分支不會因為你的寫入操作而被自動同步到你引入的遠端伺服器上,你需要明确地執行推送分支的操作。換句話說,對于無意分享的分支,你盡管保留為私人分支好了,而隻推送那些協同工作要用到的特性分支。
如果你有個叫
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 [email protected]: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 [email protected]: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
一緻,這樣你就可以在裡面繼續開發了。
7、跟蹤遠端分支
從遠端分支
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
了。
8、删除遠端分支
如果不再需要某個遠端分支了,比如搞定了某個特性并把它合并進了遠端的
master
分支(或任何其他存放穩定代碼的分支),可以用這個非常無厘頭的文法來删除它:
git push [遠端名] :[分支名]
。如果想在伺服器上删除
serverfix
分支,運作下面的指令:
$ git push origin :serverfix
# To [email protected]:schacon/simplegit.git
# - [deleted] serverfix
咚!伺服器上的分支沒了。你最好特别留心這一頁,因為你一定會用到那個指令,而且你很可能會忘掉它的文法。有種友善記憶這條指令的方法:記住我們不久前見過的
git push [遠端名] [本地分支]:[遠端分支]
文法,如果省略
[本地分支]
,那就等于是在說“在這裡提取空白然後把它變成
[遠端分支]
”。
9、分支的衍合
把一個分支中的修改整合到另一個分支的辦法有兩種:
merge
rebase
a.基本的衍合操作
請回顧我上面講到的合并(見圖 2-27),你會看到開發程序分叉到兩個不同分支,又各自送出了更新。
圖 2-27. 最初分叉的送出曆史。
前面介紹過,最容易的整合分支的方法是
merge
指令,它會把兩個分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)進行三方合并,合并的結果是産生一個新的送出對象(C5)。如圖 2-28 所示:
圖 2-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
分支的直接下遊,如圖 2-29 所示:
圖 2-29. 把 C3 裡産生的改變到 C4 上重演一遍。
現在回到
master
分支,進行一次快進合并(見圖 2-30):
圖 2-30. master 分支的快進。
現在的 C3' 對應的快照,其實和普通的三方合并,即上個例子中的 C5 對應的快照内容一模一樣了。雖然最後整合得到的結果沒有任何差別,但衍合能産生一個更為整潔的送出曆史。如果視察一個衍合過的分支的曆史記錄,看起來會更清楚:仿佛所有修改都是在一根線上先後進行的,盡管實際上它們原本是同時并行發生的。
一般我們使用衍合的目的,是想要得到一個能在遠端分支上幹淨應用的更新檔 — 比如某些項目你不是維護者,但想幫點忙的話,最好用衍合:先在自己的一個分支裡進行開發,當準備向主項目送出更新檔的時候,根據最新的
origin/master
進行一次衍合操作然後再送出,這樣維護者就不需要做任何整合工作(實際上是把解決分支更新檔同最新主幹代碼之間沖突的責任,化轉為由送出更新檔的人來解決),隻需根據你提供的倉庫位址作一次快進合并,或者直接采納你送出的更新檔。
請注意,合并結果中最後一次送出所指向的快照,無論是通過衍合,還是三方合并,都會得到相同的快照内容,隻不過送出曆史不同罷了。衍合是按照每行的修改次序重演一遍修改,而合并是把最終結果合在一起。
b.有趣的衍合
衍合也可以放到其他分支進行,并不一定非得根據分化之前的分支。以圖 2-31 的曆史為例,我們為了給伺服器端代碼添加一些功能而建立了特性分支
server
,然後送出 C3 和 C4。然後又從 C3 的地方再增加一個
client
分支來對用戶端代碼進行一些相應修改,是以送出了 C8 和 C9。最後,又回到
server
分支送出了 C10。
圖 2-31. 從一個特性分支裡再分出一個特性分支的曆史。
假設在接下來的一次軟體釋出中,我們決定先把用戶端的修改并到主線中,而暫緩并入服務端軟體的修改(因為還需要進一步測試)。這個時候,我們就可以把基于
server
分支而非
master
分支的改變(即 C8 和 C9),跳過
server
直接放到
master
分支中重演一遍,但這需要用
git rebase
--onto
選項指定新的基底分支
master
$ git rebase --onto master server client
這好比在說:“取出
client
分支,找出
client
server
分支的共同祖先之後的變化,然後把它們在
master
上重演一遍”。它的結果如圖 2-32 所示(譯注:雖然
client
裡的 C8, C9 在 C3 之後,但這僅表明時間上的先後,而非在 C3 修改的基礎上進一步改動,因為
server
client
這兩個分支對應的代碼應該是兩套檔案,雖然這麼說不是很嚴格,但應了解為在 C3 時間點之後,對另外的檔案所做的 C8,C9 修改,放到主幹重演。):
圖 2-32. 将特性分支上的另一個特性分支衍合到其他分支。
現在可以快進
master
分支了(見圖 2-33):
# step 1
$ git checkout master
# step 2
$ git merge client
圖 2-33. 快進 master 分支,使之包含 client 分支的變化。
現在我們決定把
server
分支的變化也包含進來。我們可以直接把
server
分支衍合到
master
,而不用手工切換到
server
分支後再執行衍合操作 —
git rebase [主分支] [特性分支]
指令會先取出特性分支
server
,然後在主分支
master
上重演:
$ git rebase master server
于是,
server
的進度應用到
master
的基礎上,如圖 2-34 所示:
圖 2-34. 在 master 分支上衍合 server 分支。
然後就可以快進主幹分支
master
了:
# step 1
$ git checkout master
# step 2
$ git merge server
現在
client
server
分支的變化都已經內建到主幹分支來了,可以删掉它們了。最終我們的送出曆史會變成圖 2-35 的樣子:
# step 1
$ git branch -d client
# step 2
$ git branch -d server
圖 2-35. 最終的送出曆史
這裡需要強調一點的是,使用衍合的時候必須遵守一條準則:
一旦分支中的送出對象釋出到公共倉庫,就千萬不要對該分支進行衍合操作
原文釋出時間為:2016年12月19日
原文作者:
qiangdada本文來源:
開源中國如需轉載請聯系原作者