1. 程式人生 > >git 怎麼為開源專案做貢獻,怎麼有效管理貢獻者的提交

git 怎麼為開源專案做貢獻,怎麼有效管理貢獻者的提交

,當作為專案貢獻者時,我們該怎麼做才能方便維護者採納更新;或者作為專案維護者時,又該怎樣有效管理大量貢獻者的提交。

5.1  分散式工作流程

同傳統的集中式版本控制系統(CVCS)不同,開發者之間的協作方式因著 Git 的分散式特性而變得更為靈活多樣。在集中式系統上,每個開發者就像是連線在集線器上的節點,彼此的工作方式大體相像。而在 Git 網路中,每個開發者同時扮演著節點和集線器的角色,這就是說,每一個開發者都可以將自己的程式碼貢獻到另外一個開發者的倉庫中,或者建立自己的公共倉庫,讓 其他開發者基於自己的工作開始,為自己的倉庫貢獻程式碼。於是,Git 的分散式協作便可以衍生出種種不同的工作流程,我會在接下來的章節介紹幾種常見的應用方式,並分別討論各自的優缺點。你可以選擇其中的一種,或者結合起 來,應用到你自己的專案中。

集中式工作流

通常,集中式工作流程使用的都是單點協作模型。一個存放程式碼倉庫的中心伺服器,可以接受所有開發者提交的程式碼。所有的開發者都是普通的節點,作為中心集線器的消費者,平時的工作就是和中心倉庫同步資料(見圖 5-1)。

Git詳解之五 分散式Git

圖 5-1. 集中式工作流

如果兩個開發者從中心倉庫克隆程式碼下來,同時作了一些修訂,那麼只有第一個開發者可以順利地把資料推送到共享伺服器。第二個開發者在提交他的修訂之 前,必須先下載合併伺服器上的資料,解決衝突之後才能推送資料到共享伺服器上。在 Git 中這麼用也決無問題,這就好比是在用 Subversion(或其他 CVCS)一樣,可以很好地工作。

如果你的團隊不是很大,或者大家都已經習慣了使用集中式工作流程,完全可以採用這種簡單的模式。只需要配置好一臺中心伺服器,並給每個人推送資料的 許可權,就可以開展工作了。但如果提交程式碼時有衝突, Git 根本就不會讓使用者覆蓋他人程式碼,它直接駁回第二個人的提交操作。這就等於告訴提交者,你所作的修訂無法通過快近(fast-forward)來合併,你必 須先拉取最新資料下來,手工解決衝突合併後,才能繼續推送新的提交。絕大多數人都熟悉和了解這種模式的工作方式,所以使用也非常廣泛。

整合管理員工作流

由於 Git 允許使用多個遠端倉庫,開發者便可以建立自己的公共倉庫,往裡面寫資料並共享給他人,而同時又可以從別人的倉庫中提取他們的更新過來。這種情形通常都會有 個代表著官方釋出的專案倉庫(blessed repository),開發者們由此倉庫克隆出一個自己的公共倉庫(developer public),然後將自己的提交推送上去,請求官方倉庫的維護者拉取更新合併到主專案。維護者在自己的本地也有個克隆倉庫(integration manager),他可以將你的公共倉庫作為遠端倉庫新增進來,經過測試無誤後合併到主幹分支,然後再推送到官方倉庫。工作流程看起來就像圖 5-2 所示:

  1. 專案維護者可以推送資料到公共倉庫 blessed repository。 2. 貢獻者克隆此倉庫,修訂或編寫新程式碼。
  2. 貢獻者推送資料到自己的公共倉庫 developer public。 4. 貢獻者給維護者傳送郵件,請求拉取自己的最新修訂。
  3. 維護者在自己本地的 integration manger 倉庫中,將貢獻者的倉庫加為遠端倉庫,合併更新並做測試。
  4. 維護者將合併後的更新推送到主倉庫 blessed repository。
Git詳解之五 分散式Git

圖 5-2. 整合管理員工作流

在 GitHub 網站上使用得最多的就是這種工作流。人們可以複製(fork 亦即克隆)某個專案到自己的列表中,成為自己的公共倉庫。隨後將自己的更新提交到這個倉庫,所有人都可以看到你的每次更新。這麼做最主要的優點在於,你可 以按照自己的節奏繼續工作,而不必等待維護者處理你提交的更新;而維護者也可以按照自己的節奏,任何時候都可以過來處理接納你的貢獻。

司令官與副官工作流

這其實是上一種工作流的變體。一般超大型的專案才會用到這樣的工作方式,像是擁有數百協作開發者的 Linux 核心專案就是如此。各個整合管理員分別負責整合專案中的特定部分,所以稱為副官(lieutenant)。而所有這些整合管理員頭上還有一位負責統籌的總 整合管理員,稱為司令官(dictator)。司令官維護的倉庫用於提供所有協作者拉取最新整合的專案程式碼。整個流程看起來如圖 5-3 所示:

  1. 一般的開發者在自己的特性分支上工作,並不定期地根據主幹分支(dictator 上的 master)衍合。
  2. 副官(lieutenant)將普通開發者的特性分支合併到自己的 master 分支中。
  3. 司令官(dictator)將所有副官的 master 分支併入自己的 master 分支。
  4. 司令官(dictator)將整合後的 master 分支推送到共享倉庫 blessed repository 中,以便所有其他開發者以此為基礎進行衍合。
Git詳解之五 分散式Git


圖 5-3. 司令官與副官工作流

這種工作流程並不常用,只有當專案極為龐雜,或者需要多級別管理時,才會體現出優勢。利用這種方式,專案總負責人(即司令官)可以把大量分散的整合工作委託給不同的小組負責人分別處理,最後再統籌起來,如此各人的職責清晰明確,也不易出錯(譯註:此乃分而治之)。

以上介紹的是常見的分散式系統可以應用的工作流程,當然不止於 Git。在實際的開發工作中,你可能會遇到各種為了滿足特定需求而有所變化的工作方式。我想現在你應該已經清楚,接下來自己需要用哪種方式開展工作了。下 節我還會再舉些例子,看看各式工作流中的每個角色具體應該如何操作。

5.2  為專案作貢獻

接下來,我們來學習一下作為專案貢獻者,會有哪些常見的工作模式。

不過要說清楚整個協作過程真的很難,Git 如此靈活,人們的協作方式便可以各式各樣,沒有固定不變的正規化可循,而每個專案的具體情況又多少會有些不同,比如說參與者的規模,所選擇的工作流程,每個人的提交許可權,以及 Git 以外貢獻等等,都會影響到具體操作的細節。

首當其衝的是參與者規模。專案中有多少開發者是經常提交程式碼的?經常又是多久呢?大多數兩至三人的小團隊,一天大約只有幾次提交,如果不是什麼熱門 專案的話就更少了。可要是在大公司裡,或者大專案中,參與者可以多到上千,每天都會有十幾個上百個補丁提交上來。這種差異帶來的影響是顯著的,越是多的人 參與進來,就越難保證每次合併正確無誤。你正在工作的程式碼,可能會因為合併進來其他人的更新而變得過時,甚至受創無法執行。而已經提交上去的更新,也可能 在等著稽核合併的過程中變得過時。那麼,我們該怎樣做才能確保程式碼是最新的,提交的補丁也是可用的呢?

接下來便是專案所採用的工作流。是集中式的,每個開發者都具有等同的寫許可權?專案是否有專人負責檢查所有補丁?是不是所有補丁都做過同行複閱(peer-review)再通過稽核的?你是否參與稽核過程?如果使用副官系統,那你是不是限定於只能向此副官提交?

還有你的提交許可權。有或沒有向主專案提交更新的許可權,結果完全不同,直接決定最終採用怎樣的工作流。如果不能直接提交更新,那該如何貢獻自己的程式碼呢?是不是該有個什麼策略?你每次貢獻程式碼會有多少量?提交頻率呢?

所有以上這些問題都會或多或少影響到最終採用的工作流。接下來,我會在一系列由簡入繁的具體用例中,逐一闡述。此後在實踐時,應該可以借鑑這裡的例子,略作調整,以滿足實際需要構建自己的工作流。

提交指南

開始分析特定用例之前,先來了解下如何撰寫提交說明。一份好的提交指南可以幫助協作者更輕鬆更有效地配合。Git 專案本身就提供了一份文件(Git 專案原始碼目錄中Documentation/SubmittingPatches),列數了大量提示,從如何編撰提交說明到提交補丁,不一而足。

首先,請不要在更新中提交多餘的白字元(whitespace)。Git 有種檢查此類問題的方法,在提交之前,先執行 git diff --check,會把可能的多餘白字元修正列出來。下面的示例,我已經把終端中顯示為紅色的白字元用X 替換掉:

$ git diff --check
lib/simplegit.rb:5: trailing whitespace.
+    @git_dir = File.expand_path(git_dir)XX
lib/simplegit.rb:7: trailing whitespace.
+ XXXXXXXXXXX
lib/simplegit.rb:26: trailing whitespace.
+    def command(git_cmd)XXXX

這樣在提交之前你就可以看到這類問題,及時解決以免困擾其他開發者。

接下來,請將每次提交限定於完成一次邏輯功能。並且可能的話,適當地分解為多次小更新,以便每次小型提交都更易於理解。請不要在週末窮追猛打一次性 解決五個問題,而最後拖到週一再提交。就算是這樣也請儘可能利用暫存區域,將之前的改動分解為每次修復一個問題,再分別提交和加註說明。如果針對兩個問題 改動的是同一個檔案,可以試試看git add --patch 的方式將部分內容置入暫存區域(我們會在第六章再詳細介紹)。無論是五次小提交還是混雜在一起的大提交,最終分支末端的專案快照應該還是一樣的,但分解開 來之後,更便於其他開發者複閱。這麼做也方便自己將來取消某個特定問題的修復。我們將在第六章介紹一些重寫提交歷史,同暫存區域互動的技巧和工具,以便最 終得到一個乾淨有意義,且易於理解的提交歷史。

最後需要謹記的是提交說明的撰寫。寫得好可以讓大家協作起來更輕鬆。一般來說,提交說明最好限制在一行以內,50 個字元以下,簡明扼要地描述更新內容,空開一行後,再展開詳細註解。Git 專案本身需要開發者撰寫詳盡註解,包括本次修訂的因由,以及前後不同實現之間的比較,我們也該借鑑這種做法。另外,提交說明應該用祈使現在式語態,比如, 不要說成 “I added tests for” 或 “Adding tests for” 而應該用 “Add tests for”。下面是來自 tpope.net 的 Tim Pope 原創的提交說明格式模版,供參考:

本次更新的簡要描述(50 個字元以內)

如果必要,此處展開詳盡闡述。段落寬度限定在 72 個字元以內。
某些情況下,第一行的簡要描述將用作郵件標題,其餘部分作為郵件正文。
其間的空行是必要的,以區分兩者(當然沒有正文另當別論)。
如果並在一起,rebase 這樣的工具就可能會迷惑。

另起空行後,再進一步補充其他說明。

 - 可以使用這樣的條目列舉式。

 - 一般以單個空格緊跟短劃線或者星號作為每項條目的起始符。每個條目間用一空行隔開。
   不過這裡按自己專案的約定,可以略作變化。

如果你的提交說明都用這樣的格式來書寫,好多事情就可以變得十分簡單。Git 專案本身就是這樣要求的,我強烈建議你到 Git 專案倉庫下執行 git log --no-merges 看看,所有提交歷史的說明是怎樣撰寫的。(譯註:如果現在還沒有克隆 git 專案原始碼,是時候git clone git://git.kernel.org/pub/scm/git/git.git 了。)

為簡單起見,在接下來的例子(及本書隨後的所有演示)中,我都不會用這種格式,而使用 -m 選項提交 git commit。不過請還是按照我之前講的做,別學我這裡偷懶的方式。

私有的小型團隊

我們從最簡單的情況開始,一個私有專案,與你一起協作的還有另外一到兩位開發者。這裡說私有,是指原始碼不公開,其他人無法訪問專案倉庫。而你和其他開發者則都具有推送資料到倉庫的許可權。

這種情況下,你們可以用 Subversion 或其他集中式版本控制系統類似的工作流來協作。你仍然可以得到 Git 帶來的其他好處:離線提交,快速分支與合併等等,但工作流程還是差不多的。主要區別在於,合併操作發生在客戶端而非伺服器上。讓我們來看看,兩個開發者一 起使用同一個共享倉庫,會發生些什麼。第一個人,John,克隆了倉庫,作了些更新,在本地提交。(下面的例子中省略了常規提示,用... 代替以節約版面。)

# John's Machine
$ git clone [email protected]:simplegit.git
Initialized empty Git repository in /home/john/simplegit/.git/
...
$ cd simplegit/
$ vim lib/simplegit.rb 
$ git commit -am 'removed invalid default value'
[master 738ee87] removed invalid default value
 1 files changed, 1 insertions(+), 1 deletions(-)

第二個開發者,Jessica,一樣這麼做:克隆倉庫,提交更新:

# Jessica's Machine
$ git clone [email protected]:simplegit.git
Initialized empty Git repository in /home/jessica/simplegit/.git/
...
$ cd simplegit/
$ vim TODO 
$ git commit -am 'add reset task'
[master fbff5bc] add reset task
 1 files changed, 1 insertions(+), 0 deletions(-)

現在,Jessica 將她的工作推送到伺服器上:

# Jessica's Machine
$ git push origin master
...
To [email protected]:simplegit.git
   1edee6b..fbff5bc  master -> master

John 也嘗試推送自己的工作上去:

# John's Machine
$ git push origin master
To [email protected]:simplegit.git
 ! [rejected]        master -> master (non-fast forward)
error: failed to push some refs to '[email protected]:simplegit.git'

John 的推送操作被駁回,因為 Jessica 已經推送了新的資料上去。請注意,特別是你用慣了 Subversion 的話,這裡其實修改的是兩個檔案,而不是同一個檔案的同一個地方。Subversion 會在伺服器端自動合併提交上來的更新,而 Git 則必須先在本地合併後才能推送。於是,John 不得不先把 Jessica 的更新拉下來:

$ git fetch origin
...
From [email protected]:simplegit
 + 049d078...fbff5bc master     -> origin/master

此刻,John 的本地倉庫如圖 5-4 所示:

Git詳解之五 分散式Git

圖 5-4. John 的倉庫歷史

雖然 John 下載了 Jessica 推送到伺服器的最近更新(fbff5),但目前只是 origin/master 指標指向它,而當前的本地分支master 仍然指向自己的更新(738ee),所以需要先把她的提交合並過來,才能繼續推送資料:

$ git merge origin/master
Merge made by recursive.
 TODO |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

還好,合併過程非常順利,沒有衝突,現在 John 的提交歷史如圖 5-5 所示:

Git詳解之五 分散式Git

圖 5-5. 合併 origin/master 後 John 的倉庫歷史

現在,John 應該再測試一下程式碼是否仍然正常工作,然後將合併結果(72bbc)推送到伺服器上:

$ git push origin master
...
To [email protected]:simplegit.git
   fbff5bc..72bbc59  master -> master

最終,John 的提交歷史變為圖 5-6 所示:

Git詳解之五 分散式Git

圖 5-6. 推送後 John 的倉庫歷史

而在這段時間,Jessica 已經開始在另一個特性分支工作了。她建立了 issue54 並提交了三次更新。她還沒有下載 John 提交的合併結果,所以提交歷史如圖 5-7 所示:

Git詳解之五 分散式Git

圖 5-7. Jessica 的提交歷史

Jessica 想要先和伺服器上的資料同步,所以先下載資料:

# Jessica's Machine
$ git fetch origin
...
From [email protected]:simplegit
   fbff5bc..72bbc59  master     -> origin/master

於是 Jessica 的本地倉庫歷史多出了 John 的兩次提交(738ee 和 72bbc),如圖 5-8 所示:

Git詳解之五 分散式Git

圖 5-8. 獲取 John 的更新之後 Jessica 的提交歷史

此時,Jessica 在特性分支上的工作已經完成,但她想在推送資料之前,先確認下要並進來的資料究竟是什麼,於是執行git log 檢視:

$ git log --no-merges origin/master ^issue54
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith 
    
    
    
    
    
     
     
     
     
     
Date:   Fri May 29 16:01:27 2009 -0700

    removed invalid default value
    
    
    
    
    

現在,Jessica 可以將特性分支上的工作併到 master 分支,然後再併入 John 的工作(origin/master)到自己的master 分支,最後再推送回伺服器。當然,得先切回主分支才能整合所有資料:

$ git checkout master
Switched to branch "master"
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.

要合併 origin/master 或 issue54 分支,誰先誰後都沒有關係,因為它們都在上游(upstream)(譯註:想像分叉的更新像是匯流成河的源頭,所以上游 upstream 是指最新的提交),所以無所謂先後順序,最終合併後的內容快照都是一樣的,而僅是提交歷史看起來會有些先後差別。Jessica 選擇先合併issue54

$ git merge issue54
Updating fbff5bc..4af4298
Fast forward
 README           |    1 +
 lib/simplegit.rb |    6 +++++-
 2 files changed, 6 insertions(+), 1 deletions(-)

正如所見,沒有衝突發生,僅是一次簡單快進。現在 Jessica 開始合併 John 的工作(origin/master):

$ git merge origin/master
Auto-merging lib/simplegit.rb
Merge made by recursive.
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

所有的合併都非常乾淨。現在 Jessica 的提交歷史如圖 5-9 所示:

Git詳解之五 分散式Git

圖 5-9. 合併 John 的更新後 Jessica 的提交歷史

現在 Jessica 已經可以在自己的 master 分支中訪問 origin/master 的最新改動了,所以她應該可以成功推送最後的合併結果到伺服器上(假設 John 此時沒再推送新資料上來):

$ git push origin master
...
To [email protected]:simplegit.git
   72bbc59..8059c15  master -> master

至此,每個開發者都提交了若干次,且成功合併了對方的工作成果,最新的提交歷史如圖 5-10 所示:

Git詳解之五 分散式Git

圖 5-10. Jessica 推送資料後的提交歷史

以上就是最簡單的協作方式之一:先在自己的特性分支中工作一段時間,完成後合併到自己的 master 分支;然後下載合併origin/master 上的更新(如果有的話),再推回遠端伺服器。一般的協作流程如圖 5-11 所示:

Git詳解之五 分散式Git

圖 5-11. 多使用者共享倉庫協作方式的一般工作流程時序

私有團隊間協作

現在我們來看更大一點規模的私有團隊協作。如果有幾個小組分頭負責若干特性的開發和整合,那他們之間的協作過程是怎樣的。

假設 John 和 Jessica 一起負責開發某項特性 A,而同時 Jessica 和 Josie 一起負責開發另一項功能 B。公司使用典型的整合管理員式工作流,每個組都有一名管理員負責整合本組程式碼,及更新專案主倉庫的master 分支。所有開發都在代表小組的分支上進行。

讓我們跟隨 Jessica 的視角看看她的工作流程。她參與開發兩項特性,同時和不同小組的開發者一起協作。克隆生成本地倉庫後,她打算先著手開發特性 A。於是建立了新的featureA 分支,繼而編寫程式碼:

# Jessica's Machine
$ git checkout -b featureA
Switched to a new branch "featureA"
$ vim lib/simplegit.rb
$ git commit -am 'add limit to log function'
[featureA 3300904] add limit to log function
 1 files changed, 1 insertions(+), 1 deletions(-)

此刻,她需要分享目前的進展給 John,於是她將自己的 featureA 分支提交到伺服器。由於 Jessica 沒有許可權推送資料到主倉庫的master 分支(只有整合管理員有此許可權),所以只能將此分支推上去同 John 共享協作:

$ git push origin featureA
...
To [email protected]:simplegit.git
 * [new branch]      featureA -> featureA

Jessica 發郵件給 John 讓他上來看看 featureA 分支上的進展。在等待他的反饋之前,Jessica 決定繼續工作,和 Josie 一起開發featureB 上的特性 B。當然,先建立此分支,分叉點以伺服器上的 master 為起點:

# Jessica's Machine
$ git fetch origin
$ git checkout -b featureB origin/master
Switched to a new branch "featureB"

隨後,Jessica 在 featureB 上提交了若干更新:

$ vim lib/simplegit.rb
$ git commit -am 'made the ls-tree function recursive'
[featureB e5b0fdc] made the ls-tree function recursive
 1 files changed, 1 insertions(+), 1 deletions(-)
$ vim lib/simplegit.rb
$ git commit -am 'add ls-files'
[featureB 8512791] add ls-files
 1 files changed, 5 insertions(+), 0 deletions(-)

現在 Jessica 的更新歷史如圖 5-12 所示:

Git詳解之五 分散式Git

圖 5-12. Jessica 的更新歷史

Jessica 正準備推送自己的進展上去,卻收到 Josie 的來信,說是她已經將自己的工作推到伺服器上的 featureBee 分支了。這樣,Jessica 就必須先將 Josie 的程式碼合併到自己本地分支中,才能再一起推送回伺服器。她用git fetch 下載 Josie 的最新程式碼:

$ git fetch origin
...
From [email protected]:simplegit
 * [new branch]      featureBee -> origin/featureBee

然後 Jessica 使用 git merge 將此分支合併到自己分支中:

$ git merge origin/featureBee
Auto-merging lib/simplegit.rb
Merge made by recursive.
 lib/simplegit.rb |    4 ++++
 1 files changed, 4 insertions(+), 0 deletions(-)

合併很順利,但另外有個小問題:她要推送自己的 featureB 分支到伺服器上的 featureBee 分支上去。當然,她可以使用冒號(:)格式指定目標分支:

$ git push origin featureB:featureBee
...
To [email protected]:simplegit.git
   fba9af8..cd685d1  featureB -> featureBee

我們稱此為_refspec_。更多有關於 Git refspec 的討論和使用方式會在第九章作詳細闡述。

接下來,John 發郵件給 Jessica 告訴她,他看了之後作了些修改,已經推回伺服器 featureA 分支,請她過目下。於是 Jessica 執行git fetch 下載最新資料:

$ git fetch origin
...
From [email protected]:simplegit
   3300904..aad881d  featureA   -> origin/featureA

接下來便可以用 git log 檢視更新了些什麼:

$ git log origin/featureA ^featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith 
    
    
    
    
    
     
     
     
     
     
Date:   Fri May 29 19:57:33 2009 -0700

    changed log output to 30 from 25
    
    
    
    
    

最後,她將 John 的工作合併到自己的 featureA 分支中:

$ git checkout featureA
Switched to branch "featureA"
$ git merge origin/featureA
Updating 3300904..aad881d
Fast forward
 lib/simplegit.rb |   10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)

Jessica 稍做一番修整後同步到伺服器:

$ git commit -am 'small tweak'
[featureA ed774b3] small tweak
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push origin featureA
...
To [email protected]:simplegit.git
   3300904..ed774b3  featureA -> featureA

現在的 Jessica 提交歷史如圖 5-13 所示:

Git詳解之五 分散式Git

圖 5-13. 在特性分支中提交更新後的提交歷史

現在,Jessica,Josie 和 John 通知整合管理員伺服器上的 featureA 及 featureBee 分支已經準備好,可以併入主線了。在管理員完成整合工作後,主分支上便多出一個新的合併提交(5399e),用 fetch 命令更新到本地後,提交歷史如圖 5-14 所示:

Git詳解之五 分散式Git

圖 5-14. 合併特性分支後的 Jessica 提交歷史

許多開發小組改用 Git 就是因為它允許多個小組間並行工作,而在稍後恰當時機再行合併。通過共享遠端分支的方式,無需干擾整體專案程式碼便可以開展工作,因此使用 Git 的小型團隊間協作可以變得非常靈活自由。以上工作流程的時序如圖 5-15 所示:

Git詳解之五 分散式Git

圖 5-15. 團隊間協作工作流程基本時序

公開的小型專案

上面說的是私有專案協作,但要給公開專案作貢獻,情況就有些不同了。因為你沒有直接更新主倉庫分支的許可權,得尋求其它方式把工作成果交給專案維護 人。下面會介紹兩種方法,第一種使用 git 託管服務商提供的倉庫複製功能,一般稱作 fork,比如 repo.or.cz 和 GitHub 都支援這樣的操作,而且許多專案管理員都希望大家使用這樣的方式。另一種方法是通過電子郵件寄送檔案補丁