Git:如何将两次提交之间的所有提交压缩为一个提交 [英] Git: How to squash all commits between two commits into a single commit

查看:195
本文介绍了Git:如何将两次提交之间的所有提交压缩为一个提交的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

过去的几个月里,我有一个分支机构一直在几台计算机上进行个人开发.结果是我要清理很长的历史链,然后再将其合并到master分支.最终目标是摆脱我在处理服务器代码时经常做出的所有wip提交.

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

http://imgur.com/a/I9feO

此方法的底部是我从master分支出来的地方.自从我启动该分支以来,Master进行了一些更改,但是更改是不相交的,因此合并应该是小菜一碟.我通常的工作流程是基于主服务器,然后压缩WIP提交.

我试图执行一个简单的

git rebase -i master

,然后我将提交编辑为sqush.

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

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

我相信这些冲突是由您可以看到菱形图案的链条部分引起的. (例如,在重新分类的分类器之间...和合并分支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位于这样的历史链的中间,我该怎么做.我想保留此之后所有节点的历史记录.

解决方案

可以完成此操作,但是使用通常的机制,可能会有些痛苦,甚至很多.

基本问题是,每当您要更改内容时,都必须 copy 提交新的(略有不同)提交.原因是没有提交可以更改. 1 原因是提交的哈希ID是,实际上含义:Git的哈希ID是Git查找基础对象的方式.更改对象内的任何位,它会获得一个新的不同的哈希ID. 2 因此,当您要从以下位置访问时:

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

看起来像这样:

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

B 之后的东西不能为A,它必须是另一种提交,只是闻起来像A.我们可以将此提交称为A'来区分它们:

...--B--A'-...

但是,如果我们将A复制到新的,气味更鲜(但是同一棵树)的A',该历史记录中不再包含中间内容(即A'直接连接到B),则我们还必须还要 之后复制第一个提交A'.一旦做到这一点,我们必须在那之后复制提交,依此类推.结果是:

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


1 心理学家喜欢说更改很困难,但是对于Git来说,这实际上是不可能的! :-)

2 从技术上讲,可能发生哈希冲突,但如果发生,则表示您的存储库停止添加新内容.也就是说,如果您设法提出一个与旧提交类似的新提交,但是您进行了所需的更改,具有相同的哈希ID,则Git会禁止您添加它! >


使用git rebase -i

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

复制提交的标准命令是git rebase.但是,rebase处理合并提交(如A)的效果非常差.实际上,它通常会将它们完全丢弃,而倾向于线性化所有内容:

...--B--X--Y'-C'-D'-E'   <-- branch

例如.

现在,如果合并提交A进行得很好,即X中的任何内容都不取决于Y,反之亦然,那么简单的git rebase -i <hash-of-B>就足够了.您可以将除了pick的第一个以外的所有内容更改为squash,实际上是很多提交,而pickY可能实际上是很多提交,Git删除了Y'完全支持单个合并的XY'提交,该提交具有与合并提交A相同的树.结果是:

...--B--XY'-C'-D'-E'   <-- branch

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


使用git replace

但是,如果合并很困难,则要保留合并中的 tree ,同时删除所有XY提交.此处 git replace是(或一种)正确的解决方案. Git的替换有些复杂,但是您可以指示Git进行新的提交A',就像"A一样,但以B作为其单亲哈希ID". Git现在将具有以下提交图结构:

       X
      / \
...--B   A--C--D--E   <-- branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>

这个特殊的refs/replace名称告诉Git,当它执行诸如git log之类的操作以及其他使用提交ID的命令时,Git应该把隐喻的目光从提交A上移开,而转而关注提交A' .由于A'否则是A副本,因此git checkout <hash of A>使Git查看A'并检出同一棵树.当git log放在A'而不是A时,它显示相同的日志消息.

请注意,此时仓库中同时存在AA'.它们并排存在,而Git只是向您显示A'而不是A,除非您使用特殊的--no-replace-objects标志.一旦Git向您显示(并使用)了A'而不是A,它就会沿着从A'B的反向链接,直接跳过所有XY.

将替换项永久保留,完全放弃XY

一旦对替换感到满意,您可能希望将其永久化.您可以使用git filter-branch做到这一点,它只复制提交.它从某个起点复制并在历史中向前移动前进,与Git的正常后退今天开始,历史往后前进"的方式相反.

当filter-branch制作其副本以及要复制内容的列表时,通常会执行与Git其余部分相同的惊人动作.因此,如果我们具有上面显示的历史记录,并且告诉filter-branch在branch上结束并在提交B之后立即开始,它将收集现有的提交列表为:

E, D, C, A'

,然后颠倒顺序. (实际上,如果愿意,我们可以在A'停下来,如我们所见.)

接下来,filter-branch将A'复制到新提交.此新提交将以B作为其父级,与A'相同的日志消息,相同的树,相同的作者和日期戳,等等—简而言之,它实际上是 be A'相同.因此它将获得与A'相同的哈希ID,并且实际上将被提交A'.

下一步,filter-branchC复制到新提交.此新提交将以A'作为其父级,与C相同的日志消息,以及相同的树,依此类推.这与原始C略有不同,后者的父级是A,而不是A'.因此,此新提交将获得一个不同哈希ID:它将成为提交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/名称和过滤器分支为保存原始E而创建的refs/heads/branch的备份副本.当我们这样做时,名称就消失了,我们可以重新绘制图形:

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

这就是我们想要(并获得)使用git rebase -i的目的,而不必再次进行合并.

过滤分支的机制

使用^<hash-id>^<name>告诉git filter-branch在哪里停止.否则,git filter-branch不会停止列出要复制的提交,直到其用尽为止:它将跟随提交B到其父级,该父级的父级,依此类推,直到历史.这些提交的副本将与原始副本逐位相同,这意味着它们实际上将成为原始副本,相同的哈希ID和所有副本.但是制作需要很长时间.

由于我们可以在<hash-id-of-B>甚至甚至在<hash-id-of-A'>处停止,因此可以使用^refs/replace/<hash>来标识提交A.或者我们可以只使用^<hash-id>,这实际上可能更容易.

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

git filter-branch -- <hash>..branchname

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

如果一切顺利,请删除refs/original/参考,如 ElpieKay的答案.这与以前基本上是相同的想法,但是使用复制提交"工具而不是变基以复制提交,然后将原始文档隐藏起来"工具.它有一个棘手的步骤,使用git reset --soft设置索引以匹配提交A以使提交A'.

I have a branch I've been working on personally over several computers for the past few months. The result is a long history chain that I want to clean up before I merge it onto the master branch. Ultimately the goal is to get rid of all those wip commits that I frequently make when working on server code.

Here is a screenshot of the gitk history visualization:

http://imgur.com/a/I9feO

Way at the bottom of this is the point where I branched off of master. Master has changed a bit since I started this branch, but the changes have been disjoint, so the merge should be a piece of cake. My usually workflow is to rebase onto master and then squash the wip commits.

I tried to execute a simple

git rebase -i master

and I edited the commits to sqush.

It seemed to start off well, but then it failed and wanted me to address a conflict. However, it seemed like there was no good way to address it by looking at the diffs. Each piece was using variables that were undefined in the scope, so I wasn't sure how to resolve them.

I also attempted using git rebase -i -s recursive -X theirs master, which didn't result in a conflict, but it changed the state of HEAD from the revised branch (I want to edit history in such a way that the end result in HEAD does not change).

I believe these conflicts are arising from the parts of the chain where you can see a diamond pattern. (eg. between reworeked classifiers... and Merge branch iccv).


To phrase my question better let A="Merge branch iccv", and B="reworked classifiers" refer to the example in the image. And the commits in between will be X and Y.

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

I want to rewrite history so the state of A is exactly as it is, and effectively destroy intermediate representations X and Y, so the resulting history looks like this

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

Is there a way to squash the resolved state of A, X and Y into a single commit in the middle of a history chain like this?

If A and B are the SHAIDs of the commits is there a simple command I can run (or perhaps a script) that achieves the result I want?

If A was the HEAD I believe I could do

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

to create a new head, but how can I do this if A is in the middle of a history chain like this. I want to maintain this history of all the nodes that come after it.

解决方案

This can be done, but it's anywhere from a bit of a pain, to a lot of pain, with the usual mechanisms.

The fundamental problem is that you must copy commits to new (slightly different) commits whenever you want to change things. The reason is that no commit can ever change.1 The reason is that the hash ID of a commit is the commit, in a very real sense: Git's hash IDs are how Git finds the underlying object. Change any bit within the object and it gets a new, different hash ID.2 Hence, when you want to go from:

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

to something that looks like:

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

the thing after B cannot be A, it has to be a different commit that just smells like A. We can call this commit A' to tell them apart:

...--B--A'-...

But if we copy A to a new, fresher-smelling (but same tree) A' that no longer has the intermediate stuff in its history—that is, A' connects directly to B—then we must also copy the first commit after A'. Once we do that, we must copy the commit after that one, and so on. The result is:

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


1Psychologists like to say that change is hard, but for Git, it's literally impossible! :-)

2Hash collisions are technically possible, but if they occur, they mean that your repository stops adding new things. That is, if you managed to come up with a new commit that was like the old one, but had your desired change, and had the same hash ID, Git would forbid you from adding it!


Using git rebase -i

Note: Use this method if possible; it's much easier to understand and to get right.

The standard command that copies commits like this is git rebase. However, rebase deals very poorly with merge commits like A. In fact, it normally throws them out entirely, favoring instead linearizing everything:

...--B--X--Y'-C'-D'-E'   <-- branch

for instance.

Now, if merge commit A went well, i.e., nothing in X depends on Y or vice versa, a simple git rebase -i <hash-of-B> may suffice. You can change all but the first one of the picks for commits X and Y—which may actually be many commits—to squash and everything all just goes well and you are done: Git drops X and Y' entirely in favor of a single combined XY' commit that has the same tree your merge commit A had. The result is:

...--B--XY'-C'-D'-E'   <-- branch

and if we call XY' A', and then drop all the tick marks by forgetting their original hash IDs, we get just what you wanted.


Using git replace

If the merge was difficult, though, what you want is to preserve the tree from the merge, while dropping all the X and Y commits. Here git replace is the (or a) right solution. Git's replace is somewhat complicated, but you can instruct Git to make a new commit A' that is "like A but has B as its single parent hash ID". Git will now have this commit graph structure:

       X
      / \
...--B   A--C--D--E   <-- branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>

This special refs/replace name tells Git that, when it is doing things like git log and other commands that use commit IDs, Git should turn its metaphorical eyes away from commit A and look instead at commit A'. Since A' is otherwise a copy of A, git checkout <hash of A> makes Git look at A' and check out the same tree; and git log shows the same log message when it looks aside at A' instead of A.

Note that both A and A' exist in the repository at this point. They are side-by-side, as it were, with Git just showing you A' instead of A unless you use the special --no-replace-objects flag. Once Git has shown you (and used) A' instead of A, it follows the backwards link from A' to B, skipping right over all of X and Y.

Making the replacement permanent, shedding X and Y entirely

Once you are happy with the replacement, you may want to make it permanent. You can do this with git filter-branch, which simply copies commits. It copies starting from some start point and moving forward in history, in the reverse of Git's normal backwards "start at today and work backwards in history" manner.

When filter-branch is making its copies—and its list of what to copy—it normally does this same eye-averting thing that the rest of Git does. So if we have the history shown above, and we tell filter-branch to end on branch and start just after commit B, it will gather the existing commit list as:

E, D, C, A'

and then reverse the order. (In fact, we could stop at A' if we like, as we'll see.)

Next, filter-branch will copy A' to a new commit. This new commit will have B as its parent, the same log message as A', the same tree, the same author and date-stamps and so on—in short, it will literally be identical to A'. So it will get the same hash ID as A', and actually be commit A'.

Next, filter-branch will copy C to a new commit. This new commit will have A' as its parent, the same log message as C, and the same tree and so on. This is slightly different from the original C, whose parent is A, not A'. So this new commit gets a different hash ID: it becomes commit C'.

Next, filter-branch will copy D. This will become D', in the same way C's copy was C'.

Finally, filter-branch will copy E to E' and make branch point to E', giving us this:

       X
      / \
...--B   A--C--D--E   <-- refs/original/refs/heads/branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>
       \
        C'-D'-E'  <-- branch

We can now delete the refs/replace/ name and the backup copy of refs/heads/branch that filter-branch made to save the original E. When we do that, the names get out of the way, and we can re-draw our graph:

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

which is just what we wanted (and got) from using git rebase -i, but without having to do the merge all over again.

The mechanics of filter-branch

To tell git filter-branch where to stop, use ^<hash-id> or ^<name>. Otherwise git filter-branch won't stop listing commits to copy until it runs out of commits: it will follow commit B to its parent, and to that parent's parent, and so on all the way back through history. The copies of these commits will be bit-for-bit identical to the originals, which means they will actually be the originals, same hash ID and all; but they will take a long time to make.

Since we can stop at <hash-id-of-B> or even <hash-id-of-A'>, we can use ^refs/replace/<hash> to identify commit A. Or we can just use ^<hash-id>, which is probably actually easier.

Furthermore, we can write either ^<hash> branch or <hash>..branch. Both mean the same thing (see the gitrevisions documentation for details). So:

git filter-branch -- <hash>..branchname

suffices to do the filtering to cement the replacement into place.

If all went well, delete the refs/original/ reference as shown near the end of the git filter-branch documentation, and delete the replacement reference as well, and you are done.


Using cherry-pick

As an alternative to git replace, you can also use git cherry-pick to copy commits. See ElpieKay's answer for details. This is fundamentally the same idea as before, but uses the "copy commits" tool instead of the "rebase to copy commits and then hide the originals away" tool. It has one tricky step, using git reset --soft to get the index set up to match commit A to make commit A'.

这篇关于Git:如何将两次提交之间的所有提交压缩为一个提交的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆