接下來(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ì),一天大約只有幾次提交,如果不是什么熱門(mén)項(xiàng)目的話(huà)就更少了??梢窃诖蠊纠铮蛘叽箜?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)目是否有專(zhuān)人負(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)整,以滿(mǎn)足實(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 有種檢查此類(lèi)問(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
這樣在提交之前你就可以看到這類(lèi)問(wèn)題,及時(shí)解決以免困擾其他開(kāi)發(fā)者。
接下來(lái),請(qǐng)將每次提交限定于完成一次邏輯功能。并且可能的話(huà),適當(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ō)明最好限制在一行以?xún)?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è)字符以?xún)?nèi))
如果必要,此處展開(kāi)詳盡闡述。段落寬度限定在 72 個(gè)字符以?xún)?nèi)。
某些情況下,第一行的簡(jiǎn)要描述將用作郵件標(biāo)題,其余部分作為郵件正文。
其間的空行是必要的,以區(qū)分兩者(當(dāng)然沒(méi)有正文另當(dāng)別論)。
如果并在一起,rebase 這樣的工具就可能會(huì)迷惑。
另起空行后,再進(jìn)一步補(bǔ)充其他說(shuō)明。
可以使用這樣的條目列舉式。
一般以單個(gè)空格緊跟短劃線(xiàn)或者星號(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ú)法訪(fǎng)問(wèn)項(xiàng)目倉(cāng)庫(kù)。而你和其他開(kāi)發(fā)者則都具有推送數(shù)據(jù)到倉(cāng)庫(kù)的權(quán)限。
這種情況下,你們可以用 Subversion 或其他集中式版本控制系統(tǒng)類(lèi)似的工作流來(lái)協(xié)作。你仍然可以得到 Git 帶來(lái)的其他好處:離線(xiàn)提交,快速分支與合并等等,但工作流程還是差不多的。主要區(qū)別在于,合并操作發(fā)生在客戶(hù)端而非服務(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 的話(huà),這里其實(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 分支中訪(fǎng)問(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 上的更新(如果有的話(huà)),再推回遠(yuǎn)程服務(wù)器。一般的協(xié)作流程如圖 5-11 所示:
圖 5-11. 多用戶(hù)共享倉(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
我們稱(chēng)此為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)備好,可以并入主線(xià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ù)制功能,一般稱(chēng)作 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ù),姑且稱(chēng)為 myfork:
$ git remote add myfork (url)
你需要將本地更新推送到這個(gè)倉(cāng)庫(kù)。要是將遠(yuǎn)端 master 合并到本地再推回去,還不如把整個(gè)特性分支推上去來(lái)得干脆直接。而且,假若項(xiàng)目維護(hù)者未采納你的貢獻(xiàn)的話(huà)(不管是直接合并還是 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è)新的分支上去,比如稱(chēng)作 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è)工作流程類(lèi)似上面的情形:為每個(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)。
你可以用郵件客戶(hù)端軟件發(fā)送這些補(bǔ)丁文件,也可以直接在命令行發(fā)送。有些所謂智能的郵件客戶(hù)端軟件會(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
然后,你應(yīng)該去你到草稿箱去更改你要發(fā)送的補(bǔ)丁的收件人信息,以及需要抄送的人,然后發(fā)送它。
您也可以通過(guò)SMTP服務(wù)器發(fā)送補(bǔ)丁。和上面一樣,你可以通過(guò)git config命令單獨(dú)設(shè)置每個(gè)參數(shù),也可以在你的~/.gitconfig文件中的sendemail節(jié)點(diǎn)手動(dòng)添加它們。
[sendemail]
smtpencryption = tls
smtpserver = smtp.gmail.com
smtpuser = user@gmail.com
smtpserverport = 587
配置完成后,您可以使用git send-email來(lái)發(fā)送你的補(bǔ)?。?/p>
$ 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ǔ)丁依次輸出類(lèi)似下面的日志:
(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)理.
更多建議: