Git
章节 ▾ 第二版

6.2 GitHub - 为项目贡献代码

为项目贡献代码

现在我们的账户已经设置好了,让我们来了解一些有助于您为现有项目做出贡献的细节。

Fork 项目

如果您想为一个您没有推送权限的现有项目做出贡献,您可以“fork”该项目。当您“fork”一个项目时,GitHub 会创建一个完全属于您的项目副本;它位于您的命名空间中,您可以向其推送代码。

注意

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

这样,项目就不必担心添加用户作为协作者来授予他们推送权限。人们可以 fork 一个项目,向其推送代码,并通过创建所谓的 Pull Request 将他们的更改贡献回原始仓库,我们将在后面介绍。这开启了一个包含代码审查的讨论线程,然后所有者和贡献者可以就更改进行沟通,直到所有者对此感到满意,此时所有者可以将其合并。

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

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

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

GitHub 工作流程

GitHub 是围绕特定的协作工作流程设计的,该工作流程以 Pull Request 为中心。无论您是在单个共享存储库中与紧密联系的团队协作,还是在全球分布的公司或陌生人网络通过数十个 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 Web 界面执行大多数操作。该工具可以在 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://[email protected]':
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。

或者,您可以访问 https://github.com/<user>/<project>/branches 的“分支”页面,找到您的分支并从那里打开一个新的 Pull Request。

Pull Request button
图 90. Pull Request 按钮

如果我们点击那个绿色按钮,我们会看到一个屏幕,要求我们为 Pull Request 提供标题和描述。几乎总是值得为此付出一些努力,因为良好的描述可以帮助原始项目的拥有者确定您尝试做什么,您的建议更改是否正确,以及接受更改是否会改进原始项目。

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

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

当您点击此屏幕上的“创建 Pull Request”按钮时,您 fork 的项目的拥有者会收到通知,有人建议进行更改,并会链接到包含所有这些信息的页面。

注意

尽管 Pull Request 通常用于像这样的公共项目,此时贡献者已准备好进行完整的更改,但它也常用于开发周期开始时的内部项目。由于您甚至可以在打开 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 最终结果中,您还可以看到旧的代码注释已在更新的 Pull Request 中折叠,因为它是在已更改的行上进行的。

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

Pull Request final
图 95. Pull Request 最终结果

需要注意的一件有趣的事情是,如果您点击此 Pull Request 上的“已更改的文件”选项卡,您将获得“统一”diff,即如果合并此主题分支,将引入到您的主分支的总聚合差异。在 git diff 方面,它基本上会自动向您显示此 Pull Request 所基于的分支的 git diff master…​<branch>。有关此类 diff 的更多信息,请参阅确定引入的内容

您会注意到的另一件事是,GitHub 会检查 Pull Request 是否可以干净地合并,并提供一个按钮以便您在服务器上执行合并操作。此按钮仅在您对存储库具有写入权限并且可以进行简单的合并时才会显示。如果您点击它,GitHub 将执行“非快进”合并,这意味着即使合并**可以**是快进的,它仍然会创建一个合并提交。

如果您愿意,您可以简单地将分支拉取并将其在本地合并。如果您将此分支合并到 master 分支并将其推送到 GitHub,则 Pull Request 将自动关闭。

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

注意
不仅仅是 Fork

需要注意的是,您也可以在同一个存储库中的两个分支之间打开 Pull Request。如果您与某人一起处理一项功能,并且你们都对项目具有写入权限,则可以将主题分支推送到存储库并在其上打开一个到同一项目的 master 分支的 Pull Request 以启动代码审查和讨论过程。无需 fork。

高级 Pull Request

现在我们已经介绍了在 GitHub 上为项目做出贡献的基础知识,让我们介绍一些关于 Pull Request 的有趣技巧和窍门,以便您能够更有效地使用它们。

Pull Request 作为补丁

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

这是一个重要的区别,因为通常在认为代码完美之前就会建议更改,而基于邮件列表的补丁系列贡献中这种情况要少得多。这使得维护者能够更早地进行对话,以便找到合适的解决方案更像是一种社区的努力。当使用 Pull Request 提出代码并且维护者或社区建议更改时,补丁系列通常不会重新整理,而是将差异作为新提交推送到分支,在保持先前工作上下文的条件下推动对话向前发展。

例如,如果您返回并再次查看Pull Request 最终结果,您会注意到贡献者没有重新整理他的提交并发送另一个 Pull Request。相反,他们添加了新的提交并将其推送到现有分支。这样,如果您将来返回并查看此 Pull Request,您可以轻松找到做出决策的所有上下文。网站上的“合并”按钮有意创建了一个引用 Pull Request 的合并提交,以便在必要时可以轻松地返回并研究原始对话。

跟上上游

如果您的 Pull Request 过时或无法干净地合并,您需要修复它,以便维护者可以轻松地合并它。GitHub 会为您测试这一点,并在每个 Pull Request 的底部告诉您合并是否简单。

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 now merges cleanly
图 97. 拉取请求现在可以干净地合并

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

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

参考

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

让我们从如何交叉引用另一个拉取请求或问题开始。所有拉取请求和问题都分配了编号,并且在项目中是唯一的。例如,您不能同时拥有拉取请求 #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 Flavored Markdown

链接到其他问题仅仅是您可以使用 GitHub 上几乎任何文本框执行的有趣操作的开始。在问题和拉取请求描述、评论、代码注释等中,您可以使用所谓的“GitHub Flavored Markdown”。Markdown 就像用纯文本编写,但会以丰富的方式呈现。

请参阅GitHub Flavored Markdown 的编写和呈现示例,了解如何编写评论或文本,然后使用 Markdown 呈现。

An example of GitHub Flavored Markdown as written and as rendered
图 101. GitHub Flavored 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. 表情符号自动完成器正在运行

表情符号采用:<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. 大量的表情符号评论

这并非特别有用,但它确实为一种难以表达情感的媒介增添了一些趣味和情感。

注意

实际上,如今有相当多的 Web 服务使用了表情符号字符。一个很棒的备忘单,可以参考查找表达您想说的话的表情符号,可以在以下位置找到:

图像

从技术上讲,这不是 GitHub Flavored Markdown,但它非常有用。除了向评论添加 Markdown 图像链接(可能难以找到和嵌入 URL)之外,GitHub 还允许您将图像拖放到文本区域以嵌入它们。

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

如果您查看拖放图像以上传并自动嵌入它们,您可以在文本区域上方看到一个小的“解析为 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