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).

使用保存合并时会发生什么呢?它与默认行为(没有该标志)有何不同?“重新创建”合并是什么意思等等。


与普通的git rebase一样,使用——save - merged的git首先识别在提交图的一个部分中所做的提交列表,然后在另一个部分上重放这些提交。与——preserve-merge的区别在于选择哪些提交进行重播,以及该重播如何为合并提交工作。

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

Merge-preserving rebase is willing to replay (some) merge commits, whereas normal rebase completely ignores merge commits. Because it's willing to replay merge commits, merge-preserving rebase has to define what it means to replay a merge commit, and deal with some extra wrinkles The most interesting part, conceptually, is perhaps in picking what the new commit's merge parents should be. Replaying merge commits also require explicitly checking out particular commits (git checkout <desired first parent>), whereas normal rebase doesn't have to worry about that. Merge-preserving rebase considers a shallower set of commits for replay: In particular, it will only consider replaying commits made since the most recent merge base(s) -- i.e. the most recent time the two branches diverged --, whereas normal rebase might replay commits going back to the first time the two branches diverged. To be provisional and unclear, I believe this is ultimately a means to screen out replaying "old commits" that have already been "incorporated into" a merge commit.

首先,我将尝试“充分准确地”描述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-merge相对复杂。以下是我能够做到的最简单的事情,而不会失去看起来很重要的东西:

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的合并提交。

假设我们将主题(H)重新基于主(C)之上,使用一个正常的、不保留合并的主题 变基。(例如,结帐主题;变基的主人。)在这种情况下,git会进行选择 以下文件将被提交重播:

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

然后像这样更新提交图:

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

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

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

如果我们在c之上对H进行——preserve-merges重基(例如,checkout topic;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

与普通的重基不同,保留合并的重基可以创建多个 儿童逆流而上。

例如,考虑:

  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的所有更改。

如果我们尝试做git rebase -preserve-merges of H (topic) to D (master),那么会选择以下提交进行重放:

选择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选项。

“git rebase”学会了“-rebase-merges”移植整个 在其他地方提交图的拓扑。

(注:Git 2.22, 2019年第二季度,实际上已弃用-保存-合并,而Git 2.25, 2020年第一季度,停止在“Git rebase -help”输出中宣传它)

参见Johannes Schindelin (dscho)的commit 25cff9f, commit 7543f6f, commit 1131ec9, commit 7ccdf65, commit 537e7d6, commit a9be29c, commit 8f6aed7, commit 1644c73, commit d1e8b01, commit 4c68e7d, commit 9055e40, commit cb5206e, commit a01c2a5, commit 2f6b1d1, commit bf5c057(2018年4月25日)。 参见Stefan Beller (stefanbeller)提交f431d73(2018年4月25日)。 参见Phillip Wood提交2429335(2018年4月25日)。 (由Junio C Hamano - gitster -在commit 2c18e6a中合并,2018年5月23日)

Pull: accept——rebase-merges来重新创建分支拓扑

类似于保存模式,只需传递——preserve-merges 选项设置为rebase命令时,合并模式将简单地传递 ——rebase-merges选项。 这将允许用户方便地更改非平凡提交的基准 在拉出新提交时的拓扑,而不会将它们平铺。


Git rebase手册页现在有一个完整的部分专门用于用合并来重基历史。

精华:

There are legitimate reasons why a developer may want to recreate merge commits: to keep the branch structure (or "commit topology") when working on multiple, inter-related branches. In the following example, the developer works on a topic branch that refactors the way buttons are defined, and on another topic branch that uses that refactoring to implement a "Report a bug" button. The output of git log --graph --format=%s -5 may look like this: * 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 The developer might want to rebase those commits to a newer master while keeping the branch topology, for example when the first topic branch is expected to be integrated into master much earlier than the second one, say, to resolve merge conflicts with changes to the DownloadButton class that made it into master. This rebase can be performed using the --rebase-merges option.


请看commit 1644c73的一个小例子:

rebase-helper——make-script:引入一个标志来rebase merge

音序器刚刚学会了重新创建分支的新命令 结构(在精神上类似于——preserve-merges,但带有 更少损坏的设计)。 让我们允许rebase——helper生成使用的待办事项列表 这些命令由新的——rebase- merged选项触发。 对于这样的提交拓扑(HEAD指向C): - a - b - c(头) \ / D

生成的todo列表看起来像这样: # D分支 pick 0123 A 标签分叉点 pick 1234选择D 标签维 重置分叉点 pick 2345 B C; C


保留-合并有什么区别? Commit 8f6aed7解释道:

Once upon a time, this here developer thought: wouldn't it be nice if, say, Git for Windows' patches on top of core Git could be represented as a thicket of branches, and be rebased on top of core Git in order to maintain a cherry-pick'able set of patch series? The original attempt to answer this was: git rebase --preserve-merges. However, that experiment was never intended as an interactive option, and it only piggy-backed on git rebase --interactive because that command's implementation looked already very, very familiar: it was designed by the same person who designed --preserve-merges: yours truly.

And by "yours truly", the author refers to himself: Johannes Schindelin (dscho), who is the main reason (with a few other heroes -- Hannes, Steffen, Sebastian, ...) that we have Git For Windows (even though back in the day -- 2009 -- that was not easy). He is working at Microsoft since Sept. 2015, which makes sense considering Microsoft now heavily uses Git and needs his services. That trend started in 2013 actually, with TFS. Since then, Microsoft manages the largest Git repository on the planet! And, since Oct. 2018, Microsoft acquired GitHub.

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

过了一段时间,其他开发人员(我在看着你,Andreas!) ;-))决定允许-preserve-merges到是一个好主意 与——interactive(注意!)和Git维护器结合使用 (嗯,是指Junio不在期间的临时Git维护者) 同意,这就是——保存——设计的魅力所在 开始迅速而平淡地崩溃。

乔纳森正在谈论来自Suse的Andreas Schwab。 你可以看到他们在2012年的一些讨论。

The reason? In --preserve-merges mode, the parents of a merge commit (or for that matter, of any commit) were not stated explicitly, but were implied by the commit name passed to the pick command. This made it impossible, for example, to reorder commits. Not to mention to move commits between branches or, deity forbid, to split topic branches into two. Alas, these shortcomings also prevented that mode (whose original purpose was to serve Git for Windows' needs, with the additional hope that it may be useful to others, too) from serving Git for Windows' needs. Five years later, when it became really untenable to have one unwieldy, big hodge-podge patch series of partly related, partly unrelated patches in Git for Windows that was rebased onto core Git's tags from time to time (earning the undeserved wrath of the developer of the ill-fated git-remote-hg series that first obsoleted Git for Windows' competing approach, only to be abandoned without maintainer later) was really untenable, the "Git garden shears" were born: a script, piggy-backing on top of the interactive rebase, that would first determine the branch topology of the patches to be rebased, create a pseudo todo list for further editing, transform the result into a real todo list (making heavy use of the exec command to "implement" the missing todo list commands) and finally recreate the patch series on top of the new base commit.

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

That was in 2013. And it took about three weeks to come up with the design and implement it as an out-of-tree script. Needless to say, the implementation needed quite a few years to stabilize, all the while the design itself proved itself sound. With this patch, the goodness of the Git garden shears comes to git rebase -i itself. Passing the --rebase-merges option will generate a todo list that can be understood readily, and where it is obvious how to reorder commits. New branches can be introduced by inserting label commands and calling merge <label>. And once this mode will have become stable and universally accepted, we can deprecate the design mistake that was --preserve-merges.


Git 2.19(2018年第三季度)改进了新的——rebase-merge选项,使其与——exec一起工作。

"git rebase- rebase-merges"的"——exec"选项放置了exec 命令在错误的地方,这已被纠正。

参见Johannes Schindelin (dscho)的commit 1ace63b(2018年8月09日)和commit f0880f7(2018年8月06日)。 (由Junio C Hamano - gitster -在commit 750eb11, 2018年8月20日合并)

Rebase——exec:让它与——Rebase -merges一起工作

The idea of --exec is to append an exec call after each pick. Since the introduction of fixup!/squash! commits, this idea was extended to apply to "pick, possibly followed by a fixup/squash chain", i.e. an exec would not be inserted between a pick and any of its corresponding fixup or squash lines. The current implementation uses a dirty trick to achieve that: it assumes that there are only pick/fixup/squash commands, and then inserts the exec lines before any pick but the first, and appends a final one. With the todo lists generated by git rebase --rebase-merges, this simple implementation shows its problems: it produces the exact wrong thing when there are label, reset and merge commands. Let's change the implementation to do exactly what we want: look for pick lines, skip any fixup/squash chains, and then insert the exec line. Lather, rinse, repeat. Note: we take pains to insert before comment lines whenever possible, as empty commits are represented by commented-out pick lines (and we want to insert a preceding pick's exec line before such a line, not afterward). While at it, also add exec lines after merge commands, because they are similar in spirit to pick commands: they add new commits.


Git 2.22 (Q2 2019)修复了使用refs/重写/层次结构来存储rebase中间状态的问题,这固有地使层次结构每 worktree。

参见nguyfrecn Thái ngibmc Duy (pclouds)的commit b9317d5, commit 90d31ff, commit 09e6564 (07 Mar 2019)。 (由Junio C Hamano - gitster -在commit 917f2cd中合并,2019年4月9日)

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

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

更新Add_per_worktree_entries_to_dir()以确保引用列表 查看每个工作树的引用/重写/而不是每个repo。 Common_list[]被更新,以便git_path()返回正确的 的位置。这包括"rev-parse——git-path"。 这一团糟是我造成的。 我开始尝试通过引入refs/工作树来修复它,其中所有的引用都将是每个工作树,没有特殊的处理。 不幸的是,refs/重写出现在refs/工作树之前,所以这是我们所能做的。


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

参见以利亚·纽伦(Newren)提交476998d(2019年9月04日)。 参见Johannes Schindelin (dscho)的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 2e7bac, commit 6180b20, commit d5b581f(2019年7月31日)。 (由Junio C Hamano - gitster -在提交917a319中合并,2019年9月18日)


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

参见commit f45f88b, commit c72fc40, commit 8a64881, commit 7cb8c92, commit e536b1f(2019年10月21日)by SZEDER Gábor (SZEDER)。 (由Junio C Hamano合并- gitster -在commit db806d7, 2019年11月10日)

Path.c:不调用trie_find()中没有值的match函数 署名:SZEDER Gábor

'logs/refs' is not a working tree-specific path, but since commit b9317d55a3 (Make sure refs/rewritten/ is per-worktree, 2019-03-07, v2.22.0-rc0) 'git rev-parse --git-path' has been returning a bogus path if a trailing '/' is present: $ 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/ We use a trie data structure to efficiently decide whether a path belongs to the common dir or is working tree-specific. As it happens b9317d55a3 triggered a bug that is as old as the trie implementation itself, added in 4e09cf2acf ("path: optimize common dir checking", 2015-08-31, Git v2.7.0-rc0 -- merge listed in batch #2). According to the comment describing trie_find(), it should only call the given match function 'fn' for a "/-or-\0-terminated prefix of the key for which the trie contains a value". This is not true: there are three places where trie_find() calls the match function, but one of them is missing the check for value's existence. b9317d55a3 added two new keys to the trie: 'logs/refs/rewritten', and 'logs/refs/worktree', next to the already existing 'logs/refs/bisect'. This resulted in a trie node with the path 'logs/refs/', which didn't exist before, and which doesn't have a value attached. A query for 'logs/refs/' finds this node and then hits that one callsite of the match function which doesn't check for the value's existence, and thus invokes the match function with NULL as value. When the match function check_common() is invoked with a NULL value, it returns 0, which indicates that the queried path doesn't belong to the common directory, ultimately resulting the bogus path shown above. Add the missing condition to trie_find() so it will never invoke the match function with a non-existing value. check_common() will then no longer have to check that it got a non-NULL value, so remove that condition. I believe that there are no other paths that could cause similar bogus output. AFAICT the only other key resulting in the match function being called with a NULL value is 'co' (because of the keys 'common' and 'config'). However, as they are not in a directory that belongs to the common directory the resulting working tree-specific path is expected.


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

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

Sequencer:修复do_reset()中的内存泄漏 署名:Ævar Arnfjörð Bjarmason

修复9055e40中引入的内存泄漏(“sequencer:引入新的命令来重置修订版”,2018-04-25,Git v2.18.0-rc0—merge列在批处理#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