章節 ▾ 第二版

5.2 分散式 Git - 為專案做貢獻

為專案做貢獻

描述如何為一個專案做貢獻的主要難點在於其數量眾多的變化形式。因為 Git 非常靈活,人們可以並且確實以多種方式協同工作,因此要描述你“應該”如何貢獻會很麻煩——每個專案都有所不同。涉及的變數包括活躍貢獻者數量、選定的工作流程、你的提交許可權,以及可能的外部貢獻方式。

第一個變數是活躍貢獻者數量——有多少使用者正在積極地為該專案貢獻程式碼,以及多久貢獻一次?在許多情況下,你會有兩三個開發者每天有幾次提交,或者對於一些相對沉寂的專案可能更少。對於大型公司或專案,開發者數量可能高達數千人,每天有數百甚至數千次提交。這一點很重要,因為隨著開發者人數的增加,在確保你的程式碼能夠乾淨地應用或輕鬆合併方面,你遇到的問題也會越多。你提交的更改可能會因為在你工作期間或你的更改等待稽核批准時合併進來的工作而變得過時或嚴重損壞。你如何才能保持你的程式碼始終最新,並且你的提交都有效?

下一個變數是專案正在使用的工作流程。它是中心化的嗎,每個開發者對主程式碼行都有平等的寫入許可權?專案是否有維護者或整合經理來稽核所有補丁?所有的補丁都經過同行評審和批准了嗎?你是否參與了這個過程?是否存在一個代理人系統,你必須首先將你的工作提交給他們嗎?

下一個變數是你的提交許可權。如果你擁有專案的寫入許可權,那麼貢獻專案所需的工作流程將與你沒有寫入許可權時大不相同。如果你沒有寫入許可權,專案傾向於如何接受貢獻的工作?它有政策嗎?你一次貢獻多少工作?你多久貢獻一次?

所有這些問題都會影響你如何有效地為一個專案做出貢獻,以及哪些工作流程對你來說是首選或可用的。我們將在一系列用例中涵蓋其中每個方面,從簡單到複雜;你應該能夠根據這些示例構建你在實踐中需要的具體工作流程。

提交指南

在我們開始檢視具體的用例之前,這裡有一個關於提交訊息的快速說明。擁有建立提交的良好指南並堅持執行,可以使使用 Git 和與他人協作變得更加容易。Git 專案提供了一份文件,其中包含許多關於建立提交以提交補丁的技巧——你可以在 Git 原始碼的 Documentation/SubmittingPatches 檔案中閱讀它。

首先,你的提交不應包含任何空白錯誤。Git 提供了一種簡單的方法來檢查這一點——在提交之前,執行 git diff --check,它會識別潛在的空白錯誤並列出它們。

Output of `git diff --check`
圖 56. git diff --check 的輸出

如果在提交之前執行該命令,你可以判斷你是否即將提交可能會讓其他開發者感到惱火的空白問題。

接下來,儘量使每次提交都成為一個邏輯上獨立的變更集。如果可能,儘量使你的更改易於消化——不要花整個週末在一個星期五的不同問題上編碼,然後在星期一將它們全部作為一個巨大的提交提交。即使你不在週末提交,也要在星期一使用暫存區將你的工作分成至少每個問題一個提交,併為每個提交新增有用的訊息。如果某些更改修改了同一個檔案,嘗試使用 git add --patch 部分暫存檔案(在 互動式暫存 中有詳細介紹)。只要所有更改都在某個時候被新增,無論你做一個提交還是五個提交,分支頂端的專案快照都是相同的,所以請儘量讓你的同事在審查你的更改時更容易。

這種方法也使得以後需要時更容易提取或撤銷其中一個變更集。重寫歷史 描述了許多有用的 Git 技巧,用於重寫歷史和互動式暫存檔案——使用這些工具來幫助構建乾淨且易於理解的歷史,然後再將工作傳送給他人。

最後需要記住的是提交訊息。養成建立高質量提交訊息的習慣,可以使使用 Git 和與 Git 協作變得更容易。通常,你的訊息應該以一個不超過大約 50 個字元的單行開頭,簡潔地描述變更集,然後是一個空行,然後是一個更詳細的解釋。Git 專案要求更詳細的解釋包含你進行更改的動機,並對比其實現與之前的行為——這是一個值得遵循的好指南。用祈使句寫你的提交訊息:“修復 bug” 而不是“修復了 bug”或“修復了 bug”。這是一個你可以遵循的模板,我們從 Tim Pope 最初寫的一個 稍作改編

Capitalized, short (50 chars or less) summary

More detailed explanatory text, if necessary.  Wrap it to about 72
characters or so.  In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body.  The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase will confuse you if you run the
two together.

Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
or "Fixes bug."  This convention matches up with commit messages generated
by commands like git merge and git revert.

Further paragraphs come after blank lines.

- Bullet points are okay, too

- Typically a hyphen or asterisk is used for the bullet, followed by a
  single space, with blank lines in between, but conventions vary here

- Use a hanging indent

如果你的所有提交訊息都遵循這個模型,那麼你和你協作的開發者都會更容易。Git 專案有格式良好的提交訊息——嘗試在那裡執行 git log --no-merges,看看一個格式良好的專案提交歷史是什麼樣的。

注意
照我們說的做,但不要照我們做的做。

為了簡潔起見,本書中的許多示例都沒有像這樣格式良好的提交訊息;相反,我們只是使用 git commit-m 選項。

總之,照我們說的做,但不要照我們做的做。

私有小團隊

你最可能遇到的最簡單的設定是與一兩個其他開發者合作的一個私有專案。“私有”在此上下文中意味著閉源——不對外界公開。你和其他開發者都有對倉庫的推送許可權。

在這種環境中,你可以遵循與使用 Subversion 或其他集中式系統時類似的工作流程。你仍然可以獲得離線提交以及大大簡化的分支和合並等優勢,但工作流程可以非常相似;主要區別在於合併發生在客戶端而不是在提交時發生在伺服器上。讓我們看看當兩個開發者開始使用共享倉庫協同工作時可能是什麼樣子。第一個開發者 John 克隆了倉庫,進行更改,並在本地提交。協議訊息在這些示例中已被替換為 …​ 以縮短它們。

# John's Machine
$ git clone john@githost:simplegit.git
Cloning into 'simplegit'...
...
$ cd simplegit/
$ vim lib/simplegit.rb
$ git commit -am 'Remove invalid default value'
[master 738ee87] Remove invalid default value
 1 files changed, 1 insertions(+), 1 deletions(-)

第二個開發者 Jessica 也做了同樣的事情——克隆了倉庫並提交了一個更改。

# Jessica's Machine
$ git clone jessica@githost:simplegit.git
Cloning into 'simplegit'...
...
$ 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 jessica@githost:simplegit.git
   1edee6b..fbff5bc  master -> master

上面輸出的最後一行顯示了 push 操作的一個有用的返回訊息。基本格式是 <oldref>..<newref> fromref → toref,其中 oldref 表示舊引用,newref 表示新引用,fromref 是正在推送的本地引用的名稱,toref 是正在更新的遠端引用的名稱。你將在下面的討論中看到類似的輸出,因此對含義有一個基本瞭解將有助於理解倉庫的各種狀態。更多細節可在 git-push 的文件中找到。

繼續這個例子,不久之後,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 的推送由於 Jessica 之前推送了她的更改而失敗。如果你習慣使用 Subversion,這一點尤其重要,因為你會注意到兩個開發者沒有編輯同一個檔案。雖然 Subversion 在編輯不同檔案時會在伺服器上自動進行此類合併,但在 Git 中,你必須首先在本地合併提交。換句話說,John 必須首先獲取 Jessica 的上游更改並將其合併到他的本地倉庫,然後他才會被允許推送。

作為第一步,John 獲取了 Jessica 的工作(這只是獲取了 Jessica 的上游工作,尚未將其合併到 John 的工作中)。

$ git fetch origin
...
From john@githost:simplegit
 + 049d078...fbff5bc master     -> origin/master

此時,John 的本地倉庫看起來是這樣的。

John’s divergent history
圖 57. John 分叉的歷史

現在 John 可以將他獲取的 Jessica 的工作合併到他自己的本地工作。

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

只要本地合併順利進行,John 更新後的歷史現在看起來是這樣的。

John’s repository after merging `origin/master`
圖 58. John 合併 origin/master 後的倉庫

此時,John 可能想測試這段新程式碼,以確保 Jessica 的任何工作都不會影響到他自己的程式碼,並且只要一切看起來正常,他就可以最終將新的合併工作推送到伺服器。

$ git push origin master
...
To john@githost:simplegit.git
   fbff5bc..72bbc59  master -> master

最終,John 的提交歷史將看起來是這樣的。

John’s history after pushing to the `origin` server
圖 59. John 推送到 origin 伺服器後的歷史

與此同時,Jessica 建立了一個名為 issue54 的主題分支,並向該分支提交了三個提交。她還沒有獲取 John 的更改,所以她的提交歷史看起來是這樣的。

Jessica’s topic branch
圖 60. Jessica 的主題分支

突然,Jessica 得知 John 已經將一些新工作推送到伺服器,她想看一眼,這樣她就可以用以下命令獲取她還沒有的所有伺服器上的新內容。

# Jessica's Machine
$ git fetch origin
...
From jessica@githost:simplegit
   fbff5bc..72bbc59  master     -> origin/master

這會拉取 John 此前推送的工作。Jessica 的歷史現在看起來是這樣的。

Jessica’s history after fetching John’s changes
圖 61. Jessica 獲取 John 的更改後的歷史

Jessica 認為她的主題分支已經準備好了,但她想知道她需要將 John 的哪些已獲取的工作合併到她的工作中,以便她可以推送。她執行 git log 來找出。

$ git log --no-merges issue54..origin/master
commit 738ee872852dfaa9d6634e0dea7a324040193016
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 16:01:27 2009 -0700

   Remove invalid default value

issue54..origin/master 語法是一個日誌過濾器,它要求 Git 只顯示後者分支(在此例中為 origin/master)上存在但不在第一個分支(在此例中為 issue54)上的提交。我們將在 提交範圍 中詳細介紹這種語法。

從上面的輸出中,我們可以看到 John 已經完成了一個提交,而 Jessica 還沒有將其合併到她的本地工作中。如果她合併 origin/master,這將是修改她本地工作的唯一提交。

現在,Jessica 可以將她的主題工作合併到她的 master 分支,將 John 的工作(origin/master)合併到她的 master 分支,然後再次推送到伺服器。

首先(已經提交了她 issue54 主題分支上的所有工作),Jessica 切回到她的 master 分支,準備整合所有這些工作。

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

Jessica 可以先合併 origin/masterissue54——它們都在上游,所以順序無關緊要。最終快照應該與她選擇的順序無關,只有歷史記錄會不同。她選擇先合併 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 the 'recursive' strategy.
 lib/simplegit.rb |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

一切合並順利,Jessica 的歷史現在看起來是這樣的。

Jessica’s history after merging John’s changes
圖 62. Jessica 合併 John 的更改後的歷史

現在 origin/master 可以從 Jessica 的 master 分支到達,所以她應該能夠成功推送(假設 John 此刻沒有推送更多更改)。

$ git push origin master
...
To jessica@githost:simplegit.git
   72bbc59..8059c15  master -> master

每個開發者都提交了幾次,併成功地合併了彼此的工作。

Jessica’s history after pushing all changes back to the server
圖 63. Jessica 將所有更改推回伺服器後的歷史

這是最簡單的工作流程之一。你工作一段時間(通常是在一個主題分支上),並在準備好整合時將其合併到你的 master 分支。當你想共享這項工作時,如果你拉取了 origin/master 的更改,則將其合併到你的 master 中,然後推送到伺服器上的 master 分支。一般順序如下。

General sequence of events for a simple multiple-developer Git workflow
圖 64. 簡單多開發者 Git 工作流程的通用事件順序

私有管理團隊

在下一個場景中,我們將探討一個更大私有團隊中的貢獻者角色。你將學習如何在這樣一個環境中工作:小團隊協同開發功能,然後這些基於團隊的貢獻由其他人整合。

假設 John 和 Jessica 正在合作開發一個功能(稱為“featureA”),而 Jessica 和第三位開發者 Josie 正在開發第二個功能(稱為“featureB”)。在這種情況下,公司採用了一種整合經理工作流程,其中個人團隊的工作只由某些工程師整合,並且主倉庫的 master 分支只能由這些工程師更新。在這種場景下,所有工作都在基於團隊的分支上進行,並在稍後由整合者整合。

讓我們跟蹤 Jessica 的工作流程,她在環境中並行與兩位不同的開發者合作開發她的兩個功能。假設她已經克隆了她的倉庫,她決定先處理 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 -u origin featureA
...
To jessica@githost:simplegit.git
 * [new branch]      featureA -> featureA

Jessica 傳送電子郵件給 John 告訴他她已經推送了一些工作到一個名為 featureA 的分支,他現在可以查看了。在等待 John 的反饋期間,Jessica 決定與 Josie 一起開始開發 featureB。首先,她啟動一個新的功能分支,並基於伺服器的 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 'Make ls-tree function recursive'
[featureB e5b0fdc] Make 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 的倉庫現在看起來是這樣的。

Jessica’s initial commit history
圖 65. Jessica 的初始提交歷史

她準備推送她的工作,但收到 Josie 的電子郵件,說一個包含一些初始“featureB”工作的分支已經作為 featureBee 分支推送到伺服器了。Jessica 需要在推送她的工作到伺服器之前將這些更改與她自己的合併。Jessica 首先使用 git fetch 獲取 Josie 的更改。

$ git fetch origin
...
From jessica@githost:simplegit
 * [new branch]      featureBee -> origin/featureBee

假設 Jessica 仍然處於檢出的 featureB 分支,她現在可以使用 git merge 將 Josie 的工作合併到該分支。

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

此時,Jessica 希望將所有這些合併的“featureB”工作推回到伺服器,但她不想簡單地推送她自己的 featureB 分支。相反,由於 Josie 已經啟動了一個上游 featureBee 分支,Jessica 希望推送到那個分支,她透過以下方式完成:

$ git push -u origin featureB:featureBee
...
To jessica@githost:simplegit.git
   fba9af8..cd685d1  featureB -> featureBee

這稱為refspec。有關 Git refspec 的更詳細討論以及你可以用它們做的不同事情,請參閱 Refspec。另外請注意 -u 標誌;這是 --set-upstream 的縮寫,它配置分支以便將來更輕鬆地進行推送和拉取。

突然,Jessica 收到 John 的電子郵件,他告訴她他已經將一些更改推送到他們正在協作的 featureA 分支,並請 Jessica 檢視。同樣,Jessica 執行一個簡單的 git fetch 來獲取伺服器上的*所有*新內容,當然也包括 John 的最新工作。

$ git fetch origin
...
From jessica@githost:simplegit
   3300904..aad881d  featureA   -> origin/featureA

Jessica 可以透過比較新獲取的 featureA 分支的內容與她對同一分支的本地副本來顯示 John 新工作的日誌。

$ git log featureA..origin/featureA
commit aad881d154acdaeb2b6b18ea0e827ed8a6d671e6
Author: John Smith <jsmith@example.com>
Date:   Fri May 29 19:57:33 2009 -0700

    Increase log output to 30 from 25

如果 Jessica 喜歡她所看到的,她可以使用以下命令將 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 可能想對所有這些合併的內容做一些小的修改,所以她可以自由地進行這些修改,將它們提交到她本地的 featureA 分支,然後將最終結果推回伺服器。

$ git commit -am 'Add small tweak to merged content'
[featureA 774b3ed] Add small tweak to merged content
 1 files changed, 1 insertions(+), 1 deletions(-)
$ git push
...
To jessica@githost:simplegit.git
   3300904..774b3ed  featureA -> featureA

Jessica 的提交歷史現在看起來是這樣的。

Jessica’s history after committing on a feature branch
圖 66. Jessica 在功能分支上提交後的歷史

在某個時候,Jessica、Josie 和 John 通知整合者,伺服器上的 featureAfeatureBee 分支已準備好整合到主線。在整合者將這些分支合併到主線後,一次 fetch 會拉取新的合併提交,使歷史看起來像這樣。

Jessica’s history after merging both her topic branches
圖 67. Jessica 合併了她的兩個主題分支後的歷史

許多團隊之所以轉向 Git,是因為這種能夠讓多個團隊並行工作,並在過程後期合併不同工作線路的能力。團隊中較小組能夠透過遠端分支協作,而不必 necessariamente讓整個團隊參與或受阻,這是 Git 的巨大優勢。這裡看到的工作流程的順序如下。

Basic sequence of this managed-team workflow
圖 68. 此管理團隊工作流程的基本順序

Forked 公共專案

為公共專案做貢獻有些不同。因為你沒有直接更新專案分支的許可權,所以你必須以某種其他方式將工作交給維護者。第一個示例描述了透過 Git 主機支援的易於 Fork 的方式進行貢獻。許多託管站點支援這一點(包括 GitHub、BitBucket、repo.or.cz 等),並且許多專案維護者都期望這種貢獻方式。下一節將討論偏好透過電子郵件接受貢獻補丁的專案。

首先,你可能想克隆主倉庫,為你要貢獻的補丁或補丁系列建立一個主題分支,並在那裡完成你的工作。順序基本如下。

$ git clone <url>
$ cd project
$ git checkout -b featureA
  ... work ...
$ git commit
  ... work ...
$ git commit
注意

你可能想使用 rebase -i 將你的工作壓縮成一個提交,或者重新排列提交中的工作,以便維護者更容易審查補丁——有關互動式 rebase 的更多資訊,請參閱 重寫歷史

當你的分支工作完成後,你準備好將其貢獻給維護者時,請轉到原始專案頁面,點選“Fork”按鈕,建立你自己的可寫專案副本。然後,你需要將此倉庫 URL 新增為本地倉庫的新遠端;在這個例子中,我們稱之為 myfork

$ git remote add myfork <url>

然後,你需要將你的新工作推送到這個倉庫。將你正在處理的主題分支推送到你的 Forked 倉庫比將其合併到你的 master 分支並推送該分支更容易。原因是,如果你的工作未被接受或被 cherry-pick,你就不必回滾你的 master 分支(Git 的 cherry-pick 操作在 Rebase 和 Cherry-Pick 工作流程 中有更詳細的介紹)。如果維護者 mergerebasecherry-pick 你的工作,你最終還是可以透過拉取他們的倉庫來獲得。

無論如何,你可以透過以下方式推送你的工作:

$ git push -u myfork featureA

一旦你的工作被推送到你的 Forked 倉庫,你就需要通知原始專案的維護者你有希望他們合併的工作。這通常稱為pull request,你通常透過網站——GitHub 有自己的“Pull Request”機制,我們將在 GitHub 中介紹——生成此類請求,或者你可以執行 git request-pull 命令並將後續輸出透過電子郵件傳送給專案維護者。

git request-pull 命令接受你希望將主題分支拉入的基礎分支以及他們應該從中拉取的 Git 倉庫 URL,並生成你要求拉取的所有更改的摘要。例如,如果 Jessica 想給 John 傳送一個 pull request,並且她已經提交了她剛剛推送的主題分支上的兩個提交,她可以執行這個命令:

$ git request-pull origin/master myfork
The following changes since commit 1edee6b1d61823a2de3b09c160d7080b8d1b3a40:
Jessica Smith (1):
        Create new function

are available in the git repository at:

  https://githost/simplegit.git featureA

Jessica Smith (2):
      Add limit to log function
      Increase log output to 30 from 25

 lib/simplegit.rb |   10 +++++++++-
 1 files changed, 9 insertions(+), 1 deletions(-)

此輸出可以傳送給維護者——它告訴他們工作是從哪裡分支出來的,總結了提交,並指明瞭新工作應該從哪裡拉取。

在一個你不是維護者的專案上,通常最好讓一個像 master 這樣的分支始終跟蹤 origin/master,並在主題分支上進行工作,如果它們被拒絕,你可以輕鬆丟棄。將工作主題隔離到主題分支也使你更容易在主倉庫的尖端移動的情況下 rebase 你的工作,並且你的提交不再幹淨地應用。例如,如果你想向專案提交第二個主題的工作,不要繼續處理你剛剛推送的主題分支——從主倉庫的 master 分支重新開始。

$ git checkout -b featureB origin/master
  ... work ...
$ git commit
$ git push myfork featureB
$ git request-pull origin/master myfork
  ... email generated request pull to maintainer ...
$ git fetch origin

現在,你的每個主題都包含在一個孤島中——類似於補丁佇列——你可以重寫、rebase 和修改它們,而不會相互干擾或相互依賴,如下所示。

Initial commit history with `featureB` work
圖 69. featureB 工作時的初始提交歷史

假設專案維護者已經拉取了許多其他補丁並嘗試了你的第一個分支,但它不再幹淨地合併。在這種情況下,你可以嘗試在 origin/master 之上 rebase 該分支,為維護者解決衝突,然後重新提交你的更改。

$ git checkout featureA
$ git rebase origin/master
$ git push -f myfork featureA

這將重寫你的歷史,使其看起來像 featureA 工作後的提交歷史

Commit history after `featureA` work
圖 70. featureA 工作後的提交歷史

因為你 rebase 了分支,所以你必須在你的 push 命令中指定 -f 才能用一個不是它後代的提交替換伺服器上的 featureA 分支。替代方法是將這項新工作推送到伺服器上的另一個分支(可能稱為 featureAv2)。

讓我們看一個更可能的場景:維護者查看了你第二個分支的工作,喜歡這個概念,但希望你更改一個實現細節。你還可以利用這個機會將工作基於專案當前的 master 分支。你基於當前的 origin/master 分支啟動一個新分支,將 featureB 的更改壓縮在那裡,解決任何衝突,進行實現更改,然後將其作為一個新分支推送到伺服器。

$ git checkout -b featureBv2 origin/master
$ git merge --squash featureB
  ... change implementation ...
$ git commit
$ git push myfork featureBv2

--squash 選項將已合併分支的所有工作壓縮成一個變更集,產生倉庫狀態,就像真正的合併發生一樣,而無需實際進行合併提交。這意味著你未來的提交將只有一個父項,並允許你引入另一個分支的所有更改,然後在記錄新提交之前進行更多更改。此外,--no-commit 選項在預設合併過程出現衝突時很有用,可以延遲合併提交。

此時,你可以通知維護者你已做出請求的更改,他們可以在你的 featureBv2 分支中找到這些更改。

Commit history after `featureBv2` work
圖 71. featureBv2 工作後的提交歷史

透過電子郵件傳送的公共專案

許多專案已經建立了接受補丁的程式——你需要檢查每個專案的具體規則,因為它們會有所不同。由於有幾個較老、較大的專案透過開發者郵件列表接受補丁,我們將舉一個例子。

工作流程與前面的用例類似——你為每個你工作的補丁系列建立主題分支。區別在於你如何將它們提交給專案。與其 Fork 專案並推送到你自己的可寫版本,不如生成每個提交系列的電子郵件版本,然後透過電子郵件傳送給開發者郵件列表。

$ git checkout -b topicA
  ... work ...
$ git commit
  ... work ...
$ git commit

現在你有兩個你想傳送到郵件列表的提交。你使用 git format-patch 來生成可以傳送到列表的 mbox 格式檔案——它將每個提交變成一封電子郵件,以提交訊息的第一行為主題,其餘訊息加上提交引入的補丁作為正文。這樣做的好處是,應用由 format-patch 生成的電子郵件中的補丁可以正確地保留所有提交資訊。

$ git format-patch -M origin/master
0001-add-limit-to-log-function.patch
0002-increase-log-output-to-30-from-25.patch

format-patch 命令會輸出它建立的補丁檔案的名稱。-M 開關告訴 Git 查詢重新命名。檔案最終看起來像這樣。

$ 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')
--
2.1.0

你也可以編輯這些補丁檔案,為電子郵件列表新增更多資訊,而這些資訊不希望顯示在提交訊息中。如果你在 --- 線和補丁(diff --git 線)開始之間新增文字,開發者可以閱讀它,但該內容會被補丁過程忽略。

要將此傳送到郵件列表,你可以將檔案貼上到你的電子郵件程式中,或者透過命令列程式傳送。貼上文字通常會導致格式問題,特別是對於不正確保留換行符和其他空白的“智慧”客戶端。幸運的是,Git 提供了一個工具來幫助你透過 IMAP 傳送格式正確的補丁,這可能對你來說更容易。我們將演示如何透過 Gmail 傳送補丁,碰巧這是我們最熟悉的電子郵件代理;你可以在 Git 原始碼中前面提到的 Documentation/SubmittingPatches 檔案的末尾找到關於許多郵件程式的詳細說明。

首先,你需要設定 ~/.gitconfig 檔案中的 imap 部分。你可以使用一系列 git config 命令單獨設定每個值,或者手動新增它們,但最終你的配置檔案應該看起來像這樣。

[imap]
  folder = "[Gmail]/Drafts"
  host = imaps://imap.gmail.com
  user = user@gmail.com
  pass = YX]8g76G_2^sFbd
  port = 993
  sslverify = false

如果你的 IMAP 伺服器不使用 SSL,那麼最後兩行可能不是必需的,並且 host 值將是 imap:// 而不是 imaps://。設定好之後,你就可以使用 git imap-send 將補丁系列放入指定 IMAP 伺服器的 Drafts 資料夾。

$ 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

此時,你應該能夠轉到你的 Drafts 資料夾,將 To 欄位更改為你傳送補丁到的郵件列表,可能抄送給維護者或負責該部分的人,然後傳送出去。

你也可以透過 SMTP 伺服器傳送補丁。和以前一樣,你可以使用一系列 git config 命令單獨設定每個值,或者在 ~/.gitconfig 檔案中的 sendemail 部分手動新增它們。

[sendemail]
  smtpencryption = tls
  smtpserver = smtp.gmail.com
  smtpuser = user@gmail.com
  smtpserverport = 587

完成此操作後,你可以使用 git send-email 來發送你的補丁。

$ git send-email *.patch
0001-add-limit-to-log-function.patch
0002-increase-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

然後,Git 為你傳送的每個補丁吐出一堆日誌資訊,看起來像這樣。

(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] Add 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
提示

有關配置系統和電子郵件的幫助、更多技巧和竅門,以及一個透過電子郵件傳送試用補丁的沙盒,請訪問 git-send-email.io

總結

在本節中,我們涵蓋了多種工作流程,並討論了在小型團隊上從事閉源專案與為大型公共專案做貢獻之間的區別。你知道在提交前檢查空白錯誤,並且可以編寫出色的提交訊息。你學會了如何格式化補丁,並透過電子郵件將它們傳送給開發者郵件列表。在不同工作流程的背景下,合併的處理也得到了介紹。你現在已經為協作任何專案做好了充分準備。

接下來,你將看到如何處理硬幣的另一面:維護一個 Git 專案。你將學會如何成為一個仁慈的獨裁者或整合經理。