从根本上说,这些问题的发生是因为——嗯,主要是因为——rebase 有效
通过复制提交。问题是哪些提交会被复制——what、where、when 和 why:@987654321 中的四个@。 谁也可能很有趣,对于第六个因素,如何 ...好吧,我们稍后会看到。
修复混乱很可能现在只需复制正确的提交,您可以使用git rebase 来完成,或者使用git cherry-pick 可能更容易。
背景
要正确理解这一点,您需要准确掌握什么是提交、如何找到提交以及如何复制提交。有关如何找到提交的正确深入解释,请参阅Think Like (a) Git,但我们将在此处稍作介绍。
什么是提交是分为两部分:数据——你的源的快照——和元数据,例如你的姓名和电子邮件地址以及其他信息关于提交:
快照就是:每个源文件的完整副本,但它在您提交时存在。快照实际上是由 index 制作的,而不是来自您可以看到的来源,这就是为什么您必须一遍又一遍地 git add 文件:git add somefile 真正的意思是 从我可以看到和使用的版本复制文件somefile,用这个改进的版本替换现在索引中可能存在的任何副本。 运行git commit 对索引副本进行快照。
元数据包括git log 可以显示的其他内容:您的姓名和电子邮件地址(来自您配置的user.name 和user.email);您提交的日期和时间戳;对人类最重要,但对 Git 不重要的是,您做出提交的原因:日志消息。但它还包括一个对 Git 至关重要的项目(有时是多个项目):这个新提交的每个 父 提交的 hash ID。
每个提交都由其哈希 ID 唯一标识。没有两个提交可以共享一个哈希 ID。哈希 ID 看起来是完全随机的,但实际上它是完全非-随机的:它是提交内容的加密校验和,包括源快照、您的姓名和电子邮件、日期和时间— 即使您使用相同的名称、电子邮件和日志消息偷偷提交 相同的代码 两次,时间也有助于使提交变得独一无二 — 并且(不知何故,但这并不难)哈希相同父母的 ID。
因为哈希 ID 是加密校验和,所以您和其他任何人(甚至 Git)都无法在提交后更改任何内容。如果您接受现有的提交,提取它,完全摆弄它并进行新的提交,那么您要么保留了原始提交的每一位,因此没有更改它,它仍然只是原始提交,或者,您会得到一个具有完全不同哈希 ID 的新的不同提交。这意味着在某种意义上,哈希 ID 是提交。
同时,每个提交都存储其父级的哈希 ID。对于任何简单的线性提交链,这意味着我们可以从 last 提交开始,并使用它来查找每个较早的提交。通过将提交表示为单个大写字母而不是真正的哈希 ID,我们可以将其绘制为:
... <-F <-G <-H
其中H 是某个分支中last 提交的哈希ID。此外,这也适用于多个分支:我们只需要在某处保存每个最终提交的哈希 ID:
...--F--G--H <-- somehow remember H
\
I--J <-- somehow remember J
这是 Git 分支名称的来源。分支名称的作用是记住一次提交的哈希 ID。 如果 master 记住 H 和 dev 记住 J,然后我们在 both 分支上有一些共享提交——通过提交G 的所有内容都是共享的——一个提交私有到master,两个私有提交到dev。
使用git cherry-pick 复制一个提交
如果提交是快照——它确实是——git log 或 git show 如何显示它作为差异?如何将一个提交复制到一个新的、略有不同的提交?答案在于那些相同的父连接。
假设提交J 实际上是一个重要的错误修复,与dev 上正在进行的开发无关。我们想将J 中的修复复制到master 以进行新的提交K,给我们:
...--F--G--H--K <-- master
\
I--J <-- dev
我们需要 Git 做的是拍摄I 和J 中的快照并将它们相互比较。无论从I 到J更改,这就是我们需要应用到H 的错误修复。
git cherry-pick 命令执行此操作。它使用 Git 的内部 合并机制 来做到这一点,以便可以正确维护 I 和 H 之间的任何 other 差异,但我们将忽略此处的细节:简单的情况下,合并没有复杂的工作要做,我们可以想象Git只是将I-vs-J更改为H来生成J的副本,我们将调用@ 987654365@.
这正是我们想要的:K 和 J 做同样的事情,所以K 是J 的复制品,但他们这样做是为了不同的起点,所以K 的父级是H(在master 上),而K 本身只能安全地保存在master 上。因为K 是J 的副本,Git 默认使用相同的作者和日志消息,甚至相同的时间戳。您现在是新副本K 的提交者,但创建J 的人是作者。当然,J 和 K 有不同的哈希 ID——它们是不同的提交——但 K 是 J 的副本。
rebase 只是一个整体复制
假设我们从这个开始:
...--F--G--H <-- master
\
I--J--K <-- dev
这次没有重要的错误修复。我们只想获取dev 上的三个提交,并让它们从H 开始,而不是从F 开始。
此时我们可以做的是创建一个新 dev2 分支,指向提交H,如下所示:
$ git checkout -b dev2 master
给予:
...--F--G--H <-- master, dev2 (HEAD)
\
I--J--K <-- dev
特殊名称HEAD 告诉我们(和Git)哪个分支名称会随着我们的进展而更新,因此HEAD 附加到我们的新dev2。
现在我们可以一次一个地复制提交I,然后是J,然后是K。这次我们将副本称为I'、J' 和K':
I'-J'-K' <-- dev2 (HEAD)
/
...--F--G--H <-- master
\
I--J--K <-- dev
现在让我们删除名称dev:
$ git branch -D dev
I'-J'-K' <-- dev2 (HEAD)
/
...--F--G--H <-- master
\
I--J--K ???
然后重命名 dev2 到 dev:
$ git branch -m dev2 dev
I'-J'-K' <-- dev (HEAD)
/
...--F--G--H <-- master
由于没有可以找到它们的名称,原始提交 I-J-K 似乎已经消失了。由于我们找到三个 new 提交的名称是 dev,因此 似乎 我们已经神奇地替换了原来的三个提交。我们还没有——原来的三个实际上仍在存储库中,并且通常会在那里保留一段时间,以防我们改变主意并希望它们回来。但是普通的 Git 命令不会看到它们。它们被隐藏起来,因此我们只能看到新的和改进的 dev 分支。
这 - 复制提交,就像 git cherry-pick,然后重新洗牌分支名称 - 是 git rebase 所做的。但是假设,谁不知道谁正在对 dev 进行这种改组,你,在 你的 Git 存储库中,已经做出了几个依赖于提交 K 的提交?如果你在他们有dev2 的时候抢他们的东西,你会看到:
I'-J'-K' <-- origin/dev2
/
...--F--G--H <-- master, origin/master
\
I--J--K <-- dev, origin/dev
\
L--M--N <-- branch-b
如果您等到他们替换了他们的 dev(通常情况下,您必须),那么您自己的存储库会像这样更新:
I'-J'-K' <-- origin/dev
/
...--F--G--H <-- master, origin/master
\
I--J--K <-- dev
\
L--M--N <-- branch-b
现在假设你要么删除你的dev,要么——更有可能——更新它以匹配他们的。您在存储库中看到的是:
I'-J'-K' <-- dev, origin/dev
/
...--F--G--H <-- master, origin/master
\
I--J--K--L--M--N <-- branch-b
这意味着您现在在branch-b 上没有三个而是六个 提交,而他们(运行origin/* 名称的人)没有。
就 Git 而言,所有六个提交,I-J-K-L-M-N,都是您的 提交。
如果您将branch-b 重新定位到您/他们的master 现在,Git 将尝试复制所有六个 提交。如果你重新定位到他们的origin/dev,就会发生更有趣的事情,我们稍后再讨论。
随着时间的推移,他们可能会将他们的工作合并到master,和/或您可能会将他们的工作合并到您的工作中。这是一个潜在的图表,如果他们已经将他们的K' 合并到他们的master(你的origin/master 和你现在更新的master)然后你将O 提交合并到你的branch-b 以产生合并P:
I'--J'--K' <-- dev, origin/dev
/ \
...--F--G--H-----------O <-- master, origin/master
\ \
I--J--K--L--M--N--P <-- branch-b
每当你变基时,你的 Git 都会枚举提交以复制
当您运行 git rebase <em>target</em> 时,您的 Git 必须确定要复制哪些提交。要复制的提交列表从您当前分支可访问的所有提交开始——因此,提交I-J-K-L-M-N-P,但也提交F-G-H-...-O-P,因为P 是一个合并提交。在这里,我再次建议通过Think Like (a) Git 工作。
从这个所有可访问提交的大列表中,Git 立即从目标中减去所有可访问的提交。因此,如果您选择 dev 作为目标,Git 将消除提交 F-G-H-I'-J'-K'。如果您选择master,Git 将消除F-G-H-I'-J'-K'-O。这些很明显可以从列表中删除,因为它们已经在目标中。
Git 也会自动忽略任何 merge 提交。这样就淘汰了O 和P。合并提交实际上不能被复制:复制提交需要将其与其(单个)父级进行比较,并且合并提交有两个父级。 Git 无法知道要使用哪个父级,所以它不会复制它们。
但这仍然留下I-J-K-L-M-N 作为(可能)复制的提交列表。这就是 Git 关于如何识别已复制提交的想法的用武之地。
对于上游“已经存在,请勿复制”列表中的每个提交(包括I'-J'-K'),Git 使用git patch-id 程序计算一个补丁 ID。这从本质上将每个提交减少到从该提交的父级到该提交的更改的近似值。也就是说,Git 会找到 git cherry-pick 将复制的同一组更改,并根据这些更改计算哈希 ID。
然后,Git 为所有您的 提交I-J-K-L-M-N 计算补丁 ID。如果这些补丁 ID 中的任何一个与上游提交的补丁 ID 匹配,Git 就会淘汰这些提交。 在许多情况下,这完全拯救了你的变基,一切正常。你原来的 I-J-K 提交——好吧,现在是你的,即使它们最初不是你的——现在留在您的分支中,因为他们将他们的复制到 I'-J'-K' 然后放弃了他们的 I-J-K,但您的 branch-b 保留了他们,即使他们认为他们已经永远离开了 I-J-K。但是,如果您的 I-J-K 的补丁 ID 与他们的 I'-J'-K' 匹配,那么您的 rebase 将运行良好。
如果他们的补丁ID 不匹配,那么现在你就有问题了。 Git 不能自动排除复制的提交。当 Git 将它们应用到它们的新位置以进行变基时,几乎但不完全相同的提交 将 发生冲突。这些将是您从未接触过的文件,但您在保留的提交中继承了它们的更改,他们——无论“他们”是谁——认为他们已经成功放弃了。你将他们的作品作为你自己的作品带回来,即使这不是你想要的。
你需要做什么才能恢复
从所有这些中恢复的方法是确定哪些提交应该被复制。如果幸运的话,它们可能都排成一排。在这种情况下,您可以使用git rebase --onto 将作业分成两部分。通常你运行:
git rebase <target>
和target 指定放置副本的位置,以及避免复制的提交集。但你可以运行:
git rebase --onto <target> <dontcopy>
现在 target only 指定放置副本的位置; dontcopy 参数告诉 Git 什么不要复制。相同的reachability rules from Think Like (a) Git 确定哪些提交将被放入“可能复制”列表中,哪些不会被放入。然后,Rebase 将丢弃所有合并和所有相同补丁 ID 提交,并复制剩余的所有内容。
如果您不走运,那么要复制的提交集将四处散布。您必须创建一个新的临时分支,或使用“分离 HEAD”模式,并运行一系列 git cherry-pick 命令来复制 应该 被复制的提交,即真正属于你的提交.
合并避免了这个问题,因为合并不会复制提交
当人们使用git merge 来组合工作时,原始提交不会受到干扰。这样,如果其他人——比如你!——以某种方式使用这些原始提交,Git 知道这些提交被正确合并,因为每个合并都记录了合并操作的 both 父级。这张图——从后面的提交到前面的连接线——显示了提交和开发的真实历史,Git 可以弄清楚一切。
当人们使用git rebase 来组合工作时,他们丢弃他们的原始提交,转而支持新的和(据说)改进的原始提交的副本。如果其他人都知道要立即处理这个问题,那么与这个存储库共享工作的其他人都可以在它变得一团糟之前处理它。或者,如果没有其他人见过这些提交,那么您(现在正在执行此操作的人 git rebase)可以确定没有其他人正在使用您的原件。当您放弃它们以支持新的和改进的提交时,您并没有搁置正在使用您的提交的其他人,因为没有其他人拥有您的提交。
但在这种情况下,您已经陷入困境:您正在使用其他人的提交,然后 他们 将它们撕掉(支持新的和改进的重新定位提交),他们认为每个人和他们一起完成了。但是你曾经并且仍在使用它们,现在 Git 认为这些是 你的 提交,并且无论你走到哪里都将它们拖到你身边。现在,经过这么长时间,现在你必须摆脱这些提交,而你能做到这一点的唯一方法是选择性复制。