繁体   English   中英

如何使用 git rebase -i --rebase-merges 命令压缩合并提交和正常提交

[英]How to squash merge commits and normal commits with git rebase -i --rebase-merges command

我正在与一个相对较大的主要分支合作。 我创建功能分支并将它们合并到主分支中。 大多数这样的功能分支都是孤儿分支。 有时,我将一个上层主分支合并到一个下层主分支(PROD 到 UAT)以将它们对齐在一起。

在执行git pull时,有时我会忘记使用--rebase选项,最终会看到烦人的合并提交。 我想删除它们。

请参阅此命令 TODO 的附加快照:

git rebase -i -rebase-merges HEAD~4

我有以下问题:

  • 为什么当我使用此命令而不是git rebase -i -rebase-merges HEAD~3时,TODO 文件变小了,文件中只显示了 3 或 4 个项目?
  • 不确定我到底做了什么,当我试图压缩一个在合并提交之前的正常提交时,合并提交消失了,并且合并的功能分支也从历史记录中删除。 你能解释发生了什么吗? 我记得这个过程停止了,我必须解决一些冲突,然后提交,然后继续 rebase。
  • 如何将指示的合并提交压缩为一个正常提交或合并提交?
  • 如何将指示的合并提交压缩到以前的正常提交中?
  • 我想更多地了解以resetlabel开头的行。 您能否提供一些详细信息或指向我的链接以进行更多阅读?

我感谢您的帮助。

在此处输入图像描述

git pull默认设置--rebase

git config --global pull.rebase true

要查看您的历史记录:使用git log --graph --oneline

你可以比较:

git log --graph --oneline HEAD~3..HEAD
# and
git log --graph --oneline HEAD~4..HEAD

(这些是您的git rebase命令选择的提交)

第 1 部分(此处链接到第 2 部分

您不能压缩合并提交。 这有一个简单的原因:根据定义,合并提交是具有两个父级的提交。 将两个提交压缩在一起意味着您想用一个普通提交替换两个普通(即单父)提交。 由于合并提交不是普通的提交,它根本不符合压缩的条件。

作为一个相当特殊的情况,有可能——但通常是一个坏主意——将一个普通的提交压缩成一个合并提交。 这会产生所谓的邪恶合并:请参阅git 中的邪恶合并?

为什么当我使用此命令而不是git rebase -i -rebase-merges HEAD~3时,TODO 文件变小了,文件中只显示了 3 或 4 个项目?

Rebase 是关于将提交复制到新的和改进的(或至少,可能是改进的)提交。 复制了这些提交后,您然后指示您的 Git 软件停止使用原件并开始使用副本。

这种方式只能复制普通的提交。 但是,有一个新的--rebase-merges选项(旁注:这里需要双连字符;您可以使用-r作为同义词以避免键入双连字符),首先在 Git 2.22 中提供,与后续版本中的大量修复和改进。 这告诉 Git重新执行指定的合并。 摆脱合并,您需要完全避免执行它们。 这需要详细了解变基的工作原理。

您在此处使用的参数HEAD~3HEAD~4指定不复制的提交 如果没有此信息, Git 将不得不假设您的意思是复制每个可访问的提交git rev-list --count HEAD会告诉您有多少提交,但可能是数百或数千)。 除非您使用--root告诉 Git 它应该复制每个可访问的提交,否则此参数是必需的(通常是一个坏主意,这就是为什么rebase --root多年来不在 Git 中,直到 2012 年发布 1.7.12) .

理解提交

让所有这些东西进入你的脑海是一个相当大的承诺(如果我可以在这里使用提交这个词)。 尽管如此,重要的是要做到这一点。 请记住,存储库本身主要是一个装满提交的大盒子,因此了解每个提交是什么以及它们如何单独一起工作非常重要。 更准确地说,任何 Git 存储库的两个基本组件是一个对象数据库,它保存提交对象和其他支持对象,以及一个名称数据库,它保存分支和标签以及其他名称。

我们从对象开始,尤其是提交。 (其他三种 object 类型——tree、blob 和 annotated-tag——在你面前并不像提交那样:它们大多只是工作,你不必知道细节,你做提交的方式)。 每个 Git object 都有编号,带有一个大而丑陋的随机外观hash ID ,或者更正式的ZA8ZFDE6331CD59EB6666 ID89ID1 这些对象 ID 通常可以缩写为,例如,出现在图像中的4d3abd9 ,但实际上每个对象的长度为 40 个字符(20 个字节或 160 位),至少现在是这样。 (Git 的未来版本将有朝一日拥有 256 位 = 64 个字符的 OID。)

特别是对于提交,在您(或任何人)进行提交时,每个提交都会获得一个唯一的hash ID。 一个 hash ID 现在在每个Git 存储库中永久保留,即使是尚不存在的存储库,也意味着特定的 commit 这实际上不可能永远有效,总有一天 Git 会崩溃,但是 hash ID 的绝对大小旨在将那一天放到我们不关心的未来(数十亿年或更长时间)。 为了使这项工作和它一样好——在实践中,这很好,即使它在理论上是垃圾——任何 object 的任何部分都不能更改 Git 确保其他尚未看到您的新提交并且没有您的存储库的通信链接的其他 Git 已经保留了 hash ID 的技巧是使用提交内容的加密校验和,并且该技巧仅适用如果内容无法更改。 1目前是 SHA-1 hash。

所以:提交是一个编号实体,在 Git 的对象数据库中找到——一个简单的键值存储,以 hash ID 作为键——通过查找其 hash ID。 Git迫切需要hash ID 来查找提交。 没有那个hash ID,Git就束手无策了。 这就是为什么你一直看到他们。

但是提交有什么? 提交有什么用? 答案很简单,有两个方面:

  • 提交存储(间接)每个文件的完整快照 更准确地说,它存储了 Git 被告知要存储的每个文件的完整快照,当时您或任何人进行了提交。 与所有对象一样,此快照是完全只读的。 出于多种目的,文件以一种特殊的形式存储,其中的内容经过压缩和重复数据删除 重复数据删除处理了一个明显的反对意见,即如果每次提交每次都存储每个文件,存储库将变得非常臃肿。 只要大多数提交主要重复使用来自一个或多个先前提交的大部分文件,这些文件实际上根本不占用空间,因为它们被重复删除了。

  • 提交还(直接)存储一些元数据,或有关提交本身的信息。 这是提交中唯一必须占用一些空间的部分,而且它非常小:例如,它包含提交人的姓名和 email 地址,以及一些日期和时间戳以及日志消息. 如果您的日志消息不是很长,未压缩的提交可能不超过几百字节(然后它也会被压缩)。

对于 Git 本身至关重要,任何给定提交的元数据存储 hash ID 列表。 这个列表通常只有一个条目长,这使得这个提交成为一个普通的提交,只有一个父提交。 存储提交中的 hash ID 在其元数据中,是此提交的级的 hash ID,即在此之前的提交。

任何非空存储库中的至少一个提交是特殊的,因为它没有父提交:它是第一个提交,因此它是根提交而不是普通提交。 它仍然有一个完整的快照,就像任何提交一样。 可以使用git checkout --orphangit switch --orphan进行额外的根提交,但这通常是个坏主意 (您提到您正在使用“孤儿分支”,这通常是一个坏主意,我们将看到。)

一些提交有多个父级,这使得它们合并提交 合并提交仍然只有一个快照,就像任何其他提交一样。 大多数合并提交恰好有两个父级——一些版本控制系统需要这个(例如,Mercurial),但由于 Git 有一个父级列表,因此 Git 允许任何 integer ≥ 2 。 多父合并不会做双父合并不能做的任何事情——事实上,它有点相反:双父合并可以做 3+-父合并不能做的事情。 因此它们可用于将多个功能捆绑在一起。 不过,在我自己看来,它们主要是为了炫耀。


1细心的学生或任何了解 Mercurial 的人都可以立即注意到,如果内容中有一些不变的部分,它就可以工作:如果我们有一些未包含在校验和中的可更改部分,我们很好。 不过,Git 并没有费心去实现这个,理论上任何这样的内容都可以放在其他地方(这总是正确的,但并不总是有效的,但正如 Kilgore Trout 所说,所以它会这样)。


将提交与较早的提交相关联:链中的链接

现在让我们暂停并绘制一个普通的提交。 我们不知道(也不想知道)它的实际 hash ID; 让我们称它为“哈希”的H

            <-H

H中伸出的这个小箭头是什么? 表示存储在提交元数据中的 hash ID。 此 hash ID 允许 Git 从对象数据库中检索H级。 让我们绘制父级:

        <-G <-H

我们说H指向它的父G 但是G也有一个箭头,因为像H一样, G是一个普通的提交,所以G向后指向它的父级:

... <-F <-G <-H

这会永远持续下去——或者更确切地说,直到我们到达一个没有箭头的提交(可能根提交)。 这就是我们和 Git 停止的地方和 rest。

这里有一个问题,就是我们必须告诉 Git 提交H的 hash ID。 没有人想记住 hash ID 并输入它们,所以还有一个箭头,我几乎总是会画它。 我对提交回父级的那些感到懒惰:

...--F--G--H   <--

稍后我将展示最后一个箭头右侧的内容。 现在,让我们观察一下:命名提交H隐含地命名导致并包括提交H的每个提交。 更准确地说,Git 有两种查找提交的方法:“with history”,这意味着也拖入以前的提交,或者“没有历史”,这意味着我们应该按照某个箭头一次来找到一个提交。 当我们使用“带有历史记录”的各种查找方式时,这会拖入大量提交,通过向后箭头直到我们用完提交(通过到达根)或到达我们被告知停止的地方。

分支名称找到最后一次提交:分支如何增长

现在我们填写最后一个箭头右侧的部分:

...--G--H   <-- main

右边是分支名称,或者在许多情况下,分支名称和其他名称也是如此 我们可以有多个名称来选择提交H ,如下所示:

...--G--H   <-- feature, main

每当一个分支名称指向任何一个特定的提交时——就像在这种情况下,名称featuremain节点都指向H我们说这个特定的提交是那个分支的提示提交 所以 commit H两个分支的尖端featuremain

当我们想使用 Git 时,我们将检查(或git switch到)这些分支名称之一。 我们一次只能使用一个分支名称, 2 个原因我不会在这里介绍,因为这已经太长了,但是我们将通过附加特殊名称HEAD并将其附加到其中一个分支名称来绘制它:

...--G--H   <-- feature, main (HEAD)

在这里,我们使用提交H是因为我们通过名称main选择了一个没有历史记录的提交。 如果我们运行git switch featuregit checkout feature ——它们做同样的事情——我们得到:

...--G--H   <-- feature (HEAD), main

现在我们正在使用提交H ——也就是说,我们根本没有改变我们正在使用的提交的任何内容——但我们是通过名称feature这样做的。

如果我们现在进行一个新的提交,我们的提交将:

  • 保存每个文件的完整快照(去重),因为这些文件出现在 Git 的索引/暂存区域(我们不会在这里介绍);
  • 让 Git 将我们的提交消息和其他元数据放在一起以进行新的提交,这将获得一个新的、唯一的、从未使用过、永远不会再次使用的 hash ID,我们将称之为I

这个新提交I必须将提交H作为其父级,因为这是我们在进行新提交使用的提交I 所以I会回到H

...--G--H
         \
          I

但是分支名称和它们的箭头呢? 好吧,名称main包含现有提交H的原始 hash ID,并且没有改变。 但是我们分支feature上,所以feature git commit的最后一步是写入新的 hash ID - 原始 hash ID 用于提交指向I - 而不是I feature名称H

...--G--H   <-- main
         \
          I   <-- feature (HEAD)

特殊名称HEAD仍附加到feature ,但feature现在指向I作为其提示提交。 通过H提交仍然在两个分支上,但提交I目前仅在feature


2从 Git 2.5 开始,您可以使用git worktree add添加额外的工作树:每个可以在一个分支上。 由于各种原因,每个添加的工作树必须与所有其他工作树位于不同的分支上,但这是处理多个分支的非常好的方法,例如,您需要修复一个高优先级的错误由于您正在使用的某些功能而失去动力。


提交不能改变,但分支名称可以并且可以做

请注意,我们在这里对提交所做的唯一事情就是添加新的。 这就是我们所能做的一切:我们不能删除现有的提交,也不能更改现有的提交。 如果我们不喜欢某个提交,我们所能做的就是添加另一个提交。 这就是提交的本质:这就是 Git 可以工作的方式。 提交永远不会改变; 一旦您将一个给其他 Git 存储库,该存储库就有提交,在该 hash ID下,宇宙中的所有 Git 软件都同意并且现在他们也拥有该提交

然而,分支名称可以并且确实一直在移动。 分支名称,它只是存储在存储库名称数据库中的一种特殊名称——分支名称只是一个以refs/heads/为前缀的字符串,这就是它成为分支名称的原因——它只存储一个 hash ID ,就像这样此名称数据库中的所有名称。 无论 hash ID 在该分支名称中是什么,都是该分支上的最后一次提交。 这是 Git 中的字面定义。

所以,如果我们有:

...--G--H   <-- main
         \
          I   <-- feature (HEAD)

在我们的存储库中,我们强制 Git 将名称main移动到G而不是H ,我们得到:

...--G   <-- main
      \
       H--I   <-- feature (HEAD)

现在,通过并包括G的提交都在两个分支上。 提交Gmain的提示提交。 提交HI在分支feature上。 提交本身没有任何变化。 分支名称实际上并不重要,只是我们使用它们来查找每个分支的最后一次提交

如果我们将名称main向前而不是向后移动,我们得到:

...--G--H--I   <-- feature (HEAD), main

现在所有提交都在两个分支上。 包含任何给定提交的分支集随时间变化。 提交本身不会改变,但是我们可以找到提交的名称改变。

如果我们愿意,我们可以强制 name feature返回一跳,将main留在H

...--G--H   <-- feature (HEAD), main
         \
          I   ???

提交I仍保留在存储库中,但除非您记住了它的 hash ID(或者可以通过某种方式找到它),否则您将无法再找到 请记住,Git需要hash ID 才能在其对象数据库中查找某些内容。 没有从HI向前箭头,只有从IH的向后箭头,从HG的向后箭头,依此类推。 所有 Git 操作都要求我们知道我们在哪里结束; Git 反向工作,所以我们从结尾开始。 (最后一个应该是第一个,也许?)

但是我们可以将提交复制到新的和改进的提交

假设我们和以前一样:

...--G--H   <-- main
         \
          I   <-- feature (HEAD)

我们发现提交I有一些不好的地方。 也许文件有错字。 也许提交消息有错字。 也许两者都是真的。 无论如何,我们修复任何文件的任何内容并git add它们以便我们可以重做提交,然后我们运行:

git commit --amend

这不会改变现有的提交I :它实际上不能。 相反,它做出了一个新的改进的提交,很像I ——如果文件没有任何问题,它可能有相同的快照——元数据略有不同(也许我们已经修复了提交消息中的错字;在任何情况下,提交者时间戳都是“现在”,并且自从我们制作I以来时间已经过去了)。 所以我们得到了我们的新提交I' ,它很像I ,并且 Git 将I'的 hash ID 推到名称feature中。 --amend所做的是使git commit存储,作为I'的父级, I拥有的父级,即提交H的 hash ID,而不是使用I本身作为父级。 所以现在我们有了这个:

          I'  <-- feature (HEAD)
         /
...--G--H   <-- main
         \
          I   ???

提交I似乎已经消失了。 如果我们对提交 hash ID 不保持警惕,则 commit I'似乎commit I ,好像 commit I已经改变了。 但它没有:它仍然在那里,就像以前一样。 我们只是找不到它,这就是我们想要的:commit I不再有用; I'是新的和改进的I

分支和合并

假设我们从一个包含几个提交的存储库开始,以H结尾,只有一个分支名称main

...--G--H   <-- main (HEAD)

我们现在创建两个新的分支名称br1br2 ,它们也都指向提交H

...--G--H   <-- br1, br2, main (HEAD)

我们切换到br1并以通常的方式创建两个新的提交:

          I--J   <-- br1 (HEAD)
         /
...--G--H   <-- br2, main

然后我们切换到br2并再提交两次:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2 (HEAD)

我们现在准备好使用git merge合并提交JL 请注意,我们不会在任何真正的技术意义上合并分支:我们合并提交。 这次合并的目的是合并工作。 我们想要我们“在” br1上所做的工作,以及我们“在” br2上所做的工作。 例如,我们将git switch br1然后git merge br2

我们将在此处直接跳到机制,以节省此答案的空间: Git 实现合并的方式是区分(如git diff 针对两个分支提示提交中的每一个提交的合并基础提交,以便 Z0BCC630105ADE279506B弄清楚做了什么工作 这两个分支明显的共同起点是提交H 它有一个指向它的名称( main ),但这并不重要,实际上这个名称有点碍事,所以我将停止绘制它。所以 Git 运行,实际上:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Git 然后结合差异:我们对某个文件做了什么,我们想再做一次; 他们对其他文件做了什么,我们想再做一次; 如果我们都接触了同一个文件,我们希望同时进行这两项更改。 我们对从提交H获取的快照进行了这些更改,这是两个git diff命令左侧的快照。

这会产生一个新的快照,准备好提交,或者可能产生一些合并冲突,使 Git 停止并让我们在合并完成之前修复它们。 假设一切顺利并且 Git 确实自行完成,但 Git 现在进行合并提交,这只不过是与两个父母的提交:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

请注意合并提交M如何向后指向JL 像往常一样进行新提交时, Git 已将新提交的 hash ID 填充到当前分支名称中 - 附加了HEAD的分支名称 - 所以现在br1指向提交M 如果我们愿意,现在我们可以安全地删除名称br2 ,因为 Git 通过向后走所有路径来找到提交,所以当我们 select 提交M和历史记录时,我们会得到所有提交。

这是真正合并的本质。 Git通过合并工作并将合并的工作应用于合并基础来制作内容 Git 通过从两个提示提交(沿着所有路径)向后工作来找到合并基础。但在我们的例子中,它很容易,因为只有简单的直线向后路径)直到找到一些共享提交,从技术上讲, Git 在这里使用最低公共祖先算法来查找提交H ,但对于这种情况,它是显而易见的。

构建完内容后,如果可能,Git 现在制作新快照并制作元数据,其中两个父项的列表首先显示当时的当前提交J ,另一个提交L第二。 这里的顺序通常无关紧要,但是当它这样做时, --first-parent让您可以指示 Git 忽略除第一个父项之外的所有内容。

通过git cherry-pick复制提交

在 Git 中工作时相对经常发生的事情是,我们发现了一个错误,它影响了我们的功能分支,但也影响了我们开始的更主要的分支。 也就是说,也许我们有这个:

...--X--o--*--o--P   <-- main
            \
             F--G--H   <-- feature (HEAD)

我们现在发现了一个错误,并意识到它是在提交X中重新生成的,因此该错误存在于main分支feature分支上。

有很多修复它的策略(包括一些不涉及cherry-pick的策略),但人们现在将修复这个错误作为main分支上的紧急情况是很常见的。 他们添加了一个新的提交C来修复错误:

...--X--o--*--o--P--C   <-- main
            \
             F--G--H   <-- feature (HEAD)

我们也想要修复,所以我们想添加一个很像C的新提交。 我们可以复制他们的工作并进行新的提交I ,但是如果我们可以从字面上复制C更改不是很好吗? 我们可以:

git cherry-pick main

告诉 Git 查找提交C (通过名称main找到), go 回到其父P的一跳,并将相同的更改应用于我们的提交H上的更改

从技术上讲,Git 使用与git merge相同的合并代码执行此操作,除了明显的合并基础 (commit * ) 之外,它强制合并基础是提交P ,提交C的父提交. 这样做的原因是它产生了正确的答案(如果你仔细想想,或者仔细研究所有的细节,你会发现它确实产生了正确的答案),但现在我们只是把它想象成“复制该工作”并假设它有效。 这给了我们一个新的提交C' ,我们称之为C'因为它非常像C Git 甚至为我们复制了提交消息(尽管我们可以通过添加--edit来更改它):

...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'  <-- feature (HEAD)

我们真正需要记住的是,cherry-pick 有效地将提交复制到新的和改进的提交。 这种情况下的改进是新提交继续我们的feature分支,并使用提交H的快照,该快照被从P更改为C的任何内容修改。

简单的变基案例

我们现在(终于!)准备好考虑一个简单的git rebase 假设我们已经完成了我们的功能,包括樱桃挑选:

...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'--I--J   <-- feature (HEAD)

在这一点上,我们只有一个遗憾:我们希望我们在提交C开始我们的功能,以便我们从*P之后的未命名oC本身获得所有好处。 所以我们运行:

git rebase main

这里的参数main选择提交C和 history ,因此这意味着C的每个提交都在 backs 上 这不包括我们在 commit *之后的任何提交,因为提交之间的箭头都指向backs ,并且我们的功能的第一部分 commit F是从*向前的。

如果您愿意,此处选择的提交暂时“涂成红色”:红色表示停止,请勿触摸 现在 Git 开始在我们自己的提交中添加一些临时的绿色“油漆”,从feature的尖端向后工作。 这列出了JIC'HGF的 hash ID。 *向后移动时,Git 击中“红色油漆”并停止向后移动。

Git 现在清除所有临时绘制(这个“绘制”实际上只是通过从对象数据库中读取提交而获得的一些核心数据中的一些位 - 这只是一种心理 model 如何变基工作的方式)并反转列表hash ID,以便它们按 Git 复制时需要的顺序: F , G , ..., J

现在有一个 rebase 使用的特殊技巧:它查看从C*的提交,看看它们中的任何一个是否与上面列表中的任何提交“做同样的事情”。 通常设法挑选出CC'作为彼此的副本。 使用这个技巧,Git从我们自己的列表中丢弃C' 该列表现在为FGHIJ

现在,使用名称main到 select 一个没有历史的提交,Git 切换到该提交。 当然,这是提交C Git 使用 Git 调用的分离 HEAD模式进行此操作,但如果您愿意,可以将其视为使用临时分支。 不过,我将在这里用文字分离的 HEAD 来绘制它:

...--X--o--*--o--P--C   <-- main, HEAD
            \
             F--G--H--C'--I--J   <-- feature

Git 现在开始使用cherry-pick ,一次提交一个。 首先它将F复制到一个新的和改进F' 改进之处在于我们从C快照开始,当 Git 通过挑选樱桃进行新提交时,我们的新级是C 所以我们的新提交是这样进行的:

                      F'  <-- HEAD
                     /
...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'--I--J   <-- feature

一旦F'存在, Git 复制G ,再次使用git cherry-pick 这会产生一个新的G'

                      F'-G'  <-- HEAD
                     /
...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'--I--J   <-- feature

我们重复每个提交,除了C' ,它被从列表中剔除:

                      F'-G'-H'-I'-J'  <-- HEAD
                     /
...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'--I--J   <-- feature

复制阶段已完成,因此 Git 现在强制名称feature移动到我们现在所在的位置 - 提交J' - 然后重新附加HEAD ,以便我们回到正常工作模式:

                      F'-G'-H'-I'-J'  <-- feature (HEAD)
                     /
...--X--o--*--o--P--C   <-- main
            \
             F--G--H--C'--I--J   [abandoned]

交互式变基

git rebase的交互模式将每个git cherry-pick操作变成一个单独的pick命令。 例如,这使您可以打乱提交的顺序和/或使其中一些读取fixupsquash 在遇到后者之一时,Git 有效地使用git commit --amend使其看起来像 Git 在变基期间合并了两个提交。 3

除了这个特殊的功能——让你重新排列和重新组织提交(以及相当多的额外工作,甚至将一个提交“拆分”成两个或多个单独的提交),这个交互式 rebase 还提供了新的--rebase-merges模式。 这做了一些相当激进的事情。


3这意味着你会得到很多无意义的临时提交: git fsck会找到它们并告诉你悬空提交,这是完全正常的。 他们没有得到git push ,而您自己的 Git最终可能会在适当的时间间隔后删除它们,以确保您不介意。 由于篇幅原因,我不打算对此进行介绍,但git gc是这里的清理对象。 新奇的git maintenance系列命令最终将包含git gc Noteworthy: GitHub do not use git gc : "dangling" leftovers in a GitHub repository remain there forever, for internal implementation reasons, unless you get GitHub support to do it for you.

第2部分

第 2 部分(此处链接到第 1 部分

使用合并进行变基,带或不带--rebase-merges

假设不是简单的:

...--o--*--P  <-- main
         \
          F--G--H   <-- feature (HEAD)

在开始时设置样式分支,我们有这个:

...--o--*--P  <-- main
         \
          \         I--J
           \       /    \
            F--G--H      M--N   <-- feature (HEAD)
                   \    /
                    K--L

也就是说,我们的feature分支中发生一些分支和合并的事情。 我们现在想将feature重新定位到提交P上,就好像我们从那里开始工作而不是在提交*上一样。

一个普通的git rebase main将:

  • P*涂成红色;
  • NMJ - 和 - LI - 和 - KHGF涂成绿色

这些将是我们要复制的提交。 但是常规的 rebase 会故意完全放弃M这样的合并提交。 原因是git cherry-pick字面上不能复制像M这样的合并提交。 这种“删除合并”的发生方式与“删除作为副本的提交”相同(除了它在内部更容易)。 我们最终得到:

             F'-G'-H'-I'-J'-K'-L'-N'   <-- feature (HEAD)
            /
...--o--*--P  <-- main
         \
          \         I--J
           \       /    \
            F--G--H      M--N   [abandoned]
                   \    /
                    K--L

或者,也许IJKL顺序被调换,所以我们有F'-G'-H'-K'-L'-I'-J'-N'

N'中的最终快照与N中的最终快照一样好,原因是合并提交M只是结合了IJKL的工作。 当我们使用cherry-pick 来复制每个提交时,一次一个,然后将合并扁平化,我们仍然得到相同的更改 * M不是邪恶合并这一事实意味着省略它是安全的。

如果M是一个邪恶的合并,这显然会中断。 所以也许邪恶的合并是坏的! 这就是为什么我在上面说将普通提交压缩到合并中是可能的,但通常是个坏主意 结果是一个邪恶的合并,它被这种git rebase丢弃。

--rebase-merges怎么样? 好吧,在这种情况下,你会得到一个更复杂的TODO工作表:

我想更多地了解以resetlabel开头的行。 您能否提供一些详细信息或指向我的链接以进行更多阅读?

git rebase需要在这里做的是重新创建合并提交M cherry-pick 命令仍然无法复制它,因此 Git 必须重新执行合并,而不是复制它。

说明书现在会说:

  • 复制提交FGH :三个pick命令
  • 保存提交H'的 hash ID:一个label
  • 现在复制提交IJ ,并保存提交J'的 hash ID,或者现在复制KL并保存L' :再一个label
  • 使用第一个 label 切换回提交H
  • 复制我们尚未复制的其他两个提交
  • 还有一些东西,但让我们在这一点上停下来画出我们所拥有的东西。

在我们的复制提交到新的和改进的提交过程中,我们有这个:

                       I'-J'  [labeled and possibly HEAD]
                      /
               F'-G'-H'  [labeled]
              /       \
             /         K'-L'  [labeled and possibly HEAD]
            /
...--o--*--P  <-- main
         \
          \         I--J
           \       /    \
            F--G--H      M--N   <-- feature
                   \    /
                    K--L

我们必须label提交H'以便我们可以reset它。 我们必须label至少提交J'L'之一,以便我们可以再次找到它 - 以不是HEAD为准。 label 两者都更容易,而且无论如何都有理由这样做,正如我们即将看到的那样。

我们现在准备制作M' ,我们的M的“副本”。 我们根本不能使用cherry-pick; 我们必须运行git merge 我们希望它重用来自提交M提交消息,所以我们得到一个merge -C <hash> <label> ,其中 hash ID 是原始合并的 ID,而 label 是我们要合并的提交由于两个提交的顺序很重要,如果我们在L'上,我们可能需要从reset开始以确保HEAD现在选择提交J' ' 。 如果我们这样做,我们需要一个 label来将J'重置为。 在我们生成TODO列表时,我们不知道J'的 hash ID 将是什么。

无论如何,我们现在重置为适当的第一个提交,然后git merge第二个提交,生成M' 然后我们准备好挑选N' ,这是在 rebase 可以通过移动名称feature自行完成之前的最后一个操作:

                       I'-J'
                      /    \
               F'-G'-H'     M'-N'  <-- feature (HEAD)
              /       \    /
             /         K'-L'
            /
...--o--*--P  <-- main
         \
          \         I--J
           \       /    \
            F--G--H      M--N   [abandoned]
                   \    /
                    K--L

请注意,构建提交Mgit merge执行新的合并 同样,这是安全的,因为——或者更准确地说,如果——提交M不是邪恶的合并。 所以--rebase-merges并没有使我们免于进行邪恶合并的错误。

回到你的第一个问题

我们现在终于也可以回答这个问题了:

为什么当我使用此命令代替git rebase -i --rebase-merges HEAD~3时,TODO 文件变得更小,文件中只显示了 3 或 4 个项目?

红绿绘制技巧确定要复制哪些提交。 这不依赖于--rebase-merges标志,正如我们在上面看到的那样,它只是选择 Git 是否应该重新执行合并,或者将它们展平

听起来你真的想平展你的合并。 为此,请不要使用--rebase-merges 但是,请注意,红色/绿色涂料可能会变得棘手。 特别是, git rebase只允许你选择一个提交到 select-with-history,从那里应用“红色油漆”。

当您运行git rebase时,您将始终将“绿色油漆”应用于HEAD提交,以及由于“带有历史记录”样式选择而选择的其他提交。 4请记住,提交历史,当我们向后扫描时,您过去所做的git merge会变成一个分支,就像 Git 所做的那样。


4警告:如果您运行git rebase --onto XYZgit rebase YZ ,则操作以HEAD git switch Z开始,然后提交 完全等同运行git switch Z Z然后运行git rebase --onto XYgit rebase Y ,包括当 git rebase

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM