382

Git 的命令文档rebase非常简短:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).

那么当你使用时实际发生了什么--preserve-merges?它与默认行为(没有该标志)有何不同?“重新创建”合并等是什么意思?

4

3 回答 3

491

与普通的 git rebase 一样,git with--preserve-merges首先识别在提交图的一部分中进行的提交列表,然后在另一部分的顶部重播这些提交。--preserve-merges选择哪些提交进行重播以及重播如何用于合并提交的差异。

为了更明确地了解正常和保留合并的 rebase 之间的主要区别:

  • 保留合并的 rebase 愿意重播(一些)合并提交,而普通的 rebase 完全忽略合并提交。
  • 因为它愿意重放合并提交,保留合并的 rebase 必须定义重放合并提交的含义,并处理一些额外的皱纹
    • 从概念上讲,最有趣的部分可能是选择新提交的合并父项应该是什么。
    • 重放合并提交还需要显式检查特定的提交 ( git checkout <desired first parent>),而普通的 rebase 不必担心这一点。
  • 保留合并的 rebase 考虑了一组较浅的重放提交:
    • 特别是,它只会考虑重放自最近的合并基础以来所做的提交——即最近一次两个分支分歧的时间——而正常的 rebase 可能会回放两个分支一次分歧的提交。
    • 暂时且不清楚,我相信这最终是一种筛选重放已经“合并到”合并提交中的“旧提交”的方法。

首先,我将尝试“足够准确地”描述 rebase--preserve-merges的作用,然后会有一些示例。如果这看起来更有用,当然可以从示例开始。

“简报”中的算法

如果您想真正深入了解杂草,请下载 git 源并浏览文件git-rebase--interactive.sh。(Rebase 不是 Git 的 C 核心的一部分,而是用 bash 编写的。而且,在幕后,它与“交互式 rebase”共享代码。)

但在这里,我将勾勒出我认为的本质。为了减少要考虑的事情的数量,我采取了一些自由。(例如,我不会尝试以 100% 的准确度捕捉计算发生的精确顺序,并忽略一些不太集中的主题,例如如何处理已经在分支之间挑选出来的提交)。

首先,请注意非合并保留变基相当简单。或多或少:

Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A") 
Replay all those commits onto B one at a time in order.

Rebase--preserve-merges比较复杂。这就像我能够做到的那样简单,而不会丢失看起来非常重要的东西:

Find the commits to replay:
  First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
    This (these) merge base(s) will serve as a root/boundary for the rebase.
    In particular, we'll take its (their) descendants and replay them on top of new parents
  Now we can define C, the set of commits to replay. In particular, it's those commits:
    1) reachable from B but not A (as in a normal rebase), and ALSO
    2) descendants of the merge base(s)
  If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less:
    git log A..B --not $(git merge-base --all A B)
Replay the commits:
  Create a branch B_new, on which to replay our commits.
  Switch to B_new (i.e. "git checkout B_new")
  Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
    If it's a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
    Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c':
      To create a merge commit, its parents must exist and we must know what they are.
      So first, figure out which parents to use for c', by reference to the parents of c:
        For each parent p_i in parents_of(c):
          If p_i is one of the merge bases mentioned above:
            # p_i is one of the "boundary commits" that we no longer want to use as parents
            For the new commit's ith parent (p_i'), use the HEAD of B_new.
          Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
            # Note: Because we're moving parents-before-children, a rewritten version
            # of p_i must already exist. So reuse it:
            For the new commit's ith parent (p_i'), use the rewritten version of p_i.
          Otherwise:
            # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it
            For the new commit's ith parent (p_i'), use p_i, i.e. the old commit's ith parent.
      Second, actually create the new commit c':
        Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
        Merge in the other parent(s):
          For a typical two-parent merge, it's just "git merge p_2'".
          For an octopus merge, it's "git merge p_2' p_3' p_4' ...".
        Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
  Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")

--onto C参数的变基应该非常相似。不是在 B 的 HEAD 开始提交播放,而是在 C 的 HEAD 开始提交播放。(并使用 C_new 而不是 B_new。)

示例 1

例如,采取提交图

  B---C <-- master
 /                     
A-------D------E----m----H <-- topic
         \         /
          F-------G

m 是与父母 E 和 G 的合并提交。

假设我们使用正常的、非保留合并的 rebase 在 master (C) 之上 rebase 主题 (H)。(例如,结帐主题;rebase master。)在这种情况下,git 会选择以下提交进行重播:

  • 选D
  • 选E
  • 选F
  • 选G
  • 选H

然后像这样更新提交图:

  B---C <-- master
 /     \                
A       D'---E'---F'---G'---H' <-- topic

(D' 是 D 的重放等价物。)

请注意,未选择合并提交 m 进行重播。

如果我们改为在--preserve-mergesC 之上对 H 进行 rebase(例如,checkout topic; rebase --preserve-merges master。)在这种新情况下,git 将选择以下提交进行重播:

  • 选D
  • 选E
  • 选择 F(在“子主题”分支中选择 D')
  • 选择 G(到“子主题”分支中的 F')
  • 选择将分支“子主题”合并到主题中
  • 选H

现在选择 m进行重播。另请注意,在合并提交 m 之前选择了合并父级 E 和 G 进行包含。

这是生成的提交图:

 B---C <-- master
/     \                
A      D'-----E'----m'----H' <-- topic
        \          / 
         F'-------G'

同样,D' 是 D 的精选(即重新创建)版本。对于 E' 等也是如此。每个不在 master 上的提交都已重播。E 和 G(m 的合并父级)都被重新创建为 E' 和 G' 以充当 m' 的父级(在 rebase 之后,树历史仍然保持不变)。

示例 2

与普通 rebase 不同,保留合并的 rebase 可以创建上游头部的多个子级。

例如,考虑:

  B---C <-- master
 /                     
A-------D------E---m----H <-- topic
 \                 |
  ------- F-----G--/ 

如果我们在 C(master)之上 rebase H(topic),那么为 rebase 选择的提交是:

  • 选D
  • 选E
  • 选F
  • 选G
  • 选米
  • 选H

结果是这样的:

  B---C  <-- master
 /    | \                
A     |  D'----E'---m'----H' <-- topic
       \            |
         F'----G'---/

示例 3

在上面的示例中,合并提交及其两个父级都是重放提交,而不是原始合并提交具有的原始父级。但是,在其他变基中,重放的合并提交可能会以合并之前已经在提交图中的父级结束。

例如,考虑:

  B--C---D <-- master
 /    \                
A---E--m------F <-- topic

如果我们将主题 rebase 到 master(保留合并),那么重播的提交将是

  • 选择合并提交 m
  • 选F

重写的提交图将如下所示:

                     B--C--D <-- master
                    /       \             
                   A-----E---m'--F'; <-- topic

这里重放的合并提交 m' 获取提交图中预先存在的父级,即 D(master 的 HEAD)和 E(原始合并提交 m 的父级之一)。

示例 4

在某些“空提交”情况下,保留合并的 rebase 可能会混淆。至少这只是一些旧版本的 git(例如 1.7.8)。

拿这个提交图:

                   A--------B-----C-----m2---D <-- master
                    \        \         /
                      E--- F--\--G----/
                            \  \
                             ---m1--H <--topic

请注意,提交 m1 和 m2 都应该包含来自 B 和 F 的所有更改。

如果我们尝试将git rebase --preserve-mergesH(主题)放到 D(主)上,则选择以下提交进行重放:

  • 选择m1
  • 选H

请注意,合并在 m1 中的更改 (B, F) 应该已经合并到 D 中。(这些更改应该已经合并到 m2 中,因为 m2 将 B 和 F 的子级合并在一起。)因此,从概念上讲,在D 可能应该是无操作或创建一个空提交(即连续修订之间的差异为空的提交)。

然而,git 可能会拒绝在 D 之上重放 m1 的尝试。您可能会收到如下错误:

error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed

看起来好像忘记向 git 传递一个标志,但潜在的问题是 git 不喜欢创建空提交。

于 2013-04-10T01:29:50.193 回答
123

Git 2.18(2018 年第二季度)将--preserve-merge通过添加一个新选项显着改进该选项。

" git rebase"学会" --rebase-merges"将提交图的整个拓扑移植到别处

(注:Git 2.22, Q2 2019, 实际上弃用 --preserve-merge了,Git 2.25, Q1 2020,停止在“ git rebase --help”输出中宣传它)

参见提交25CFF9F提交7543F6F提交1131EC9提交7CCDF65提交537E7D6提交A9BE29C提交8F6AED7提交1644C73提交D1E8B01提交4C68E7D COM​​MAN 4C68EE7D,COM​​MAN 9055E40 ,提交CB5E40,2526 EAPRAB5112.APR.2APR A NAPRA A NAPRA A NAPR A N01,A01 ,A01a 0.Aptra a n01A .约翰内斯辛德林( ) . 请参阅Stefan Beller ( ) 的提交 f431d73(2018 年 4 月 25 日dscho
stefanbeller
请参阅Phillip Wood ( ) 的提交 2429335(2018 年 4 月 25 日(由Junio C Hamano 合并 -- --提交 2c18e6a中,2018 年 5 月 23 日)phillipwood
gitster

pull: 接受--rebase-merges重新创建分支拓扑

preserve与简单地将选项传递--preserve-mergesrebase命令的模式类似,merges模式只是传递 --rebase-merges选项。

这将允许用户在拉取新提交时方便地 rebase 非平凡的提交拓扑,而不会使它们变平。


git rebase手册页现在有一个完整的部分,专门用于使用 merges 重新定位历史

提炼:

开发人员可能想要重新创建合并提交有正当理由:在处理多个相互关联的分支时保持分支结构(或“提交拓扑”)。

在以下示例中,开发人员处理一个重构按钮定义方式的主题分支,以及另一个使用该重构实现“报告错误”按钮的主题分支。
的输出git log --graph --format=%s -5可能如下所示:

*   Merge branch 'report-a-bug'
|\
| * Add the feedback button
* | Merge branch 'refactor-button'
|\ \
| |/
| * Use the Button class for all buttons
| * Extract a generic Button class from the DownloadButton one

开发人员可能希望在保持分支拓扑的同时将这些提交变基为更新master ,例如,当第一个主题分支预计master比第二个主题分支更早集成时,例如,以解决与更改的 DownloadButton类的合并冲突它进入master.

可以使用该--rebase-merges选项执行此变基。


请参阅提交 1644c73以获取一个小示例:

rebase-helper --make-script: 引入一个标志来重新合并

定序器刚刚学习了旨在重新创建分支结构的新命令(在精神上与 类似--preserve-merges,但设计的破坏性大大降低)。

让我们允许rebase--helper使用这些命令生成待办事项列表,这些命令由新--rebase-merges选项触发。
对于这样的提交拓扑(HEAD 指向 C):

- A - B - C (HEAD)
   \   /
     D

生成的待办事项列表如下所示:

# branch D
pick 0123 A
label branch-point
pick 1234 D
label D

reset branch-point
pick 2345 B
merge -C 3456 D # C

有什么区别--preserve-merge
提交 8f6aed7解释说:

曾几何时,这个这里的开发人员想:如果说,如果 Git for Windows 的补丁在核心 Git 之上可以表示为一个灌木丛,并在核心 Git 之上重新建立,这不是很好吗?维护一套可挑选的补丁系列?

回答这个问题的最初尝试是:git rebase --preserve-merges.

但是,该实验从未打算作为交互式选项,它只是被捎带,git rebase --interactive因为该命令的实现看起来已经非常非常熟悉:它是由设计的同一个人设计的--preserve-merges:真正的你。

作者用“你的真心”指的是他自己: Johannes Schindelin ( dscho),他是我们拥有 Git For Windows 的主要原因(与其他几位英雄——Hannes、Steffen、Sebastian...)(尽管回到过去——2009 年——这并不容易)。他自 2015 年 9 月以来
一直在微软工作,考虑到微软现在大量使用 Git 并需要他的服务,这是有道理的。 这种趋势实际上始于 2013 年的 TFS。从那时起,微软管理着地球上最大的 Git 存储库!而且,自 2018 年 10 月起,微软收购了 GitHub

您可以在2018 年 4 月的 Git Merge 2018视频中看到Johannes 的演讲。

一段时间后,其他一些开发人员(我在看着你,安德烈亚斯!;-))决定允许--preserve-merges--interactive(有警告!)和 Git 维护者(嗯,临时 Git 维护者)结合是一个好主意在朱尼奥缺席期间,即)同意,那是--preserve-merges设计的魅力开始迅速而乏味地分崩离析的时候。

Jonathan 在这里谈论的是来自 Suse 的Andreas Schwab 。您可以在 2012 年
看到他们的一些讨论。

原因?--preserve-mergesmode 中,合并提交(或就此而言,任何提交)的父级没有明确说明,但 通过传递给command的提交名称暗示pick

例如,这使得重新排序 commits 变得不可能
更不用说在分支之间移动提交,或者上帝禁止将主题分支分成两个。

唉,这些缺点也阻止了该模式(其最初目的是为 Git 服务于 Windows 的需求,并希望它也可能对其他人有用)为 Git 服务于 Windows 的需求。

五年后,当在 Git for Windows 中拥有一个部分相关、部分不相关的补丁系列变得非常笨拙、大杂烩变得真的站不住脚时,这些补丁不时地重新基于核心 Git 的标签(引起了开发人员不应有的愤怒)命运多舛的 git-remote-hg系列首先淘汰了 Git for Windows 的竞争方法,后来在没有维护者的情况下被放弃)真的站不住脚,“ Git 花园剪刀诞生了:一个脚本,搭载在交互式 rebase 之上,这将首先确定要重新定位的补丁的分支拓扑,创建一个伪待办事项列表以供进一步编辑,将结果转换为真正的待办事项列表(大量使用exec命令来“实现”缺少的待办事项列表命令),最后在新的基本提交之上重新创建补丁系列。

(此补丁在提交 9055e40中引用了 Git 园艺剪刀脚本)

那是在 2013 年。
大约花了三周时间才提出设计并将其作为树外脚本实施。毋庸置疑,实施需要相当长的时间才能稳定下来,而设计本身证明自己是合理的。

有了这个补丁,Git 花园剪刀的优点就体现git rebase -i出来了。
传递该--rebase-merges选项将生成一个易于理解的待办事项列表,并且很明显如何重新排序提交
可以通过插入label命令和调用来引入新的分支merge <label>
一旦这种模式变得稳定并被普遍接受,我们就可以弃用曾经的设计错误--preserve-merges


Git 2.19(2018 年第三季度)--rebase-merges通过使其与--exec.

“”的“ --exec”选项git rebase --rebase-merges将执行命令放置在错误的位置,已更正。

请参阅Johannes Schindelin ( ) 的提交1ace63b ( 2018 年 8 月 9 日)和提交 f0880f7(2018 年 8 月 6 日(由Junio C Hamano 合并——提交 750eb11中,2018 年 8 月 20 日)dscho
gitster

rebase --exec:让它与--rebase-merges

的想法--execexec在每个pick.

自从引入fixup!/squash!提交以来,这个想法被扩展为适用于“pick,可能后跟一个 fixup/squash 链”,即不会在 apick和它的任何对应的 fixuporsquash行之间插入一个 exec。

当前的实现使用了一个肮脏的技巧来实现这一点:它假设只有 pick/fixup/squash 命令,然后 在除第一个之外的任何行之前插入行,并附加最后一个。execpick

使用 生成的待办事项列表git rebase --rebase-merges,这个简单的实现显示了它的问题:当有和命令时label,它会产生完全错误的东西。resetmerge

让我们改变实现来做我们想做的事:寻找 pick行,跳过任何修正/壁球链,然后插入exec。起泡,冲洗,重复。

注意:我们尽可能在注释行之前插入,因为空提交由注释掉的选择行表示(我们希望在这样的行之前插入前面的选择的 exec 行,而不是之后)。

同时在命令exec之后添加行merge,因为它们在本质上与pick命令相似:它们添加新的提交。


Git 2.22(2019 年第 2 季度)修复了使用 refs/rewritten/ 层次结构来存储变基中间状态,这本质上是每个工作树的层次结构。

请参阅Nguyễn Thai Ngọc Duy ( )的提交 b9317d5提交 90d31ff提交 09e6564(2019 年 3 月 7 日) 。(由Junio C Hamano 合并 -- --提交 917f2cd中,2019 年 4 月 9 日)pclouds
gitster

确保 refs/rewritten/ 是每个工作树的

a9be29c(sequencer:make refs 由label命令 worktree-local,2018-04-25,Git 2.19 生成)添加refs/rewritten/为每个工作树的参考空间。
不幸的是(我的错)有几个地方需要更新以确保它真的是每个工作树。

  • add_per_worktree_entries_to_dir()已更新以确保 ref 列表查看每个工作树refs/rewritten/而不是每个 repo 一个。

  • common_list[]已更新,以便git_path()返回正确的位置。这包括“ rev-parse --git-path”。

这个烂摊子是我造成的。
我开始尝试通过引入refs/worktree,所有 refs 将在每个工作树中的位置来解决它,而无需特殊处理。
不幸的 refs/rewritten 出现在 refs/worktree 之前,所以这就是我们所能做的。


借助 Git 2.24(2019 年第四季度),“ git rebase --rebase-merges”学会了驱动不同的合并策略并将策略特定选项传递给它们。

请参阅Elijah Newren ( ) 的提交 476998d(2019 年 9 月 4 日。 See commit e1fac53 , commit a63f990 , commit 5dcdd74 , commit e145d99 , commit 4e6023b , commit f67336d , commit a9c7107 , commit b8c6f24 , commit d51b771 , commit c248d32 , commit 8c1e240 , commit 5efed0e , commit 68b54f6 , commit 2e7bbac , commit 6180b20 , commit d5b581f (31 2019 年 7 月)newren
约翰内斯辛德林(dscho
(由Junio C Hamano 合并gitster——提交 917a319中,2019 年 9 月 18 日)


在 Git 2.25(2020 年第一季度)中,用于区分工作树本地和存储库全局引用的逻辑是固定的,以促进保留合并。

请参阅SZEDER Gábor ( ) 的提交 f45f88b提交 c72fc40提交 8a64881提交 7cb8c92提交 e536b1f(2019 年 10 月 21 日(由Junio C Hamano 合并 -- --提交 db806d7中,2019 年 11 月 10 日)szeder
gitster

path.c: 不要调用match没有值的函数trie_find()

签字人:SZEDER Gábor

“logs/refs”不是特定于工作树的路径,但由于提交 b9317d55a3(确保 refs/rewritten/ 是每个工作树,2019-03-07,v2.22.0-rc0)“ git rev-parse --git-path”一直在返回虚假路径如果存在尾随“ /”:

$ git -C WT/ rev-parse --git-path logs/refs --git-path logs/refs/
/home/szeder/src/git/.git/logs/refs
/home/szeder/src/git/.git/worktrees/WT/logs/refs/

我们使用trie数据结构来有效地确定路径是属于公共目录还是特定于工作树。

碰巧b9317d55a3trie触发了一个与实现本身一样古老的错误,在4e09cf2acf中添加(“ path:优化公共目录检查”,2015-08-31,Git v2.7.0-rc0 -在批次 #2中列出的合并)。

  • 根据描述的评论trie_find(),它应该只调用给定的匹配函数'fn',以获得“trie 包含值的键的/-或-\0-终止前缀”。
    这不是真的:trie_find() 在三个地方调用了 match 函数,但其​​中一个地方缺少对值是否存在的检查。

  • b9317d55a3向 中添加了两个新键trie

  • ' logs/refs/rewritten' 和

  • ' logs/refs/worktree',在已经存在的 ' ' 旁边logs/refs/bisect
    这导致了一个trie具有路径“ logs/refs/”的节点,该节点以前不存在,并且没有附加值。
    对 ' ' 的查询logs/refs/会找到该节点,然后点击该match函数的一个调用点,该调用点不检查该值是否存在,从而使用as 值调用该match函数。NULL

  • match函数check_common()被调用时NULL,返回0,表示查询的路径不属于公共目录,最终导致如上所示的虚假路径。

添加缺失条件,trie_find()使其永远不会使用不存在的值调用匹配函数。

check_common()然后将不再需要检查它是否获得了非 NULL 值,因此删除该条件。

我相信没有其他路径可以导致类似的虚假输出。

AFAICT 导致使用NULL值调用匹配函数的唯一其他键是' co'(因为键' common'和' config')。

但是,由于它们不在属于公共目录的目录中,因此需要生成特定于工作树的路径。


确保使用 Git 2.34(2021 年第四季度),以避免内存泄漏。

请参阅Ævar Arnfjörð Bjarmason ( ) 的提交 6e65854提交 0c52cf8(2021 年 10 月 13 日)和提交 e5a917f avar 2021 年 10 月 7 日
请参阅Junio C Hamano ( ) 的提交 9d05b45(2021 年 10 月 7 日(由Junio C Hamano 合并 -- --提交 bfa646c中,2021 年 10 月 25 日)gitster
gitster

sequencer: 修复内存泄漏do_reset()

签字人:Ævar Arnfjörð Bjarmason

修复了9055e40中引入的内存泄漏(“ sequencer:引入新命令以重置修订版”,2018-04-25,Git v2.18.0-rc0 -批处理中列出的合并#6),它setup_unpack_trees_porcelain()在没有相应调用的情况下调用clear_unpack_trees_porcelain().

于 2018-05-27T19:26:37.743 回答
3

对于那些仅仅因为他们拉动并收到消息而在这里结束的人:

git pull
(...)
warning: git rebase --preserve-merges is deprecated. Use --rebase-merges instead.

查看您的 ~/.gitconfig 和 /etc/gitconfig 并搜索此选项:

[pull]
  rebase = preserve

然后根据您的需要前往该文档了解和修复:https ://www.git-scm.com/docs/git-config#Documentation/git-config.txt-pullrebase

于 2021-08-04T15:07:12.323 回答