章节 ▾ 第二版

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 项目。你将学会如何成为一个仁慈的独裁者或集成经理。