章节 ▾ 第二版

6.2 GitHub - 参与项目

参与项目

现在我们的账户已经设置好了,让我们来详细了解一些可以帮助你参与到现有项目中的细节。

Forking Projects (复制项目)

如果你想参与到一个你没有推送权限的现有项目中,你可以“fork”该项目。当你“fork”一个项目时,GitHub 会为你创建一个完全属于你的项目副本;它会存放在你的命名空间下,你可以向其中推送。

注意

历史上,“fork”这个词在语境中带有一定的负面含义,表示有人将一个开源项目导向了不同的方向,有时会创建一个竞争项目并分裂贡献者。在 GitHub 中,“fork”仅仅是指在你自己的命名空间下的同一个项目,允许你公开地对项目进行修改,从而以一种更开放的方式进行贡献。

这样,项目就无需担心添加用户作为协作者来给予他们推送权限。人们可以 fork 一个项目,向其中推送,并通过创建一个称为“Pull Request”(拉取请求)的东西将他们的更改贡献回原始仓库,我们将在下一节介绍它。这会开启一个带有代码审查的讨论线程,然后项目所有者和贡献者可以就更改进行沟通,直到所有者满意为止,届时所有者就可以将其合并。

要 fork 一个项目,请访问项目页面,然后点击页面右上角的“Fork”按钮。

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

几秒钟后,你就会被带到你的新项目页面,里面是代码的一个可写副本。

The GitHub Flow (GitHub 工作流)

GitHub 是围绕一个特定的协作工作流设计的,该工作流以 Pull Requests 为中心。这个工作流适用于你是在一个共享仓库中与紧密协作的团队合作,还是在一个全球分布的公司或由众多 fork 贡献项目的陌生人网络中工作。它以 Git 分支 中介绍的 主题分支 工作流为中心。

大致工作流程如下:

  1. Fork 项目。

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

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

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

  5. 在 GitHub 上打开一个 Pull Request。

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

  7. 项目所有者合并或关闭 Pull Request。

  8. 将更新后的 master 同步回你的 fork。

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

让我们通过一个例子来演示如何使用这个工作流来向 GitHub 上托管的开源项目提出更改。

提示

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

创建 Pull Request

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. 将项目 fork 克隆到本地。

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

  3. 对代码进行更改。

  4. 检查更改是否正确。

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

  6. 将新的主题分支推送到你的 GitHub fork。

现在,如果我们回到 GitHub 上的 fork,可以看到 GitHub 注意到我们推送了一个新的主题分支,并提供了一个大大的绿色按钮让我们检查更改并向原始项目打开一个 Pull Request。

你也可以选择通过访问“Branches”页面 https://github.com/<user>/<project>/branches 来定位你的分支并从那里打开一个新的 Pull Request。

Pull Request button
图 90. Pull Request 按钮

如果点击那个绿色按钮,我们会看到一个屏幕,要求我们为 Pull Request 提供一个标题和描述。花费一些精力在这一点上几乎总是值得的,因为一个好的描述可以帮助原始项目的作者了解你的意图,你的提议的更改是否正确,以及接受这些更改是否会改进原始项目。

我们还看到了我们主题分支中“ahead”(超前)于 master 分支的提交列表(在本例中只有一个),以及如果该分支被项目作者合并,将引入的所有更改的统一 diff。

Pull Request creation page
图 91. Pull Request 创建页面

当我们点击此屏幕上的“Create pull request”按钮时,你 fork 的项目的作者会收到一个通知,告知有人在建议更改,并且会链接到一个包含所有这些信息的页面。

注意

虽然 Pull Requests 常用于像这样的公共项目,当贡献者准备好完成更改时,它也经常在内部项目**开发周期早期**使用。由于你甚至可以在 Pull Request 打开**之后**继续推送到主题分支,所以通常会提前打开它,并用它作为团队在一个上下文中迭代工作的工具,而不是在流程的最后才打开。

迭代 Pull Request

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

分布式 Git 中介绍的工作流中,这种对话可能会通过电子邮件进行,而在 GitHub 上,则在线进行。项目所有者可以审查统一 diff,并通过点击任意行来留下评论。

Comment on a specific line of code in a Pull Request
图 92. 对 Pull Request 中的特定代码行发表评论

一旦维护者发表了这条评论,提交 Pull Request 的人(实际上,任何关注仓库的人)都会收到通知。稍后我们会详细介绍如何自定义,但如果他开启了邮件通知,Tony 会收到如下邮件:

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

任何人都可以对 Pull Request 发表一般性评论。在 Pull Request 讨论页面 中,我们可以看到一个例子,项目作者既评论了代码行,又在讨论区发表了一般性评论。可以看到,代码评论也被带入了对话中。

Pull Request discussion page
图 94. Pull Request 讨论页面

现在贡献者可以看到他们需要做什么才能使他们的更改被接受。幸运的是,这非常简单。在电子邮件中,你可能需要重新打包你的系列并重新提交给邮件列表,而在 GitHub 上,你只需再次提交到主题分支并推送,就会自动更新 Pull Request。在 Pull Request final 中,你还可以看到旧的代码评论在更新的 Pull Request 中已被折叠,因为它是在一个已经被更改过的行上发表的。

向现有的 Pull Request 添加提交不会触发通知,所以一旦 Tony 推送了他的更正,他就会决定发表一条评论来通知项目作者他已完成请求的更改。

Pull Request final
图 95. Pull Request final

值得注意的是,如果你点击此 Pull Request 上的“Files Changed”选项卡,你会看到“unified”diff —— 也就是说,如果将此主题分支合并到主分支,将引入的总聚合差异。以 git diff 的术语来说,它基本上会自动显示 git diff master…​<branch> 对于这个 Pull Request 所基于的分支。有关这种 diff 的更多信息,请参阅 确定引入的内容

你还会注意到,GitHub 会检查 Pull Request 是否能干净地合并,并提供一个按钮让你在服务器上执行合并。此按钮仅在你拥有仓库的写入权限且可以进行简单合并时才会显示。如果你点击它,GitHub 将执行一个“非快进式”合并,这意味着即使合并**可以**是快进式,它仍然会创建一个合并提交。

如果你愿意,你也可以简单地将分支拉下来并在本地合并。如果你将此分支合并到 master 分支并推送到 GitHub,Pull Request 将自动关闭。

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

注意
不只是 Fork

需要注意的是,你也可以在同一个仓库的两个分支之间打开 Pull Request。如果你和某人正在开发一个功能,并且你们俩都拥有该项目的写入权限,你可以将一个主题分支推送到仓库,并就该分支向同一项目的 master 分支打开一个 Pull Request,以启动代码审查和讨论过程。无需 fork。

高级 Pull Requests

现在我们已经介绍了在 GitHub 上参与项目贡献的基础知识,接下来让我们介绍一些关于 Pull Requests 的有趣技巧和窍门,以便你更有效地使用它们。

Pull Requests 作为 Patch (补丁)

重要的是要理解,许多项目并不真正将 Pull Requests 视为应该干净地按顺序应用的完美补丁队列,就像大多数基于邮件列表的项目认为补丁系列贡献一样。大多数 GitHub 项目将 Pull Request 分支视为围绕提议更改的迭代对话,最终通过合并应用一个统一的 diff。

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

例如,如果你回去重新看看 Pull Request final,你会注意到贡献者没有变基他的提交并发送另一个 Pull Request。相反,他们添加了新的提交并将其推送到现有的分支。这样,如果你将来回去查看这个 Pull Request,你可以轻松找到所有关于为什么做出决定的上下文。在该网站上按下“Merge”按钮会故意创建一个引用 Pull Request 的合并提交,这样就可以轻松地回去研究原始对话,如有必要。

跟进上游 (Upstream)

如果你的 Pull Request 过时或合并不干净,你需要修复它,以便维护者可以轻松地将其合并。GitHub 会为你测试,并在每个 Pull Request 的底部告诉你合并是否是简单的(trivial)或不是。

Pull Request does not merge cleanly
图 96. Pull Request 未能干净合并

如果你看到像 Pull Request 未能干净合并 这样的信息,你就需要修复你的分支,使其变成绿色,并且维护者不必做额外的工作。

你有两个主要选项来实现这一点。你可以将你的分支变基到目标分支(通常是你 fork 的仓库的 master 分支)之上,或者你可以将目标分支合并到你的分支中。

GitHub 上的大多数开发者会选择后者,原因与我们在上一节中提到的相同。重要的是历史记录和最终合并,所以变基并没有给你带来太多,除了一个稍微干净的历史记录,但它**更加**困难且容易出错。

如果你想合并目标分支以使你的 Pull Request 可合并,你需要将原始仓库添加为新的远程,从中抓取,将该仓库的主分支合并到你的主题分支,修复任何问题,最后将其推回到你打开 Pull Request 的同一个分支。

例如,让我们假设在我们之前使用的“tonychacon”示例中,原始作者进行了一个会造成 Pull Request 冲突的更改。让我们一步步地完成这些步骤。

$ 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 将会自动更新并重新检查是否可以干净地合并。

Pull Request now merges cleanly
图 97. Pull Request 现在可以干净合并了

Git 的一个优点是你可以持续地这样做。如果你有一个长期运行的项目,你可以轻松地反复地从目标分支进行合并,并且只需要处理自上次合并以来出现的冲突,这使得整个过程非常易于管理。

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

引用

你的下一个问题可能是“如何引用旧的 Pull Request?”。事实证明,在 GitHub 中几乎任何可以写文本的地方,都有许多方法可以引用其他内容。

让我们从如何交叉引用另一个 Pull Request 或 Issue 开始。所有 Pull Requests 和 Issues 都分配有编号,并且在项目中是唯一的。例如,你不能同时拥有 Pull Request #3 和 Issue #3。如果你想从任何其他 Pull Request 或 Issue 引用,你只需在任何评论或描述中输入 #<num>。如果你想引用的 Issue 或 Pull Request 位于其他地方,你可以更具体地写 username#<num> 来引用仓库的 fork 中的 Issue 或 Pull Request,或者 username/repo#<num> 来引用另一个仓库中的内容。

让我们看一个例子。假设我们变基了前一个示例中的分支,为它创建了一个新的 Pull Request,现在我们想从新的 Pull Request 中引用旧的 Pull Request。我们还想引用 fork 仓库中的一个 Issue 和一个完全不同的项目中的一个 Issue。我们可以像 Pull Request 中的交叉引用 中那样填写描述。

Cross references in a Pull Request
图 98. Pull Request 中的交叉引用

当我们提交这个 Pull Request 时,我们会看到所有这些内容都像 在 Pull Request 中渲染的交叉引用 一样被渲染。

Cross references rendered in a Pull Request
图 99. 在 Pull Request 中渲染的交叉引用

请注意,我们输入的完整 GitHub URL 被缩短为仅包含必要的信息。

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

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

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

GitHub Flavored Markdown (GitHub 風味 Markdown)

链接到其他 Issue 仅仅是你在 GitHub 的几乎任何文本框中可以做到的有趣事情的开始。在 Issue 和 Pull Request 的描述、评论、代码评论等等中,你可以使用所谓的“GitHub Flavored Markdown”。Markdown 就像写纯文本,但它会以丰富的格式渲染。

请参阅 GitHub Flavored Markdown 的书写和渲染示例,了解注释或文本如何编写然后使用 Markdown 渲染。

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

Markdown 的 GitHub 风味在基本 Markdown 语法之外增加了更多功能。当创建有用的 Pull Request 或 Issue 评论或描述时,这些功能都非常有用。

任务列表

第一个真正有用的 GitHub 特有的 Markdown 功能,尤其适用于 Pull Requests,就是任务列表。任务列表是你想完成的事情的复选框列表。将它们放入 Issue 或 Pull Request 通常表示你想在考虑完成该项目之前需要完成的事情。

你可以像这样创建一个任务列表:

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

如果我们将它包含在我们的 Pull Request 或 Issue 的描述中,我们将看到它像 Markdown 评论中渲染的任务列表 一样被渲染。

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

这通常用于 Pull Requests,以表明在 Pull Request 可以合并之前,你想在分支上完成的所有工作。最酷的部分是你可以简单地点击复选框来更新评论——你不需要直接编辑 Markdown 来勾选任务。

更重要的是,GitHub 会查找 Issues 和 Pull Requests 中的任务列表,并在列出它们的页面上将它们显示为元数据。例如,如果你有一个带有任务的 Pull Request,并且查看所有 Pull Requests 的概述页面,你可以看到它完成了多少。这有助于人们将 Pull Requests 分解为子任务,并帮助其他人跟踪分支的进度。你可以在 Pull Request 列表中的任务列表摘要 中看到一个例子。

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

当你在早期打开一个 Pull Request 并用它来跟踪功能实现进度时,这些内容非常有帮助。

代码片段

你也可以在评论中添加代码片段。当你想要展示一些你**可能**会尝试做但还没有实际实现为分支提交的事情时,这一点尤其有用。它也常用于添加不起作用的代码示例,或者这个 Pull Request 可以实现的代码。

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

```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 Issue 和 Pull Request 的评论中被广泛使用。GitHub 中甚至有一个表情符号助手。如果你正在输入评论并且以 : 字符开头,一个自动完成器会帮助你找到你想要的东西。

Emoji autocompleter in action
图 106. 表情符号自动完成器生效中

表情符号的形式是 :<name>:,可以在评论的任何地方使用。例如,你可以写一些像这样的内容:

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 Flavored Markdown,但它非常有用。除了在评论中添加 Markdown 图片链接(这可能很难找到和嵌入 URL)之外,GitHub 还允许你将图片拖放到文本区域来嵌入它们。

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

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

保持你的 GitHub 公共仓库更新

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

This branch is 5 commits behind progit:master.

但你的 GitHub 仓库永远不会由 GitHub 自动更新;这是你需要自己做的事情。幸运的是,这非常容易做到。

一种不需要任何配置的方法是:例如,如果你从 https://github.com/progit/progit2.git fork,你可以像这样保持你的 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

这样做有效,但每次都得写出 fetch 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 远程进行 fetch。

  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,因为该分支实际上属于上游仓库。