拉取请求(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的地方.稳定分支以其版本命名,例如:
3.1
and2.1
.它们用于将 “master “分支中的错误修复和增强功能回传到当前维护的稳定版本(例如3.1.2或2.1.6).根据经验,最后一个稳定分支会被维护到下一个主要版本(例如,3.0
分支被维护到Godot 3.1的发布).如果你想针对被维护的稳定分支做PR,请先检查你的修改是否也与master
分支有关,如果是,请优先为master
分支做PR.发布管理者可以在相关的情况下将修复的内容挑选到稳定分支.偶尔也会有特性分支,通常是为了在某个时候合并到
master
分支.
分叉和克隆
第一步是在GitHub上 分叉 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中工作时要了解的基本命令.
下面,我们将假设你想在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
创建新分支之前,请务必始终返回到” master”分支,因为您当前的分支将用作新分支的基础.或者,您可以在新分支的名称之后指定一个自定义基础分支:
$ git checkout -b my-new-feature master
更新您的分支
第一次(在您分叉上游存储库之后)不需要这样做. 但是,下次您想要处理某些事情时,您会注意到您的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``,或者进行了其他任何导致不希望的历史记录的更改,那么最好的选择是在上游分支上使用一个 interactive rebase. 有关说明,请参见 dedicated section 专用部分.
小技巧
如果您想在任何时候将本地分支”重置”为给定的提交或分支,则可以使用 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会要求您提供用户名和密码,更改将发送到您的远程分支.如果您在GitHub上查看fork的页面,则应该看到一个带有已添加提交的新分支.
发出拉取请求
当你在GitHub上加传你的fork的分支时,你应该看到一行字: *“This branch is 2 commits ahead of godotengine:master.”*(如果你的 master
分支与上游的 master
分支不同步,则可能落后一些提交).
在这一行,有一个 “Pull request” 链接.点击它将打开一个表单,让你在 godotengine/godot
上游仓库发出一个Pull request.它应该显示你的两个提交,并说明 “Able to merge” .如果不是这样(例如,它有更多的提交,或者说有合并冲突),不要创建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工作流程中,我们偏向于将代码库从一种功能状态转换为另一种功能状态的提交,而没有中间提交来解决您自己的代码或样式问题中的错误.在大多数情况下,我们会倾向于在给定的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
交互式衍合
如果你没有严格按照上面的步骤,将 修改 作为提交,而不是创建修复提交,或者你在编写修改时没有注意到我们的工作流程和Git使用技巧,审稿人可能会要求你 重新建立 分支,将一些或所有的提交成一个.
确实,如果对某些修订进行了一些修订,以解决原始提交中的错误,错别字等,则它们与将来的变更日志阅读器无关,后者希望了解Godot代码库中发生了什么,或者何时以及如何进行给定文件是最后修改的.
为了把那些不相干的提交压缩到主要的提交中,我们将不得不 重写历史记录 .没错,我们有这个权力.你可能会读到这是一种不好的做法,当涉及到上游仓库的分支时,这确实是事实.但在你的fork中,你可以做任何你想做的事,而且一切都可以得到整洁的PR :)
我们将使用 交互式重新设置基线 git rebase -i
来执行此操作.此命令将使用提交哈希作为参数,并允许您修改该提交哈希与分支的最后一个提交之间的所有提交,即所谓的 HEAD
.
虽然您可以将任何提交ID赋给”git rebase-i”并查看其间的所有内容,但最常见和最方便的工作流涉及在*upstream``master``branch*上进行重定,您可以使用:
$ 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 docs <https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#\_core\_editor> __ 进行配置您最喜欢的一个),内容可能如下所示:
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://akien-mga@github.com/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
您还可以从GitHub PR本身删除远程分支,一旦合并或关闭它就会出现一个按钮.