章节 ▾ 第二版

5.2 分布式 Git - 为项目做贡献

为项目做贡献

描述如何为项目做贡献的主要困难在于其方式多种多样。由于 Git 非常灵活,人们可以通过多种方式进行协作,因此很难给出一个统一的指导——每个项目都有所不同。涉及的变量包括活跃贡献者数量、所选的工作流程、你的提交权限,以及可能存在的外部贡献方式。

第一个变量是活跃贡献者数量——有多少用户在积极地为该项目贡献代码,频率如何?在许多情况下,你可能会遇到两三个开发人员每天提交几次代码,对于一些处于停滞状态的项目,频率可能会更低。对于大型公司或项目,开发人员的数量可能达到成千上万,每天有数百或数千次提交。这一点很重要,因为随着开发人员越来越多,在确保代码干净地应用或轻松合并方面,你会遇到更多问题。你提交的更改可能会因为在你工作期间或等待审核/应用期间合并入库的其他工作而变得过时或严重损坏。你该如何保持代码的持续更新并确保你的提交有效?

下一个变量是项目所使用的工作流程。它是中心化的吗?每个开发人员是否对主代码行拥有平等的写入权限?该项目是否有负责检查所有补丁的维护者或集成经理?所有的补丁是否都经过同行评审和批准?你是否参与了该过程?是否有副手系统(lieutenant system),你是否必须先将工作提交给他们?

下一个变量是你的提交权限。如果你拥有项目的写入权限,那么为项目做贡献所需的工作流程与没有写入权限时会有很大不同。如果你没有写入权限,项目倾向于以何种方式接受贡献?它是否有相关政策?你一次贡献多少工作量?你贡献的频率如何?

所有这些问题都会影响你如何有效地为项目做贡献,以及哪些工作流程是首选或可用的。我们将通过一系列用例来涵盖这些方面,从简单到复杂;你应该能够从这些示例中构建出你在实践中需要的特定工作流程。

提交指南

在开始研究具体的用例之前,先说明一下关于提交信息(commit message)的注意事项。制定一套良好的提交指南并坚持执行,会使使用 Git 和与他人协作变得容易得多。Git 项目提供了一份文档,其中列出了许多关于创建补丁提交的良好建议——你可以在 Git 源代码的 Documentation/SubmittingPatches 文件中阅读它。

首先,你的提交不应包含任何空格错误。Git 提供了一种简单的方法来检查这一点——在提交之前,运行 git diff --check,它会识别可能的空格错误并为你列出它们。

Output of `git diff --check`
图 56. git diff --check 的输出

如果你在提交前运行该命令,就可以知道是否即将提交会导致其他开发人员烦恼的空格问题。

其次,尽量使每次提交都是逻辑上独立的变更集。如果可以的话,尽量让你的更改易于消化——不要在整个周末编码解决五个不同的问题,然后在周一将它们作为一次巨大的提交提交上去。即使你在周末不提交,周一也应使用暂存区将你的工作拆分为每个问题至少一次提交,并为每次提交提供有用的信息。如果某些更改修改了同一个文件,尽量使用 git add --patch 来部分暂存文件(详见 交互式暂存)。只要所有更改最终都被添加,分支顶端的项目快照无论你是一次提交还是五次提交都是相同的,所以当你的同行需要审查你的更改时,尽量让他们轻松一些。

这种方法在你以后需要撤销或回滚其中一个变更集时也会更容易。 重写历史 描述了许多用于重写历史和交互式暂存文件的实用 Git 技巧——在将工作发送给他人之前,使用这些工具来帮助构建一个干净且易于理解的历史记录。

最后需要记住的是提交信息。养成创建高质量提交信息的习惯会使使用和协作 Git 容易得多。通常情况下,你的信息应以一行不超过 50 个字符的简短描述开头,概括变更集,然后是一个空行,接着是更详细的解释。Git 项目要求详细解释应包括你进行更改的动机以及与之前行为的对比——这是一个很好的遵循指南。以祈使句编写你的提交信息:“Fix bug” 而不是 “Fixed bug” 或 “Fixes 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

上述输出的最后一行显示了推送操作返回的一个有用信息。基本格式为 <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 获取(fetch)了 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. 合并 origin/master 后 John 的仓库

此时,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. 推送到 origin 服务器后 John 的历史记录

与此同时,Jessica 创建了一个名为 issue54 的新特性分支(topic branch),并对该分支进行了三次提交。她还没有获取 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. 获取 John 的更改后 Jessica 的历史记录

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(-)

没有出现问题;如你所见,这是一个简单的快进(fast-forward)合并。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. 合并 John 的更改后 Jessica 的历史记录

现在可以从 Jessica 的 master 分支访问到 origin/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 引用规格及其可执行操作的更详细讨论。还要注意 -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 是因为这种能够让多个团队并行工作,并在过程后期合并不同工作线的能力。团队内较小的小组通过远程分支进行协作,而不必涉及或阻碍整个团队,这是 Git 的一大优势。你在这里看到的工作流程顺序大致如下。

Basic sequence of this managed-team workflow
图 68. 此受管团队工作流程的基本顺序

派生的公共项目(Forked Public Project)

为公共项目做贡献略有不同。因为你没有直接更新项目分支的权限,所以必须通过其他方式将工作提交给维护者。第一个示例描述了通过在支持轻松派生(forking)的 Git 托管平台上进行派生来做贡献。许多托管网站都支持这一点(包括 GitHub、BitBucket、repo.or.cz 等),许多项目维护者也期望这种贡献风格。下一节将处理那些倾向于通过电子邮件接受贡献补丁的项目。

首先,你可能需要克隆主仓库,为补丁或你计划贡献的补丁系列创建一个特性分支,并在那里进行工作。顺序基本上是这样的。

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

你可能想使用 rebase -i 将你的工作压缩(squash)为一个单独的提交,或者重新排列提交中的工作,以使维护者更容易审查补丁——关于交互式变基(interactive rebasing)的更多信息,请参阅 重写历史

当你的分支工作完成并且你准备好将其贡献回维护者时,请转到原始项目页面并点击 “Fork” 按钮,创建你自己可写的项目派生副本。然后,你需要将此仓库 URL 添加为本地仓库的一个新远程仓库;在这个例子中,我们称之为 myfork

$ git remote add myfork <url>

然后,你需要将你的新工作推送到该仓库。最好将你正在工作的特性分支推送到你派生的仓库中,而不是将该工作合并到你的 master 分支并进行推送。原因在于,如果你的工作未被接受或被挑选(cherry-picked),你不必倒退你的 master 分支(Git cherry-pick 操作在 变基和挑选工作流 中有更详细的介绍)。如果维护者 mergerebasecherry-pick 你的工作,无论如何,你最终都会通过从他们的仓库拉取而取回它。

无论如何,你可以使用以下命令推送你的工作。

$ git push -u myfork featureA

一旦你的工作被推送到你的仓库派生副本,你需要通知原始项目的维护者你有想要他们合并的工作。这通常称为拉取请求(pull request),你通常可以通过网站生成此类请求——GitHub 有其自己的 “Pull Request” 机制,我们将在 GitHub 中介绍——或者你可以运行 git request-pull 命令,并将随后的输出手动通过电子邮件发送给项目维护者。

git request-pull 命令获取你希望拉取特性分支的基础分支,以及你希望他们从中拉取的 Git 仓库 URL,并生成你要求拉取的所有更改的摘要。例如,如果 Jessica 想给 John 发送一个拉取请求,并且她在她刚刚推送的特性分支上做了两个提交,她可以运行以下命令。

$ 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,并在你可以轻松丢弃的特性分支中进行工作(如果它们被拒绝的话)。将工作主题隔离在特性分支中,也使你在主仓库顶端移动且你的提交不再能干净应用时,更容易重新变基你的工作。例如,如果你想向项目提交第二个工作主题,不要继续在你刚刚推送的特性分支上工作——从主仓库的 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

现在,你的每个主题都包含在一个容器中——类似于一个补丁队列——你可以重写、变基和修改它,而不会使主题相互干扰或相互依赖,就像这样。

Initial commit history with `featureB` work
图 69. 包含 featureB 工作的初始提交历史

假设项目维护者已经拉取了一堆其他补丁并尝试了你的第一个分支,但它不再能干净地合并。在这种情况下,你可以尝试在 origin/master 之上变基该分支,为维护者解决冲突,然后重新提交你的更改。

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

这会重写你的历史记录,使其现在看起来像 featureA 工作后的提交历史

Commit history after `featureA` work
图 70. featureA 工作后的提交历史

因为你变基了该分支,所以你必须在推送命令中指定 -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 工作后的提交历史

通过电子邮件的公共项目

许多项目有既定的补丁接受程序——你需要检查每个项目的具体规则,因为它们会有所不同。由于有几个较老、较大的项目通过开发人员邮件列表接受补丁,我们现在来看看这方面的示例。

工作流程与前一个用例类似——你为你开发的每个补丁系列创建特性分支。区别在于你如何将它们提交给项目。你不是派生项目并推送到你自己的可写版本,而是生成每个提交系列的电子邮件版本,并将它们发送到开发人员邮件列表。

$ 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

此时,你应该能够进入你的草稿文件夹,将收件人(To)字段更改为你发送补丁的邮件列表,可能抄送(CC)维护者或该部分的负责人,然后发送出去。

你也可以通过 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 项目。你将学习如何成为一名仁慈的独裁者或集成经理。