拉取请求工作流程
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.1
和2.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”按钮:
点击它,一段时间后,你应该被重定向到你自己的 Godot 仓库分叉,并将 GitHub 用户名作为名称空间:
然后你可以 克隆 你的分叉, 即创建在线仓库的本地副本(在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时, 这将很有用. 你有另一个名为 origin
的远程引用, 它指向你的fork( USERNAME/godot
).
你只需要做一次上面的步骤, 只要你保留本地的 godot
文件夹(你可以随意移动它, 相关的元数据隐藏在它的 .git
子文件夹中).
备注
开分支、拉取、编码、暂存、提交、推送、变基……非常有技术含量。
这对Daft Punk的 技术 的不良看法显示了Git初学者对其工作流程的一般概念: 许多奇怪的命令可以通过复制和粘贴来学习, 希望它们能按预期运行. 这实际上并不是一种糟糕的学习方式, 只要你好奇并且在迷失时毫不犹豫地询问你的搜索引擎, 因此, 我们将为你提供在Git中工作时要了解的基本命令.
In the following, we will assume as an example that you want to implement a feature in Godot’s Project Manager, which is coded in the editor/project_manager.cpp
file.
分支
默认情况下, 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
创建新分支之前,请务必始终返回到 master
分支,因为你当前的分支将用作新分支的基础。或者,你可以在新分支的名称之后指定一个自定义基础分支:
$ git checkout -b my-new-feature master
更新你的分支
第一次(在你 fork 上游仓库之后)是不需要这样做的。但是,下次你想要处理某些事情时,你会注意到你的 fork 的 master
落后于上游 master
分支几个提交:其他贡献者的拉取请求同时被合并。
为了确保你开发的功能与当前的上游 master
分支之间不会发生冲突,你将不得不通过拉取上游分支来更新你的分支。
$ 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历史记录的样子:
# 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 will ask you for your username and password. For your password, enter your GitHub Personal Access Token (PAT). If you do not have a GitHub Personal Access Token, or do not have one with the correct permissions for your newly forked repository, you will need to create one. Follow this link to create your Personal Access Token: Creating a personal access token.
After you have successfully verified your account using your PAT, the changes will be sent to your remote repository. If you check the fork’s page on GitHub, you should see a new branch with your added commits.
发出拉取请求
当你在 GitHub 上加传你的 fork 的分支时,你应该看到一行字:“This branch is 2 commits ahead of godotengine:master.”(如果你的 master
分支与上游的 master
分支不同步,则可能落后一些提交)。
在这一行,有一个“Pull request”链接。点击它将打开一个表单,让你在 godotengine/godot
上游仓库发出 Pull request。它应该显示你的两个提交,并说明“Able to merge”(能够合并)。如果不是这样(例如,它有更多的提交,或者说有合并冲突),请勿创建 PR,因为出现了问题。到 Godot 贡献者聊天去寻求支持吧 :)
为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 工作流程中,我们偏向于将代码库从一种功能状态转换为另一种功能状态的提交,而没有中间提交来解决你自己的代码或样式问题中的错误。在大多数情况下,我们会倾向于在给定的 PR 中进行一次提交(除非有充分的理由将更改分开)。与其撰写新的提交,不如考虑使用 git commit --amend
来修改先前提交的修复。上面的示例将变为:
# 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
# --amend will change the previous commit, so you will have the opportunity
# to edit its commit message if relevant.
$ git commit --amend
# As we modified the last commit, it no longer matches the one from your
# remote branch, so we need to force push to overwrite that branch.
$ git push --force origin better-project-manager
交互式变基
If you didn’t follow the above steps closely to amend changes into a commit instead of creating fixup commits, or if you authored your changes without being aware of our workflow and Git usage tips, reviewers might request you to rebase your branch to squash some or all of the commits into one.
确实, 如果对某些修订进行了一些修订, 以解决原始提交中的错误, 错别字等, 则它们与将来的变更日志阅读器无关, 后者希望了解Godot代码库中发生了什么, 或者何时以及如何进行给定文件是最后修改的.
为了把那些不相干的提交压缩到主要的提交中, 我们将不得不 重写历史记录 . 没错, 我们有这个权力. 你可能会读到这是一种不好的做法, 当涉及到上游仓库的分支时, 这确实是事实. 但在你的fork中, 你可以做任何你想做的事, 而且一切都可以得到整洁的PR :)
我们将使用交互式变基 git rebase -i
来执行此操作。此命令将使用提交哈希作为参数,并允许你修改该提交哈希与分支的最后一个提交之间的所有提交,即所谓的 HEAD
。
虽然你可以将任何提交 ID 赋给 git rebase-i
并查看其间的所有内容,但最常见和最方便的工作流涉及在上游 master
分支上进行变基,你可以使用:
$ 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 文档 把它配置为你最喜欢的一个),内容可能如下所示:
pick 1b4aad7 Add a pretty banner to the Project Manager
pick e07077e Fix a typo in the banner's title
编辑器还将显示有关如何对这些提交采取行动的说明. 特别是, 它应该告诉你 pick
意味着使用该提交(什么都不做), 并且 squash
和 fixup
可以用于在其父提交中 合并 提交. squash
和 fixup
之间的区别在于 fixup
会从压缩的提交中丢弃提交日志. 在我们的示例中, 我们对保持 修复错字
提交的日志不感兴趣, 因此我们使用:
pick 1b4aad7 Add a pretty banner to the Project Manager
fixup e07077e Fix a typo in the banner's title
保存并退出编辑器后,就会进行变基。第二个提交将被合并到第一个提交中,现在应该能够通过 git log
和 git show
确认你只有一个具有先前两个提交的更改的提交。
但!你改写了历史, 现在你的本地和远程分支有分歧. 实际上, 以上示例中的提交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.
This is reasonable behavior, Git will not let you push changes that would override remote content. But that’s actually what we want to do here, so we will have to force it:
$ git push --force origin better-project-manager
还有没错! Git很乐意用你在本地拥有的东西 替换 你的远程分支(所以使用 git log
确保你想要的东西). 这也将相应地更新PR.
Rebasing onto another branch
If you have accidentally opened your PR on the wrong branch, or need to target another branch for some reason, you might need to filter out a lot of commits that differ between the old branch (for example 4.2
) and the new branch (for example master
). This can make rebasing difficult and tedious. Fortunately git
has a command just for this situation, git rebase --onto
.
If your PR was created from the 4.2
branch and you want to update it to instead start at master
the following steps should fix this in one step:
$ git rebase -i --onto master 4.2
This will take all the commits on your branch after the 4.2
branch, and then splice them on top of master
, ignoring any commits from the 4.2
branch not on the master
branch. You may still need to do some fixing, but this command should save you a lot of tedious work removing commits.
Just like above for the interactive rebase you need to force push your branch to handle the different changes:
$ git push --force origin better-project-manager
删除 Git 分支
在你的提交请求合并之后, 你应该做的最后一件事是: 删除用于PR的Git分支. 如果不删除分支不会有问题, 但是这样做是一个好习惯. 你将需要执行两次, 一次是对本地分支, 另一次是对GitHub上的远程分支.
To delete our better Project Manager branch locally, use this command:
$ git branch -d better-project-manager
或者, 如果分支尚未合并, 我们想要删除它, 不是使用 -d
, 而是使用 -D
.
接下来, 要删除GitHub上的远程分支, 请使用以下命令:
$ git push origin -d better-project-manager
你还可以从GitHub PR本身删除远程分支, 一旦合并或关闭它就会出现一个按钮.