为什么 GitHub 需要我在 MR 中没有任何更改时重新设置合并请求的基址
Why does GitHub need me to rebase a Merge Request when nothing has changed in the MR
在使用 GitHub 时,我不明白我是否在三天后发出合并请求,当我去批准 MR 时它说我需要 rebase MR?谁能解释一下这让我发疯。我是 GitHub 的新手,所以请提供任何帮助,谢谢。
Git——和GitHub——不一定需要这个。需要它的是人,and/or 人强加的规则。以下内容较长,但我建议值得一读。
Long: rebase 是什么
要有效地使用 Git 和 GitHub,您应该了解以下内容:
Git 没有拉取请求或合并请求。这些是 add-ons,由各种托管站点(GitHub、Bitbucket、GitLab 等)提供。
GitHub 调用他们的 add-ons 拉取请求; 是 GitLab 调用他们的 合并请求.
这些都是相对次要的,但是因为 Git 术语已经非常混乱,所以最好尽可能清楚。
无论他们如何称呼它们以及如何在内部和外部实施它们——这些也因托管站点而异——这些都建立在一些基本的 Git 技术之上。掌握这些会有帮助。以下是需要了解的内容:
Git 是围绕 提交 构建的。提交是 Git 的 raison d'être。除了为提交服务之外,其他一切都不重要。
每个提交都有一个唯一的编号,通常用 hexadecimal 表示,看起来像一串丑陋的字母和数字。在一个重要的方面,那个数字 是 提交,这就是为什么它需要是唯一的。两个 Git 相互交谈时,只会交换原始数字以查看它们是否都有提交。如果不是,一个 Git 可能必须将提交发送给另一个 Git。 (Git 存储库“喜欢”向自己添加新提交,并且“不喜欢”忘记任何提交。1)我们称这些数字为 哈希 ID.
每个提交包含两件事:
每个提交都有某个项目的所有文件的快照。这里的内部存储格式很复杂,并没有真正相关,但值得知道的是(1)它充当快照和(2)其中的所有文件都是 de-duplicated 反对任何其他提交中存在的所有副本,因此大多数提交大多包含与大多数其他提交相同的文件这一事实不会使存储库膨胀太多。
每个提交都有一些元数据,或者关于提交本身的信息:例如,谁在什么时候提交的。元数据包括您的姓名和电子邮件地址(来自 user.name
和 user.email
设置)以及您输入的任何日志消息。而且——从 Git 的观点来看至关重要——每个提交都包含一些早期提交的原始提交哈希 ID。
任何提交的所有部分,一旦创建,都是 read-only。这样做的一个原因是提交的哈希 ID 只是提交的 内容 的加密校验和。2 如果您进行提交— 或任何内部对象 — 从 Git 数据库中进行一些更改,然后将结果写回,您只会得到一个具有不同哈希 ID 的新对象。原始对象仍然存在。
这最后一点使 Git 存储库通常 append-only(这解释了添加新提交的拟人化“喜欢”)。它 是 可能采取一些现有的提交是不是“不够好”,并将它们复制到 new-and-improved 提交, 和 停止使用旧的承诺。如果 每个 Git 存储库都这样做,旧的提交最终会“消失”。3 这就是git rebase
是关于.
1不要将计算机拟人化——他们讨厌那样!
2这意味着每次提交都必须是唯一的。例如,先前提交的存储哈希 ID,加上 time-stamps,在这里可以提供帮助。这也意味着 Git 最终一定会失败:pigeonhole principle 告诉我们任何哈希方案最终都会发生冲突。哈希 ID 的 大小 决定了 Git 可能失败的速度。它经过精心设计,需要几十年才能发生。不过,对 SHA-1 的恶意攻击可能会导致更早的失败,并且存储库的大小总体上在增长,这两者都导致 Git 最终从 SHA-1 迁移到 SHA-256。
3细节比较复杂,这里就不赘述了
在 Git 存储库中工作
当人类在 Git 存储库中工作时,过程通常是这样的:
- 如果需要克隆一些现有的存储库,或者,如果需要更新,然后使用现有的克隆。
- 做一些工作。进行新的提交,因为 Git 就是关于 提交 .
- 根据需要进行测试和返工。
- 宣布准备就绪。
- 完成任何审核流程;这可能会将一个人送回第 2 步或第 3 步。
- 合并作品。
如果任何时候只有一个人在做任何工作,并且没有返工或 cycling-through-steps 发生,这个过程非常简单。唯一的松鼠部分发生在第 1 步和第 6 步。如果我们忽略这些,我们会看到一个漂亮、简单的过程,如下所示。在这里,我将使用单个大写字母来表示它们的哈希 ID:
... <-F <-G <-H <--main
现在,哈希 ID 为 H
的提交是 最新的 提交 main
或 master
b运行通道。 Git 本身根本不关心 b运行ch 名称:它只是使用它们来 find 提交。具体来说,一个 b运行ch 名称包含 one 提交的哈希 ID,并且那个提交 是 最新的提交,我们——或者Git—将调用“b运行ch 的一部分”。
因为每个提交都持有一个较早提交的哈希 ID——或者有时是两个较早的提交;我们稍后会看到这一点——提交 H
包含早期提交 G
的原始哈希 ID。我们说提交 H
指向 提交 G
,因此上图中的向后箭头。
提交 H
还包含每个文件的完整快照。当我们 运行 git checkout main
时,这些是我们可以处理/使用的文件。请注意,我们处理/使用的文件进入我们的工作树。它们是 从 提交中复制出来的:在提交中,它们采用一些特殊的奇怪 Git-only 格式,压缩和 de-duplicated,大多数人无法使用计算机上的软件。
Git found 使用存储在 name main
中的哈希 ID 提交 H
.这就是 git checkout main
(或 git switch main
,在 Git 2.23 或更高版本中)从中获取所有文件的方式。而且,这就是 git log
向您显示信息 about commit H
的方式:它使用名称 main
来查找哈希 ID,然后使用哈希 ID 以查找大 database-of-all-Git-commits-and-other-supporting-objects.
中的内部提交
由于提交 H
存储了提交 G
的哈希 ID,Git 也可以使用它来钓鱼提交 G
的文件,并且可以 比较 G
中的快照与 H
中的快照。通过这样做,Git 可以向我们显示哪些文件(如果有)已更改,以及进行了哪些更改,即使 H
只是一个快照。
当然,commit G
是一个完整的commit,有一个之前的commit hash ID F
,所以Git可以同时加载commit F
和[=46] =] 并使用它来显示提交 G
中的更改。 git log
命令还可以显示日志消息 for commit G
,已从提交 H
.
中找到哈希 ID
当然,提交 F
是一个完整的提交,所以 Git 可以继续这样做。它可以一直保持到有史以来的第一次提交。 那个 提交在一个方面是特殊的:它不会 指向任何更早的提交。 Git 知道,在完成该提交后,停止倒退。
所以,Git 工作 向后 。但是,如何进行 new 提交呢? 这实际上也非常简单。不过,在我们进行新提交之前,让我们创建一个新的 b运行ch name,如下所示:
...--F--G--H <-- develop, main
Git 要求我们挑出 one b运行ch name 作为 current b运行ch 。我们用 git checkout
或 git switch
来做到这一点。为了记住我们选择了哪一个,我们将在括号中绘制特殊名称 HEAD
, 附加到 这些 b运行ch 名称之一:
...--F--G--H <-- develop, main (HEAD)
这里是 on branch main
,正如 git status
所说,并使用提交 H
。如果我们 git checkout develop
,我们得到:
...--F--G--H <-- develop (HEAD), main
现在我们 on branch develop
,正如 git status
所说,但仍然 使用提交 H
。请注意,此时每个提交都在两个 b运行ches 上。
我们现在以通常的方式修改工作树中的一些文件(它们是实际上不在 Git 中的普通文件)然后 运行 git add
让他们为提交做好准备。略过一些其他相当重要的东西,这个 替换了 Git 的暂存区 中的文件副本。这些额外的副本是 Git 用来进行新提交的,暂存区实际上有相同文件的副本 Git 复制 out 提交 H
.此时它们只是 ready-to-commit、压缩和 de-duplicated 之前的形式。使用 git add
告诉 Git 用新副本替换其中一些文件:Git 将在 git add
时压缩和 de-duplicate 文件,因此 git commit
可以直接使用 Git 索引 中的任何内容 .
最后,我们运行git commit
。这个:
从我们那里获取元数据:要放入新提交的日志消息,现在 user.name
和 user.email
的设置,等等。日期和时间戳是“现在”,parent 提交,对于新提交,是 current 提交,由当前 b 找到运行通道名称。所以在这种情况下就是提交 H
。
立即从 Git 索引中的任何内容制作永久快照。因为我们使用 git add
来更新这些文件,所以这是正确的快照。
将所有这些作为新提交写出。这会为新的、唯一的提交获取一个新的、唯一的哈希 ID。提交对象进入大数据库:
...--F--G--H
\
I
请注意新 I
是如何指向现有提交 H
的。 (我不能在这里画大箭头,所以我去了线。 H
不能指向 I
不过:H
是很久以前制作的,不能改变了。所以 I
必须指向回 H
。)
最后,Git 有它的特殊技巧:它将 new 提交的哈希 ID 写入 current b运行通道名称.
最后一个技巧让我们得到:
...--F--G--H <-- main
\
I <-- develop (HEAD)
这让我们可以添加新的提交,一次一个,每一个新的提交都会推进 current b运行ch (develop
,因为那是附加 HEAD
的地方)。如果我们再添加一个提交,我们会得到:
...--F--G--H <-- main
\
I--J <-- develop (HEAD)
如果这里一切看起来都不错,我们现在可以简单地 git checkout main
并告诉 Git:commit J
很棒,将其用作 main
也是。跳过细节——Git 用它所谓的 fast-forward 合并 来做到这一点——结果是:
...--F--G--H--I--J <-- develop, main (HEAD)
现在所有提交都在两个 b运行ches 上,删除任何一个名称都是安全的——另一个会找到所有提交。
请注意,之前删除名称 main
是安全的(在某种意义上)。我们可以通过从 J
开始并向后工作来找到所有提交。这就是 b运行ch 名字的意义所在:它们为我们提供了起点和倒退的地方。 only keep main
的理由是为了暂时记住H
,但这是一个相当不错的理由——特别是如果我们认为 I-J
是 可怕的 提交并想把它们扔掉。
丢弃旧的提交
假设我们做决定I-J
很糟糕。这是将它们丢弃而不是合并的一种方法:
git checkout main
git branch -D develop
第一步,如果我们想进行 fast-forward 合并,我们也会这样做,得到我们:
...--G--H <-- main (HEAD)
\
I--J <-- develop
如果我们现在运行 git log
,我们不会看到 提交I-J
。我们必须运行git log develop
才能看到他们,使用名字develop
才能找到J
.
second命令告诉Git删除名字develop
——强制的,因为没有强制Git,它会说不:这将使我们无法访问提交 I-J
。通过删除名称 develop
,我们最终得到:
...--G--H <-- main (HEAD)
\
I--J [abandoned]
通过删除 name,我们无法再找到提交,我们再也不会被它们打扰了——因为只要我们还没有发送它们给任何其他 Git,那就是。
我们现在可以再次创建 develop
,再次指向 H
,并再次尝试我们的开发工作,这次知道我们做错了什么:
K--L <-- develop (HEAD)
/
...--G--H <-- main
\
I--J [abandoned]
当我们合并(新的,好的)提交时,如果我们愿意,我们可以将它们称为 I-J
,就好像被放弃的提交完全消失了一样。他们的真实姓名是一些又大又丑的哈希 ID;毕竟,我们只是在编造这些 one-letter 名字。
如果我们是唯一一个做任何工作的人,那就太好了,但在很多情况下这是不现实的。
并行开发
让我们从两个用户开始。我将在这里使用标准的“爱丽丝和鲍勃”,尽管显然这个成语由于某种原因正在失宠。每个人都制作自己的分身,这样每个人都有自己的b运行ch名字。这进入了一个小的侧面讨论:
- 每个存储库 共享 提交。
- 但是每个存储库都有 自己的 b运行ch 名称。
在 Alice 的系统上,她得到:
...--G--H <-- main
在 Bob 上,他得到:
...--G--H <-- main
当 Alice 进行两次新提交(在 main
或任何其他 b运行ch 名称上)时,她的提交将获得 unique 哈希 ID:
I--J <-- alice
/
...--G--H
同时,当 Bob 进行两次新提交时,他的提交也会获得唯一的哈希 ID:
...--G--H
\
K--L <-- bob
如果我们获取所有这些提交并将它们合并到一个存储库中,并使用名称 alice
和 bob
查找最后的提交,我们得到这张图片:
I--J <-- alice
/
...--G--H
\
K--L <-- bob
(也许 main
指向 H
——尽管我们 不需要 H
的名称,因为我们可以通过从任一 b运行ch 尖端开始并向后工作来找到它。
鉴于并行开发的存在,我们现在有一个pr问题:我们如何加入这些平行的发展线?
Merge(真正的合并)
一种方法是使用 Git 的合并工作能力。我们获得所有提交,进入某个 Git 存储库中的某个地方,并使用 b运行ch 名称来查找它们。然后我们选择两个 b运行 中的一个检查/切换到,运行 git merge
和另一个:
git checkout alice
git merge bob
例如
Git 的合并引擎现在执行我喜欢称之为 合并动词的操作: 查找 自某些共同开始以来的变化的操作点。从图中可以明显看出共同的起点:提交 H
.
Git 现在将使用它的比较软件——git diff
,或多或少——来比较 commit H
中的快照在提交 J
中,查看 Alice 更改了哪些文件,以及 Alice 对这些文件做了哪些更改。 Git 也将使用 git diff
来比较 H
和 L
,看看 Bob 改变了什么。然后,对于每个文件:
- 如果没有人更改文件,我们只拿原始文件。
- 如果一个人更改了文件而另一个人没有,我们将采用 已更改 的文件。
- 如果两个人都修改了同一个文件,我们Git努力合并他们的变化。
Git 的合并是通过一个简单而愚蠢的算法完成的,它只查看 line-by-line 的变化。如果更改的行不“接触”或“重叠”,Git 将进行 both 更改。如果他们这样做 touch-or-overlap,Git 通常会声明 合并冲突 并强制人类 运行ning git merge
清理混乱.这里有很多特殊情况,但如果 Alice 和 Bob 在系统的不同部分工作,Git 通常可以自己完成所有 work-combining。
由于我们在这里没有真正正确地涵盖 git merge
,我们假设 Git 认为一切顺利,以便 Git 为您做出自己的新提交。 Git 从 共同起点 将 合并的 更改应用于快照——Git 称之为 merge base—in commit H
,保留两组更改。 Git 将所有结果文件写入您的工作树和 Git 自己的索引 / staging-area。然后,Git 从这些文件中进行新的提交:
I--J
/ \
...--G--H M <-- alice (HEAD)
\ /
K--L <-- bob
因为我们 运行 git checkout alice
开始这个,我们在 b运行ch alice
,所以新合并提交的哈希 ID 进入名称 alice
。生成的提交有一个快照——就像任何提交一样——通过将 组合的 更改应用到 H
的快照来制作。它有元数据,就像任何普通提交一样,说明我们刚刚进行了此提交。此提交的唯一特殊之处在于,它不是指向 just 提交 I
,而是指向 both branch-tip 提交,I
和 K
.
我们现在可以删除不需要的任何名称。我们在这里不需要的名称是 bob
:我们可以通过 M
向后查找提交 K
。 Git 将向后工作到 both 提交,自动跟随两个 backwards-pointing 箭头。
这是真正的合并,是合并工作的一种方式。 Git可以做到这一点; Git集线器可以做到这一点;只要不存在合并冲突,GitHub pull request 就可以通过这种流程处理。但是有些人类不喜欢这样做。
变基而不是合并
假设我们是爱丽丝,遇到这种情况:
I--J <-- alice (HEAD)
/
...--G--H <-- main
但是 Bob 首先将 他的 提交添加到某个存储库:
...--G--H--K--L <-- main
我们现在可以 运行 git fetch
针对这个另一个存储库——我们在这里称之为 origin
——来获取新的提交 K-L
。这是我们将在我们自己的本地存储库中看到的内容:
I--J <-- alice (HEAD)
/
...--G--H <-- main
\
K--L <-- origin/main
如果我们有一个“不合并”规则——谁知道为什么我们有这个规则4——我们必须接受我们完美的 I-J
提交并“改进" 他们,通过 添加 到 L
的新提交。
要手动执行此操作,我们将使用 git cherry-pick
两次,使用新的临时 b运行ch 名称,然后(例如)更改 b运行ch 名称。但是 git rebase
命令可以一次性为我们做这件事:
git rebase origin/main
变基操作复制了一些提交集的效果。为此,它必须使用 Git 的合并机制,即想法的 merge-as-a-verb 部分。 git cherry-pick
命令实现了这一点,一次提交一个,并且 git merge
运行s git cherry-pick
重复。5 一旦它有对快照准备好了,每个cherry-pick步骤提交这个快照,re-using原始提交的日志消息,但是作为常规single-parent提交,而不是作为一个合并提交。所以一旦这个阶段完成我们有:
I--J <-- alice
/
...--G--H <-- main
\
K--L <-- origin/main
\
I'-J' <-- HEAD
其中 I'
是 Git 提交 I
的自动副本,而 J'
是 Git 提交 [=103] 的自动副本=].此图还说明了 rebase 内部使用的一个技巧:它 运行s 在 Git 调用的 detached HEAD 模式中,以避免不得不组成临时 b运行通道名称.6
不过,一旦完成所有复制,Git 使用另一个内部命令强制原始 b运行ch 名称指向最后复制的提交。然后re-attachesHEAD
,所以我们有:
I--J [abandoned]
/
...--G--H <-- main
\
K--L <-- origin/main
\
I'-J' <-- alice (HEAD)
请注意,这与我们故意丢弃 never-sent-to-anyone-else 次提交时发生的情况有何相似之处。
(这里的一个不幸的副作用是,通常,使用这种 work-flow,我们已经 将这些提交 发送到某处以供审查。我们将触摸下一节将对此进行讨论。)
4在我看来,这并不是一个很好的规则。我以前在项目中遵循过它——这不是一个 可怕的 规则——但我认为只是盲目地说“不合并”是错误的。尽管如此,人们还是喜欢它。
5事实上,git rebase
是一个非常复杂的命令,可以通过多种不同的方式之一完成它的工作。在现代 Git 中,它现在默认在内部使用 git cherry-pick
。在稍旧的 Git 版本中,您需要 -m
或 -i
或类似选项才能使用 git cherry-pick
。其他选项添加特殊功能,并且 rebase 已经具有许多特殊功能,因此一些选项 disable 这些功能。但主要是,它是关于将一些现有的 not-quite-good-enough 提交复制到 new-and-improved 提交,这也是 git cherry-pick
的内容,因此它们密切相关。
6如果 rebase 必须停止以获得合并冲突的帮助,或者如果您使用 git rebase -i
,这个分离的 HEAD 模式实现细节就会“泄露”变体并让它故意停止:当 rebase 在操作中间停止时,你仍然处于这种 detached-HEAD 模式。您必须告诉 Git 恢复变基或终止变基,以退出 detached-HEAD 模式。这会变得很混乱,你应该小心不要使用 git checkout
来退出分离的 HEAD 模式。
在 GitHub 分叉世界
以上都是我们在基地Git可以做的事情。然而,GitHub 和其他托管网站添加了一系列功能,希望我们会喜欢这些功能,从而实际 为这些托管网站上的服务支付 .
第一个 GitHub 特定功能是 GitHub 分支。 GitHub 上的一个分支就像一个克隆,但有两个变化:
通常情况下,如果我们使用git clone
克隆一个仓库:
git clone -b foo ssh://git@github.com/user/repo.git
我们从该存储库中获取所有 提交,以及他们的 b运行ches none .我们的 git clone
最后在本地创建 one b运行ch 名称,使用我们在此处为 -b
参数提供的名称。如果我们没有给出 -b
参数,我们的 Git 会询问他们的 (GitHub's) Git 他们推荐的 b运行ch 名称,以及我们的 Git 然后根据他们的推荐创建了一个 b运行ch。
我们的Git对他们的b运行ch名字所做的就是把他们全部改成remote-tracking名字: 他们的 main
变成了我们的 origin/main
,他们的 develop
变成了我们的 origin/develop
,他们的 feature/short
变成了我们的 origin/feature/short
,等等上。
Git 这样做是因为我们的 b运行ch 名称是 我们的,我们可以随意使用。 Git 将 所有 的 origin/*
名称复制回来是可以的,但是无论如何,Git 仍然需要这种“重命名”他们的 b运行ches,并使用我们自己的名字”技巧,这样我们在获得更新时就不会丢失 our new 提交来自 他们的 Git.
使用 GitHub 分支,我们的 Git 在我们的分支中为每个 b运行ch 名称创建一个 b运行ch 名称原创Git。这是无害的,并且在某些方面很好(见上一段)。
并且,在幕后,GitHub 共享 底层提交和其他内部对象,以便保存 space在他们自己的服务器上——这总是可以的,因为每个对象都有自己唯一的哈希 ID——and 为我们记住 他们的 存储库,这样我们就可以提出拉取请求。
这种提出拉取请求的能力是最畅销的功能之一。 (GitHub 管理评论、问题等的能力是另一个。那也是 add-on,在基础 Git 中不存在。)
拉取请求的核心只是一种方式,当我们单击 FORK 按钮时,我们可以向我们使用的存储库的所有者发送电子邮件或其他一些警报,让他们知道,在我们的 GitHub 分支中,我们添加了一些我们现在的提交让他们看看 and/or 添加到 他们的 克隆。他们是否接受我们的提交取决于他们 as-is——这要求他们使用原始 Git,或 GitHub MERGE 按钮——或使对它们进行某种更改,或要求我们对它们进行更改,或其他任何方式。
如果他们确实要求我们更改我们的提交,我们可以做到,然后使用git push --force
将我们的 new 提交到我们自己的 GitHub 分支。
我之前提到 Git“不喜欢”放弃提交。当我们使用 git rebase
或任何其他进程 将现有提交替换为 new-and-improved 替换时,如果我们曾经发送过 原件 任何地方,我们现在发送替换件,我们将要求其他 Git 存储库 放弃 原件,取而代之的是替换件。
当我们 进行 替换时,我们的 Git 知道我们正在这样做,如果我们至少使用 git rebase
的话。 (如果我们手动完成,我们可能不得不 强制 我们的 Git 进行替换。)Git 在 GitHub 没有认为我们的 new-and-improved 替换提交是 git rebase
的结果,因此我们必须强制 Git 在 GitHub 将它们 视为 [=619] =] 替换。所以我们必须 git push --force
我们的叉子。
GitHub 使用的 add-on 软件会注意到这个 forced-push 到我们在提出合并请求时使用的 b运行ch,并且会 自动更新其他人在查看我们的 PR 时看到的 PR。
这并没有告诉我们为什么我们必须变基。它不能,因为 Git 本身,以及 GitHub 作为 add-on,首先 不需要 变基。是某个地方的一些人提出了这个要求。由于这么多人 喜欢 这个过程(无论出于何种原因),GitHub 甚至可能在今天或明天自动执行该要求。
无论如何,如果您做到了这一点,您现在就知道如何去做,并且任何底层软件都不需要它。但还有一件事需要考虑。
无法在 GitHub
上完成无法自动化的合并
(这实际上是一种夸大的说法,因为 GitHub 一直在添加新功能。但通常它 是 正确的,并且它会影响合并和变基。)
当 Git 可以自行组合更改时,它会这样做,GitHub 对 Git 的使用也会这样做。当Git无法自行合并更改时,需要寻求他人的帮助。
如果合并有合并冲突,Git 将停止并在您的工作树中留下一团糟。现在你的工作是解决这个问题。 Git 存储库 on GitHub 没有工作树 (出于各种内部原因),因此无处可修复乱七八糟的。
因此,在 GitHub 让您提出合并请求之前,他们首先 运行 一个 测试合并 。这个测试合并要么成功,因为 Git 可以自己做所有事情,要么失败,因为它不能。如果测试合并失败,GitHub 会让你知道你的 PR 有冲突。
现在,假设您在笔记本电脑上有一个 fork 和存储库的本地副本,并且您做了一些工作并进行了一些提交,它们都已准备就绪:
I--J <-- alice
/
...--G--H <-- main
您将此发送到您的 GitHub 分支,以便您的 GitHub 克隆有一个 alice
b运行ch,提交 J
作为它的最后一次提交。您分叉的存储库仍然有提交 H
作为其在 他们的 main
上的最后一次提交,并且您进行了 PR。
来了我们烦人的 Bob,他做出了一些承诺:
I--J <-- alice
/
...--G--H <-- main
\
K--L <-- bob
Bob 已将这些提交添加到他们的 main
,但他们尚未接收您的工作:
I--J <-- alice
/
...--G--H--K--L <-- main
之前你们的工作(扩展他们的提交H
)之间没有冲突,但是现在有因为鲍勃触及了相同的行 的 相同文件 并进行了不兼容的更改。
GitHub 系统无法再将您在 I-J
中的工作与其 b运行ch 提示 L
结合起来。在 GitHub 的条款中,您的 PR 已经变得 冲突 。他们 (GitHub) 在向他们的 (GitHub 的 other-persons-clone 中添加提交 K-L
时注意到了这一点,您从中分叉了 main
.
GitHub 会通知您,以便您可以完全撤回您的 PR,或者对合并冲突采取一些措施。 由于基础 Git 要求,您不必重新设置 PR 的基础。由于基本 Git 要求,您 do 必须解决合并冲突,也许可以通过在 J
之后添加一个可以与 [=135 很好地合并的新提交来解决=]. GitHubadd-ons可能会改这张图,但是冲突本身导致需要更新PR。要求更新 不一定 变基。
在使用 GitHub 时,我不明白我是否在三天后发出合并请求,当我去批准 MR 时它说我需要 rebase MR?谁能解释一下这让我发疯。我是 GitHub 的新手,所以请提供任何帮助,谢谢。
Git——和GitHub——不一定需要这个。需要它的是人,and/or 人强加的规则。以下内容较长,但我建议值得一读。
Long: rebase 是什么
要有效地使用 Git 和 GitHub,您应该了解以下内容:
Git 没有拉取请求或合并请求。这些是 add-ons,由各种托管站点(GitHub、Bitbucket、GitLab 等)提供。
GitHub 调用他们的 add-ons 拉取请求; 是 GitLab 调用他们的 合并请求.
这些都是相对次要的,但是因为 Git 术语已经非常混乱,所以最好尽可能清楚。
无论他们如何称呼它们以及如何在内部和外部实施它们——这些也因托管站点而异——这些都建立在一些基本的 Git 技术之上。掌握这些会有帮助。以下是需要了解的内容:
Git 是围绕 提交 构建的。提交是 Git 的 raison d'être。除了为提交服务之外,其他一切都不重要。
每个提交都有一个唯一的编号,通常用 hexadecimal 表示,看起来像一串丑陋的字母和数字。在一个重要的方面,那个数字 是 提交,这就是为什么它需要是唯一的。两个 Git 相互交谈时,只会交换原始数字以查看它们是否都有提交。如果不是,一个 Git 可能必须将提交发送给另一个 Git。 (Git 存储库“喜欢”向自己添加新提交,并且“不喜欢”忘记任何提交。1)我们称这些数字为 哈希 ID.
每个提交包含两件事:
每个提交都有某个项目的所有文件的快照。这里的内部存储格式很复杂,并没有真正相关,但值得知道的是(1)它充当快照和(2)其中的所有文件都是 de-duplicated 反对任何其他提交中存在的所有副本,因此大多数提交大多包含与大多数其他提交相同的文件这一事实不会使存储库膨胀太多。
每个提交都有一些元数据,或者关于提交本身的信息:例如,谁在什么时候提交的。元数据包括您的姓名和电子邮件地址(来自
user.name
和user.email
设置)以及您输入的任何日志消息。而且——从 Git 的观点来看至关重要——每个提交都包含一些早期提交的原始提交哈希 ID。
任何提交的所有部分,一旦创建,都是 read-only。这样做的一个原因是提交的哈希 ID 只是提交的 内容 的加密校验和。2 如果您进行提交— 或任何内部对象 — 从 Git 数据库中进行一些更改,然后将结果写回,您只会得到一个具有不同哈希 ID 的新对象。原始对象仍然存在。
这最后一点使 Git 存储库通常 append-only(这解释了添加新提交的拟人化“喜欢”)。它 是 可能采取一些现有的提交是不是“不够好”,并将它们复制到 new-and-improved 提交, 和 停止使用旧的承诺。如果 每个 Git 存储库都这样做,旧的提交最终会“消失”。3 这就是git rebase
是关于.
1不要将计算机拟人化——他们讨厌那样!
2这意味着每次提交都必须是唯一的。例如,先前提交的存储哈希 ID,加上 time-stamps,在这里可以提供帮助。这也意味着 Git 最终一定会失败:pigeonhole principle 告诉我们任何哈希方案最终都会发生冲突。哈希 ID 的 大小 决定了 Git 可能失败的速度。它经过精心设计,需要几十年才能发生。不过,对 SHA-1 的恶意攻击可能会导致更早的失败,并且存储库的大小总体上在增长,这两者都导致 Git 最终从 SHA-1 迁移到 SHA-256。
3细节比较复杂,这里就不赘述了
在 Git 存储库中工作
当人类在 Git 存储库中工作时,过程通常是这样的:
- 如果需要克隆一些现有的存储库,或者,如果需要更新,然后使用现有的克隆。
- 做一些工作。进行新的提交,因为 Git 就是关于 提交 .
- 根据需要进行测试和返工。
- 宣布准备就绪。
- 完成任何审核流程;这可能会将一个人送回第 2 步或第 3 步。
- 合并作品。
如果任何时候只有一个人在做任何工作,并且没有返工或 cycling-through-steps 发生,这个过程非常简单。唯一的松鼠部分发生在第 1 步和第 6 步。如果我们忽略这些,我们会看到一个漂亮、简单的过程,如下所示。在这里,我将使用单个大写字母来表示它们的哈希 ID:
... <-F <-G <-H <--main
现在,哈希 ID 为 H
的提交是 最新的 提交 main
或 master
b运行通道。 Git 本身根本不关心 b运行ch 名称:它只是使用它们来 find 提交。具体来说,一个 b运行ch 名称包含 one 提交的哈希 ID,并且那个提交 是 最新的提交,我们——或者Git—将调用“b运行ch 的一部分”。
因为每个提交都持有一个较早提交的哈希 ID——或者有时是两个较早的提交;我们稍后会看到这一点——提交 H
包含早期提交 G
的原始哈希 ID。我们说提交 H
指向 提交 G
,因此上图中的向后箭头。
提交 H
还包含每个文件的完整快照。当我们 运行 git checkout main
时,这些是我们可以处理/使用的文件。请注意,我们处理/使用的文件进入我们的工作树。它们是 从 提交中复制出来的:在提交中,它们采用一些特殊的奇怪 Git-only 格式,压缩和 de-duplicated,大多数人无法使用计算机上的软件。
Git found 使用存储在 name main
中的哈希 ID 提交 H
.这就是 git checkout main
(或 git switch main
,在 Git 2.23 或更高版本中)从中获取所有文件的方式。而且,这就是 git log
向您显示信息 about commit H
的方式:它使用名称 main
来查找哈希 ID,然后使用哈希 ID 以查找大 database-of-all-Git-commits-and-other-supporting-objects.
由于提交 H
存储了提交 G
的哈希 ID,Git 也可以使用它来钓鱼提交 G
的文件,并且可以 比较 G
中的快照与 H
中的快照。通过这样做,Git 可以向我们显示哪些文件(如果有)已更改,以及进行了哪些更改,即使 H
只是一个快照。
当然,commit G
是一个完整的commit,有一个之前的commit hash ID F
,所以Git可以同时加载commit F
和[=46] =] 并使用它来显示提交 G
中的更改。 git log
命令还可以显示日志消息 for commit G
,已从提交 H
.
当然,提交 F
是一个完整的提交,所以 Git 可以继续这样做。它可以一直保持到有史以来的第一次提交。 那个 提交在一个方面是特殊的:它不会 指向任何更早的提交。 Git 知道,在完成该提交后,停止倒退。
所以,Git 工作 向后 。但是,如何进行 new 提交呢? 这实际上也非常简单。不过,在我们进行新提交之前,让我们创建一个新的 b运行ch name,如下所示:
...--F--G--H <-- develop, main
Git 要求我们挑出 one b运行ch name 作为 current b运行ch 。我们用 git checkout
或 git switch
来做到这一点。为了记住我们选择了哪一个,我们将在括号中绘制特殊名称 HEAD
, 附加到 这些 b运行ch 名称之一:
...--F--G--H <-- develop, main (HEAD)
这里是 on branch main
,正如 git status
所说,并使用提交 H
。如果我们 git checkout develop
,我们得到:
...--F--G--H <-- develop (HEAD), main
现在我们 on branch develop
,正如 git status
所说,但仍然 使用提交 H
。请注意,此时每个提交都在两个 b运行ches 上。
我们现在以通常的方式修改工作树中的一些文件(它们是实际上不在 Git 中的普通文件)然后 运行 git add
让他们为提交做好准备。略过一些其他相当重要的东西,这个 替换了 Git 的暂存区 中的文件副本。这些额外的副本是 Git 用来进行新提交的,暂存区实际上有相同文件的副本 Git 复制 out 提交 H
.此时它们只是 ready-to-commit、压缩和 de-duplicated 之前的形式。使用 git add
告诉 Git 用新副本替换其中一些文件:Git 将在 git add
时压缩和 de-duplicate 文件,因此 git commit
可以直接使用 Git 索引 中的任何内容 .
最后,我们运行git commit
。这个:
从我们那里获取元数据:要放入新提交的日志消息,现在
user.name
和user.email
的设置,等等。日期和时间戳是“现在”,parent 提交,对于新提交,是 current 提交,由当前 b 找到运行通道名称。所以在这种情况下就是提交H
。立即从 Git 索引中的任何内容制作永久快照。因为我们使用
git add
来更新这些文件,所以这是正确的快照。将所有这些作为新提交写出。这会为新的、唯一的提交获取一个新的、唯一的哈希 ID。提交对象进入大数据库:
...--F--G--H \ I
请注意新
I
是如何指向现有提交H
的。 (我不能在这里画大箭头,所以我去了线。H
不能指向I
不过:H
是很久以前制作的,不能改变了。所以I
必须指向回H
。)最后,Git 有它的特殊技巧:它将 new 提交的哈希 ID 写入 current b运行通道名称.
最后一个技巧让我们得到:
...--F--G--H <-- main
\
I <-- develop (HEAD)
这让我们可以添加新的提交,一次一个,每一个新的提交都会推进 current b运行ch (develop
,因为那是附加 HEAD
的地方)。如果我们再添加一个提交,我们会得到:
...--F--G--H <-- main
\
I--J <-- develop (HEAD)
如果这里一切看起来都不错,我们现在可以简单地 git checkout main
并告诉 Git:commit J
很棒,将其用作 main
也是。跳过细节——Git 用它所谓的 fast-forward 合并 来做到这一点——结果是:
...--F--G--H--I--J <-- develop, main (HEAD)
现在所有提交都在两个 b运行ches 上,删除任何一个名称都是安全的——另一个会找到所有提交。
请注意,之前删除名称 main
是安全的(在某种意义上)。我们可以通过从 J
开始并向后工作来找到所有提交。这就是 b运行ch 名字的意义所在:它们为我们提供了起点和倒退的地方。 only keep main
的理由是为了暂时记住H
,但这是一个相当不错的理由——特别是如果我们认为 I-J
是 可怕的 提交并想把它们扔掉。
丢弃旧的提交
假设我们做决定I-J
很糟糕。这是将它们丢弃而不是合并的一种方法:
git checkout main
git branch -D develop
第一步,如果我们想进行 fast-forward 合并,我们也会这样做,得到我们:
...--G--H <-- main (HEAD)
\
I--J <-- develop
如果我们现在运行 git log
,我们不会看到 提交I-J
。我们必须运行git log develop
才能看到他们,使用名字develop
才能找到J
.
second命令告诉Git删除名字develop
——强制的,因为没有强制Git,它会说不:这将使我们无法访问提交 I-J
。通过删除名称 develop
,我们最终得到:
...--G--H <-- main (HEAD)
\
I--J [abandoned]
通过删除 name,我们无法再找到提交,我们再也不会被它们打扰了——因为只要我们还没有发送它们给任何其他 Git,那就是。
我们现在可以再次创建 develop
,再次指向 H
,并再次尝试我们的开发工作,这次知道我们做错了什么:
K--L <-- develop (HEAD)
/
...--G--H <-- main
\
I--J [abandoned]
当我们合并(新的,好的)提交时,如果我们愿意,我们可以将它们称为 I-J
,就好像被放弃的提交完全消失了一样。他们的真实姓名是一些又大又丑的哈希 ID;毕竟,我们只是在编造这些 one-letter 名字。
如果我们是唯一一个做任何工作的人,那就太好了,但在很多情况下这是不现实的。
并行开发
让我们从两个用户开始。我将在这里使用标准的“爱丽丝和鲍勃”,尽管显然这个成语由于某种原因正在失宠。每个人都制作自己的分身,这样每个人都有自己的b运行ch名字。这进入了一个小的侧面讨论:
- 每个存储库 共享 提交。
- 但是每个存储库都有 自己的 b运行ch 名称。
在 Alice 的系统上,她得到:
...--G--H <-- main
在 Bob 上,他得到:
...--G--H <-- main
当 Alice 进行两次新提交(在 main
或任何其他 b运行ch 名称上)时,她的提交将获得 unique 哈希 ID:
I--J <-- alice
/
...--G--H
同时,当 Bob 进行两次新提交时,他的提交也会获得唯一的哈希 ID:
...--G--H
\
K--L <-- bob
如果我们获取所有这些提交并将它们合并到一个存储库中,并使用名称 alice
和 bob
查找最后的提交,我们得到这张图片:
I--J <-- alice
/
...--G--H
\
K--L <-- bob
(也许 main
指向 H
——尽管我们 不需要 H
的名称,因为我们可以通过从任一 b运行ch 尖端开始并向后工作来找到它。
鉴于并行开发的存在,我们现在有一个pr问题:我们如何加入这些平行的发展线?
Merge(真正的合并)
一种方法是使用 Git 的合并工作能力。我们获得所有提交,进入某个 Git 存储库中的某个地方,并使用 b运行ch 名称来查找它们。然后我们选择两个 b运行 中的一个检查/切换到,运行 git merge
和另一个:
git checkout alice
git merge bob
例如
Git 的合并引擎现在执行我喜欢称之为 合并动词的操作: 查找 自某些共同开始以来的变化的操作点。从图中可以明显看出共同的起点:提交 H
.
Git 现在将使用它的比较软件——git diff
,或多或少——来比较 commit H
中的快照在提交 J
中,查看 Alice 更改了哪些文件,以及 Alice 对这些文件做了哪些更改。 Git 也将使用 git diff
来比较 H
和 L
,看看 Bob 改变了什么。然后,对于每个文件:
- 如果没有人更改文件,我们只拿原始文件。
- 如果一个人更改了文件而另一个人没有,我们将采用 已更改 的文件。
- 如果两个人都修改了同一个文件,我们Git努力合并他们的变化。
Git 的合并是通过一个简单而愚蠢的算法完成的,它只查看 line-by-line 的变化。如果更改的行不“接触”或“重叠”,Git 将进行 both 更改。如果他们这样做 touch-or-overlap,Git 通常会声明 合并冲突 并强制人类 运行ning git merge
清理混乱.这里有很多特殊情况,但如果 Alice 和 Bob 在系统的不同部分工作,Git 通常可以自己完成所有 work-combining。
由于我们在这里没有真正正确地涵盖 git merge
,我们假设 Git 认为一切顺利,以便 Git 为您做出自己的新提交。 Git 从 共同起点 将 合并的 更改应用于快照——Git 称之为 merge base—in commit H
,保留两组更改。 Git 将所有结果文件写入您的工作树和 Git 自己的索引 / staging-area。然后,Git 从这些文件中进行新的提交:
I--J
/ \
...--G--H M <-- alice (HEAD)
\ /
K--L <-- bob
因为我们 运行 git checkout alice
开始这个,我们在 b运行ch alice
,所以新合并提交的哈希 ID 进入名称 alice
。生成的提交有一个快照——就像任何提交一样——通过将 组合的 更改应用到 H
的快照来制作。它有元数据,就像任何普通提交一样,说明我们刚刚进行了此提交。此提交的唯一特殊之处在于,它不是指向 just 提交 I
,而是指向 both branch-tip 提交,I
和 K
.
我们现在可以删除不需要的任何名称。我们在这里不需要的名称是 bob
:我们可以通过 M
向后查找提交 K
。 Git 将向后工作到 both 提交,自动跟随两个 backwards-pointing 箭头。
这是真正的合并,是合并工作的一种方式。 Git可以做到这一点; Git集线器可以做到这一点;只要不存在合并冲突,GitHub pull request 就可以通过这种流程处理。但是有些人类不喜欢这样做。
变基而不是合并
假设我们是爱丽丝,遇到这种情况:
I--J <-- alice (HEAD)
/
...--G--H <-- main
但是 Bob 首先将 他的 提交添加到某个存储库:
...--G--H--K--L <-- main
我们现在可以 运行 git fetch
针对这个另一个存储库——我们在这里称之为 origin
——来获取新的提交 K-L
。这是我们将在我们自己的本地存储库中看到的内容:
I--J <-- alice (HEAD)
/
...--G--H <-- main
\
K--L <-- origin/main
如果我们有一个“不合并”规则——谁知道为什么我们有这个规则4——我们必须接受我们完美的 I-J
提交并“改进" 他们,通过 添加 到 L
的新提交。
要手动执行此操作,我们将使用 git cherry-pick
两次,使用新的临时 b运行ch 名称,然后(例如)更改 b运行ch 名称。但是 git rebase
命令可以一次性为我们做这件事:
git rebase origin/main
变基操作复制了一些提交集的效果。为此,它必须使用 Git 的合并机制,即想法的 merge-as-a-verb 部分。 git cherry-pick
命令实现了这一点,一次提交一个,并且 git merge
运行s git cherry-pick
重复。5 一旦它有对快照准备好了,每个cherry-pick步骤提交这个快照,re-using原始提交的日志消息,但是作为常规single-parent提交,而不是作为一个合并提交。所以一旦这个阶段完成我们有:
I--J <-- alice
/
...--G--H <-- main
\
K--L <-- origin/main
\
I'-J' <-- HEAD
其中 I'
是 Git 提交 I
的自动副本,而 J'
是 Git 提交 [=103] 的自动副本=].此图还说明了 rebase 内部使用的一个技巧:它 运行s 在 Git 调用的 detached HEAD 模式中,以避免不得不组成临时 b运行通道名称.6
不过,一旦完成所有复制,Git 使用另一个内部命令强制原始 b运行ch 名称指向最后复制的提交。然后re-attachesHEAD
,所以我们有:
I--J [abandoned]
/
...--G--H <-- main
\
K--L <-- origin/main
\
I'-J' <-- alice (HEAD)
请注意,这与我们故意丢弃 never-sent-to-anyone-else 次提交时发生的情况有何相似之处。
(这里的一个不幸的副作用是,通常,使用这种 work-flow,我们已经 将这些提交 发送到某处以供审查。我们将触摸下一节将对此进行讨论。)
4在我看来,这并不是一个很好的规则。我以前在项目中遵循过它——这不是一个 可怕的 规则——但我认为只是盲目地说“不合并”是错误的。尽管如此,人们还是喜欢它。
5事实上,git rebase
是一个非常复杂的命令,可以通过多种不同的方式之一完成它的工作。在现代 Git 中,它现在默认在内部使用 git cherry-pick
。在稍旧的 Git 版本中,您需要 -m
或 -i
或类似选项才能使用 git cherry-pick
。其他选项添加特殊功能,并且 rebase 已经具有许多特殊功能,因此一些选项 disable 这些功能。但主要是,它是关于将一些现有的 not-quite-good-enough 提交复制到 new-and-improved 提交,这也是 git cherry-pick
的内容,因此它们密切相关。
6如果 rebase 必须停止以获得合并冲突的帮助,或者如果您使用 git rebase -i
,这个分离的 HEAD 模式实现细节就会“泄露”变体并让它故意停止:当 rebase 在操作中间停止时,你仍然处于这种 detached-HEAD 模式。您必须告诉 Git 恢复变基或终止变基,以退出 detached-HEAD 模式。这会变得很混乱,你应该小心不要使用 git checkout
来退出分离的 HEAD 模式。
在 GitHub 分叉世界
以上都是我们在基地Git可以做的事情。然而,GitHub 和其他托管网站添加了一系列功能,希望我们会喜欢这些功能,从而实际 为这些托管网站上的服务支付 .
第一个 GitHub 特定功能是 GitHub 分支。 GitHub 上的一个分支就像一个克隆,但有两个变化:
通常情况下,如果我们使用
git clone
克隆一个仓库:git clone -b foo ssh://git@github.com/user/repo.git
我们从该存储库中获取所有 提交,以及他们的 b运行ches none .我们的
git clone
最后在本地创建 one b运行ch 名称,使用我们在此处为-b
参数提供的名称。如果我们没有给出-b
参数,我们的 Git 会询问他们的 (GitHub's) Git 他们推荐的 b运行ch 名称,以及我们的 Git 然后根据他们的推荐创建了一个 b运行ch。我们的Git对他们的b运行ch名字所做的就是把他们全部改成remote-tracking名字: 他们的
main
变成了我们的origin/main
,他们的develop
变成了我们的origin/develop
,他们的feature/short
变成了我们的origin/feature/short
,等等上。Git 这样做是因为我们的 b运行ch 名称是 我们的,我们可以随意使用。 Git 将 所有 的
origin/*
名称复制回来是可以的,但是无论如何,Git 仍然需要这种“重命名”他们的 b运行ches,并使用我们自己的名字”技巧,这样我们在获得更新时就不会丢失 our new 提交来自 他们的 Git.使用 GitHub 分支,我们的 Git 在我们的分支中为每个 b运行ch 名称创建一个 b运行ch 名称原创Git。这是无害的,并且在某些方面很好(见上一段)。
并且,在幕后,GitHub 共享 底层提交和其他内部对象,以便保存 space在他们自己的服务器上——这总是可以的,因为每个对象都有自己唯一的哈希 ID——and 为我们记住 他们的 存储库,这样我们就可以提出拉取请求。
这种提出拉取请求的能力是最畅销的功能之一。 (GitHub 管理评论、问题等的能力是另一个。那也是 add-on,在基础 Git 中不存在。)
拉取请求的核心只是一种方式,当我们单击 FORK 按钮时,我们可以向我们使用的存储库的所有者发送电子邮件或其他一些警报,让他们知道,在我们的 GitHub 分支中,我们添加了一些我们现在的提交让他们看看 and/or 添加到 他们的 克隆。他们是否接受我们的提交取决于他们 as-is——这要求他们使用原始 Git,或 GitHub MERGE 按钮——或使对它们进行某种更改,或要求我们对它们进行更改,或其他任何方式。
如果他们确实要求我们更改我们的提交,我们可以做到,然后使用git push --force
将我们的 new 提交到我们自己的 GitHub 分支。
我之前提到 Git“不喜欢”放弃提交。当我们使用 git rebase
或任何其他进程 将现有提交替换为 new-and-improved 替换时,如果我们曾经发送过 原件 任何地方,我们现在发送替换件,我们将要求其他 Git 存储库 放弃 原件,取而代之的是替换件。
当我们 进行 替换时,我们的 Git 知道我们正在这样做,如果我们至少使用 git rebase
的话。 (如果我们手动完成,我们可能不得不 强制 我们的 Git 进行替换。)Git 在 GitHub 没有认为我们的 new-and-improved 替换提交是 git rebase
的结果,因此我们必须强制 Git 在 GitHub 将它们 视为 [=619] =] 替换。所以我们必须 git push --force
我们的叉子。
GitHub 使用的 add-on 软件会注意到这个 forced-push 到我们在提出合并请求时使用的 b运行ch,并且会 自动更新其他人在查看我们的 PR 时看到的 PR。
这并没有告诉我们为什么我们必须变基。它不能,因为 Git 本身,以及 GitHub 作为 add-on,首先 不需要 变基。是某个地方的一些人提出了这个要求。由于这么多人 喜欢 这个过程(无论出于何种原因),GitHub 甚至可能在今天或明天自动执行该要求。
无论如何,如果您做到了这一点,您现在就知道如何去做,并且任何底层软件都不需要它。但还有一件事需要考虑。
无法在 GitHub
上完成无法自动化的合并(这实际上是一种夸大的说法,因为 GitHub 一直在添加新功能。但通常它 是 正确的,并且它会影响合并和变基。)
当 Git 可以自行组合更改时,它会这样做,GitHub 对 Git 的使用也会这样做。当Git无法自行合并更改时,需要寻求他人的帮助。
如果合并有合并冲突,Git 将停止并在您的工作树中留下一团糟。现在你的工作是解决这个问题。 Git 存储库 on GitHub 没有工作树 (出于各种内部原因),因此无处可修复乱七八糟的。
因此,在 GitHub 让您提出合并请求之前,他们首先 运行 一个 测试合并 。这个测试合并要么成功,因为 Git 可以自己做所有事情,要么失败,因为它不能。如果测试合并失败,GitHub 会让你知道你的 PR 有冲突。
现在,假设您在笔记本电脑上有一个 fork 和存储库的本地副本,并且您做了一些工作并进行了一些提交,它们都已准备就绪:
I--J <-- alice
/
...--G--H <-- main
您将此发送到您的 GitHub 分支,以便您的 GitHub 克隆有一个 alice
b运行ch,提交 J
作为它的最后一次提交。您分叉的存储库仍然有提交 H
作为其在 他们的 main
上的最后一次提交,并且您进行了 PR。
来了我们烦人的 Bob,他做出了一些承诺:
I--J <-- alice
/
...--G--H <-- main
\
K--L <-- bob
Bob 已将这些提交添加到他们的 main
,但他们尚未接收您的工作:
I--J <-- alice
/
...--G--H--K--L <-- main
之前你们的工作(扩展他们的提交H
)之间没有冲突,但是现在有因为鲍勃触及了相同的行 的 相同文件 并进行了不兼容的更改。
GitHub 系统无法再将您在 I-J
中的工作与其 b运行ch 提示 L
结合起来。在 GitHub 的条款中,您的 PR 已经变得 冲突 。他们 (GitHub) 在向他们的 (GitHub 的 other-persons-clone 中添加提交 K-L
时注意到了这一点,您从中分叉了 main
.
GitHub 会通知您,以便您可以完全撤回您的 PR,或者对合并冲突采取一些措施。 由于基础 Git 要求,您不必重新设置 PR 的基础。由于基本 Git 要求,您 do 必须解决合并冲突,也许可以通过在 J
之后添加一个可以与 [=135 很好地合并的新提交来解决=]. GitHubadd-ons可能会改这张图,但是冲突本身导致需要更新PR。要求更新 不一定 变基。