发生了什么
“复制提交”正是git rebase 所做的。它复制一些提交,然后将分支指针打乱,以便“忘记”或“放弃”原始提交。 (但见下文。)
这是git rebase 如何进行复制的说明。单个字母代表commit,右边的名字是branch names,实际上只指向一个 commit,即“分支的尖端”。每个提交都指向其父提交,即A--B 连接线实际上是指向左的箭头(对角线的箭头也仍然指向左侧,指向较早的提交,后来的提交指向右侧):
C--D <-- branch1
/
A--B
\
E <-- branch2
这是“之前”图片,您只有“原始”提交。您现在决定git checkout branch1 和git rebase branch2,以便C 和D 在之后 E。但 Git 实际上根本无法更改原来的 C--D,所以它会将它们复制到新的副本,C' 和 D',以及新的副本略有不同:它们出现在 E 之后(并且还使用您在 E 中所做的任何代码更改):
C--D [abandoned]
/
A--B
\
E <-- branch2
\
C'-D' <-- branch1
在这里完全忘记原来的C--D 是可以的,但是如果您认为这是一个坏主意怎么办? rebase 将分支的原始值保留在“reflogs”中以记住它。它还使用特殊名称ORIG_HEAD。这更容易使用,但只有 一个 ORIG_HEAD,而 reflog 条目的数量可能是无限的。 Reflog 条目默认保留至少 30 天,让您有时间改变主意。回头看第二张图,想象一下添加了ORIG_HEAD。
现在,您遇到的问题发生了,因为它不是 只是 记住以前提交的分支名称。每个提交也记住它自己的之前的提交,通过那些连接的,向左的箭头。因此,让我们看看如果有另一个名称或其他(合并)提交,记住C 和D,会发生什么。例如,如果我们有这个更复杂的起始图怎么办:
.-----F <-- branch3
/ /
/ C--D <-- branch1
/ /
A--B
\
E <-- branch2
如果我们现在“变基”branch1,我们会得到:
.-----F <-- branch3
/ /
/ C--D [ORIG_HEAD and reflog]
/ /
A--B
\
E <-- branch2
\
C'-D' <-- branch1
提交F 是一个合并提交:它同时指向提交A和提交D。所以它保留了原来的D,它保留了原来的C,给我们带来了一些混乱。
F 可以是一个普通的提交,只指向D,我们会看到同样的问题。不过,普通的提交更容易复制,所以如果F 不是 合并——如果我们的F 只指向D 而不是A——我们可以小心也将branch3 变基,将F 复制到F',其中F' 位于我们的新D' 之后。也可以重新进行合并,但这有点棘手(并不是说正确复制 F 不是那么容易 - 很容易“迷路”并复制 C--D 再次 错误)。
发生这种情况时
每当您复制您或其他人所做的提交时,您都会遇到此问题,并且您和“其他人”(也许是“其他您”)也仍在使用 原件。这发生在我们的提交 F 中,例如:我们仍在使用原始的 C--D 链。我们可以通过创建一个新的F' 并使用它来解决这个问题,只要我们是唯一使用branch3 的人。但是,如果branch3 已发布,或者就此而言,如果我们已经发布了branch1,那么其他人可能会将它们作为origin/branch1 或origin/branch3,我们就失去了对C--D的原件。
因此,标准建议是仅对 private(未发布)提交进行变基,因为您知道谁在使用它们——当然只有你自己——并且您可以自己检查并确保您是不使用它们,或者可以复制它们,因为您还计划复制或以其他方式重新执行诸如 F 之类的提交。
如果您已经完成了变基——制作了副本—— 发布了它们(将它们推送到origin),那么你有点卡住了。无论如何,您都可以“撤消”您的变基,并请求所有共享origin 使用的其他人确保他们不要将您的C'-D' 类型副本用于任何事情,因为您将原件回来。
(对于更高级的用户组,你们甚至都可以同意某些分支会定期进行 rebase,并且您和他们都必须认识到何时发生这种情况,然后你们所有人都会小心切换到新的提交副本。但是,这可能不是您现在想要做的!)
撤消它
所以,如果你 (a) 可以 并且 (b) 想要“撤消”你的 rebase,那么现在 reflog 或保存的 ORIG_HEAD 真的来了派上用场。让我们再次举第二个例子,看看我们忘记了branch3仍然记得原来的C-D提交后得到了什么:
.-----F <-- branch3
/ /
/ C--D [ORIG_HEAD and reflog]
/ /
A--B
\
E <-- branch2
\
C'-D' <-- branch1
现在,假设我们从底行删除名称 branch1 并写入一个新的 <-- branch1 指向提交 D:
.-----F <-- branch3
/ /
/ C--D <-- branch1
/ /
A--B
\
E <-- branch2
\
C'-D' [abandoned]
既然我们已经放弃了C'-D',那就别看它了。将此图与原始图进行比较,瞧!这就是你想要的!
像这样以任意方式“移动”分支标签的命令是git reset(它移动当前分支,所以你必须在branch1上)。在 reflog 中查找 D 的原始提交哈希,或检查 ORIG_HEAD 是否正确,或使用 reflog 拼写来识别提交 D。 (对于新手,我发现原始哈希的剪切和粘贴是可行的方法。)例如,尝试:
$ git log --graph --decorate --oneline ORIG_HEAD
查看ORIG_HEAD 是否为您提供正确的哈希值。如果没有,请尝试git reflog branch1(在此处查看branch1 的特定引用日志)来查找哈希,然后使用:
$ git log --graph --decorate --oneline branch1@{1}
(或剪切并粘贴原始哈希而不是使用branch1@{1})。找到所需的“原始”提交后,您可以:
$ git status # to make sure you're on the right branch
# and that everything is clean, because
# "git reset --hard" wipes out in-progress work!
$ git reset --hard ORIG_HEAD
(或者像往常一样放入branch1@{1},或原始哈希ID,代替ORIG_HEAD)。1这会移动当前分支(我们刚刚检查过)以便它指向到给定的提交(branch1@{1},来自 reflog,或 ORIG_HEAD 或原始哈希 ID),让我们返回最终的图形。 --hard 设置我们的索引/暂存区域和我们的工作树,以匹配我们刚刚重新指向我们的分支的新提交。
1这里的一般想法,在 Git 中一直重复出现,是我们必须命名一些特定的提交,Git 从中找到 rest 必要时提交。任何名称都有效:分支名称、HEAD 之类的名称、master@{1} 之类的 reflog 名称或原始提交哈希 ID。 Git 并不真正关心 如何 你告诉它“看这里的提交”;最终,Git 将该名称解析为那些丑陋的 SHA-1 哈希 ID,并使用它。