章节 ▾ 第二版

6.2 GitHub - 为项目贡献

为项目贡献

既然我们的账户已经设置好了,下面我们将介绍一些有助于您参与现有项目的有用细节。

派生项目

如果您想为现有项目贡献,但没有推送权限,您可以“派生(fork)”该项目。当您“派生”一个项目时,GitHub 会创建一个完全属于您的项目副本;它位于您的命名空间下,您可以向其推送内容。

注意

从历史上看,“fork”这个词语在某些语境下带有一些负面含义,意味着有人将一个开源项目引向了不同的方向,有时会创建一个竞争项目并导致贡献者分裂。但在 GitHub 中,“fork”仅仅是您自己的命名空间下的同一个项目,它允许您以更开放的方式公开地对项目进行更改,从而进行贡献。

通过这种方式,项目无需担心为了授予推送权限而将用户添加为协作者。人们可以派生一个项目,向其推送内容,然后通过创建所谓的“拉取请求”(Pull Request)将他们的更改贡献回原始仓库,我们将在后面介绍这一点。这将开启一个包含代码审查的讨论线程,项目所有者和贡献者可以就更改进行沟通,直到所有者满意为止,届时所有者就可以将其合并进来。

要派生一个项目,请访问项目页面并点击页面右上角的“Fork”(派生)按钮。

The “Fork” button
图 88. “Fork”按钮

几秒钟后,您将被带到新的项目页面,其中包含您自己的可写入代码副本。

GitHub 流程

GitHub 是围绕特定的协作工作流程设计的,其核心是拉取请求(Pull Request)。无论您是与一个紧密协作的团队在单个共享仓库中协作,还是一个全球分布的公司或陌生人网络通过数十个派生(fork)为项目贡献,这个流程都能奏效。它以主题分支工作流程为中心,该工作流程在Git 分支一章中有所介绍。

其通常的工作方式如下:

  1. 派生项目。

  2. master 分支创建主题分支。

  3. 进行一些提交以改进项目。

  4. 将此分支推送到您的 GitHub 项目。

  5. 在 GitHub 上打开一个拉取请求(Pull Request)。

  6. 讨论,并可选择继续提交。

  7. 项目所有者合并或关闭拉取请求。

  8. 将更新后的 master 分支同步回您的派生。

这基本上就是集成管理者工作流程中所介绍的工作方式,但不同的是,团队使用 GitHub 的基于 Web 的工具来沟通和审查更改,而不是通过电子邮件。

让我们通过一个例子,演示如何使用此流程向 GitHub 上托管的开源项目提出更改建议。

提示

您可以使用官方的 GitHub CLI 工具来完成大部分操作,而非 GitHub 网页界面。该工具可在 Windows、macOS 和 Linux 系统上使用。请访问 GitHub CLI 主页获取安装说明和手册。

创建拉取请求

Tony 正在为他的 Arduino 可编程微控制器寻找代码,并在 GitHub 上找到了一个很棒的程序文件:https://github.com/schacon/blink

The project we want to contribute to
图 89. 我们想要贡献的项目

唯一的问题是闪烁速率太快了。我们认为在每次状态更改之间等待 3 秒而不是 1 秒会好得多。所以,让我们改进程序,并将其作为建议的更改提交回项目。

首先,我们点击前面提到的“Fork”按钮,获取我们自己的项目副本。这里我们的用户名是“tonychacon”,所以这个项目的副本位于 https://github.com/tonychacon/blink,我们可以在那里进行编辑。我们将在本地克隆它,创建一个主题分支,进行代码更改,最后将更改推送到 GitHub。

$ git clone https://github.com/tonychacon/blink (1)
Cloning into 'blink'...

$ cd blink
$ git checkout -b slow-blink (2)
Switched to a new branch 'slow-blink'

$ sed -i '' 's/1000/3000/' blink.ino (macOS) (3)
# If you're on a Linux system, do this instead:
# $ sed -i 's/1000/3000/' blink.ino (3)

$ git diff --word-diff (4)
diff --git a/blink.ino b/blink.ino
index 15b9911..a6cc5a5 100644
--- a/blink.ino
+++ b/blink.ino
@@ -18,7 +18,7 @@ void setup() {
// the loop routine runs over and over again forever:
void loop() {
  digitalWrite(led, HIGH);   // turn the LED on (HIGH is the voltage level)
  [-delay(1000);-]{+delay(3000);+}               // wait for a second
  digitalWrite(led, LOW);    // turn the LED off by making the voltage LOW
  [-delay(1000);-]{+delay(3000);+}               // wait for a second
}

$ git commit -a -m 'Change delay to 3 seconds' (5)
[slow-blink 5ca509d] Change delay to 3 seconds
 1 file changed, 2 insertions(+), 2 deletions(-)

$ git push origin slow-blink (6)
Username for 'https://github.com': tonychacon
Password for 'https://tonychacon@github.com':
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 340 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/tonychacon/blink
 * [new branch]      slow-blink -> slow-blink
  1. 在本地克隆我们派生的项目。

  2. 创建一个描述性的主题分支。

  3. 对代码进行修改。

  4. 检查更改是否良好。

  5. 将更改提交到主题分支。

  6. 将新主题分支推送回我们的 GitHub 派生。

现在,如果我们回到 GitHub 上的派生,我们会看到 GitHub 注意到我们推送了一个新的主题分支,并为我们提供了一个大大的绿色按钮,用于查看我们的更改并向原始项目打开拉取请求(Pull Request)。

您也可以前往 https://github.com/<user>/<project>/branches 的“Branches”(分支)页面,找到您的分支并从那里打开新的拉取请求。

Pull Request button
图 90. 拉取请求按钮

如果我们点击那个绿色按钮,我们会看到一个屏幕,要求我们为拉取请求提供标题和描述。这几乎总是值得付出一些努力的,因为好的描述有助于原始项目的所有者确定您想要做什么,您的提议更改是否正确,以及接受这些更改是否会改进原始项目。

我们还会看到主题分支中“领先”于 master 分支的提交列表(本例中只有一个),以及如果此分支被项目所有者合并,将进行的所有更改的统一差异(diff)。

Pull Request creation page
图 91. 拉取请求创建页面

当您在此屏幕上点击“Create pull request”(创建拉取请求)按钮时,您所派生项目的所有者将收到有人建议更改的通知,并会链接到一个包含所有这些信息的页面。

注意

尽管拉取请求通常用于像这样贡献者已经准备好完整更改的公共项目,但它也常在内部项目的开发周期开始时使用。由于即使在拉取请求打开之后,您也可以继续向主题分支推送,因此它通常会尽早打开,并作为团队在特定上下文中迭代工作的一种方式,而不是在流程的最后才打开。

迭代拉取请求

此时,项目所有者可以查看建议的更改并进行合并、拒绝或评论。假设他喜欢这个想法,但希望灯灭的时间比亮的时间稍长一些。

分布式 Git 中介绍的工作流程中,这种对话可能通过电子邮件进行,但在 GitHub 上,这发生在在线。项目所有者可以查看统一差异(unified diff)并通过点击任意行来留下评论。

Comment on a specific line of code in a Pull Request
图 92. 在拉取请求中对特定代码行进行评论

一旦维护者发表此评论,打开拉取请求的人(以及任何关注该仓库的人)都将收到通知。我们稍后将介绍如何自定义此功能,但如果 Tony 开启了电子邮件通知,他将收到这样的电子邮件:

Comments sent as email notifications
图 93. 作为电子邮件通知发送的评论

任何人都可以对拉取请求留下一般性评论。在拉取请求讨论页面中,我们可以看到一个项目所有者既对一行代码进行评论,又在讨论区留下一般性评论的例子。您可以看到代码评论也被带入到对话中。

Pull Request discussion page
图 94. 拉取请求讨论页面

现在贡献者可以看到他们需要做什么才能使他们的更改被接受。幸运的是,这非常简单。通过电子邮件,您可能需要重新打包您的系列并重新提交到邮件列表,而在 GitHub 上,您只需再次提交到主题分支并推送,这将自动更新拉取请求。在拉取请求最终页面中,您还可以看到旧的代码评论在更新后的拉取请求中已被折叠,因为它是在已更改的行上进行的。

向现有拉取请求添加提交不会触发通知,因此一旦 Tony 推送了他的更正,他决定留下评论,告知项目所有者他已进行了请求的更改。

Pull Request final
图 95. 最终拉取请求

一个有趣的地方是,如果您点击此拉取请求的“Files Changed”(文件更改)选项卡,您将获得“统一”差异(unified diff)——也就是说,如果此主题分支被合并,将引入到您的主分支的总聚合差异。用 git diff 的术语来说,它基本上会自动显示该拉取请求所基于的分支的 git diff master…​<branch>。有关此类差异的更多信息,请参阅确定引入了什么

您还会注意到的另一件事是,GitHub 会检查拉取请求是否可以干净地合并,并提供一个按钮让您在服务器上执行合并。此按钮仅在您对仓库具有写入权限且可以进行简单合并时才会显示。如果您点击它,GitHub 将执行“非快进式”(non-fast-forward)合并,这意味着即使合并可以是快进式的,它仍然会创建一个合并提交(merge commit)。

如果您愿意,您可以简单地将该分支拉取下来并在本地合并。如果您将此分支合并到 master 分支并推送到 GitHub,拉取请求将自动关闭。

这是大多数 GitHub 项目使用的基本工作流程。创建主题分支,在其上打开拉取请求,随后进行讨论,可能在该分支上进行更多工作,最终请求被关闭或合并。

注意
不仅仅是派生

值得注意的是,您也可以在同一个仓库中的两个分支之间打开拉取请求。如果您与某人合作开发一个功能,并且您们都对项目具有写入权限,您可以将一个主题分支推送到仓库,并在此主题分支上向同一个项目的 master 分支打开一个拉取请求,以启动代码审查和讨论过程。无需进行派生。

高级拉取请求

既然我们已经介绍了在 GitHub 上为项目贡献的基础知识,接下来我们将介绍一些关于拉取请求的有趣技巧和窍门,以便您更有效地使用它们。

作为补丁的拉取请求

重要的是要理解,许多项目并不真正将拉取请求视为应该按顺序干净应用的完美补丁队列,而大多数基于邮件列表的项目则将补丁系列贡献视为如此。大多数 GitHub 项目将拉取请求分支视为围绕提议更改的迭代对话,最终通过合并应用一个统一的差异(diff)。

这是一个重要的区别,因为通常在代码被认为是完美之前就提出了更改建议,这在基于邮件列表的补丁系列贡献中更为罕见。这使得可以与维护者进行更早的对话,从而使找到正确解决方案成为一项社区努力。当通过拉取请求提出代码并且维护者或社区建议更改时,补丁系列通常不会重新生成,而是将差异作为新的提交推送到分支,在保留先前工作上下文的情况下推进对话。

例如,如果您再次查看最终拉取请求,您会注意到贡献者没有重定基他的提交并发送另一个拉取请求。相反,他们添加了新的提交并推送到现有分支。这样,如果您将来回溯查看此拉取请求,您可以轻松找到所有关于决策原因的上下文信息。网站上的“合并”按钮特意创建了一个引用拉取请求的合并提交,以便在必要时方便回溯和研究原始对话。

与上游保持同步

如果您的拉取请求过期或无法干净地合并,您需要修复它,以便维护者可以轻松合并。GitHub 会为您测试并告知您每个拉取请求的底部,合并是否简单。

Pull Request does not merge cleanly
图 96. 拉取请求无法干净合并

如果您看到类似拉取请求无法干净合并的情况,您会希望修复您的分支,使其变为绿色,这样维护者就无需额外工作了。

您有两种主要的选择来完成此操作。您可以将您的分支重定基到目标分支之上(通常是您派生仓库的 master 分支),或者您可以将目标分支合并到您的分支中。

GitHub 上的大多数开发者会选择后者,原因与我们上一节中讨论的相同。重要的是历史和最终合并,所以重定基除了能带来稍微干净一些的历史外,并没有太大的好处,而且它比合并更困难、更容易出错。

如果您想合并目标分支以使您的拉取请求可合并,您需要将原始仓库添加为新的远程仓库,从中抓取(fetch),将该仓库的主分支合并到您的主题分支中,修复所有问题,最后将其推送回您打开拉取请求的同一个分支。

例如,假设在我们之前使用的“tonychacon”示例中,原作者做了一个会在拉取请求中产生冲突的更改。让我们过一遍这些步骤。

$ git remote add upstream https://github.com/schacon/blink (1)

$ git fetch upstream (2)
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
Unpacking objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
From https://github.com/schacon/blink
 * [new branch]      master     -> upstream/master

$ git merge upstream/master (3)
Auto-merging blink.ino
CONFLICT (content): Merge conflict in blink.ino
Automatic merge failed; fix conflicts and then commit the result.

$ vim blink.ino (4)
$ git add blink.ino
$ git commit
[slow-blink 3c8d735] Merge remote-tracking branch 'upstream/master' \
    into slower-blink

$ git push origin slow-blink (5)
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 682 bytes | 0 bytes/s, done.
Total 6 (delta 2), reused 0 (delta 0)
To https://github.com/tonychacon/blink
   ef4725c..3c8d735  slower-blink -> slow-blink
  1. 将原始仓库添加为名为 upstream 的远程仓库。

  2. 从该远程仓库抓取最新工作。

  3. 将该仓库的主分支合并到您的主题分支。

  4. 解决发生的冲突。

  5. 推送回同一主题分支。

完成这些操作后,拉取请求将自动更新并重新检查是否可以干净地合并。

Pull Request now merges cleanly
图 97. 拉取请求现在可以干净合并

Git 的一个优点是您可以持续地进行此操作。如果您的项目运行时间很长,您可以轻松地一次又一次地从目标分支合并,并且只需要处理自上次合并以来出现的冲突,这使得整个过程非常易于管理。

如果您确实希望重定基分支以清理它,当然可以这样做,但强烈建议不要强制推送到已经打开拉取请求的分支。如果其他人已经拉取并在此分支上进行了更多工作,您将遇到重定基的风险中概述的所有问题。相反,将重定基后的分支推送到 GitHub 上的一个新分支,并打开一个新的拉取请求,引用旧的拉取请求,然后关闭原始的。

引用

您的下一个问题可能是“我如何引用旧的拉取请求?”。事实证明,在 GitHub 中,您几乎可以在任何可写入的地方,以多种方式引用其他内容。

让我们从如何交叉引用另一个拉取请求或议题(Issue)开始。所有拉取请求和议题都被分配了编号,并且在项目内是唯一的。例如,您不能同时拥有拉取请求 #3 议题 #3。如果您想从任何其他拉取请求或议题中引用某个拉取请求或议题,您只需在任何评论或描述中放入 #<num>。如果议题或拉取请求位于其他地方,您也可以更具体;如果您引用的是您所在仓库派生中的议题或拉取请求,请写入 username#<num>,或使用 username/repo#<num> 来引用另一个仓库中的内容。

让我们看一个例子。假设我们在前面的例子中重定基了分支,为其创建了一个新的拉取请求,现在我们想在新拉取请求中引用旧的拉取请求。我们还想引用仓库派生中的一个议题以及一个完全不同项目中的议题。我们可以像拉取请求中的交叉引用那样填写描述。

Cross references in a Pull Request
图 98. 拉取请求中的交叉引用

当我们提交此拉取请求时,我们将看到所有内容呈现,如同拉取请求中渲染的交叉引用

Cross references rendered in a Pull Request
图 99. 拉取请求中渲染的交叉引用

请注意,我们放入的完整 GitHub URL 被缩短为仅显示所需的信息。

现在,如果 Tony 返回并关闭原始拉取请求,我们会看到通过在新拉取请求中提及它,GitHub 已自动在拉取请求时间线中创建了一个回溯事件。这意味着任何访问此拉取请求并看到它已关闭的人都可以轻松链接回替代它的那个。该链接将看起来像在已关闭的拉取请求时间线中链接回新的拉取请求

Link back to the new Pull Request in the closed Pull Request timeline
图 100. 在已关闭的拉取请求时间线中链接回新的拉取请求

除了议题编号,您还可以通过 SHA-1 引用特定的提交。您必须指定完整的 40 个字符的 SHA-1,但如果 GitHub 在评论中看到它,它将直接链接到该提交。同样,您可以像引用议题一样,在派生或其他仓库中引用提交。

GitHub 风味 Markdown

链接到其他议题只是您在 GitHub 上几乎任何文本框中可以做的有趣事情的开始。在议题和拉取请求的描述、评论、代码评论以及更多地方,您可以使用所谓的“GitHub 风味 Markdown”。Markdown 就像写纯文本一样,但它可以被丰富地渲染。

有关如何使用 Markdown 编写和渲染评论或文本的示例,请参阅GitHub 风味 Markdown 的编写和渲染示例

An example of GitHub Flavored Markdown as written and as rendered
图 101. GitHub 风味 Markdown 的编写和渲染示例

GitHub 风味的 Markdown 在基本 Markdown 语法之外增加了更多功能。这些在创建有用的拉取请求或议题评论或描述时都非常有用。

任务列表

第一个真正有用的 GitHub 特有 Markdown 功能,尤其适用于拉取请求,是任务列表。任务列表是您想要完成的待办事项的复选框列表。将其放入议题或拉取请求中通常表示在您认为项目完成之前需要完成的事项。

您可以创建这样的任务列表:

- [X] Write the code
- [ ] Write all the tests
- [ ] Document the code

如果我们将此包含在我们的拉取请求或议题的描述中,我们将看到它呈现为Markdown 评论中渲染的任务列表

Task lists rendered in a Markdown comment
图 102. Markdown 评论中渲染的任务列表

这经常用于拉取请求中,以指示在拉取请求准备好合并之前,您希望在该分支上完成的所有工作。真正酷的部分是,您可以简单地点击复选框来更新评论——您无需直接编辑 Markdown 来勾选任务。

更重要的是,GitHub 会在您的议题和拉取请求中查找任务列表,并在列出它们的页面上将其显示为元数据。例如,如果您有一个包含任务的拉取请求,并且您查看所有拉取请求的概览页面,您可以看到它完成了多少。这有助于人们将拉取请求分解为子任务,并帮助其他人跟踪分支的进度。您可以在拉取请求列表中的任务列表摘要中看到一个示例。

Task list summary in the Pull Request list
图 103. 拉取请求列表中的任务列表摘要

当您尽早打开拉取请求并将其用于跟踪功能实现进度时,这些功能非常有用。

代码片段

您还可以在评论中添加代码片段。如果您想在实际作为提交在分支上实现之前,展示您可以尝试做的事情,这尤其有用。这通常也用于添加不工作的示例代码,或者此拉取请求可以实现什么。

要添加代码片段,您必须用反引号将其“围起来”。

```java
for(int i=0 ; i < 5 ; i++)
{
   System.out.println("i is : " + i);
}
```

如果您像我们那样添加语言名称(例如 'java'),GitHub 也会尝试对片段进行语法高亮。在上面的示例中,它最终会呈现为渲染后的围栏代码示例

Rendered fenced code example
图 104. 渲染后的围栏代码示例

引用

如果您正在回复长评论的一小部分,可以通过在行前加上 > 字符来选择性地引用其他评论中的内容。事实上,这非常常见且有用,以至于有一个键盘快捷键。如果您高亮显示评论中要直接回复的文本并按下 r 键,它将自动为您在评论框中引用该文本。

引用内容看起来像这样:

> Whether 'tis Nobler in the mind to suffer
> The Slings and Arrows of outrageous Fortune,

How big are these slings and in particular, these arrows?

渲染后,评论将显示为渲染后的引用示例

Rendered quoting example
图 105. 渲染后的引用示例

表情符号

最后,您还可以在评论中使用表情符号。这实际上在许多 GitHub 议题和拉取请求的评论中被广泛使用。GitHub 中甚至有一个表情符号助手。如果您正在输入评论并以 : 字符开头,自动完成器将帮助您找到您想要的内容。

Emoji autocompleter in action
图 106. 正在运行的表情符号自动完成器

表情符号在评论中的任何地方都以 :<名称>: 的形式出现。例如,您可以这样写:

I :eyes: that :bug: and I :cold_sweat:.

:trophy: for :microscope: it.

:+1: and :sparkles: on this :ship:, it's :fire::poop:!

:clap::tada::panda_face:

渲染后,它看起来会像大量使用表情符号的评论

Heavy emoji commenting
图 107. 大量使用表情符号的评论

这不是说它非常有用,但它确实为原本难以表达情感的媒介增添了趣味性和情感元素。

注意

现在实际上有相当多的网络服务使用表情符号。一个很好的参考备忘单,可以找到表达您想要说的表情符号,可以在以下地址找到:

图片

这在技术上并非 GitHub 风味 Markdown,但它非常有用。除了在评论中添加 Markdown 图片链接(这可能难以找到和嵌入 URL)之外,GitHub 还允许您将图片拖放到文本区域中以进行嵌入。

Drag and drop images to upload them and auto-embed them
图 108. 拖放图片以上传并自动嵌入它们

如果您查看拖放图片以上传并自动嵌入它们,您会看到文本区域上方有一个小小的“Parsed as Markdown”(解析为 Markdown)提示。点击它将为您提供一个完整的 GitHub Markdown 功能备忘单。

保持您的 GitHub 公开仓库同步更新

一旦您派生了一个 GitHub 仓库,您的仓库(您的“派生”)就独立于原始仓库存在。特别是当原始仓库有新的提交时,GitHub 会通过类似这样的消息通知您:

This branch is 5 commits behind progit:master.

但是您的 GitHub 仓库永远不会由 GitHub 自动更新;这是您必须自己完成的操作。幸运的是,这非常容易。

一种无需配置即可实现此目的的可能性。例如,如果您从 https://github.com/progit/progit2.git 派生,您可以像这样保持您的 master 分支最新:

$ git checkout master (1)
$ git pull https://github.com/progit/progit2.git (2)
$ git push origin master (3)
  1. 如果您在其他分支,请返回 master 分支。

  2. https://github.com/progit/progit2.git 抓取更改并将其合并到 master 分支。

  3. 将您的 master 分支推送到 origin

这可行,但是每次都要拼写出抓取 URL 有点繁琐。您可以通过一些配置来自动化这项工作:

$ git remote add progit https://github.com/progit/progit2.git (1)
$ git fetch progit (2)
$ git branch --set-upstream-to=progit/master master (3)
$ git config --local remote.pushDefault origin (4)
  1. 添加源仓库并为其命名。这里我选择将其命名为 progit

  2. 获取 progit 分支的引用,特别是 master

  3. 将您的 master 分支设置为从 progit 远程仓库抓取。

  4. 将默认推送仓库定义为 origin

完成此操作后,工作流程将变得更简单:

$ git checkout master (1)
$ git pull (2)
$ git push (3)
  1. 如果您在其他分支,请返回 master 分支。

  2. progit 抓取更改并将更改合并到 master 分支。

  3. 将您的 master 分支推送到 origin

这种方法可能很有用,但并非没有缺点。Git 会很高兴地默默为您完成这项工作,但如果您向 master 提交、从 progit 拉取,然后推送到 origin,它不会发出警告——所有这些操作在此设置下都是有效的。因此,您必须注意永远不要直接提交到 master,因为该分支实际上属于上游仓库。

scroll-to-top