说 git rebase 等同于从另一个方向对某些提交进行 git cherry-pick 是正确的吗?

Is it correct to say that a git rebase is equivalent to a git cherry-pick of certain commits from the other direction?

我正在努力加深对 git 命令的理解(和沟通)。

这样说对吗

git checkout A
git rebase B

完全等同于

git checkout B
git cherry-pick <all_commits_from_common_ancestor_of_<A>_and_<B>_to_<A>>

如果不是,他们在什么情况下会出现分歧?

完全正确。

这里有几个绊脚石。首先,可能没有共同的祖先,也可能不止一个。 (这是非常小的:这只是意味着所有提交都被复制,或者所有共同的祖先都被省略。)其次,我们可能不会在这里检查 B 指定的提交。第三,一些提交可以省略,根据 rebase 的形式,这可能会变得有点复杂。最后,复制发生在 detached HEAD 模式下,然后,rebase 将分支名称拉到周围(就像通过 git checkout -Bgit switch -C,或 git branch -f 后跟 git checkoutgit switch).

要枚举的实际提交取决于 rebase 的 upstream 参数,可以这样指定:

git rebase --onto <target> <upstream>

或:

git rebase <upstream>

如果省略--onto <target>选项,则targetupstream[=相同153=]。这是被签出的提交(在 detached-HEAD 模式下)。

The rebase documentation 首先建议要枚举的提交是:

upstream..HEAD

(当然是在结帐步骤之前,因为它会移动 HEAD)。这不完全正确,因此当前文档立即自行更正了一点:

This is the same set of commits that would be shown by git log <upstream>..HEAD; or by git log 'fork_point'..HEAD, if --fork-point is active (see the description on --fork-point below); or by git log HEAD, if the --root option is specified.

稍后,它添加了这个:

Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).

直到很久以后它才提到 合并提交 被完全省略,除非你使用(现已弃用)-p 选项或(Git 2.18) -r 选项。

不过,这里真正发生的事情是 Git 使用 git rev-list 的 three-dot 语法和 --left-right 模式。 1 基本 three-dot 语法:

git rev-list upstream...HEAD

枚举可从 either 提交访问的所有提交,但 not 可从 both 访问的所有提交提交。在图论术语中,这就是 对称差 。在每次提交的 the gitrevisions documentation. This forces the revision-walking code to examine commits reachable from HEAD but not upstream and commits reachable from upstream but not HEAD. While it's doing that, Git performs a git patch-id 中对其进行了简要描述。这允许 git rebase 找到“相同”的提交(根据它们的更改),因此如果它们已经 cherry-picked 到上游分支则忽略它们。

那么,假设你有:

...--o--*--D--E--B'--F   <-- their-branch
         \
          A--B--C   <-- your-branch (HEAD)

和你 运行 git rebase their-branch 复制你的三个 A-B-C 提交到 F 之后。 rebase 代码将从 ABC 计算 patch-IDs,以及 DE 计算 patch-IDs ]、B'F。鉴于提交 B' 是您的提交 B 的副本,它可能 2 具有相同的 patch-ID。因此 Git 将从要复制的提交列表中 省略 B

--fork-point模式描述的有点拐弯抹角,但你首先应该注意,--fork-point在某些情况下是默认选项,而--no-fork-point在其他情况下是默认选项。 fork-point 模式的工作方式是使用您的引用日志。有关详细信息,请参阅

有一个相对较新的 --keep-base 选项可以真正进行合并基础计算。您可以使用 three-dot 语法直接调用它,或使用 --keep-base 选项将其打开。

最后,合并提交的遗漏(-r 或您应该避免的 -p 选项除外)是因为 Git 字面上 不能复制合并。合并的省略与运行宁git rev-list时使用--no-merges选项基本相同。当您使用 -r 选项时,Git 将枚举合并并记录它们,并将使用更高级的新交互式脚本模式 re-perform 合并。也就是说,给定这样的图形片段:

...--o--*-------F   <-- their-branch
         \
          \   B
           \ / \
            A   D--E   <-- your-branch (HEAD)
             \ /
              C

a git rebase -r 将产生:

                      B'
                     / \
                    A'  G--E'   <-- your-branch (HEAD)
                   / \ /
                  /   C'
                 /
...--o--*-------F   <-- their-branch
         \
          \   B
           \ / \
            A   D--E   [abandoned]
             \ /
              C

其中新的合并提交 G 是由字面上的 运行ning git merge 在提交 B'C' 上生成的。如果你使用 git merge --no-commitD 作为 evil merge,那么邪恶将在此 re-merging 期间消失。剩余的提交,标有素数后缀(A' 等),是通过复制,使用底层 cherry-pick 机制完成的。3


1以前git rebase就是几个shell剧本,其中一个真的做到了运行git rev-list 像这样,虽然我记得它使用 --right-only --cherry-pick。从那时起,它就被用 C 语言重写了,现在……更复杂了。 :-)

2是否相同patch-ID看有没有人在复制的时候修改过。有关详细信息,请参阅 the git patch-id documentation

3在旧版本的Git中,默认实际上是使用git format-patchgit am,或内部等价物。这在物理上也无法复制合并,并且遗漏了 cherry-pick 检测到的一些重命名案例。在添加新的 -r 选项期间,一切都设置为将默认值切换为使用 cherry-pick,并且最近(2.25ish?)成为新的默认值。