如何合理利用 Git 進行團隊協作

TommyBetz 9年前發布 | 39K 次閱讀 Git 版本控制系統

前言

上篇我就Git協議講了兩大點,Git的起步,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</pre> 

也可以加上 -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: git@github.com: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)。

圖 3-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、分支的新建與合并

現在讓我們來看一個簡單的分支與合并的例子,實際工作中大體也會用到這樣的工作流程:

  1. 開發某個網站。
  2. 為實現某個新的需求,創建一個分支。
  3. 在這個分支上開展工作。

假設此時,你突然接到一個電話說有個很嚴重的問題需要緊急修補,那么可以按照下面的方式處理:

  1. 返回到原先已經發布到生產服務器上的分支。
  2. 為這次緊急修補建立一個新分支,并在其中修復問題。
  3. 通過測試后,回到生產服務器所在的分支,將修補分支合并進來,然后再推送到生產服務器上。
  4. 切換到之前實現新需求的分支,繼續工作。

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.support@github.com</div>
    =======
    <div id="footer">
    please contact us at support@github.com
    </div>
    >>>>>>> iss53:index.html

可以看到 ======= 隔開的上半部分,是  HEAD (即  master 分支,在運行  merge 命令時所切換到的分支)中的內容,下半部分是在  iss53 分支中的內容。解決沖突的辦法無非是二者選其一或者由你親自整合到一起。比如你可以通過把這段內容替換為下面這樣來解決:

<div id="footer">
    please contact us at email.support@github.com
</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)

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

為了演示擁有多個遠程分支(在不同的遠程服務器上)的項目是如何工作的,我們假設你還有另一個僅供你的敏捷開發小組使用的內部服務器 git.team1.ourcompany.com 。可以用我上面提到的  git remote add 命令把它加為當前項目的遠程分支之一。我們把它命名為  teamone ,以便代替完整的 Git URL 以方便使用(見圖 2-25)

圖 3-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 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 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 git@github.com: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. 最終的提交歷史

這里需要強調一點的是,使用衍合的時候必須遵守一條準則:

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

三、小結

讀到這里,首先我表示對你的耐心感到贊許,而這個時候的你應該已經學會了如何創建分支并切換到新分支,在不同分支間轉換,合并本地分支,把分支推送到共享服務器上,使用共享分支與他人協作,以及在分享之前進行衍合。

 

 

來自:https://my.oschina.net/qiangdada/blog/808527

 

 本文由用戶 TommyBetz 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!