拉取请求(PR)工作流程

Godot使用的所谓 PR工作流程 对于许多使用Git的项目来说都很常见,并且对于资深自由软件贡献者应该很熟悉。这个想法是只有少数(如果有的话)直接提交给 master 分支。相反,贡献者 fork 项目(即创建它的副本,他们可以按照自己的意愿修改),然后使用GitHub接口从其fork的一个分支请求 pull 到原始(通常命名为 upstream )存储库的一个分支。

然后,生成的 拉取请求 (PR)可以由其他贡献者审查,可能批准它、拒绝它、或者最常要求修改它。一旦获得批准,PR就可以由其中一个核心开发人员合并,其提交将成为目标分支(通常是 master 分支)的一部分。

我们将一起通过一个示例展示典型的工作流程和相关的Git命令。但首先,让我们快速了解Godot的Git存储库的组织结构。

Git源存储库

GitHub上的 存储库 是一个 Git 代码存储库以及一个嵌入式问题跟踪器和PR系统。

注解

如果您正在为文档做贡献,可以在 这里 找到它的存储库。

Git版本控制系统是用于跟踪源代码的连续编辑的工具——为了高效地为Godot做贡献,强烈 建议学习Git命令行的基础知识。Git有一些图形界面,但是它们通常会鼓励用户养成关于Git和PR工作流程的不良习惯,因此我们建议不要使用它们。特别是,我们建议不要使用GitHub的在线编辑器进行代码贡献(尽管可以进行较小的修复或文档更改),因为它会对每个文件和每个修改强制执行一次提交,因此很快导致PR的Git历史记录不可读(尤其是在同行评审之后)。

参见

Git的“书”的第一部分很好地介绍了该工具的原理以及您在日常工作流程中需要掌握的各种命令。您可以在 Git SCM 网站上在线阅读它们。

Git存储库上的分支被组织如下:

  • master 分支是开发下一个主要版本的地方。作为开发分支,它可能不稳定,不适合用于生产。这是应该优先进行PR的地方。
  • The stable branches are named after their version, e.g. 3.1 and 2.1. They are used to backport bugfixes and enhancements from the master branch to the currently maintained stable release (e.g. 3.1.2 or 2.1.6). As a rule of thumb, the last stable branch is maintained until the next major version (e.g. the 3.0 branch was maintained until the release of Godot 3.1). If you want to make PRs against a maintained stable branch, you will have to check if your changes are also relevant for the master branch.
  • 有时可能会有功能分支,通常意味着在某个时候合并到 master 分支。

分叉和克隆

第一步是在GitHub上 分叉 godotengine/godot 库。 为此,您需要拥有一个GitHub帐户并登录。在存储库的GitHub页面的右上角,您应该看到如下所示的 Fork 按钮:

../../_images/github_fork_button.png

点击它,一段时间后,您应该被重定向到您自己的Godot存储库分叉,并将GitHub用户名作为名称空间:

../../_images/github_fork_url.png

然后您可以 克隆 您的分叉,即创建在线存储库的本地副本(在Git中叫做 origin remote)。如果您还没有,若您使用的是Windows或macOS,请从 其网站 下载Git;若您使用的是Linux,请通过您的软件包管理器安装它。

注解

如果您使用的是Windows,请打开Git Bash键入命令。macOS和Linux用户可以使用各自的终端。

要从GitHub克隆您的fork,请使用以下命令:

$ git clone https://github.com/USERNAME/godot

注解

在我们的示例中,$ 字符表示典型UNIX shell上的命令行提示符。 它不是命令的一部分,不应该键入。

稍后,您应该在当前工作目录中有一个 godot 目录。 使用 cd 命令进入它:

$ cd godot

我们将从建立对我们分叉的原始存储库的引用开始:

$ git remote add upstream https://github.com/godotengine/godot
$ git fetch upstream

这将创建一个名为 upstream 的引用,指向原始的godotengine/godot存储库。当您想从它的 master 分支中提取新的提交来更新您的fork时,这将非常有用。您有另一个名为 originremote 引用,它指向您的fork。

您只需要做一次上面的步骤,只要您保留本地的 godot 文件夹(您可以随意移动它,相关的元数据隐藏在它的 .git 子文件夹中)。

注解

分支、拉取、编码、暂存、提交、推送、重新设置基线……技术。

这对Daft Punk的 技术 的不良看法显示了Git初学者对其工作流程的一般概念:许多奇怪的命令可以通过复制和粘贴来学习,希望它们能按预期运行。这实际上并不是一种糟糕的学习方式,只要您好奇并且在迷失时毫不犹豫地询问您的搜索引擎,因此,我们将为您提供在Git中工作时要了解的基本命令。

在下文中,我们假设您要在Godot的项目管理器中实现一个功能,该功能已在 editor/project_manager.cpp 文件中编码。

分支

默认情况下,git clone 应该让您进入fork(origin)的 master 分支。要开始自己的功能开发,我们将创建一个功能分支:

# Create the branch based on the current branch (master)
$ git branch better-project-manager

# Change the current branch to the new one
$ git checkout better-project-manager

此命令是等效的:

# Change the current branch to a new named one, based on the current branch
$ git checkout -b better-project-manager

如果您想回到 master 分支,您会使用:

$ git checkout master

您可以使用 git branch 命令查看当前使用的分支:

$ git branch
  2.1
* better-project-manager
  master

更新您的分支

第一次(在您分叉上游存储库之后)不需要这样做。 但是,下次您想要处理某些事情时,您会注意到您的fork的 master 落后于上游 master 分支几个提交:其他贡献者的拉取请求同时被合并。

为了确保您开发的功能与当前的上游 master 分支之间不会发生冲突,您将不得不通过 上游分支来更新您的分支。

$ git pull upstream master

However, if you had local commits, this method will create a so-called “merge commit”, and you will soon hear from fellow contributors that those are not wanted in PRs. To update the branch without creating a merge commit, you will have to use the --rebase option, so that your local commits are replayed on top of the updated upstream master branch. It will effectively modify the Git history of your branch, but that is for the greater good.

因此,您应该(几乎)始终使用的命令是:

$ git pull --rebase upstream master

If you have already pushed the merge commit without using rebase, or have made any other changes that have resulted in undesired history, you may use a hard reset to revert to a specific commit and try again:

$ git reset --hard [The ID of the last desired commit]

Once you have done this, you may run --rebase to merge master correctly.

If you have already pushed the wrong commits to your remote branch, you will have to force push by using git push --force.

警告

git reset --hard can be a dangerous operation, especially if you have untracked or uncommitted changes. However, if you have committed changes that you reset using git reset --hard, you may still be able to recover them by resetting to a commit ID found with the git reflog command.

做出变更

然后,您将使用常用的开发环境(文本编辑器,IDE等)对我们的示例的 editor/project_manager.cpp 文件进行更改。

默认情况下,这些更改是 未暂存的(unstaged)。暂存区域是您的工作目录(您进行修改的位置)和本地git存储库(.git 文件夹中的提交和所有元数据)之间的一个层。要将工作目录中的更改带到Git存储库,您需要使用 git add 命令对它们进行 暂存(stage),然后使用 git commit 命令提交它们。

在暂存之前,暂存后和提交之后,您应该了解各种命令来查看当前工作。

  • git diff 将显示当前未暂存的更改,即工作目录和暂存区域之间的差异。
  • git checkout -- <files> 将撤消给定文件的未暂存更改。
  • git add <files>暂存 列出的文件的更改。
  • git diff --staged 将显示当前的暂存的更改,即暂存区域和上次提交之间的差异。
  • git reset HEAD <files>取消暂存 列出的文件的更改。
  • git status 将显示当前暂存和未暂存的修改。
  • git commit 将提交暂存文件。它将打开一个文本编辑器(您可以使用 GIT_EDITOR 环境变量或Git配置中的 core.editor 设置来定义要使用的编辑器),以便您编写提交日志。您可以使用 git commit -m "Cool commit log" 直接写日志。
  • git log 将显示当前分支的最后提交。 如果您做了本地提交,它们应该显示在顶部。
  • git show 将显示上次提交的更改。您还可以指定提交哈希以查看该提交的更改。

要记住的东西太多了!不用担心,当您需要进行更改时,只需检查一下备忘单,然后边做边学即可。

以下是我们的示例中shell历史记录的样子:

# It's nice to know where you're starting from
$ git log

# Do changes to the project manager with the nano text editor
$ nano editor/project_manager.cpp

# Find an unrelated bug in Control and fix it
$ nano scene/gui/control.cpp

# Review changes
$ git status
$ git diff

# We'll do two commits for our unrelated changes,
# starting by the Control changes necessary for the PM enhancements
$ git add scene/gui/control.cpp
$ git commit -m "Fix handling of margins in Control"

# Check we did good
$ git log
$ git show
$ git status

# Make our second commit
$ git add editor/project_manager.cpp
$ git commit -m "Add a pretty banner to the project manager"
$ git log

有了这个,我们应该在 better-project-manager 分支中有两个新的提交,这些提交不在 master 分支中。它们仍然只是本地的,远程分支不知道它们,上游存储库也不知道。

将更改推送到远程

这就是 git push 将发挥作用的地方。在Git中,提交总是在本地存储库中完成(与Subversion不同,其提交将直接修改远程存储库)。您需要 推送 新提交到远程分支以与世界共享它们。这个语法是:

$ git push <remote> <local branch>[:<remote branch>]

如果您希望远程分支与本地分支具有相同的名称,则可以省略有关远程分支的部分,在本示例中就是这种情况,因此我们将执行以下操作:

$ git push origin better-project-manager

Git会要求您提供用户名和密码,更改将发送到您的远程分支。如果您在GitHub上查看fork的页面,则应该看到一个带有已添加提交的新分支。

发出拉取请求

当您在GitHub上加载fork的分支时,您应该看到一行说 “此分支比godotengine:master提前2个提交。” 如果您的 master 分支与上游 master 分支不同步,则可能会有一些提交。

../../_images/github_fork_make_pr.png

在那一行,有一个 拉取请求 链接。点击它将打开一个表单,该表单使您可以在godotengine/godot上游存储库上发出拉取请求。它应该显示您的两个提交,并声明 能够合并。 如果没有(例如,它有更多的提交,或说有合并冲突),不要创建PR,出错了。去IRC并寻求支持:)

为PR使用明确的标题,并将必要的详细信息放在注释区域。您可以拖放屏幕截图,GIF或压缩的项目(如果相关),以展示您的工作实现的内容。点击“创建拉取请求”,没错!

修改拉取请求

虽然它是由其他贡献者审核的,但您经常需要对尚未合并的PR进行更改,或者是因为贡献者要求他们,或者是因为您在测试时发现了自己的问题。

好消息是您可以简单地通过对您发出拉取请求的分支进行操作来修改拉取请求。 您可以,例如在该分支上进行新的提交,将其推送到您的分支,PR将自动更新:

# Check out your branch again if you had changed in the meantime
$ git checkout better-project-manager

# Fix a mistake
$ nano editor/project_manager.cpp
$ git add editor/project_manager.cpp
$ git commit -m "Fix a typo in the banner's title"
$ git push origin better-project-manager

这应该是诀窍,但……

掌握PR工作流程:重新设置基线

在上面提到的情况下,您的对于Git历史特别迂腐的贡献者,可能会要求您 重新设置基线 您的分支来 压缩 或者 合并 最后两个提交到一起(即两个与这个项目 manager 有关), 因为第二个提交基本上解决了第一个提交的问题。

PR合并后,将与变更日志读者即PR编写者无关将让人误解。相反,我们只想保留从一个工作状态进入另一工作状态的提交。

要将这两个提交压缩在一起,我们将不得不 重写历史记录 。对,我们有这种能力。您可能会了解到这是一个不好的做法,确实,当涉及到上游存储库的分支时。但是在您的分叉中,您可以做任何您想做的事情,并且一切都被允许以获得整洁的PR :)

我们将使用 交互式重新设置基线 git rebase -i 来执行此操作。此命令将使用提交哈希作为参数,并允许您修改该提交哈希与分支的最后一个提交之间的所有提交,即所谓的 HEAD。在我们的示例中,我们希望对最后两次提交采取行动,因此我们将执行以下操作:

# The HEAD~X syntax means X commits before HEAD
$ git rebase -i HEAD~2

这将打开一个文本编辑器:

pick 1b4aad7 Add a pretty banner to the project manager
pick e07077e Fix a typo in the banner's title

编辑器还将显示有关如何对这些提交采取行动的说明。特别是,它应该告诉您 pick 意味着使用该提交(什么都不做),并且 squashfixup 可以用于在其父提交中 合并 提交。squashfixup 之间的区别在于 fixup 会从压缩的提交中丢弃提交日志。在我们的示例中,我们对保持 修复错字 提交的日志不感兴趣,因此我们使用:

pick 1b4aad7 Add a pretty banner to the project manager
fixup e07077e Fix a typo in the banner's title

保存并退出编辑器后,将发生重新设置基线。第二个提交将被合并到第一个提交中,并且 git loggit show 现在应该确认您只有一个具有先前两个提交的更改的提交。

注解

您可以在修复拼写错误时使用 git commit --amend 来避免这种重新设置基线。此命令将暂存更改直接写入 最后的 提交(HEAD),而不是像我们在此示例中那样创建新提交。因此,这等效于我们对新提交进行的操作,然后进行了重新设置基线以将其标记为 fixup

但!你改写了历史,现在你的本地和远程分支有分歧。实际上,以上示例中的提交1b4aad7将已更改,因此获得了新的提交哈希。如果您尝试推送到远程分支,将引发错误:

$ git push origin better-project-manager
To https://github.com/akien-mga/godot
 ! [rejected]        better-project-manager -> better-project-manager (non-fast-forward)
error: failed to push some refs to 'https://[email protected]/akien-mga/godot'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.

这是一种理智的行为,Git不允许您推送将覆盖远程内容的更改。 但这实际上是我们要在此处执行的操作,因此我们将不得不 强制 执行此操作:

$ git push --force origin better-project-manager

还有没错! Git很乐意用您在本地拥有的东西 替换 您的远程分支(所以使用 git log 确保您想要的东西)。这也将相应地更新PR。

删除Git分支

在您的提交请求合并之后,您应该做的最后一件事是:删除用于PR的Git分支。如果不删除分支不会有问题,但是这样做是一个好习惯。您将需要执行两次,一次是对本地分支,另一次是对GitHub上的远程分支。

要在本地删除 better project manager 分支,请使用以下命令:

$ git branch -d better-project-manager

或者,如果分支尚未合并,我们想要删除它,不是使用 -d,而是使用 -D

接下来,要删除GitHub上的远程分支,请使用以下命令:

$ git push origin -d better-project-manager