首页 > 解决方案 > 从 master 获取当前差异并将其转换为单个提交,而无需将每个提交应用在彼此之上

问题描述

我想在 master 之前采取一些 X 提交的分支,基本上快照当前的差异并从中创建一个单一的提交。

通常我使用git rebase -i, squash 除最新提交之外的所有提交,这很有效。

但是,如果在修复中途存在冲突或问题,我必须手动完成它。我不一定要在彼此之上应用每个提交 - 相反,我想获取我的分支的 HEAD 和 master 之间的当前差异,并从中创建一个单一的提交。

我已经通过手动区分我的整个分支和 master 来完成它,创建mychanges.diff,然后将其应用到一个干净的分支,但感觉在 git 中应该有一种原生的、干净的方式,而不是通过应用每个提交来重新设置基础,而是快照当前状态并创建与 master 差异的单个提交。

标签: git

解决方案


我相信你想要的是git merge --squash

git checkout master
git merge --squash otherbranch

[resolve any conflicts if necessary, then]

git diff --cached     # to see what you're about to commit
git commit            # to make a single non-merge commit on master

更长

差异不是快照,因此这里存在根本的不匹配。让我试试旧的温度类比。假设我告诉你,今天这里比昨天暖了十度。(虽然实际上是一样的 :-)) 现在我有一个问题要问你:今天的温度是多少?或者,等效地,昨天的温度是多少?

快照是绝对的。如果我告诉你它是 68˚F / 20˚C,或者这比昨天高 10˚F,或者昨天是 58˚F,那么这两条信息就足够了。一个快照或一个差异是不够的:您需要两个快照,或者一个快照和一个差异。(请注意,无限数量的差异是永远不够的:您总是需要序列中某处的至少一个快照。否则可能会有一些东西在开始时是这样,并且从未改变过,您将看不到它完全没有。)

不过,我认为您所描述的情况是这样的:

           C--D--E--F--G   <-- branch
          /
....--A--B--H   <-- master

甚至只是:

           C--D--E--F--G   <-- branch
          /
....--A--B   <-- master

在这两种情况下,您都可以:

  • 比较中的快照B和中的快照G,这会为您提供一要转换B为的更改G;接着
  • H如果快照存在,则将相同的更改应用于快照,G如果不存在,则将相同的更改应用于快照H

现在,如果H根本不存在,这种“应用更改”的事情完全是微不足道的。结果是 中的快照G。在这种情况下,您可以将 G 的快照复制到一个新快照中——我们称之为它,G'而不是H帮助提醒我们它完全匹配G——使用一些假设的 Git 命令1来执行此操作。结果将是:

           C--D--E--F--G   <-- branch
          /
....--A--B--G'  <-- master (HEAD)

但是,如果H 确实存在,您可能希望将-vs-的更改与-vs-的更改结合起来,这样您就不会消除一开始所做更改的影响。结合这些更改的命令是.BGBHHgit merge

一个正常的和完全的合并,你会得到git checkout master; git merge branch

  • 找到合并基础,在这种情况下是 commit B
  • 将合并基础提交与当前分支提示进行比较,H以找到“我们的”更改;
  • 将相同的基于合并的提交与“他们的”(仍然是我们的,但--theirs在合并期间)提交进行比较G,以找到“他们的”更改;和
  • 尝试结合这些变化。

如果存在合并冲突,此正常合并将停止并让您完成工作。如果没有,这个正常的合并会与两个父级进行新的提交:

           C--D--E--F--G   <-- branch
          /             \
....--A--B--H------------M   <-- master (HEAD)

我们将跳过 Git 所称的快进合并(根本不是合并)的概念,直接讨论做什么git merge --squash

添加--squash选项时,您告诉 Git 做两件事:

  1. 在合并步骤后停止,即使合并有效。(你可以得到同样git merge --no-commit的效果,没有第2 项,使用--no-commit
  2. 当您(用户)通过运行完成git commit合并时,Git 将生成,而不是合并提交M,而是压缩提交S

               C--D--E--F--G   <-- branch
              /
    ....--A--B--H------------S   <-- master (HEAD)
    

    M和之间的主要区别在于SS指向H 提交,而不是根本提交G。(默认的提交消息文本S也不同,但由于您在提交消息上打开编辑器时有机会替换此文本git commit,因此这种差异并不重要。)

如果提交H不存在,该git merge --squash操作仍然执行完全相同的过程。合并基础仍然是 commit B

           C--D--E--F--G   <-- branch
          /
....--A--B   <-- master (HEAD)

所以这两个差异是:

  • Bvs B,看看我们改变了什么:当然没有;和
  • Bvs G,看看它们发生了什么变化:G当然,使下一个快照看起来像 commit 所需的一切。

通过结合这两个差异并进行新的提交S,我们得到了一个匹配的提交,如果我们愿意G,我们可以调用G'它,但我仍然会调用S

           C--D--E--F--G   <-- branch
          /
....--A--B--S   <-- master (HEAD)

请注意,在git merge --squash这样使用之后,几乎可以肯定分支branch应该完全被杀死。除非您将commitSmaster.

(您可能想知道为什么git merge --squash要先停止。唯一明智的答案是,这是一个历史事故,其行为从那时起就一直保留下来。如果默认情况下它没有停止,那么您可以git merge --squash --no-commit在您确实希望它停止时运行。但第一个squash-merge 的实现通过git merge提前退出来工作,就在它运行自己的 internal 之前git commit。这样编码更容易。冲突的合并和--no-commit案例再做一步,即在内部记录合并正在进行状态文件;然后它们退出。当你运行时git commit,Git 要么注意到“合并进行中”状态文件,要么没有,这取决于是否git merge创造了它。然后它要么进行正常的非合并提交,因为没有记录状态,要么进行合并提交,因为记录了合并状态。)


1有多个 Git 命令可以实现这个结果。没有一个命令可以一步完成:至少需要两个 Git 命令才能做到这一点。即使使用 也是如此git merge --squash


推荐阅读