将Git交互式变基中的Squash合并到下一个提交中

20

在 Git 中,我可以使用交互式 rebase 来重写历史记录。这很棒,因为在我的特性分支中,我会使用大量部分工作的代码进行多次提交,以便探索不同的重构和完成方式。

在将分支 rebase 或合并到主分支之前,我希望将许多提交压缩在一起。

按照从上到下的顺序列出了一些虚构的提交。

1. Initial commit on feature branch "Automatic coffee maker UI"
2. Add hot chocolate as product
3. Add tea as product. Products are now generic
4. Create in memory data store for adapter tests
5. Cry because I can't get entity framework to create a composite key. Integration tests broken.
6. Implemented composite key!!
7. All tests green and feature done!

假设我想保留提交3、4和7。
使用rebase,我想要“压缩”提交记录:
- 1和2合并为3。 - 4不变。 - 5和6合并为7。
在交互式rebase中,最好按以下操作:
1. squash
2. squash
3. pick (contains the work of 1 & 2)
4. pick 
5. squash
6. squash
7. pick (contains the work of 5 & 6)

但这是错误的,因为压缩合并会将一个提交与其前一个提交合并。我不知道如何使其向前压缩。
我是否太固执了,应该接受这种方法行不通(我宁愿让它工作),还是有办法实现这一点?
我正在使用以下命令:
git checkout My-feature-branch
git rebase master -i

然后我正在编辑出现的提交列表,并尝试通过保存文件和编辑器来完成它,这通常对我有用。


我不太确定你遇到了什么问题。squash 命令会完全符合你的要求;将 1 和 2 合并为 3,将 5 和 6 合并为 7。剩下的将是(表面上)提交记录 3、4 和 7。你是否在使用这些命令时遇到了问题?请注意,如果你有一个可以重置回去的远程存储库,那么你可以在本地存储库上尝试这些命令。 - Makoto
我收到了一条消息“错误:无法在没有先前提交的情况下进行'压缩'”,这让我感到困惑,并让我认为#1试图将其压缩到#0,但#0并不存在,而不是#2。 - Josh R
你是如何调用它的?请展示一下那个命令。 - Makoto
1
@Makoto,你错了,正好相反,就像Josh在他的问题中已经说过的那样。;-) 另外,由于默认情况下存在reflog,你总是可以轻松地撤消rebase所做的操作,而无需任何远程仓库的存在。;-) - Vampire
很棒的问题。我正需要这个东西,因为重新排序提交会导致冲突。@Vampire的答案是一个很好的解决方案,不会引起任何额外的工作。 - Filip Kubicz
6个回答

7
在交互式 rebase 过程中,确实可以将一个 commit 压缩到其 后面 的 commit 中,并且完全保留第二个 commit 的身份(包括作者、日期等)。
但这种方法有些复杂,因此仍然希望 git rebase -i 可以原生支持此功能。
我将演示如何将 A 折叠到 B 中并保留 B 的身份,假设有三个提交记录 aaaaaaa Abbbbbbb Bccccccc C:(该方法可轻松推广至更多提交记录)
  • git rebase -i aaaaaaa^
  • Modify the edit script to stop after commit B and exit the editor:
    pick aaaaaaa A
    edit bbbbbbb B
    pick ccccccc C
    
  • When rebase has stopped after B, revert the latest commit twice, then continue:
    git revert HEAD
    git revert HEAD
    git rebase --continue
    
    Because reverting the revert restores the previous state, all following commits will apply cleanly. The sequence of B and Revert "B" together has no effect, but the first of those commits carries the full identity of commit B (in fact, at this stage it still is commit B).
    B and Revert "B" could be squashed together to form a no-op commit carrying the identity of commit B forward with a separate rebase -i --keep-empty aaaaaaa^. This is very useful and recommended to prevent bogus merge conflicts, especially in more complex cases. However we'll skip that step here and just keep the commits together.
    The important commit now is Revert "Revert "B"" which will carry all the changes originally made by B.
  • Now rebase -i aaaaaaa^ again to move A past both B and Revert "B", while squashing B into both Revert "B" and A:
    pick   bbbbbbb B
    squash b111111 Revert "B"
    squash a222222 A
    squash b333333 Revert "Revert "B""
    pick   c444444 C
    
    Because the sequence of B and Revert "B" has no effect, you can move any commits past it while creating only trivially resolvable merge conflicts.
  • When rebase stops with a merge conflict, use git mergetool with a tool that can automatically resolve trivial conflicts, and let the tools do just that.
  • Rebase will stop again to let you edit the commit message of the squashed commit. Edit to your taste.
  • git rebase --continue and you are done.

1
这应该是答案。它正确地解决了问题。 - Diogo
我同意,尽管我的观点可能有偏见。不确定我们是否能在四年后说服@JoshR来费心,但我很高兴你发现了我的答案有帮助。 - tera

7

如果可能的话,您还需要重新排序提交,以便要保留的提交在要合并的提交之前。(另一种选择请参见答案结尾处的更新内容)

如果这样做不可行,因为您将得到不想解决的冲突,那就这么做吧。

1. pick
2. squash
3. squash
4. pick 
5. pick
6. squash
7. squash

当提交完成后,您可以编辑提交消息以包含您希望最终提交具有的消息。易如反掌。 :-)

您甚至可以做到

1. pick
2. fixup
3. squash
4. pick 
5. pick
6. fixup
7. squash

我认为只应该在第一次启动提交消息编辑器时,因为使用fixup时,前一个提交消息会被简单地采用而不会启动编辑器。
在合并时,当提交消息编辑器启动时,你会得到两个提交消息,一个是要合并的提交和要合并的提交,所以你可以简单地删除你不想保留的提交消息。

更新:

为了使用第4和第7个的作者日期和提交信息,从而更符合OP最初的要求,您可以使用以下设置:

1. edit
2. fixup
3. fixup
4. pick 
5. edit
6. fixup
7. fixup

然后在第一次中断时使用:

git commit --amend -C master~4 # or any other way to reference commit number 3

然后你继续变基并在第二次中断时使用:
git commit --amend -C master # or any other way to reference commit number 7

然后你继续进行变基操作,完成后就可以了。
或者可以自动化处理:
1. pick
   exec git commit --amend -C master~4 # or any other way to reference commit number 3
2. fixup
3. fixup
4. pick 
5. pick
   exec git commit --amend -C master # or any other way to reference commit number 7
6. fixup
7. fixup

7
它保留了第一次提交的日期 - 我该如何让它使用最后一次提交的日期? - Michael Hewson
@MichaelHewson 我想没有简单的方法,我会手动更改日期。 - luizfls
@MichaelHewson 我在我的回答中加入了一个示例,说明如何轻松地保留3和7的消息和作者日期。 - Vampire

4
Vampire的回答是正确的,但我想提供一个不同的观点。我认为这里你比必要的更紧张:你从以下开始:
“假设我想保留提交3、4和7。”
但接着又加上:
  • 1和2合并到3中。
  • 4保留。
  • 5和6合并到7中。
但这意味着你想要保留(内容的)所有1、2、3、4、5、6和7……只是不作为独立的提交。交互式变基 squash 不意味着“扔掉”,甚至不意味着“扔掉提交消息”,它只是意味着“合并”。如果动词是“合并”、“融合”或“混合”等,则会更好。因此,序列(pick、squash、squash)的含义是:在保留这三个提交的同时按顺序应用它们,然后将它们合成一个大提交。 如已指出的那样,一旦 Git 将它们合并成一个大提交,Git 会再次给您编辑这三个组合提交消息以形成一个大提交消息的机会。
当变基完成时,您没有保留任何原始提交。相反,您已经创建了新的提交。您的第一个新提交由哪些部分组成并不重要,只有最终来源和在一个大提交消息中放入什么是重要的。

3
称呼我要求严格,虽然完全正确,但这并没有回答问题,只是告诉提问者他用错了措辞,不是吗?如果是这样的话,应该在评论中提出,而不是作为一个答案。 - Vampire
@Vampire:是的,可能应该是一条注释。但是注释的限制(空间和格式)使其不太适合。而且,这并不仅仅是“措辞”问题:提交更改ID的想法,因此根本不是原始提交,这一点很重要,因为其他分支可能会通过其原始ID保留原始提交。 - torek
1
@JoshR 如果你放弃了这些提交,那么你也会丢失其中所做的更改。 - Vampire
1
正确的做法是,你不想要“删除”它们,而是想将它们汇总到一个新的提交1a中,该提交等于源更改方面的1 + 2 + 3(由三个git cherry-pick -n操作组装而成)。 - torek
1
在交互式变基中,可以压缩或修复提交,以实现他想要的效果。他只是混淆了各个段落的工作顺序。使用 cherry-pick 可以解决这个问题,但如果可以简单地进行交互式变基,那么这种方法会非常麻烦。 - Vampire
显示剩余3条评论

2

我处理这种情况的方法通常是简单地使用reset加上一个commit -C

git rebase -i origin/main # or some other commit

这会打开你的编辑器,我让它看起来像这样:
...
pick aafaa Merge me forward
edit bbfbb Into me
...

然后当变基停在bbfbb时,执行以下操作

git reset --soft HEAD^
git commit --amend -C HEAD@{1} # could put an explicit commit here
git rebase --continue

它也可以放入单个rebase待办事项列表中,并轻松简化为仅修改,因为您想要的提交哈希在上一行中。

...
pick aafaa Squash me forward
fixup bbfbb Into me
exec git commit --amend -C bbfbb
...

虽然tera的回答很巧妙,但这个解决方案更简单,同时也“正确”地回答了OP的问题。 - mloughran

0
为了改进golvok的答案,如果你想要“前向压缩”多个提交,你可以按照以下步骤进行:
  1. (可选) 如果您在未推送的提交中修改并随后取消跟踪了文件,请确保先存储或清理未跟踪的文件。

  2. 开始变基:

    git rebase -i origin/destination_branch
    
  3. 使用 fixup 标记您想要压缩的第一个和最后一个提交之间的所有提交,并在目标提交后放置以下 exec 命令:

    pick a52ff17 Feature A added
    pick b69a7d9 Feature B WIP
    fixup da37b5a Feature B WIP
    fixup 009557e Feature B WIP
    pick 4857cdd Feature B added
    exec git reset --soft HEAD^ && git commit --amend -C HEAD@{1}
    pick 15ffacb Feature C added
    

好了,轻而易举。


-1
你想要的是:
1. pick
2. squash
3. fixup
4. pick
5. squash
6. squash
7. fixup

然后当新的合并提交在编辑器中出现时,删除选定提交的行并保留修复提交。这样得到的功能补丁应该具有系列中最后一个提交的提交消息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接