Git 变基不能正常工作?

Git rebasing not working correctly?

Git rebase 似乎没有按照我期望的方式工作,基于我的 对 rebase 的理解,以及基于我在 水银。我已经生成了一个示例来说明奇怪的行为,并且 希望有人能解释为什么 Git 会这样。考虑这个 DAG 的状态:

在这种情况下,我已经在 master 上提交了 f7 和 f8,但我想移动 相反,这些新提交到功能分支上。即我在 错误的分支,并希望纠正错误。我可以从 资源树:

SourceTree 证实了我的意图:

但结果完全不是我所期望的:

虽然节点在DAG中的位置是正确的,但是分支头是 不正确!通过将 f7 和 f8 变基到 f6,我希望看到 master 重置为它的 原点位置,并期望 feature/AAA-1 前进到 f8。像这样:

这是我期望的行为,基于 Mercurial 中的变基,只是 通常基于重新定位正在做什么。为什么 Git 这样做,我该怎么做 使其行为正确?

看来您对变基不太感兴趣。您只想移动分支以指向新的头。在这种情况下,这很容易。我不使用 SourceTree,但在 CLI 中使用它非常简单:

git checkout feature/AAA-1
git reset --hard master
git checkout master
git reset --hard origin/master

您的期望是正确的,但我认为混淆只是在术语上。来自 git-scm:

In this example, you’d run the following:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

It works by going to the common ancestor of the two branches (the one you’re on and the one you’re rebasing onto)...

注意本例中“on”和“onto”的用法。在 git 中,您要变基的分支是变基后将成为子树的分支。我怀疑你对 SourceTree 的使用只是倒退 git.

来自 Mercurial,您期望提交 永久附加到 特定分支。也就是说,如果您设法孤立地提取一些提交,您会看到一些内容:

I am a commit on branch foo.
I change file bar.

Git 不能这样工作:提交 独立于 任何分支(名称),事实上,分支名称——标签,如果你愿意的话——可以随意剥离并粘在其他地方。它们没有任何用处1 除了对试图解释混乱的人类而言。

在 Mercurial 中,当你 "rebase" 一些变更集时,你(至少实际上)将它们作为与它们的基础的差异转储出来,然后你切换到你想要它们的另一个分支并且在另一个分支上进行新的提交。 Mercurial 曾经(也许现在仍然)称这个第一步为 "grafting"。这些新提交现在永久附加到(且仅附加到)另一个不同的分支:

master:    f1 - f2 - f3 - f4 - f7 - f8
                             \
feature/AAA-1:                 f5 - f6

变为:

master:    f1 - f2 - f3 - f4 - f7 - f8
                             \
feature/AAA-1:                 f5 - f6 - 9 - 10

此时您可以 "safely undo" f7 和 f8,将它们从 master 行中删除,并且您的 rebase 仅在另一个分支上完成副本。

请注意,我在此处的左侧绘制了分支标签。这是安全的,因为所有的提交都永久地粘在它们的分支上,所以一旦一个变更集在它的分支线上,它总是在它的分支线上。唯一一次违反 "changeset goes on the (single) line of its branch" 规则的是合并,当变更集附加到(恰好)两个分支时:它位于其主分支上,但绘制了与另一个分支的连接。

另一方面,在 git 中,提交可以被认为是 "on" 零个或多个 分支(没有 "exactly 1 or 2" 约束),并且提交的 分支集是 "on" 是动态的,因为可以随时添加或删除分支名称。 (还要注意单词 "branch" 有 at least two meanings in git。)

Git 的 rebase 与 Mercurial 的 rebase 非常相似:它实际上 复制 提交。但是有一个重要的区别要开始:副本不是特定的 "on" 任何分支(事实上,rebase 过程在 no 分支上运行,使用什么 git调用 "detached HEAD")。然后,最后还有一个更重要的区别。

和以前一样,我们可以从绘制图形开始,但这次我画得有点不同:

                     f7 <- f8   <-- master
                    /
f1 <- f2 <- f3 <- f4
                    \
                     f5 <- f6   <-- feature/AAA-1

这一次,标签在右边,带有箭头。名称 master 实际上直接指向提交 f8f8 指向 f7 并且 f7 指向 f4 等等上。

这意味着,现在 f1f4 是 "on" 两个 分支。对于 git,最好说这些提交是 "contained in"(历史)两个分支。这些提交中没有任何内容可以说明它们最初是哪个分支 "made on":它们带有父指针、源代码树 ID、作者和提交者姓名(以及时间戳等),但没有 "source branch name"。 (我认为来自 hg 的 git 的新手经常觉得这很令人沮丧。)

如果您现在要求 git 将 f7f8 变基到 feature/AAA-1,git 将复制两个提交:

                     f7 <- f8
                    /
f1 <- f2 <- f3 <- f4
                    \
                     f5 <- f6 <- f7' <- f8'

' 标记,或 f7prime 和 f8prime,表示这些是原件的副本——git cherry-picks,类似于 hg 的移植)。但现在我们谈到了关键的区别,让你感到困惑的是:git 现在 "peels off" 原始的 master 标签,并使其指向最尖端的新提交。这意味着最终图表如下所示:

                     f7 <- f8   [abandoned -- was master]
                    /
f1 <- f2 <- f3 <- f4
                    \
                     f5 <- f6   <-- feature/AAA-1
                             \
                              f7' <- f8'   <-- master

Mercurial 不能 做到这一点:分支标签不能被剥离和随意移动并重新粘贴到别处。所以它没有,这就是为什么它的 rebase 工作方式不同的原因。

在 git 中,您在这里要做的只是简单地将两个提交挑选到 feature/AAA-1 分支中,然后将它们从 master 分支中删除:

$ git checkout feature/AAA-1
$ git cherry-pick master~2..master   # copy the commits
$ git checkout master
$ git reset --hard master~2          # back up over the originals

这里的想法是你根本没有变基 master,你甚至没有真正变基你的功能分支:相反,你只是将两个提交复制到你的功能分支中,然后将它们从 master 中移除。


1这有点夸大其词,因为在之间传输 个存储库——git fetchgit push - 也使用分支和标记标签。此外,您需要 一些 对提交的引用以使它们保持活动状态,否则 git 的垃圾收集器最终会将它们收割为 "unreachable".

git rebase 存在的目的是:您向 feature/AAA 提交了一些内容,同时其他人将更改推送到 origin/feature/AAA。现在 feature/AAA(当前)和 origin/feature/AAA 是不同的。您可以合并它们,但项目规则规定历史应该是线性的。因此,您 运行 git 变基,并且您当前的分支 feature/AAA 在最新的 origin/feature/AAA.

上变为 based

你的情况不同。可能有命令将提交从一个分支移动到另一个分支,明确命名两者,但这不是变基。