拉取请求工作流程

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 网站上在线阅读。你还可以尝试 GitHub 的交互式指南

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

  • master 分支是开发下一个主要版本的地方. 作为开发分支, 它可能不稳定, 不适合用于生产. 这是应该优先进行PR的地方.

  • 稳定分支以其版本命名,例如:3.12.1。它们用于将 master 分支中的错误修复和增强功能回传到当前维护的稳定版本(例如 3.1.2 或 2.1.6)。一般说来,最后一个稳定分支会被维护到下一个次要版本(例如 3.0 分支被维护到了 Godot 3.1 的发布)。如果你想针对被维护的稳定分支做 PR,请先检查你的修改是否也与 master 分支有关,如果是,请优先为 master 分支做 PR。发布管理者可以在相关的情况下将修复的内容挑选到稳定分支。

  • 偶尔也会有特性分支, 通常是为了在某个时候合并到 master 分支.

分叉和克隆

第一步是在 GitHub 上 fork 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, 请使用以下命令:

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

备注

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

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

  1. $ cd godot

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

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

这将创建一个名为 upstream 的引用, 指向原始的 godotengine/godot 版本库. 当你想从它的 master 分支拉取新的提交来更新你的fork时, 这将很有用. 你有另一个名为 origin 的远程引用, 它指向你的fork( USERNAME/godot ).

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

备注

开分支、拉取、编码、暂存、提交、推送、变基……非常有技术含量。

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

下面, 我们将假设你想在Godot的项目管理器中实现一个功能, 该功能在 editor/project_manager.cpp 文件中编码.

分支

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

  1. # Create the branch based on the current branch (master)
  2. $ git branch better-project-manager
  3. # Change the current branch to the new one
  4. $ git checkout better-project-manager

此命令是等效的:

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

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

  1. $ git checkout master

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

  1. $ git branch
  2. 2.1
  3. * better-project-manager
  4. master

创建新分支之前,请务必始终返回到 master 分支,因为您当前的分支将用作新分支的基础。或者,您可以在新分支的名称之后指定一个自定义基础分支:

  1. $ git checkout -b my-new-feature master

更新您的分支

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

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

  1. $ git pull --rebase upstream master

--rebase 参数将确保您所提交的任何本地更改将被“重新应用”到“拉动”分支的顶部,这通常是我们在 PR 工作流程中想要的。这样,当您打开请求请求时,您自己的提交将是与上游 master 分支的唯一区别。

变基时,如果您提交同时在上游分支中已更改的修改后的代码,则可能会发生冲突。如果发生这种情况,Git 将在冲突的提交处停止,并要求您解决冲突。您可以使用任何文本编辑器执行此操作,然后进行更改(稍后再进行介绍),然后继续执行 git rebase --continue。如果以后的提交也有冲突,请重复该操作,直到变基操作完成。

如果您不确定变基期间发生了什么并且感到恐慌(不用担心,我们都要做前几次),则可以使用 git rebase --abort 中止变基。这样您就返回了该分支在调用 git pull --rebase 前的原始状态。

备注

如果省略了 --rebase 参数,则将创建一个合并提交,告诉 Git 如何处理两个不同的分支。如果发生任何冲突,将通过此合并提交立即解决所有冲突。

虽然这是有效的工作流程,并且默认行为是 git pull,但是我们的 PR 工作流程并不赞成在 PR 中使用合并提交。我们仅在将 PR 合并到上游分支时才使用它们。

我们的理念是,PR 应该代表对代码库所做更改的最后阶段,并且我们对合并之前在中间阶段所做的错误和修复不感兴趣。Git 提供了很棒的工具来“改写历史”,让它好像我们第一次就做对了一样,并且我们乐于使用它来确保更改在合并之后很容易被查看和理解。

如果您已经创建了一个合并提交而不使用 rebase,或者进行了其他任何导致不希望的历史记录的更改,那么最好的选择是在上游分支上进行交互式变基。相关说明请参见专门的章节

小技巧

如果您想在任何时候将本地分支重置为给定的提交或分支,则可以使用 git reset --hard <commit ID>git reset --hard <remote>/<branch> 来实现。(例如 git reset --hard upstream/master)。

请注意,这将删除您可能已在此分支中提交的所有更改。如果您错误地丢失了提交,请使用 git reflog 命令查找您想要还原的先前状态的提交 ID,并将其用作 git reset --hard 的参数,回到那个状态。

做出变更

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

默认情况下,这些修改是未暂存的。暂存区是位于工作目录(你进行修改的地方)和本地 Git 仓库(提交和所有元数据在 .git 文件夹中)之间的一层。要把工作目录中的修改带到 Git 仓库,你需要用 git add 命令把它们归档,然后用 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 commit --amend 允许您使用当前暂存的更改来修改最后一次提交(使用 git add 添加)。如果要修复上一次提交中的错误(错误、错别字、样式问题等),这是最佳选择。

  • git log 将显示当前分支的最后提交. 如果您做了本地提交, 它们应该显示在顶部.

  • git show 将显示上次提交的更改. 您还可以指定提交哈希以查看该提交的更改.

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

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

  1. # It's nice to know where you're starting from
  2. $ git log
  3. # Do changes to the project manager with the nano text editor
  4. $ nano editor/project_manager.cpp
  5. # Find an unrelated bug in Control and fix it
  6. $ nano scene/gui/control.cpp
  7. # Review changes
  8. $ git status
  9. $ git diff
  10. # We'll do two commits for our unrelated changes,
  11. # starting by the Control changes necessary for the PM enhancements
  12. $ git add scene/gui/control.cpp
  13. $ git commit -m "Fix handling of margins in Control"
  14. # Check we did good
  15. $ git log
  16. $ git show
  17. $ git status
  18. # Make our second commit
  19. $ git add editor/project_manager.cpp
  20. $ git commit -m "Add a pretty banner to the project manager"
  21. $ git log

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

将更改推送到远程

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

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

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

  1. $ git push origin better-project-manager

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

发出拉取请求

当你在 GitHub 上加传你的 fork 的分支时,你应该看到一行字:“This branch is 2 commits ahead of godotengine:master.”(如果你的 master 分支与上游的 master 分支不同步,则可能落后一些提交)。

../../_images/github_fork_make_pr.png

在这一行,有一个“Pull request”链接。点击它将打开一个表单,让你在 godotengine/godot 上游仓库发出 Pull request。它应该显示你的两个提交,并说明“Able to merge”(能够合并)。如果不是这样(例如,它有更多的提交,或者说有合并冲突),请勿创建 PR,因为出现了问题。到 Godot 贡献者聊天去寻求支持吧 :)

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

修改拉取请求

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

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

  1. # Check out your branch again if you had changed in the meantime
  2. $ git checkout better-project-manager
  3. # Fix a mistake
  4. $ nano editor/project_manager.cpp
  5. $ git add editor/project_manager.cpp
  6. $ git commit -m "Fix a typo in the banner's title"
  7. $ git push origin better-project-manager

However, be aware that in our PR workflow, we favor commits that bring the codebase from one functional state to another functional state, without having intermediate commits fixing up bugs in your own code or style issues. Most of the time, we will prefer a single commit in a given PR (unless there’s a good reason to keep the changes separate). Instead of authoring a new commit, consider using git commit --amend to amend the previous commit with your fixes. The above example would then become:

  1. # Check out your branch again if you had changed in the meantime
  2. $ git checkout better-project-manager
  3. # Fix a mistake
  4. $ nano editor/project_manager.cpp
  5. $ git add editor/project_manager.cpp
  6. # --amend will change the previous commit, so you will have the opportunity
  7. # to edit its commit message if relevant.
  8. $ git commit --amend
  9. # As we modified the last commit, it no longer matches the one from your
  10. # remote branch, so we need to force push to overwrite that branch.
  11. $ git push --force origin better-project-manager

交互式变基

如果你没有严格按照上面的步骤对提交进行修订,而是创建了修复提交,或者你在编写修改时没有注意到我们的工作流程和 Git 使用技巧,审核人可能会要求将你的分支进行变基,将其中一些或者所有提交压缩成一个。

确实, 如果对某些修订进行了一些修订, 以解决原始提交中的错误, 错别字等, 则它们与将来的变更日志阅读器无关, 后者希望了解Godot代码库中发生了什么, 或者何时以及如何进行给定文件是最后修改的.

为了把那些不相干的提交压缩到主要的提交中, 我们将不得不 重写历史记录 . 没错, 我们有这个权力. 你可能会读到这是一种不好的做法, 当涉及到上游仓库的分支时, 这确实是事实. 但在你的fork中, 你可以做任何你想做的事, 而且一切都可以得到整洁的PR :)

我们将使用交互式变基 git rebase -i 来执行此操作。此命令将使用提交哈希作为参数,并允许您修改该提交哈希与分支的最后一个提交之间的所有提交,即所谓的 HEAD

虽然您可以将任何提交 ID 赋给 git rebase-i 并查看其间的所有内容,但最常见和最方便的工作流涉及在上游 master 分支上进行变基,您可以使用:

  1. $ git rebase -i upstream/master

备注

由于远程分支和本地分支之间的区别, 在Git中引用分支有点棘手. 这里, upstream/master (带有 / )是从 upstream remote的 master 分支中提取的本地分支.

交互式变基只能在本地分支上进行,因此 / 在这里很重要。由于上游远程服务器会经常变动,您的本地 upstream/master 分支可能会过时,因此您可以使用 git fetch upstream master 来更新。与 git pull--rebase upstream master 相反,fetch 只会更新 upstream/master 的引用(与本地 master 分支不同……是的,这很混乱,但你会逐渐熟悉这一点)。

这将打开一个文本编辑器(默认为 vi,请参阅 Git 文档 把它配置为您最喜欢的一个),内容可能如下所示:

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

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

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

保存并退出编辑器后,就会进行变基。第二个提交将被合并到第一个提交中,现在应该能够通过 git loggit show 确认您只有一个具有先前两个提交的更改的提交。

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

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

这是一个理智的行为,Git不会让你推送会覆盖远程内容的修改. 但这正是我们想做的, 所以我们必须 强制 它:

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

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

删除 Git 分支

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

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

  1. $ git branch -d better-project-manager

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

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

  1. $ git push origin -d better-project-manager

您还可以从GitHub PR本身删除远程分支, 一旦合并或关闭它就会出现一个按钮.