章节 ▾ 第二版

5.2 分布式 Git - 贡献项目

贡献项目

描述如何向项目贡献代码的主要困难在于其多种多样的变体。由于 Git 非常灵活,人们可以并且确实以多种方式协作,因此很难确切说明您应该如何贡献——每个项目都有所不同。涉及的一些变量包括活跃贡献者数量、所选工作流程、您的提交权限以及可能的外部贡献方式。

第一个变量是活跃贡献者数量——有多少用户正在积极地向此项目贡献代码,以及频率如何?在许多情况下,您会遇到两三个开发者每天提交几次,对于一些不那么活跃的项目可能更少。对于大型公司或项目,开发者数量可能达到数千,每天有数百或数千次提交。这很重要,因为随着开发者数量的增加,您会遇到更多问题,需要确保您的代码能够干净地应用或轻松合并。您提交的更改可能会因为在您工作期间或您的更改等待批准或应用期间合并进来的工作而变得过时或严重损坏。您如何才能使代码持续保持最新并确保提交有效?

下一个变量是项目所使用的工作流程。它是中心化的吗,每个开发者都对主代码线拥有相同的写入权限吗?项目是否有维护者或集成管理员来检查所有补丁?所有补丁都经过同行评审和批准吗?您是否参与了该过程?是否存在一个中尉(lieutenant)系统,您是否必须先向他们提交工作?

下一个变量是您的提交权限。如果您拥有项目的写入权限,贡献项目所需的工作流程与您没有写入权限时大不相同。如果您没有写入权限,项目偏好以何种方式接受贡献的工作?它是否有相应的策略?您一次贡献多少工作?您贡献的频率如何?

所有这些问题都会影响您如何有效地向项目贡献代码,以及哪些工作流程是受青睐或可供您使用的。我们将在一系列用例中涵盖这些方面的各个方面,从简单到复杂;您应该能够根据这些示例,在实践中构建您所需的特定工作流程。

提交规范

在我们开始查看具体用例之前,这里有一个关于提交信息的简短说明。为创建提交制定并遵循良好的规范,将使使用 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 项目要求更详细的解释应包括您进行此更改的动机,并将其实现与以前的行为进行对比——这是一个很好的遵循准则。用命令式语气编写您的提交信息:“修复错误”(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,看看一个格式优美的项目提交历史是什么样子。

注意
照我们说的做,不要照我们做的做。

为了简洁起见,本书中的许多示例都没有像这样格式良好的提交信息;相反,我们只是简单地使用 -m 选项进行 git commit

简而言之,照我们说的做,不要照我们做的做。

私有小型团队

您可能遇到的最简单的设置是一个私有项目,其中有一两个其他开发者。“私有”在此上下文中意味着闭源——外部世界无法访问。您和其他开发者都拥有对仓库的推送(push)权限。

在这种环境中,您可以遵循类似于使用 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 已更改,则从 origin/master 获取并合并您的 master,最后推送到服务器上的 master 分支。一般顺序如下所示

General sequence of events for a simple multiple-developer Git workflow
图 64. 简单多开发者 Git 工作流程的一般事件顺序

私有受管团队

在下一个场景中,您将了解大型私有团队中的贡献者角色。您将学习如何在小型团队协作完成功能的环境中工作,之后这些团队的贡献将由另一方集成。

假设 John 和 Jessica 正在合作开发一个功能(称之为“功能A”),而 Jessica 和第三位开发者 Josie 正在合作开发第二个功能(例如“功能B”)。在这种情况下,公司使用的是一种集成管理员工作流程,其中各个团队的工作仅由特定的工程师集成,主仓库的 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 的电子邮件,说一个包含一些初始“功能B”工作的分支已经以 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 想要将所有这些合并后的“功能B”工作推回到服务器,但她不想仅仅推送她自己的 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 的电子邮件,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 具备这种能力,允许多个团队并行工作,并在流程后期合并不同的工作线。团队中较小的子组能够通过远程分支进行协作,而无需强制涉及或阻碍整个团队,这是 Git 的一个巨大优势。您在此处看到的工作流程顺序大致如下

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

分叉公共项目

向公共项目贡献代码有些不同。因为您没有直接更新项目分支的权限,所以您必须通过其他方式将工作交给维护者。第一个例子描述了在支持轻松分叉的 Git 托管平台(包括 GitHub、BitBucket、repo.or.cz 等)上通过分叉进行贡献。许多项目维护者都期望这种贡献方式。下一节将讨论那些偏好通过电子邮件接受贡献补丁的项目。

首先,您可能需要克隆主仓库,为计划贡献的补丁或补丁系列创建一个主题分支,并在那里进行您的工作。其顺序基本如下所示

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

您可能希望使用 rebase -i 将您的工作压缩成单个提交,或者重新排列提交中的工作,以便维护者更容易审查补丁——有关交互式变基的更多信息,请参阅重写历史

当您的分支工作完成后,并且您准备好将其贡献回维护者时,请转到原始项目页面并点击“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 有自己的“拉取请求”机制,我们将在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 工作后的提交历史

因为您对分支进行了变基,所以您必须在 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 工作后的提交历史

通过电子邮件向公共项目贡献

许多项目都有既定的补丁接受流程——您需要查看每个项目的具体规则,因为它们会有所不同。由于有一些较旧、较大的项目通过开发者邮件列表接受补丁,我们现在将介绍一个示例。

工作流程与之前的用例相似——您为您处理的每个补丁系列创建主题分支。不同之处在于您如何将它们提交到项目。您不是分叉项目并推送到您自己的可写版本,而是生成每个提交系列的电子邮件版本,并通过电子邮件将其发送到开发者邮件列表。

$ 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 服务器的“草稿”文件夹中

$ 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

此时,您应该可以前往您的“草稿”文件夹,将“收件人”字段更改为您要发送补丁的邮件列表,可以抄送维护者或负责该部分的人员,然后发送出去。

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

scroll-to-top