接下來(lái),我們來(lái)學(xué)習(xí)一下作為項(xiàng)目貢獻(xiàn)者,會(huì)有哪些常見(jiàn)的工作模式。
不過(guò)要說(shuō)清楚整個(gè)協(xié)作過(guò)程真的很難,Git 如此靈活,人們的協(xié)作方式便可以各式各樣,沒(méi)有固定不變的范式可循,而每個(gè)項(xiàng)目的具體情況又多少會(huì)有些不同,比如說(shuō)參與者的規(guī)模,所選擇的工作流程,每個(gè)人的提交權(quán)限,以及 Git 以外貢獻(xiàn)等等,都會(huì)影響到具體操作的細(xì)節(jié)。
首當(dāng)其沖的是參與者規(guī)模。項(xiàng)目中有多少開(kāi)發(fā)者是經(jīng)常提交代碼的?經(jīng)常又是多久呢?大多數(shù)兩至三人的小團(tuán)隊(duì),一天大約只有幾次提交,如果不是什么熱門項(xiàng)目的話就更少了。可要是在大公司里,或者大項(xiàng)目中,參與者可以多到上千,每天都會(huì)有十幾個(gè)上百個(gè)補(bǔ)丁提交上來(lái)。這種差異帶來(lái)的影響是顯著的,越是多的人參與進(jìn)來(lái),就越難保證每次合并正確無(wú)誤。你正在工作的代碼,可能會(huì)因?yàn)楹喜⑦M(jìn)來(lái)其他人的更新而變得過(guò)時(shí),甚至受創(chuàng)無(wú)法運(yùn)行。而已經(jīng)提交上去的更新,也可能在等著審核合并的過(guò)程中變得過(guò)時(shí)。那么,我們?cè)撛鯓幼霾拍艽_保代碼是最新的,提交的補(bǔ)丁也是可用的呢?
接下來(lái)便是項(xiàng)目所采用的工作流。是集中式的,每個(gè)開(kāi)發(fā)者都具有等同的寫(xiě)權(quán)限?項(xiàng)目是否有專人負(fù)責(zé)檢查所有補(bǔ)???是不是所有補(bǔ)丁都做過(guò)同行復(fù)閱(peer-review)再通過(guò)審核的?你是否參與審核過(guò)程?如果使用副官系統(tǒng),那你是不是限定于只能向此副官提交?
還有你的提交權(quán)限。有或沒(méi)有向主項(xiàng)目提交更新的權(quán)限,結(jié)果完全不同,直接決定最終采用怎樣的工作流。如果不能直接提交更新,那該如何貢獻(xiàn)自己的代碼呢?是不是該有個(gè)什么策略?你每次貢獻(xiàn)代碼會(huì)有多少量?提交頻率呢?
所有以上這些問(wèn)題都會(huì)或多或少影響到最終采用的工作流。接下來(lái),我會(huì)在一系列由簡(jiǎn)入繁的具體用例中,逐一闡述。此后在實(shí)踐時(shí),應(yīng)該可以借鑒這里的例子,略作調(diào)整,以滿足實(shí)際需要構(gòu)建自己的工作流。
開(kāi)始分析特定用例之前,先來(lái)了解下如何撰寫(xiě)提交說(shuō)明。一份好的提交指南可以幫助協(xié)作者更輕松更有效地配合。Git 項(xiàng)目本身就提供了一份文檔(Git 項(xiàng)目源代碼目錄中 Documentation/SubmittingPatches
),列數(shù)了大量提示,從如何編撰提交說(shuō)明到提交補(bǔ)丁,不一而足。
首先,請(qǐng)不要在更新中提交多余的白字符(whitespace)。Git 有種檢查此類問(wèn)題的方法,在提交之前,先運(yùn)行 git diff --check
,會(huì)把可能的多余白字符修正列出來(lái)。下面的示例,我已經(jīng)把終端中顯示為紅色的白字符用 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
這樣在提交之前你就可以看到這類問(wèn)題,及時(shí)解決以免困擾其他開(kāi)發(fā)者。
接下來(lái),請(qǐng)將每次提交限定于完成一次邏輯功能。并且可能的話,適當(dāng)?shù)胤纸鉃槎啻涡「?,以便每次小型提交都更易于理解。?qǐng)不要在周末窮追猛打一次性解決五個(gè)問(wèn)題,而最后拖到周一再提交。就算是這樣也請(qǐng)盡可能利用暫存區(qū)域,將之前的改動(dòng)分解為每次修復(fù)一個(gè)問(wèn)題,再分別提交和加注說(shuō)明。如果針對(duì)兩個(gè)問(wèn)題改動(dòng)的是同一個(gè)文件,可以試試看 git add --patch
的方式將部分內(nèi)容置入暫存區(qū)域(我們會(huì)在第六章再詳細(xì)介紹)。無(wú)論是五次小提交還是混雜在一起的大提交,最終分支末端的項(xiàng)目快照應(yīng)該還是一樣的,但分解開(kāi)來(lái)之后,更便于其他開(kāi)發(fā)者復(fù)閱。這么做也方便自己將來(lái)取消某個(gè)特定問(wèn)題的修復(fù)。我們將在第六章介紹一些重寫(xiě)提交歷史,同暫存區(qū)域交互的技巧和工具,以便最終得到一個(gè)干凈有意義,且易于理解的提交歷史。
最后需要謹(jǐn)記的是提交說(shuō)明的撰寫(xiě)。寫(xiě)得好可以讓大家協(xié)作起來(lái)更輕松。一般來(lái)說(shuō),提交說(shuō)明最好限制在一行以內(nèi),50 個(gè)字符以下,簡(jiǎn)明扼要地描述更新內(nèi)容,空開(kāi)一行后,再展開(kāi)詳細(xì)注解。Git 項(xiàng)目本身需要開(kāi)發(fā)者撰寫(xiě)詳盡注解,包括本次修訂的因由,以及前后不同實(shí)現(xiàn)之間的比較,我們也該借鑒這種做法。另外,提交說(shuō)明應(yīng)該用祈使現(xiàn)在式語(yǔ)態(tài),比如,不要說(shuō)成 “I added tests for” 或 “Adding tests for” 而應(yīng)該用 “Add tests for”。 下面是來(lái)自 tpope.net 的 Tim Pope 原創(chuàng)的提交說(shuō)明格式模版,供參考:
本次更新的簡(jiǎn)要描述(50 個(gè)字符以內(nèi))
如果必要,此處展開(kāi)詳盡闡述。段落寬度限定在 72 個(gè)字符以內(nèi)。
某些情況下,第一行的簡(jiǎn)要描述將用作郵件標(biāo)題,其余部分作為郵件正文。
其間的空行是必要的,以區(qū)分兩者(當(dāng)然沒(méi)有正文另當(dāng)別論)。
如果并在一起,rebase 這樣的工具就可能會(huì)迷惑。
另起空行后,再進(jìn)一步補(bǔ)充其他說(shuō)明。
- 可以使用這樣的條目列舉式。
- 一般以單個(gè)空格緊跟短劃線或者星號(hào)作為每項(xiàng)條目的起始符。每個(gè)條目間用一空行隔開(kāi)。
不過(guò)這里按自己項(xiàng)目的約定,可以略作變化。
如果你的提交說(shuō)明都用這樣的格式來(lái)書(shū)寫(xiě),好多事情就可以變得十分簡(jiǎn)單。Git 項(xiàng)目本身就是這樣要求的,我強(qiáng)烈建議你到 Git 項(xiàng)目倉(cāng)庫(kù)下運(yùn)行 git log --no-merges
看看,所有提交歷史的說(shuō)明是怎樣撰寫(xiě)的。(譯注:如果現(xiàn)在還沒(méi)有克隆 git 項(xiàng)目源代碼,是時(shí)候 git clone git://git.kernel.org/pub/scm/git/git.git
了。)
為簡(jiǎn)單起見(jiàn),在接下來(lái)的例子(及本書(shū)隨后的所有演示)中,我都不會(huì)用這種格式,而使用 -m
選項(xiàng)提交 git commit
。不過(guò)請(qǐng)還是按照我之前講的做,別學(xué)我這里偷懶的方式。
我們從最簡(jiǎn)單的情況開(kāi)始,一個(gè)私有項(xiàng)目,與你一起協(xié)作的還有另外一到兩位開(kāi)發(fā)者。這里說(shuō)私有,是指源代碼不公開(kāi),其他人無(wú)法訪問(wèn)項(xiàng)目倉(cāng)庫(kù)。而你和其他開(kāi)發(fā)者則都具有推送數(shù)據(jù)到倉(cāng)庫(kù)的權(quán)限。
這種情況下,你們可以用 Subversion 或其他集中式版本控制系統(tǒng)類似的工作流來(lái)協(xié)作。你仍然可以得到 Git 帶來(lái)的其他好處:離線提交,快速分支與合并等等,但工作流程還是差不多的。主要區(qū)別在于,合并操作發(fā)生在客戶端而非服務(wù)器上。 讓我們來(lái)看看,兩個(gè)開(kāi)發(fā)者一起使用同一個(gè)共享倉(cāng)庫(kù),會(huì)發(fā)生些什么。第一個(gè)人,John,克隆了倉(cāng)庫(kù),作了些更新,在本地提交。(下面的例子中省略了常規(guī)提示,用 ...
代替以節(jié)約版面。)
# John's Machine
$ git clone john@githost: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(-)
第二個(gè)開(kāi)發(fā)者,Jessica,一樣這么做:克隆倉(cāng)庫(kù),提交更新:
# Jessica's Machine
$ git clone jessica@githost: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(-)
現(xiàn)在,Jessica 將她的工作推送到服務(wù)器上:
# Jessica's Machine
$ git push origin master
...
To jessica@githost:simplegit.git
1edee6b..fbff5bc master -> master
John 也嘗試推送自己的工作上去:
# John's Machine
$ git push origin master
To john@githost:simplegit.git
! [rejected] master -> master (non-fast forward)
error: failed to push some refs to 'john@githost:simplegit.git'
John 的推送操作被駁回,因?yàn)?Jessica 已經(jīng)推送了新的數(shù)據(jù)上去。請(qǐng)注意,特別是你用慣了 Subversion 的話,這里其實(shí)修改的是兩個(gè)文件,而不是同一個(gè)文件的同一個(gè)地方。Subversion 會(huì)在服務(wù)器端自動(dòng)合并提交上來(lái)的更新,而 Git 則必須先在本地合并后才能推送。于是,John 不得不先把 Jessica 的更新拉下來(lái):
$ git fetch origin
...
From john@githost:simplegit
+ 049d078...fbff5bc master -> origin/master
此刻,John 的本地倉(cāng)庫(kù)如圖 5-4 所示:
圖 5-4. John 的倉(cāng)庫(kù)歷史
雖然 John 下載了 Jessica 推送到服務(wù)器的最近更新(fbff5),但目前只是 origin/master
指針指向它,而當(dāng)前的本地分支 master
仍然指向自己的更新(738ee),所以需要先把她的提交合并過(guò)來(lái),才能繼續(xù)推送數(shù)據(jù):
$ git merge origin/master
Merge made by recursive.
TODO | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
還好,合并過(guò)程非常順利,沒(méi)有沖突,現(xiàn)在 John 的提交歷史如圖 5-5 所示:
圖 5-5. 合并 origin/master 后 John 的倉(cāng)庫(kù)歷史
現(xiàn)在,John 應(yīng)該再測(cè)試一下代碼是否仍然正常工作,然后將合并結(jié)果(72bbc)推送到服務(wù)器上:
$ git push origin master
...
To john@githost:simplegit.git
fbff5bc..72bbc59 master -> master
最終,John 的提交歷史變?yōu)閳D 5-6 所示:
圖 5-6. 推送后 John 的倉(cāng)庫(kù)歷史
而在這段時(shí)間,Jessica 已經(jīng)開(kāi)始在另一個(gè)特性分支工作了。她創(chuàng)建了 issue54
并提交了三次更新。她還沒(méi)有下載 John 提交的合并結(jié)果,所以提交歷史如圖 5-7 所示:
圖 5-7. Jessica 的提交歷史
Jessica 想要先和服務(wù)器上的數(shù)據(jù)同步,所以先下載數(shù)據(jù):
# Jessica's Machine
$ git fetch origin
...
From jessica@githost:simplegit
fbff5bc..72bbc59 master -> origin/master
于是 Jessica 的本地倉(cāng)庫(kù)歷史多出了 John 的兩次提交(738ee 和 72bbc),如圖 5-8 所示:
圖 5-8. 獲取 John 的更新之后 Jessica 的提交歷史
此時(shí),Jessica 在特性分支上的工作已經(jīng)完成,但她想在推送數(shù)據(jù)之前,先確認(rèn)下要并進(jìn)來(lái)的數(shù)據(jù)究竟是什么,于是運(yùn)行 git log
查看:
$ git log --no-merges origin/master ^issue54
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <jsmith@example.com>
Date: Fri May 29 16:01:27 2009 -0700
removed invalid default value
現(xiàn)在,Jessica 可以將特性分支上的工作并到 master
分支,然后再并入 John 的工作(origin/master
)到自己的 master
分支,最后再推送回服務(wù)器。當(dāng)然,得先切回主分支才能集成所有數(shù)據(jù):
$ git checkout master
Switched to branch "master"
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
要合并 origin/master
或 issue54
分支,誰(shuí)先誰(shuí)后都沒(méi)有關(guān)系,因?yàn)樗鼈兌荚谏嫌危╱pstream)(譯注:想像分叉的更新像是匯流成河的源頭,所以上游 upstream 是指最新的提交),所以無(wú)所謂先后順序,最終合并后的內(nèi)容快照都是一樣的,而僅是提交歷史看起來(lái)會(huì)有些先后差別。Jessica 選擇先合并 issue54
:
$ git merge issue54
Updating fbff5bc..4af4298
Fast forward
README | 1 +
lib/simplegit.rb | 6 +++++-
2 files changed, 6 insertions(+), 1 deletions(-)
正如所見(jiàn),沒(méi)有沖突發(fā)生,僅是一次簡(jiǎn)單快進(jìn)?,F(xiàn)在 Jessica 開(kāi)始合并 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(-)
所有的合并都非常干凈?,F(xiàn)在 Jessica 的提交歷史如圖 5-9 所示:
圖 5-9. 合并 John 的更新后 Jessica 的提交歷史
現(xiàn)在 Jessica 已經(jīng)可以在自己的 master
分支中訪問(wèn) origin/master
的最新改動(dòng)了,所以她應(yīng)該可以成功推送最后的合并結(jié)果到服務(wù)器上(假設(shè) John 此時(shí)沒(méi)再推送新數(shù)據(jù)上來(lái)):
$ git push origin master
...
To jessica@githost:simplegit.git
72bbc59..8059c15 master -> master
至此,每個(gè)開(kāi)發(fā)者都提交了若干次,且成功合并了對(duì)方的工作成果,最新的提交歷史如圖 5-10 所示:
圖 5-10. Jessica 推送數(shù)據(jù)后的提交歷史
以上就是最簡(jiǎn)單的協(xié)作方式之一:先在自己的特性分支中工作一段時(shí)間,完成后合并到自己的 master
分支;然后下載合并 origin/master
上的更新(如果有的話),再推回遠(yuǎn)程服務(wù)器。一般的協(xié)作流程如圖 5-11 所示:
圖 5-11. 多用戶共享倉(cāng)庫(kù)協(xié)作方式的一般工作流程時(shí)序
現(xiàn)在我們來(lái)看更大一點(diǎn)規(guī)模的私有團(tuán)隊(duì)協(xié)作。如果有幾個(gè)小組分頭負(fù)責(zé)若干特性的開(kāi)發(fā)和集成,那他們之間的協(xié)作過(guò)程是怎樣的。
假設(shè) John 和 Jessica 一起負(fù)責(zé)開(kāi)發(fā)某項(xiàng)特性 A,而同時(shí) Jessica 和 Josie 一起負(fù)責(zé)開(kāi)發(fā)另一項(xiàng)功能 B。公司使用典型的集成管理員式工作流,每個(gè)組都有一名管理員負(fù)責(zé)集成本組代碼,及更新項(xiàng)目主倉(cāng)庫(kù)的 master
分支。所有開(kāi)發(fā)都在代表小組的分支上進(jìn)行。
讓我們跟隨 Jessica 的視角看看她的工作流程。她參與開(kāi)發(fā)兩項(xiàng)特性,同時(shí)和不同小組的開(kāi)發(fā)者一起協(xié)作。克隆生成本地倉(cāng)庫(kù)后,她打算先著手開(kāi)發(fā)特性 A。于是創(chuàng)建了新的 featureA
分支,繼而編寫(xiě)代碼:
# 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(-)
此刻,她需要分享目前的進(jìn)展給 John,于是她將自己的 featureA
分支提交到服務(wù)器。由于 Jessica 沒(méi)有權(quán)限推送數(shù)據(jù)到主倉(cāng)庫(kù)的 master
分支(只有集成管理員有此權(quán)限),所以只能將此分支推上去同 John 共享協(xié)作:
$ git push origin featureA
...
To jessica@githost:simplegit.git
* [new branch] featureA -> featureA
Jessica 發(fā)郵件給 John 讓他上來(lái)看看 featureA
分支上的進(jìn)展。在等待他的反饋之前,Jessica 決定繼續(xù)工作,和 Josie 一起開(kāi)發(fā) featureB
上的特性 B。當(dāng)然,先創(chuàng)建此分支,分叉點(diǎn)以服務(wù)器上的 master
為起點(diǎn):
# 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(-)
現(xiàn)在 Jessica 的更新歷史如圖 5-12 所示:
圖 5-12. Jessica 的更新歷史
Jessica 正準(zhǔn)備推送自己的進(jìn)展上去,卻收到 Josie 的來(lái)信,說(shuō)是她已經(jīng)將自己的工作推到服務(wù)器上的 featureBee
分支了。這樣,Jessica 就必須先將 Josie 的代碼合并到自己本地分支中,才能再一起推送回服務(wù)器。她用 git fetch
下載 Josie 的最新代碼:
$ git fetch origin
...
From jessica@githost: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(-)
合并很順利,但另外有個(gè)小問(wèn)題:她要推送自己的 featureB
分支到服務(wù)器上的 featureBee
分支上去。當(dāng)然,她可以使用冒號(hào)(:)格式指定目標(biāo)分支:
$ git push origin featureB:featureBee
...
To jessica@githost:simplegit.git
fba9af8..cd685d1 featureB -> featureBee
我們稱此為refspec。更多有關(guān)于 Git refspec 的討論和使用方式會(huì)在第九章作詳細(xì)闡述。
接下來(lái),John 發(fā)郵件給 Jessica 告訴她,他看了之后作了些修改,已經(jīng)推回服務(wù)器 featureA
分支,請(qǐng)她過(guò)目下。于是 Jessica 運(yùn)行 git fetch
下載最新數(shù)據(jù):
$ git fetch origin
...
From jessica@githost:simplegit
3300904..aad881d featureA -> origin/featureA
接下來(lái)便可以用 git log
查看更新了些什么:
$ git log origin/featureA ^featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith <jsmith@example.com>
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 稍做一番修整后同步到服務(wù)器:
$ git commit -am 'small tweak'
[featureA 774b3ed] small tweak
1 files changed, 1 insertions(+), 1 deletions(-)
$ git push origin featureA
...
To jessica@githost:simplegit.git
3300904..774b3ed featureA -> featureA
現(xiàn)在的 Jessica 提交歷史如圖 5-13 所示:
圖 5-13. 在特性分支中提交更新后的提交歷史
現(xiàn)在,Jessica,Josie 和 John 通知集成管理員服務(wù)器上的 featureA
及 featureBee
分支已經(jīng)準(zhǔn)備好,可以并入主線了。在管理員完成集成工作后,主分支上便多出一個(gè)新的合并提交(5399e),用 fetch 命令更新到本地后,提交歷史如圖 5-14 所示:
圖 5-14. 合并特性分支后的 Jessica 提交歷史
許多開(kāi)發(fā)小組改用 Git 就是因?yàn)樗试S多個(gè)小組間并行工作,而在稍后恰當(dāng)時(shí)機(jī)再行合并。通過(guò)共享遠(yuǎn)程分支的方式,無(wú)需干擾整體項(xiàng)目代碼便可以開(kāi)展工作,因此使用 Git 的小型團(tuán)隊(duì)間協(xié)作可以變得非常靈活自由。以上工作流程的時(shí)序如圖 5-15 所示:
圖 5-15. 團(tuán)隊(duì)間協(xié)作工作流程基本時(shí)序
上面說(shuō)的是私有項(xiàng)目協(xié)作,但要給公開(kāi)項(xiàng)目作貢獻(xiàn),情況就有些不同了。因?yàn)槟銢](méi)有直接更新主倉(cāng)庫(kù)分支的權(quán)限,得尋求其它方式把工作成果交給項(xiàng)目維護(hù)人。下面會(huì)介紹兩種方法,第一種使用 git 托管服務(wù)商提供的倉(cāng)庫(kù)復(fù)制功能,一般稱作 fork,比如 repo.or.cz 和 GitHub 都支持這樣的操作,而且許多項(xiàng)目管理員都希望大家使用這樣的方式。另一種方法是通過(guò)電子郵件寄送文件補(bǔ)丁。
但不管哪種方式,起先我們總需要克隆原始倉(cāng)庫(kù),而后創(chuàng)建特性分支開(kāi)展工作?;竟ぷ髁鞒倘缦拢?/p>
$ git clone (url)
$ cd project
$ git checkout -b featureA
$ (work)
$ git commit
$ (work)
$ git commit
你可能想到用 rebase -i
將所有更新先變作單個(gè)提交,又或者想重新安排提交之間的差異補(bǔ)丁,以方便項(xiàng)目維護(hù)者審閱 -- 有關(guān)交互式衍合操作的細(xì)節(jié)見(jiàn)第六章。
在完成了特性分支開(kāi)發(fā),提交給項(xiàng)目維護(hù)者之前,先到原始項(xiàng)目的頁(yè)面上點(diǎn)擊“Fork”按鈕,創(chuàng)建一個(gè)自己可寫(xiě)的公共倉(cāng)庫(kù)(譯注:即下面的 url 部分,參照后續(xù)的例子,應(yīng)該是 git://githost/simplegit.git
)。然后將此倉(cāng)庫(kù)添加為本地的第二個(gè)遠(yuǎn)端倉(cāng)庫(kù),姑且稱為 myfork
:
$ git remote add myfork (url)
你需要將本地更新推送到這個(gè)倉(cāng)庫(kù)。要是將遠(yuǎn)端 master 合并到本地再推回去,還不如把整個(gè)特性分支推上去來(lái)得干脆直接。而且,假若項(xiàng)目維護(hù)者未采納你的貢獻(xiàn)的話(不管是直接合并還是 cherry pick),都不用回退(rewind)自己的 master 分支。但若維護(hù)者合并或 cherry-pick 了你的工作,最后總還可以從他們的更新中同步這些代碼。好吧,現(xiàn)在先把 featureA 分支整個(gè)推上去:
$ git push myfork featureA
然后通知項(xiàng)目管理員,讓他來(lái)抓取你的代碼。通常我們把這件事叫做 pull request??梢灾苯佑?GitHub 等網(wǎng)站提供的 “pull request” 按鈕自動(dòng)發(fā)送請(qǐng)求通知;或手工把 git request-pull
命令輸出結(jié)果電郵給項(xiàng)目管理員。
request-pull
命令接受兩個(gè)參數(shù),第一個(gè)是本地特性分支開(kāi)始前的原始分支,第二個(gè)是請(qǐng)求對(duì)方來(lái)抓取的 Git 倉(cāng)庫(kù) URL(譯注:即下面 myfork
所指的,自己可寫(xiě)的公共倉(cāng)庫(kù))。比如現(xiàn)在Jessica 準(zhǔn)備要給 John 發(fā)一個(gè) pull requst,她之前在自己的特性分支上提交了兩次更新,并把分支整個(gè)推到了服務(wù)器上,所以運(yùn)行該命令會(huì)看到:
$ git request-pull origin/master myfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
John Smith (1):
added a new function
are available in the git repository at:
git://githost/simplegit.git featureA
Jessica Smith (2):
add limit to log function
change log output to 30 from 25
lib/simplegit.rb | 10 +++++++++-
1 files changed, 9 insertions(+), 1 deletions(-)
輸出的內(nèi)容可以直接發(fā)郵件給管理者,他們就會(huì)明白這是從哪次提交開(kāi)始旁支出去的,該到哪里去抓取新的代碼,以及新的代碼增加了哪些功能等等。
像這樣隨時(shí)保持自己的 master
分支和官方 origin/master
同步,并將自己的工作限制在特性分支上的做法,既方便又靈活,采納和丟棄都輕而易舉。就算原始主干發(fā)生變化,我們也能重新衍合提供新的補(bǔ)丁。比如現(xiàn)在要開(kāi)始第二項(xiàng)特性的開(kāi)發(fā),不要在原來(lái)已推送的特性分支上繼續(xù),還是按原始 master
開(kāi)始:
$ git checkout -b featureB origin/master
$ (work)
$ git commit
$ git push myfork featureB
$ (email maintainer)
$ git fetch origin
現(xiàn)在,A、B 兩個(gè)特性分支各不相擾,如同竹筒里的兩顆豆子,隊(duì)列中的兩個(gè)補(bǔ)丁,你隨時(shí)都可以分別從頭寫(xiě)過(guò),或者衍合,或者修改,而不用擔(dān)心特性代碼的交叉混雜。如圖 5-16 所示:
圖 5-16. featureB 以后的提交歷史
假設(shè)項(xiàng)目管理員接納了許多別人提交的補(bǔ)丁后,準(zhǔn)備要采納你提交的第一個(gè)分支,卻發(fā)現(xiàn)因?yàn)榇a基準(zhǔn)不一致,合并工作無(wú)法正確干凈地完成。這就需要你再次衍合到最新的 origin/master
,解決相關(guān)沖突,然后重新提交你的修改:
$ git checkout featureA
$ git rebase origin/master
$ git push -f myfork featureA
自然,這會(huì)重寫(xiě)提交歷史,如圖 5-17 所示:
圖 5-17. featureA 重新衍合后的提交歷史
注意,此時(shí)推送分支必須使用 -f
選項(xiàng)(譯注:表示 force,不作檢查強(qiáng)制重寫(xiě))替換遠(yuǎn)程已有的 featureA
分支,因?yàn)樾碌?commit 并非原來(lái)的后續(xù)更新。當(dāng)然你也可以直接推送到另一個(gè)新的分支上去,比如稱作 featureAv2
。
再考慮另一種情形:管理員看過(guò)第二個(gè)分支后覺(jué)得思路新穎,但想請(qǐng)你改下具體實(shí)現(xiàn)。我們只需以當(dāng)前 origin/master
分支為基準(zhǔn),開(kāi)始一個(gè)新的特性分支 featureBv2
,然后把原來(lái)的 featureB
的更新拿過(guò)來(lái),解決沖突,按要求重新實(shí)現(xiàn)部分代碼,然后將此特性分支推送上去:
$ git checkout -b featureBv2 origin/master
$ git merge --no-commit --squash featureB
$ (change implementation)
$ git commit
$ git push myfork featureBv2
這里的 --squash
選項(xiàng)將目標(biāo)分支上的所有更改全拿來(lái)應(yīng)用到當(dāng)前分支上,而 --no-commit
選項(xiàng)告訴 Git 此時(shí)無(wú)需自動(dòng)生成和記錄(合并)提交。這樣,你就可以在原來(lái)代碼基礎(chǔ)上,繼續(xù)工作,直到最后一起提交。
好了,現(xiàn)在可以請(qǐng)管理員抓取 featureBv2
上的最新代碼了,如圖 5-18 所示:
圖 5-18. featureBv2 之后的提交歷史
許多大型項(xiàng)目都會(huì)立有一套自己的接受補(bǔ)丁流程,你應(yīng)該注意下其中細(xì)節(jié)。但多數(shù)項(xiàng)目都允許通過(guò)開(kāi)發(fā)者郵件列表接受補(bǔ)丁,現(xiàn)在我們來(lái)看具體例子。
整個(gè)工作流程類似上面的情形:為每個(gè)補(bǔ)丁創(chuàng)建獨(dú)立的特性分支,而不同之處在于如何提交這些補(bǔ)丁。不需要?jiǎng)?chuàng)建自己可寫(xiě)的公共倉(cāng)庫(kù),也不用將自己的更新推送到自己的服務(wù)器,你只需將每次提交的差異內(nèi)容以電子郵件的方式依次發(fā)送到郵件列表中即可。
$ git checkout -b topicA
$ (work)
$ git commit
$ (work)
$ git commit
如此一番后,有了兩個(gè)提交要發(fā)到郵件列表。我們可以用 git format-patch
命令來(lái)生成 mbox 格式的文件然后作為附件發(fā)送。每個(gè)提交都會(huì)封裝為一個(gè) .patch
后綴的 mbox 文件,但其中只包含一封郵件,郵件標(biāo)題就是提交消息(譯注:額外有前綴,看例子),郵件內(nèi)容包含補(bǔ)丁正文和 Git 版本號(hào)。這種方式的妙處在于接受補(bǔ)丁時(shí)仍可保留原來(lái)的提交消息,請(qǐng)看接下來(lái)的例子:
$ git format-patch -M origin/master
0001-add-limit-to-log-function.patch
0002-changed-log-output-to-30-from-25.patch
format-patch
命令依次創(chuàng)建補(bǔ)丁文件,并輸出文件名。上面的 -M
選項(xiàng)允許 Git 檢查是否有對(duì)文件重命名的提交。我們來(lái)看看補(bǔ)丁文件的內(nèi)容:
$ cat 0001-add-limit-to-log-function.patch
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] add limit to log function
Limit log functionality to the first 20
---
lib/simplegit.rb | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 76f47bc..f9815f1 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -14,7 +14,7 @@ class SimpleGit
end
def log(treeish = 'master')
- command("git log #{treeish}")
+ command("git log -n 20 #{treeish}")
end
def ls_tree(treeish = 'master')
--
1.6.2.rc1.20.g8c5b.dirty
如果有額外信息需要補(bǔ)充,但又不想放在提交消息中說(shuō)明,可以編輯這些補(bǔ)丁文件,在第一個(gè) ---
行之前添加說(shuō)明,但不要修改下面的補(bǔ)丁正文,比如例子中的 Limit log functionality to the first 20
部分。這樣,其它開(kāi)發(fā)者能閱讀,但在采納補(bǔ)丁時(shí)不會(huì)將此合并進(jìn)來(lái)。
你可以用郵件客戶端軟件發(fā)送這些補(bǔ)丁文件,也可以直接在命令行發(fā)送。有些所謂智能的郵件客戶端軟件會(huì)自作主張幫你調(diào)整格式,所以粘貼補(bǔ)丁到郵件正文時(shí),有可能會(huì)丟失換行符和若干空格。Git 提供了一個(gè)通過(guò) IMAP 發(fā)送補(bǔ)丁文件的工具。接下來(lái)我會(huì)演示如何通過(guò) Gmail 的 IMAP 服務(wù)器發(fā)送。另外,在 Git 源代碼中有個(gè) Documentation/SubmittingPatches
文件,可以仔細(xì)讀讀,看看其它郵件程序的相關(guān)導(dǎo)引。
首先在 ~/.gitconfig
文件中配置 imap 項(xiàng)。每個(gè)選項(xiàng)都可用 git config
命令分別設(shè)置,當(dāng)然直接編輯文件添加以下內(nèi)容更便捷:
[imap]
folder = "[Gmail]/Drafts"
host = imaps://imap.gmail.com
user = user@gmail.com
pass = p4ssw0rd
port = 993
sslverify = false
如果你的 IMAP 服務(wù)器沒(méi)有啟用 SSL,就無(wú)需配置最后那兩行,并且 host 應(yīng)該以 imap://
開(kāi)頭而不再是有 s
的 imaps://
。 保存配置文件后,就能用 git send-email
命令把補(bǔ)丁作為郵件依次發(fā)送到指定的 IMAP 服務(wù)器上的文件夾中(譯注:這里就是 Gmail 的 [Gmail]/Drafts
文件夾。但如果你的語(yǔ)言設(shè)置不是英文,此處的文件夾 Drafts 字樣會(huì)變?yōu)閷?duì)應(yīng)的語(yǔ)言。):
$ cat *.patch |git imap-send
Resolving imap.gmail.com... ok
Connecting to [74.125.142.109]:993... ok
Logging in...
sending 2 messages
100% (2/2) done
At this point, you should be able to go to your Drafts folder, change the To field to the mailing list you’re sending the patch to, possibly CC the maintainer or person responsible for that section, and send it off.
You can also send the patches through an SMTP server. As before, you can set each value separately with a series of git config
commands, or you can add them manually in the sendemail section in your ~/.gitconfig
file:
[sendemail]
smtpencryption = tls
smtpserver = smtp.gmail.com
smtpuser = user@gmail.com
smtpserverport = 587
After this is done, you can use git send-email
to send your patches:
$ git send-email *.patch
0001-added-limit-to-log-function.patch
0002-changed-log-output-to-30-from-25.patch
Who should the emails appear to be from? [Jessica Smith <jessica@example.com>]
Emails will be sent from: Jessica Smith <jessica@example.com>
Who should the emails be sent to? jessica@example.com
Message-ID to be used as In-Reply-To for the first email? y
接下來(lái),Git 會(huì)根據(jù)每個(gè)補(bǔ)丁依次輸出類似下面的日志:
(mbox) Adding cc: Jessica Smith <jessica@example.com> from
\line 'From: Jessica Smith <jessica@example.com>'
OK. Log says:
Sendmail: /usr/sbin/sendmail -i jessica@example.com
From: Jessica Smith <jessica@example.com>
To: jessica@example.com
Subject: [PATCH 1/2] added limit to log function
Date: Sat, 30 May 2009 13:29:15 -0700
Message-Id: <1243715356-61726-1-git-send-email-jessica@example.com>
X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty
In-Reply-To: <y>
References: <y>
Result: OK
本節(jié)主要介紹了常見(jiàn) Git 項(xiàng)目協(xié)作的工作流程,還有一些幫助處理這些工作的命令和工具。接下來(lái)我們要看看如何維護(hù) Git 項(xiàng)目,并成為一個(gè)合格的項(xiàng)目管理員,或是集成經(jīng)理。
更多建議: