Git:如何将两次提交之间的所有提交压缩成一次提交

Ero*_*mic 6 git rebase

我有一个分支机构,过去几个月我一直在几台计算机上亲自工作.结果是一个很长的历史链,我想在将它合并到主分支之前进行清理.最终目标是摆脱我在处理服务器代码时经常做的所有wip提交.

以下是gitk历史可视化的屏幕截图:

在此输入图像描述http://imgur.com/a/I9feO

在这个底部的方式是我从主人分支的点.自从我开始这个分支以来,Master已经改变了一点,但是这些变化是不相交的,所以合并应该是小菜一碟.我通常的工作流程是重新加入master,然后压缩wip提交.

我试着执行一个简单的

git rebase -i master
Run Code Online (Sandbox Code Playgroud)

我编辑了对sqush的提交.

它似乎开始很好,但后来失败了,并希望我解决冲突.然而,似乎没有好办法通过观察差异来解决它.每个部分都使用范围中未定义的变量,因此我不确定如何解决它们.

我也试图使用git rebase -i -s recursive -X theirs master,但没有导致冲突,但是它改变了修改后的分支的HEAD状态(我想以这样的方式编辑历史记录,即HEAD中的最终结果不会改变).

我相信这些冲突来自于你可以看到钻石图案的链条部分.(例如,在重新分类的分类器......和Merge分支iccv之间).


要更好地A说出我的问题,请让" =合并分支iccv"和B="返工分类器"参考图像中的示例.中间的提交将是XY.

      ...
       |
       |
       A 
     /  \
    |   X
    Y   |
     \ /
      B
      |
      |
     ...
Run Code Online (Sandbox Code Playgroud)

我想改写历史这样的状态A是完全一样的,并有效地破坏中间表示XY,因此产生的历史是这样的

      ...
       |
       |
       A 
       |
       |
       B
       |
       | 
      ...
Run Code Online (Sandbox Code Playgroud)

有没有办法在这样的历史链中间压缩已解决的状态A,XY进入单个提交?

如果A并且B是提交的SHAID是否有一个简单的命令我可以运行(或者可能是一个脚本)来实现我想要的结果?

如果A是HEAD,我相信我能做到

git reset B
git commit -am "recreating the A state"
Run Code Online (Sandbox Code Playgroud)

创建一个新的头,但如果A在这样的历史链中间,我该怎么做呢.我想保留它之后的所有节点的历史记录.

Elp*_*Kay 10

首先使当前工作树清理,然后运行以下命令:

#initial state
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git branch backup thesis4
git checkout -b tmp thesis4
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git reset A --hard
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git reset B --soft
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git commit
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git cherry-pick A..thesis4
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git checkout thesis4
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

git reset tmp --hard
git branch -D tmp
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

S是壁球X,Y,A.M'相当于MN'N.如果要恢复初始状态,请运行

git checkout thesis4
git reset backup --hard
Run Code Online (Sandbox Code Playgroud)

  • 这就像一种魅力,增加了我对 git 功能的理解。我能够对所有钻石进行线性化,然后正常的变基工作就可以了。 (2认同)

tor*_*rek 5

这是可以做到的,但它是从一点点痛苦到很多痛苦的任何地方,使用通常的机制。

根本的问题是,无论何时你想改变事情,你都必须提交复制到新的(略有不同的)提交。原因是没有提交可以改变1 原因是提交的哈希 ID就是提交,在非常真实的意义上:Git 的哈希 ID 是 Git 查找底层对象的方式。更改对象内的任何位,它都会获得一个新的、不同的哈希 ID。2 因此,当您想从:

       X
      / \
...--B   A--C--D--E   <-- branch
      \ /
       Y
Run Code Online (Sandbox Code Playgroud)

看起来像:

...--B--A--C--D--E   <-- branch
Run Code Online (Sandbox Code Playgroud)

之后的事情B 不可能A,它必须是一个不同的提交,只是闻起来像A。我们可以调用这个提交A'来区分它们:

...--B--A'-...
Run Code Online (Sandbox Code Playgroud)

但是,如果我们复制A到一个新的,更新鲜的气味(但同树)A'中不再拥有在其历史上,这是中间的东西,A'直接连接到B-那么我们必须复制了第一次提交 A'。一旦我们这样做了,我们必须在那个之后复制提交,依此类推。结果是:

...--B--A'-C'-D'-E'  <-- branch
Run Code Online (Sandbox Code Playgroud)

1心理学家喜欢说改变很难,但对于 Git 来说,这简直是不可能的!:-)

2哈希冲突在技术上是可能的,但如果发生,则意味着您的存储库停止添加新内容。也就是说,如果你设法想出一个与旧提交类似的新提交,但有你想要的更改,并且具有相同的哈希 ID,Git 将禁止你添加它!


使用 git rebase -i

注意:如果可能,请使用此方法;它更容易理解和正确。

像这样复制提交的标准命令是git rebase. 然而,rebase 处理像A. 实际上,它通常会将它们完全丢弃,而是倾向于将所有内容线性化:

...--B--X--Y'-C'-D'-E'   <-- branch
Run Code Online (Sandbox Code Playgroud)

例如。

现在,如果合并提交A进展顺利,即没有任何X依赖,Y反之亦然,一个简单的git rebase -i <hash-of-B>可能就足够了。您可以更改除第一个之外的所有pick提交X和(Y实际上可能是许多提交)squash,一切顺利,您就完成了:Git 删除XY'完全支持XY'具有相同树的单个组合提交你的合并提交A了。结果是:

...--B--XY'-C'-D'-E'   <-- branch
Run Code Online (Sandbox Code Playgroud)

如果我们调用XY' A',然后通过忘记它们的原始哈希 ID 来删除所有刻度线,我们就会得到您想要的。


使用 git replace

但是,如果合并很困难,您想要的是保留合并中的,同时删除所有XY提交。这git replace是(或一个)正确的解决方案。Git 的替换有点复杂,但是您可以指示 Git 进行一个新的提交A',该提交“类似于A但具有B作为其单一父哈希 ID”。Git 现在将具有此提交图结构:

       X
      / \
...--B   A--C--D--E   <-- branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>
Run Code Online (Sandbox Code Playgroud)

这个特殊的refs/replace名称告诉 Git,当它执行类似git log和其他使用提交 ID 的命令时,Git 应该将其隐喻的目光从 commit 上移开,A而转向commit A'。由于A'否则是复制Agit checkout <hash of A>做的Git看看A',并检查了同一棵树; 并git log在查看A'而不是时显示相同的日志消息A

请注意,此时A和 都A'存在于存储库中。 它们是并排的,就像 Git 只是向您展示A'而不是A除非您使用特殊--no-replace-objects标志。一旦 Git 向您展示(并使用)A'而不是A,它就会跟随从A'to的向后链接,直接B跳过所有XY

使更换永久、脱落XY完全

一旦您对更换感到满意,您可能希望将其永久化。你可以用 来做到这一点git filter-branch,它只是简单地复制提交。它从某个起点开始复制并在历史中向前移动,与 Git 正常的向后“从今天开始并在历史中向后工作”方式相反。

当 filter-branch 制作它的副本时——以及它要复制的内容的列表——它通常会做与 Git 的其余部分相同的令人眼花缭乱的事情。因此,如果我们有上面显示的历史记录,并且我们告诉 filter-branchbranch在 commit 之后结束并开始B,它将收集现有的提交列表:

E, D, C, A'
Run Code Online (Sandbox Code Playgroud)

然后颠倒顺序。(事实上​​,A'如果我们愿意,我们可以停下来,正如我们将要看到的。)

接下来,filter-branch 将复制A'到一个新的提交。这种新的承诺将B作为其母公司,因为相同的日志消息A',同一棵树,同样的作者和日期,邮票等,总之,它会从字面上相同的A'。因此它将获得与 相同的哈希 ID A',并且实际上是 commit A'

接下来,filter-branch将复制C到一个新的提交。这个新提交将A'作为它的父提交,与 相同的日志消息C,以及相同的树等等。这与原始的略有不同C,它的父级是A,不是A'。所以这个新的提交获得了一个不同的哈希 ID:它变成了 commit C'

接下来,filter-branch将复制D. 这将成为D',以同样的方式C的副本了C'

最后,filter-branch将复制EE'branch指向E',给我们这个:

       X
      / \
...--B   A--C--D--E   <-- refs/original/refs/heads/branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>
       \
        C'-D'-E'  <-- branch
Run Code Online (Sandbox Code Playgroud)

我们现在可以删除该过滤器分支的refs/replace/名称和备份副本,refs/heads/branch以保存原始E. 当我们这样做时,名称就会消失,我们可以重新绘制我们的图形:

...--B--A'-C'-D'-E'  <-- branch
Run Code Online (Sandbox Code Playgroud)

这正是我们想要(并得到)使用的东西git rebase -i,但不必重新进行合并。

过滤器分支的力学

要告诉git filter-branch在哪里停止,使用^<hash-id>^<name>。否则git filter-branch不会停止列出要复制的提交,直到它用完提交:它将跟随提交B到其父级,以及该父级的父级,依此类推,一直追溯到历史。这些提交的副本将与原件逐位相同,这意味着它们实际上原件,具有相同的哈希 ID 等等;但他们需要很长时间才能制作。

由于我们可以停在<hash-id-of-B>甚至<hash-id-of-A'>,我们可以使用^refs/replace/<hash>来识别 commit A。或者我们可以使用^<hash-id>,这实际上可能更容易。

此外,我们可以编写^<hash> branch<hash>..branch。两者的意思相同(有关详细信息,请参阅gitrevisions 文档)。所以:

git filter-branch -- <hash>..branchname
Run Code Online (Sandbox Code Playgroud)

足以进行过滤以将替换物固定到位。

如果一切顺利,删除refs/original/参考如图所示接近年底git filter-branch文件,并删除替换基准为好,和你做。


使用樱桃挑选

作为 的替代git replace,您还可以使用git cherry-pick复制提交。有关详细信息,请参阅ElpieKay 的回答。这与以前的想法基本相同,但使用“复制提交”工具而不是“rebase 复制提交然后隐藏原始文件”工具。它有一个棘手的步骤,git reset --soft用于设置索引以匹配 commitA以进行 commit A'