git's &;rebase -preserve-merges&;做(为什么?)

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时,实际发生了什么?它与默认行为(没有该标志)有何不同?“重新创建”合并是什么意思等等。

128057 次浏览

与普通的git rebase一样,带有--preserve-merges的git首先识别在提交图的一个部分中所做的提交列表,然后在另一个部分之上重放这些提交。与--preserve-merges的区别在于选择哪些提交进行重播,以及如何对合并提交进行重播。

更明确地说明普通和保留合并的rebase之间的主要区别:

  • 保留合并的重设数据库愿意重放(一些)合并提交,而普通的重设数据库则完全忽略合并提交。
  • 因为它愿意重放合并提交,所以保留合并的rebase必须定义意味着来重放合并提交,并处理一些额外的麻烦
    • 从概念上讲,最有趣的部分可能是选择新提交的合并父节点应该是什么。
    • 重放合并提交还需要显式地检出特定的提交(git checkout <desired first parent>),而正常的重基则不必担心这一点。
    • 李< / ul > < / >
    • Merge-preserving rebase考虑一个较浅的提交集重放:
      • 特别地,它将只考虑重放自最近合并基数以来的提交——即两个分支分开的最近的时间——而正常的重放可能会重放回到两个分支分开的第一个时间的提交。
      • 暂时的和不清楚的是,我认为这最终是一种筛选已经“合并”到合并提交中的“旧提交”的方法。
      • 李< / ul > < / >

      首先,我将尝试“充分准确地”描述rebase --preserve-merges做了什么,然后会有一些例子。我们当然可以从例子开始,如果那看起来更有用的话。

      《简要》中的算法

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

      但在这里,我将概述我认为它的本质。为了减少要考虑的事情,我做了一些自由的事情。(例如,我不会试图100%准确地捕捉计算发生的精确顺序,并忽略一些不那么核心的主题,例如,如何处理已经在分支之间被挑选的提交)。

      首先,请注意,不保留合并的rebase相当简单。它差不多是:

      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参数Rebase应该非常类似。不是在B的HEAD开始提交回放,而是在C的HEAD开始提交回放。(并使用C_new而不是B_new。)

      示例1

      例如,以提交图为例

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

      m是父节点E和G的合并提交。

      假设我们在master (C)的顶部使用一个正常的,不合并保存的主题(H) 变基。(例如,检出主题;变基主。)在这种情况下,git会进行选择

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

      然后像这样更新提交图:

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

      (D'是重复播放的D,等等。)

      注意,合并提交m没有被选择重播。

      相反,如果我们在c之上对H进行--preserve-merges重基(例如,检出主题;Rebase—保存合并主.)在这种新情况下,git将选择以下提交进行重播:

      • 选D
      • 选择E
      • 选择F(到“subtopic”分支中的D')
      • 选择G(到F'在'subtopic'分支)
      • 将分支“subtopic”合并为topic
      • 选择H

      现在m 选择重播。还要注意合并父结点E和G 在合并提交之前选中包含m.

      下面是提交的结果图:

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

      同样,D'是D的精选(即重新创建)版本,E'也是如此,等等。每一次没有上主的提交都被重播。E和G (m的合并父结点)都被重新创建为E'和G',作为m'的父结点(重基后,树的历史仍然保持不变)。

      示例2

      与普通的rebase不同,保留合并的rebase可以创建多个

      例如,考虑:

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

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

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

      结果是这样的:

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

      示例3

      在上面的例子中,合并提交和它的两个父提交都是重放的提交,而不是原始合并提交的原始父提交。然而,在其他rebase中,重放的合并提交可能会以合并前已经在提交图中的父节点结束。

      例如,考虑:

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

      如果我们将topic重新基底到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的所有更改。

      如果我们尝试对D (master)执行H (topic)的git rebase --preserve-merges,则会选择以下提交进行重播:

      • 选择m1
      • 选择H

      注意,m1中合并的变化(B, F)应该已经包含在d中(这些变化应该已经包含在m2中,因为m2将B和F的子元素合并在一起)。因此,从概念上讲,在D之上重播m1可能是一个无操作,或者创建一个空提交(即连续修订之间的差异为空)。

      然而,git可能会拒绝在d上重放m1的尝试。你可能会得到这样的错误:

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

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

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

< p >“git rebase"学会了“--rebase-merges"到移植整体 其他地方提交图的拓扑> < /强劲。< / p >

(注:Git 2.22, Q2 2019,实际上是< em >不赞成< / em > --preserve-merge, Git 2.25, Q1 2020, 停止在“git rebase --help"输出)

< p >看到提交25 cff9f 提交7543年f6f提交1131年ec9 提交7 ccdf65, 提交537年e7d6, 提交a9be29c, 提交8 f6aed7, 提交1644年c73, 提交d1e8b01, 提交25 cff9f0, 提交25 cff9f1, 提交25 cff9f2, 提交25 cff9f3, 提交25 cff9f4, 提交25 cff9f5 提交25 cff9f6(2018年4月25日)。
参见Stefan Beller (stefanbeller)提交f431d73(2018年4月25日)。
参见菲利普·伍德(phillipwood)提交2429335(2018年4月25日)。
(由Junio C Hamano—gitster提交2 c18e6a中合并,2018年5月23日)

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

类似于preserve模式,简单地传递--preserve-merges 选项赋值给rebase命令,则merges模式简单地将 --rebase-merges选项。< / p > 这将允许用户方便地改变非平凡提交的基数


git rebase手册页现在有一个全节致力于用合并重新建立历史

精华:

开发者想要这么做是有正当理由的 重新创建合并提交:以保持分支结构(或“commit”)

在下面的例子中,开发人员在一个主题分支上工作 在另一个主题分支上重构定义按钮的方式 使用重构来实现“报告bug”;按钮。
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选项来执行。


请看提交1644年c73中的一个小例子:

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

排序器刚刚学习了用于重新创建分支的新命令 结构(在本质上与--preserve-merges相似,但带有一个 ).

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

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

生成的todo列表看起来像这样:

# 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有什么不同?
提交8 f6aed7解释说:< / p >

从前,这里的开发人员想:如果, 比如说,Git在核心Git之上的Windows补丁可以表示为 一大堆分支,并基于核心Git之上重新构建,以便 维护一个可挑选的补丁系列?< / p >

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

然而,这个实验从来没有打算作为一个互动选项, 它只依附于git rebase --interactive,因为 Command的实现看起来已经非常非常熟悉了:的确如此 由设计--preserve-merges的同一人设计:yours truly.

通过“yours truly”,作者指的是他自己:Johannes Schindelin (dscho) .,他是主要原因(和其他一些英雄——Hannes, Steffen, Sebastian,…),我们有Git For Windows(即使在2009年,这并不容易)。
他在微软2015年9月起工作,考虑到微软现在大量使用Git并需要他的服务,这是有道理的。
这一趋势实际上始于2013年的TFS。从那时起,微软管理地球上最大的Git仓库!2018年10月,微软收购GitHub。< / p >

你可以在2018年4月看到Git Merge 2018的Johannes在这段视频中发言

过了一段时间,一些其他开发人员(我正在看着你,Andreas!) ;-))决定允许--preserve-merges为 与--interactive(注意!)和Git维护器结合使用 (嗯,是指Junio不在期间的临时Git维护者) 这就是--preserve-merges设计的魅力所在

这里Jonathan正在谈论来自Suse的Andreas Schwab
你可以看到一些他们在2012年的讨论.

< p >的原因吗?在--preserve-merges模式中,合并提交(或 因此,任何 commit)没有显式声明,但为 通过传递给pick命令的提交名称隐含

< p > 例如,这使得重新排序提交成为不可能
更不用说在分支之间移动提交,或者将主题分支拆分为两个 唉,这些缺点也阻止了该模式(其原始 目的是为Windows的需求服务Git,还有额外的希望 它可能对其他人也有用)从为Windows提供Git 需求。< / p > 5年后,当它真的无法维持一个笨重的, 大杂烩补丁系列部分相关,部分不相关的补丁 在Git for Windows中重新基于核心Git的标签 时间(游戏邦注:引起开发商的愤怒) git-remote-hg系列,第一次淘汰了Git的Windows竞争 方法,只是后来没有维护者就放弃了)是真的 吉特花园剪"出生:脚本, 在互动式的基础上,那将是第一个 确定待重基补丁的分支拓扑,创建 伪待办事项列表进行进一步编辑,将结果转换为真实的 待办事项列表(大量使用exec命令来“实现”;的 缺少待办事项列表命令),最后在上重新创建补丁系列

(Git花园剪切脚本在提交9055年e40补丁中引用)

那是在2013年。
我花了大约三周的时间来设计和实现它作为一个树外脚本。不用说,它的实施需要好几年的时间才能稳定下来,与此同时,设计本身也证明了它的合理性 < p > 有了这个补丁,Git花园剪刀的优点就到了git rebase -i本身
传递--rebase-merges选项将生成 一份容易理解的待办事项清单 如何重新排序提交。
可以通过插入label命令并调用merge <label>来引入新的分支。
一旦此模式变得稳定并被普遍接受,我们就可以弃用--preserve-merges的设计错误。


Git 2.19(2018年Q3)改进了新的--rebase-merges选项,使其与--exec一起工作。

< p >“--exec"选项为“__abc1”;放置执行人员

.命令在错误的地方,这已被纠正

参见提交1 ace63b (09 Aug 2018)和提交f0880f7 (06 Aug 2018) by 约翰内斯·辛德林(dscho)
(由Junio C Hamano—gitster in 提交750年eb11合并,20 Aug 2018)

rebase --exec:使它与--rebase-merges一起工作

--exec的思想是在每个pick之后追加一个exec调用。

自从fixup!/squash!提交的引入,这个想法得到了扩展 适用于“pick”,后面可能跟着“fixup/squash chain”,即an exec不会被插入pick和它的任何对应对象之间 fixupsquash行 当前的实现使用了一个肮脏的技巧来实现:它 假设只有拾取/修复/压缩命令,然后 插入在除第一个pick之外的任何exec行之前,并追加 最后一个。

使用git rebase --rebase-merges生成的todo列表 简单的实现显示了它的问题:它产生了完全错误的结果 当有labelresetmerge命令时,

. bb0 让我们改变实现来做我们想要的 pick行,跳过任何固定/挤压链,然后插入exec . 0 < / >强。

注意:只要可能,我们就会努力插入之前注释行, 因为空提交是由注释掉的选择行表示的(我们 想要插入前一个pick的执行行之前这样的行,不是 后来)。< / p > 同时,在merge命令后添加exec行,因为它们 在精神上类似于pick命令:它们添加新的提交。


Git 2.22 (Q2 2019)修复了使用refs/重写/层次结构来存储重基中间状态,这固有地使层次结构每 worktree . < / p >

参见提交b9317d5提交90年d31ff提交09 e6564 (07 Mar 2019) by ngibmc Duy (pclouds)
(由Junio C Hamano—gitster in 提交917年f2cd合并,09 Apr 2019)

确保refs/重写/是每个工作树

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

  • add_per_worktree_entries_to_dir()被更新以确保引用列表 查看per-worktree refs/rewritten/而不是per-repo one

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

这个烂摊子是我造成的。
我开始尝试通过引入refs/worktree,来修复它,其中所有的引用都将是每个工作树,没有特殊处理。
不幸的是,refs/ rewrite出现在refs/worktree之前,所以我们只能这样做

在Git 2.24(2019年Q4)中,"git rebase --rebase-merges"学会了驱动不同的合并策略,并将策略特定的选项传递给它们。

参见提交476998 d (04 Sep 2019) by Elijah Newren (newren)
看到提交e1fac53, 提交a63f990 提交5 dcdd74提交e145d99 提交4 e6023b, 提交f67336d, 提交a9c7107, 提交b8c6f24, 提交d51b771, 提交e1fac530, 提交e1fac531, 提交e1fac532, 提交e1fac533, 提交e1fac534, 提交e1fac535, 提交e1fac536 提交e1fac537(2019年7月31日)。
(由Junio C Hamano—gitster in 提交917年a319合并,18 Sep 2019)


在Git 2.25 (Q1 2020)中,用来区分工作树本地引用和存储库全局引用的逻辑是固定的,以方便保存-合并。

< p >看到提交f45f88b 提交c72fc40提交8 a64881 提交7 cb8c92, 提交e536b1f SZEDER Gábor (szeder)(2019年10月21日)。
(由Junio C Hamano—gitster提交db806d7中合并,10 Nov 2019)

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

署名:SZEDER Gábor

'logs/refs'不是工作树特定的路径,但由于提交b9317d55a3(确保refs/重写/是每个工作树,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数据结构来有效地确定路径是属于公共目录还是特定于工作树。

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

  • 根据描述trie_find()的注释,它应该只调用给定的匹配函数'fn'对于以"/-或-\0结尾的前缀的键,其中trie包含值"。
    这是不对的:有三个地方trie_find()调用match函数,但其中一个缺少值是否存在的检查

  • b9317d55a3trie添加了两个新键:

  • < p >“logs/refs/rewritten

  • 'logs/refs/worktree',在已经存在的'logs/refs/bisect'旁边。
    这导致路径为'logs/refs/'的trie节点,之前不存在,也没有附加值。
    对'logs/refs/'的查询找到这个节点,然后命中match函数的一个调用点,该调用点不检查该值是否存在,因此调用以NULL为值的match函数

  • 当使用NULL值调用match函数check_common()时,它返回0,这表明查询的路径不属于公共目录,最终导致如上所示的伪路径。

将缺少的条件添加到trie_find(),这样它将永远不会调用不存在值的match函数。

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

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

在afact中,导致匹配函数被NULL值调用的唯一其他键是'co'(因为键'common'和'config')。

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


确保使用Git 2.34 (Q4 2021),以避免内存泄漏。

参见Ævar Arnfjörð Bjarmason (avar)提交6 e65854提交0 c52cf8 (13 Oct 2021)和提交e5a917f (07 Oct 2021)。
参见juno C Hamano (gitster)提交9 d05b45 (07 Oct 2021)。
(由Junio C Hamano—gitster提交bfa646c中合并,25 Oct 2021)

sequencer:修复do_reset()中的内存泄漏

署名:Ævar Arnfjörð Bjarmason

修复在9055年e40中引入的内存泄漏(“sequencer:引入新的命令来重置修订版”,2018-04-25,Git v2.18.0-rc0——合并列在批# 6中),它调用setup_unpack_trees_porcelain()而没有对clear_unpack_trees_porcelain()进行相应的调用。

对于那些仅仅因为他们得到了这样的信息而在这里结束的人:

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