Git: 为什么之前的fetch+merge commit在fetch+rebase之后就消失了

Git: Why does previous fetch+merge commit disappear after fetch+rebase

TL;DR: 如果我将远程更改获取到本地 git 存储库,然后进行合并,一段时间后我获取一些新更改,但是这次我做的是变基而不是合并,然后之前创建的合并提交就消失了。为什么?

例子

考虑以下由命令 git log --all --graph --decorate --oneline 创建的起点:

* 28992d3 (repo1/master) hello4
* 3610bdf hello3
| * f113d63 (HEAD -> master) bye-bye
| * cabc896 bye
|/  
* 75f7ca9 hello2
* 525cb4a hello1

即,有一个 git 回购,有一个 master 分支,其中有一些本地的、未推送的更改。刚刚从远程获取了一些其他更改(在本例中 repo1)。

下一个命令:git merge repo1/master。结果:

*   b94aa29 (HEAD -> master) Merge remote-tracking branch 'repo1/master'
|\  
| * 28992d3 (repo1/master) hello4
| * 3610bdf hello3
* | f113d63 bye-bye
* | cabc896 bye
|/  
* 75f7ca9 hello2
* 525cb4a hello1

现在假设在本地和远程 repo1 中都有一些新提交,然后再次通过 git fetch repo1 masterrepo1 获取远程内容.结果如下所示:

* 2e3d749 (repo1/master) hello6
* b17983d hello5
| * 2e49819 (HEAD -> master) see ya
| * c2f2d5a good-bye
| *   b94aa29 Merge remote-tracking branch 'repo1/master'
| |\  
| |/  
|/|   
* | 28992d3 hello4
* | 3610bdf hello3
| * f113d63 bye-bye
| * cabc896 bye
|/  
* 75f7ca9 hello2
* 525cb4a hello1

到目前为止一切顺利。 现在让我们做 git rebase repo1/master,结果是一个很好的线性提交日志:

* 101e524 (HEAD -> master) see ya
* 3ce7543 good-bye
* 849cbd4 bye-bye
* 483bab8 bye
* 2e3d749 (repo1/master) hello6
* b17983d hello5
* 28992d3 hello4
* 3610bdf hello3
* 75f7ca9 hello2
* 525cb4a hello1

问题:提交 b94aa29 Merge remote-tracking branch 'repo1/master' 去了哪里?(据我所知,即使是 "dead" 提交也没有保留,例如在独立的头脑中进行提交。)

备注:

TL;答案的 DR 版本

git rebase 在功能上意味着:

  • 挑选出一组要复制的提交;
  • 复制这些提交,一次一个,就像 git cherry-pick;
  • 完成后,更改当前分支名称(无论是什么)以指向最终复制的提交。

字面上的复制不能复制合并,所以通常不用费心去尝试。

更长(但更多信息请参阅我的其他答案)

这里的总体思路是进行一系列提交:

             A--B--C--D   <-- topic (HEAD)
            /
...--o--o--*--o--o   <-- mainline

并将它们移植到一系列 new-and-improved 提交中:

             A--B--C--D   [abandoned]
            /
...--o--o--*--o--o   <-- mainline
                  \
                   A'-B'-C'-D'  <-- topic (HEAD)

"improvement" 是将新链建立在其他分支的顶端,例如 mainline。为了实现这一点,Git 字面上 必须 复制原始提交 - A-B-C-D,此处 - 到具有不同哈希 ID 的不同提交,因为每个提交一旦完成, 是永久的1 并且一成不变;即使只有一点不同的提交也会为您提供一个新的 不同 提交哈希 ID,即使唯一的区别是存储在新提交中的 parent ID。因此,即使快照 A' 中的源树与快照 A 中的源树相匹配——而且可能不匹配——A' 的提交 ID 与 [= 的提交 ID 不同20=].

(当然,这也会通过其余的提交进行。)

你给git rebase的参数select:

  • 承诺复制,
  • 从哪里开始复制(first-copied 提交的地方)。

通常情况下,您可以为这两个名称取一个名字。例如,git rebase mainline表示将副本放在mainline指向的提交之后,并复制从topic(当前分支名称)指向的提交可以到达的那些提交——即,D——不包括从 mainline 的顶端可以到达的任何提交。 复制的第一个提交是提交*,两个分支重新加入(在这种情况下永远)。

在某些情况下,您可能需要使用git rebase --onto来区分这两个概念。使用 --onto,您告诉 rebase 将副本 放在哪里,释放剩余的参数来表示 不复制的内容 。这里不需要。

有一堆 kinds/flavors 的变基:git rebase 没有参数使用 git format-patch | git am 来复制提交,而不是实际 运行 git cherry-pick,而 git rebase -i 实际上使用 git cherry-pick。 (在 Git 的旧版本中,git rebase -i 是一个 shell 脚本,它实际上运行 git cherry-pick。为了使 Windows 更快,git rebase 是修改后 -i 内置于 Git 的 sequencer,这是实现 cherry-pick 和恢复的代码。)

请注意,所有这些一次复制一个,最终会构建一个线性提交链即使输入可能包含合并也会发生这种情况,如:

          A--B--M--C--D   <-- master
         /     /
...--o--*--o--S------o--T   <-- repo1/master

你现在要求 Git 变基(即复制)一些提交——在这种情况下,一些提交在 master——--onto 目标是 T,并且限制是*从 T / origin/master 到达的第一个提交,它也在 master 上,即提交 *.

此类提交的完整列表是 A 然后 B 然后 M 然后 C 然后 D。但是Git应该怎么复制M呢?如果尝试,结果可能看起来很像:

          A--B--M--C--D   [abandoned]
         /     /
...--o--*--o--S------o--T   <-- repo1/master
                         \
                          A'-B'-M'-C'-D   <-- master (HEAD)
                               /
                  ???----------

除了 M',要合并,需要有 两个 parent。 parent 还应该有什么?如果它的另一个parent是S,嗯,那是可能,但是它带来什么价值呢?

(合并的要点是结合两条不同开发线的变化。由于A'基于TT基于SA' 已经包含了 S 中的所有内容,因此无需合并它。)

一般来说,Git 只是 忽略了 合并完全在这里提交,所以它最终只复制 A-B-C-D。请注意,如果您对包含 internal 合并的内容进行变基,则会发生同样的事情:Git 只是复制合并的两个 "sides",线性化结果:

                 C--D
                /    \
            A--B      M--G   <-- topic (HEAD)
           /    \    /
          /      E--F
         /
...--o--*--o--o   <-- mainline

此处 git rebase 将复制 A-B-C-D-E-F-GA-B-E-F-C-D-G,删除 M 并展平拓扑。

git rebase -i 有一个 -p 标志,它的拼写较长 --preserve-merges,但它实际上 保留 合并(也不 cherry-pick 它们,这是不可能的)。相反,它 进行新的合并 (通过 运行 git merge)。这非常棘手,但可用于变基上述 A-B-(C-D, E-F)-M-G 拓扑。请注意,如果您在 M 中解决了合并冲突,则当 Git 进行合并 D'F' 的新合并 M' 时,您将不得不再次解决它们( git rerere 在这里可能会有用)。


1永久,也就是说,直到整个提交被放弃足够长的时间 Git 以确保没有人愿意是它;然后它被 git gc.

清除