Skip to content
This page has been auto-translated and may contain errors.View in English

拉取请求流程 ​

docs.scrimba.com

你一直在自己的分支上开发一个功能,现在它已经完成了。但这个项目是共享的,别人也会往里面提交代码,所以你不能直接推到 main 上碰运气。你希望这个改动经过审查,如果需要就讨论一下,然后以团队其他人都能看到的方式合并进去。从你机器上的一个分支,到代码最终落地到 main,这整条路径就是拉取请求流程,几乎所有在 GitHub 上协作的团队都是这么把改动合并进去的。这个流程里 Git 那一侧用到的都是前面章节学过的命令:分支、提交、推送。

bash
$ git switch -c add-five-day-forecast
Switched to a new branch 'add-five-day-forecast'
$ git add src/forecast.js
$ git commit -m "Add five-day forecast panel"
$ git push -u origin add-five-day-forecast
Branch 'add-five-day-forecast' set up to track 'origin/add-five-day-forecast'.

这次推送把你的分支发到了 GitHub 上。此时还没有任何东西被合并,甚至还没有提出审查请求。下一步——发起拉取请求,是在 GitHub 网站上完成的。

发起拉取请求之后会发生什么 ​

在 GitHub 上,像上面这样推送之后,你通常会看到一条横幅,提示可以把你的新分支和 main 进行对比。点它,写一个标题和一段简短说明,介绍改了什么、为什么改,然后打开它生成的页面。这个页面就是一个**拉取请求**,通常简称 PR:一个把一个分支合并到另一个分支的请求,并附带一条持续进行的对话。

拉取请求提出一次合并申请,然后等待批准。有人——可能是你自己,也可能是团队里的其他人——会阅读这个改动,留下评论,觉得没问题了才会合并。在此之前,你的分支和 main 会保持和你打开这个页面之前完全一样的状态。

你不需要等功能全部做完才发起 PR。很多团队会提前打开一个 PR,并把它标记为草稿,这表示"还没准备好接受审查",但同时也能让审查者提前对开发方向留言。一份好的描述会关联相关的 issue,并说明你测试过什么,这样审查者就不必单靠 diff 自己去还原这些信息。

GitHub 会在远程仓库上为每一个拉取请求保留一个 ref,即使你还没有把 PR 对应的分支添加到自己的仓库里也是如此:refs/pull/<number>/head。你可以直接抓取这个 ref,把同事的 PR 检出到自己的机器上,而不用先把他们的 fork 添加为一个远程。

bash
$ git fetch origin pull/42/head:pr-42
From github.com:mara-chen/weather-app
 * [new ref]         refs/pull/42/head -> pr-42
$ git switch pr-42
Switched to branch 'pr-42'

现在 pr-42 就是一个真正的本地分支,是从第 42 号拉取请求构建出来的,你可以在留下第一条评论之前先运行和测试一下。

Juno发起拉取请求之后会发生什么 拉取请求是一个把你的分支合并到另一个分支的请求,是你推送之后在 GitHub 上发起的。先由某人审查这个改动,之后才会合并。日常流程就是:建分支、推送、发起拉取请求、经过审查、合并。
Juno发起拉取请求之后会发生什么 PR 提出一次合并申请,并承载围绕它展开的讨论,你可以提前把它开成草稿,在工作完成之前先拿到反馈。写描述的时候要想着审查者真的会去读它:改了什么、为什么改、测试过什么。
Juno发起拉取请求之后会发生什么 GitHub 为每个拉取请求都保留着一个隐藏的 ref,refs/pull/<number>/head,所以你可以用 git fetch origin pull/42/head:pr-42 直接把同事的 PR 抓到自己机器上,而不用把他们的 fork 添加为一个远程。在留下第一条评论之前,先在本地测试一下这个改动。

Fork 和分支的区别 ​

当你对仓库有写权限时,直接从分支发起拉取请求就可以了,这也是在你自己团队的项目里的常规做法。如果要给一个你没有写权限的项目贡献代码,就需要多一步:fork,也就是在你自己的 GitHub 账号下建一份该仓库的副本。

在你自己 fork 出来的仓库里建分支、提交代码,和在自己拥有的项目里做法完全一样,推送到你的 fork 上之后,再从你 fork 的分支向原始仓库发起拉取请求。GitHub 对比这两个仓库的方式,和它在同一个仓库里对比两个分支的方式是一样的,所以无论哪种情况,审查和合并的流程看起来都完全相同。

如果你打算给一个自己 fork 过的项目发多个 PR,值得把原始仓库添加为第二个远程,通常命名为 upstream,同时让 origin 继续指向你自己的 fork。运行 git fetch upstream,再把 upstream/main 合并到你的分支上,就能跟上这个项目在你 fork 之后的最新进展——用的还是 远程仓库与 GitHub 那一章里讲过的、适用于任何远程的那套步骤。

JunoFork 和分支的区别 当你对某个仓库有写权限时——也就是自己团队的项目——直接建分支就行。没有写权限时就 fork:fork 是别人仓库在你名下的一份副本,你在这份副本里建分支、推送,然后再向原始仓库发起拉取请求。
JunoFork 和分支的区别 fork 是一个仓库在你账号下的完整副本,用于给你没有写权限的项目贡献代码。你在自己的 fork 里正常地建分支、提交、推送,GitHub 会把你 fork 里的分支和原始仓库进行对比,方式和它在同一个项目里对比两个分支完全一样。
JunoFork 和分支的区别 在一个 fork 出来的项目上保留两个远程:origin 指向你自己的 fork,upstream 指向原始仓库,这样你就能持续抓取并合并原始仓库后续的更新。PR 本身始终是拿你 fork 里的分支去和原始仓库的基础分支做对比。

让你的拉取请求顺利通过审查 ​

一旦 PR 发起,它就变成一场围绕这份 diff 持续进行的讨论。审查者会阅读这个改动,在具体的行上留下评论,然后要么批准,要么要求修改。当有人要求修改时,继续往同一个分支提交、推送就好:每一次推送更新的都是同一个 PR,而不会创建一个新的。一旦审查者批准,并且所有必须通过的检查都通过了,这个 PR 就可以合并了。

GitHub 把审查结果归为三种状态之一:批准(approved)、要求修改(changes requested)、或仅评论(commented,只是反馈意见,不下结论)。当你处理评论时记得回复,并把对话标记为已解决,这样审查者一眼就能看出还剩下什么没处理。如果你推送了一个针对某条具体评论的修复,最好在提交信息或回复里说明一下,因为审查者在翻一个很长的 PR 时,正是在找这类信息。

一个 PR 需要多少个批准,以及是否要求每一条评论线索都被标记为已解决才能解锁合并按钮,在管理规范的项目里,这通常是绑定在基础分支本身上的一条规则。

Juno让你的拉取请求顺利通过审查 审查者会在你的拉取请求上留言,可能会要求你做出修改。往同一个分支继续提交、推送新的改动就能应对,PR 会自动更新。有人批准之后,就可以合并了。
Juno让你的拉取请求顺利通过审查 审查结果落在三种状态之一:批准、要求修改、或仅评论。往同一个分支推送修复来更新 PR,处理评论时回复对方,让审查者在一条很长的讨论里始终清楚状况。
Juno让你的拉取请求顺利通过审查 审查状态决定合并按钮是否解锁,但具体的标准——需要多少批准、是否要求每条评论线索都被解决——在管理规范的项目里,通常是绑定在基础分支本身上的一条规则。

为什么拉取请求合并不了 ​

有时候 PR 上的合并按钮会变成灰色不可点,GitHub 会在那个位置显示一条消息:"This branch has conflicts that must be resolved"(这个分支有必须解决的冲突),或者"This branch is out of date with the base branch"(这个分支落后于基础分支)。这两条消息描述的都是一种正常、可以修复的状态:main 在你建分支之后又往前推进了,通常是因为在这期间别人的 PR 合并进去了,你的分支需要先跟上进度,GitHub 才能干净地把两者合并起来。任何有多个贡献者的项目都会遇到这种情况,所以要把它当作意料之中的事,而不是害怕的事。

在本地修复它的方式,和你把任何两个分支合到一起的方式一样:

bash
$ git switch add-five-day-forecast
$ git fetch origin
From github.com:mara-chen/weather-app
   9f8e7d6..b7c9e21  main       -> origin/main
$ git merge origin/main

如果没有任何重叠,合并会自动完成,你推送上去,PR 就会更新,按钮也会解锁。如果双方改了同一行,Git 会在文件里标出冲突,等你来解决,具体做法和 合并与冲突 那一章讲的完全一样:打开文件,删掉冲突标记,改成你想要的结果,然后 git add、git commit 完成这次合并,再推送一次。

卡住的拉取请求,需要的无非是补上进度,或者解决掉冲突。

如果一个 PR 保持开着的时间超过一两天,最好每隔一段时间就把 main 合并到你的分支里,而不是等到按钮被锁死才处理。落后一天的分支通常合并起来完全没有冲突;而落后三周的分支,两边的改动碰到同一行的机会就大得多。GitHub 在 PR 页面上自带的"Update branch"按钮,在没有需要手动解决的冲突时,就是替你做这同一次合并。

有些团队会把功能分支 rebase 到 main 上,而不是合并它,这样最终的历史记录会是线性的,不会给每个 PR 都留下一个合并提交。这个取舍和 合并与冲突 那一章讲的是同一回事:rebase 会重写你分支上的提交,只要没有别人手里也拿着这些提交的副本,这样做就是安全的,所以一个只存在于你自己机器上、连同它的 PR 都只有你一个人在动的功能分支,是一个合理的 rebase 对象。但如果别人已经拉取了这个分支,并在它上面继续开发,那就不适合再 rebase 了,因为那样会重写他们正依赖着的历史。

Juno为什么拉取请求合并不了 合并按钮变灰,说明你的分支落后于 main,或者和它有冲突。这不是出了什么故障。切到你的分支,fetch,再把 main 合并进来,用和平时一样的方式解决任何冲突,然后再推送一次。
Juno为什么拉取请求合并不了 不要等到 GitHub 锁死合并按钮才处理,隔一段时间就把 main 合并到长期存在的分支里。更小、更频繁的追赶意味着更小的冲突,GitHub 的"Update branch"按钮可以替你处理没有冲突的情况。
Juno为什么拉取请求合并不了 把你的分支合并到 main,或者把它 rebase 到 main 上,两种方式都能解决一个卡住的 PR,而一个只有你自己在用的功能分支,rebase 起来是安全的,因为没有别人手里有它提交的副本。一旦别人已经拉取了这个分支,就改用合并,别再 rebase 了。

拉取请求底层到底是什么 ​

拉取请求不是一个 Git 对象,也没有哪个 git 命令能创建它。Git 和 GitHub 不是一回事:Git 只知道提交、分支和其他 ref(ref 就是指向某个提交的一个名字)。GitHub 是在这基础上,对比它选定的两个 ref——你的分支(head)和你要合并进去的那个分支(base,通常是 main)——搭建出了整个 PR 页面、评论区和合并按钮。正因为这是一次比较行为,所以你只能在 GitHub 网站上(或者通过 GitHub 自己的命令行工具或 API)发起拉取请求,而不能用一个普通的 git 命令。

因为 PR 存在于 GitHub 上,和仓库的历史记录是分开的,所以它的评论和审查对话不会跟着提交一起走。如果你把一个项目迁移到另一个托管平台,每一个提交都会原样带过去,但拉取请求本身和它们的讨论会留在 GitHub 上,因为它们本来就从来不属于 Git 的数据。

GitHub 计算一个 PR 的 diff 时,会先找到合并基点——head ref 和 base ref 共同拥有的那个最近的提交——然后拿 head 和这个点做对比,而不是拿它和此刻 main 长什么样做对比。每一次向该分支推送,都是往同一个 PR 里添加一个新版本,而不是重新创建一次对比。**分支保护**是架在这整套机制之上的策略层:你(或你的组织)绑定在某个分支上——通常是 main——的一条规则,可以要求必须通过某些状态检查、要求达到一定数量的批准评审,并且阻止直接推送,强制所有改动都必须走拉取请求。Git 本身对此完全没有强制力。在 GitHub 的规则拒绝这个请求之前,Git 会一直允许你从终端里直接推到 main。

Juno拉取请求底层到底是什么 拉取请求是架在 Git 之上的一个 GitHub 功能:Git 负责跟踪提交和分支,GitHub 通过对比其中两个,搭出了 PR 页面和合并按钮。在脑子里把 Git 和 GitHub 分开,很多看起来像魔法的地方就一下子说得通了。
Juno拉取请求底层到底是什么 PR 拿你的分支去和一个基础分支做对比,整个过程完全发生在 GitHub 上,所以就算仓库以后换了托管平台,它的评论和审查历史也不会跟着搬过去。真正属于 Git 的,只有提交本身。
Juno拉取请求底层到底是什么 PR 本质上是 GitHub 拿一个 head ref 和一个 base ref、从它们共同的合并基点开始做对比,每次推送都会更新一次。分支保护是架在这之上强制执行的策略,要求合并前必须通过检查或获得评审。Git 本身对此没有任何强制力:在 GitHub 的规则拦住之前,它会一直允许你直接推到 main。