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

时间:2017-05-06 00:48:59

标签: git rebase

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

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

enter image description here http://imgur.com/a/I9feO

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

我试图执行一个简单的

git rebase -i master

我编辑了sqush的提交。

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

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

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

要更好地表达我的问题,请A ="合并分支iccv"和B ="重写分类器"请参阅图像中的示例。其间的提交将是XY

      ...
       |
       |
       A 
     /  \
    |   X
    Y   |
     \ /
      B
      |
      |
     ...

我想重写历史记录,以便A的状态完全正确,并有效地破坏中间表示XY,因此生成的历史记录如下所示

      ...
       |
       |
       A 
       |
       |
       B
       |
       | 
      ...

有没有办法将已解决的AXY状态压缩到像这样的历史链中间的单个提交中?

如果AB是提交的SHAID,那么我可以运行一个简单的命令(或者可能是脚本)来实现我想要的结果吗?

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

git reset B
git commit -am "recreating the A state"

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

2 个答案:

答案 0 :(得分:10)

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

#initial state

enter image description here

git branch backup thesis4
git checkout -b tmp thesis4

enter image description here

git reset A --hard

enter image description here

git reset B --soft

enter image description here

git commit

enter image description here

git cherry-pick A..thesis4

enter image description here

git checkout thesis4

enter image description here

git reset tmp --hard
git branch -D tmp

enter image description here

SX,Y,A的壁球。 M'相当于MN'N。如果要恢复初始状态,请运行

git checkout thesis4
git reset backup --hard

答案 1 :(得分:4)

这可以做到,但是通过常规机制,它可以从痛苦到痛苦,到很多痛苦。

根本问题在于,每当您想要更改内容时,您必须复制提交新的(略有不同的)提交。原因是没有提交可以改变 1 原因是提交的哈希ID是提交,非常真实感觉:Git的哈希ID是Git如何找到底层对象的。更改对象中的任何位,它将获得一个新的,不同的哈希ID。 2 因此,当您想要来自:

       X
      / \
...--B   A--C--D--E   <-- branch
      \ /
       Y

看起来像:

...--B--A--C--D--E   <-- branch

B 之后的事物不能<{1}},它必须是一个不同的提交,只是闻起来像A。我们可以调用此提交A来区分它们:

A'

但是,如果我们将...--B--A'-... 复制到一个新的,更新鲜的(但同一棵树)A,其历史记录中不再有中间内容 - 即A'直接连接到A' - 然后我们必须复制 B之后的第一个提交。一旦我们这样做,我们必须在那之后复制提交,依此类推。结果是:

A'

1 心理学家喜欢说change is hard,但对于Git来说,它几乎是不可能的! : - )

2 Hash collisions are technically possible,但如果它们发生,则表示您的存储库停止添加新内容。也就是说,如果你设法提出了一个类似旧的提交但是有了你想要的更改,具有相同的哈希ID,Git会禁止你添加它!

使用...--B--A'-C'-D'-E' <-- branch

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

复制此类提交的标准命令是git rebase -i。但是,rebase与git rebase之类的合并提交交易非常糟糕。实际上,它通常会完全抛弃它们,而是倾向于将所有内容线性化:

A
例如

现在,如果合并提交...--B--X--Y'-C'-D'-E' <-- branch 进展顺利,即A中的任何内容都不依赖于X,反之亦然,那么简单的Y就足够了。您可以更改提交git rebase -i <hash-of-B>pick的{​​{1}}中的第一个,这可能实际上是很多提交到X,一切都很顺利你完成了:Git完全支持Ysquash一个合并提交X具有相同树的单个组合Y'提交。结果是:

XY'

如果我们调用A ...--B--XY'-C'-D'-E' <-- branch ,然后通过忘记原始哈希ID来删除所有刻度标记,我们就会得到您想要的内容。

使用XY'

如果合并很困难,那么你想要的是保留合并中的,同时删除所有A'git replace提交。这里git replace is the (or a) right solution。 Git的替换有点复杂,但是你可以指示Git进行一个新的提交X,就像Y一样,但A'作为其单父哈希ID& #34 ;. Git现在将具有此提交图结构:

A

这个特殊的B名称告诉Git,当它执行 X / \ ...--B A--C--D--E <-- branch |\ / | Y \ A' <-- refs/replace/<complicated-thing> 和其他使用提交ID的命令时,Git应该将其隐喻的眼睛从提交refs/replace转移到外观而是在提交git log。由于AA'副本A'会让Git查看A并查看同一棵树;并且git checkout <hash of A>A'而不是git log旁边显示相同的日志消息。

请注意,此时存储库中都存在A'A它们是并排的,Git只是向您展示{ {1}}代替A,除非您使用特殊的A'标记。一旦Git向您显示(并使用)A'而不是A,它就会跟随从--no-replace-objectsA'的向后链接,跳过所有AA'

使替换永久化,完全脱离BX

一旦您对替换感到满意,您可能希望将其永久化。您可以使用Y执行此操作,它只是复制提交。它从一些起点开始复制并在历史中移动前进,与Git的正常向后相反&#34;从今天开始,在历史中向后工作&#34;方式。

当filter-branch正在制作副本时 - 以及它的副本列表 - 它通常会像Git的其余部分一样避免这种情况。因此,如果我们有上面显示的历史记录,并且我们告诉filter-branch在X结束并在commit Y之后开始,它将收集现有的提交列表:

git filter-branch

然后颠倒顺序。 (事实上​​,如果我们愿意的话,我们可以在branch停留,正如我们所见。)

接下来,filter-branch会将B复制到新的提交。这个新提交将以E, D, C, A' 作为其父项,与A'相同的日志消息,相同的树,相同的作者和日期戳等等 - 简而言之,它将完全成为A'相同。因此它将获得与B相同的哈希ID,并且实际上是提交A'

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

接下来,A'将复制C'。这将成为filter-branch,与D的副本D'相同。

最后,C会将C'复制到filter-branch,并E指向E',告诉我们:

branch

我们现在可以删除E'名称以及用于保存原始 X / \ ...--B A--C--D--E <-- refs/original/refs/heads/branch |\ / | Y \ A' <-- refs/replace/<complicated-thing> \ C'-D'-E' <-- branch 的过滤分支refs/replace/的备份副本。当我们这样做时,名字就会消失,我们可以重新绘制图表:

refs/heads/branch

这正是我们想要(并获得)使用E的原因,但无需再次进行合并。

filter-branch的机制

要告诉...--B--A'-C'-D'-E' <-- branch 停止的位置,请使用git rebase -igit filter-branch。否则^<hash-id>不会停止列出要复制的提交,直到它用完提交:它将跟随提交^<name>到其父级,以及父级的父级,依此类推回溯历史的方式。这些提交的副本将与原始文件一点一点地相同,这意味着它们实际上原始文件,相同的哈希ID和所有文件;但是他们需要很长时间才能完成。

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

此外,我们可以写A^<hash-id>。两者的意思相同(详见the gitrevisions documentation)。所以:

^<hash> branch

足以进行过滤以将替换粘合到位。

如果一切顺利,请删除the git filter-branch documentation末尾附近显示的<hash>..branch引用,并删除替换引用,您就完成了。

使用cherry-pick

作为git filter-branch -- <hash>..branchname 的替代方法,您还可以使用refs/original/复制提交。有关详细信息,请参阅ElpieKay's answer。这基本上和以前一样,但是使用&#34;复制提交&#34;工具而不是&#34; rebase来复制提交然后隐藏原件&#34;工具。它有一个棘手的步骤,使用git replace来设置索引以匹配提交git cherry-pick以进行提交git reset --soft