简体   繁体   English

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

[英]Git: How to squash all commits between two commits into a single commit

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. 最终目标是摆脱我在处理服务器代码时经常做的所有wip提交。

Here is a screenshot of the gitk history visualization: 以下是gitk历史可视化的屏幕截图:

在此输入图像描述 http://imgur.com/a/I9feO 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. 自从我开始这个分支以来,Master已经改变了一点,但是这些变化是不相交的,所以合并应该是小菜一碟。 My usually workflow is to rebase onto master and then squash the wip commits. 我通常的工作流程是重新加入master,然后压缩wip提交。

I tried to execute a simple 我试着执行一个简单的

git rebase -i master

and I edited the commits to sqush. 我编辑了对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). 我也尝试使用git rebase -i -s recursive -X theirs master the git rebase -i -s recursive -X theirs master ,这不会导致冲突,但它改变了HEAD从修改后的分支状态(我想以最终结果的方式编辑历史记录)在HEAD中不会改变)。

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). (例如,在重新分类的分类器......和Merge分支iccv之间)。


To phrase my question better let A ="Merge branch iccv", and B ="reworked classifiers" refer to the example in the image. 为了更好地表达我的问题,让A =“Merge branch iccv”, B =“reworked classifiers”指的是图像中的示例。 And the commits in between will be X and Y . 中间的提交将是XY

      ...
       |
       |
       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的状态完全如此,并有效地破坏中间表示XY ,因此生成的历史记录看起来像这样

      ...
       |
       |
       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? 有没有办法将AXY的已解决状态Y到像这样的历史链中间的单个提交中?

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

If A was the HEAD I believe I could do 如果A是HEAD,我相信我能做到

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. 创建一个新头,但如果A位于这样的历史链中间,我该怎么做呢。 I want to maintain this history of all the nodes that come after it. 我想保留它之后的所有节点的历史记录。

First make the current working tree clean and then run these commands: 首先使当前工作树清理,然后运行以下命令:

#initial state

在此输入图像描述

git branch backup thesis4
git checkout -b tmp thesis4

在此输入图像描述

git reset A --hard

在此输入图像描述

git reset B --soft

在此输入图像描述

git commit

在此输入图像描述

git cherry-pick A..thesis4

在此输入图像描述

git checkout thesis4

在此输入图像描述

git reset tmp --hard
git branch -D tmp

在此输入图像描述

S is the squash of X,Y,A . SX,Y,A的壁球。 M' is equivalent to M and N' to N . M'相当于MN'对于N In case you want to restore the initial state, run 如果要恢复初始状态,请运行

git checkout thesis4
git reset backup --hard

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. 1原因是提交的哈希ID 提交,在非常真实的意义上:Git的哈希ID是Git如何找到底层对象。 Change any bit within the object and it gets a new, different hash ID. 更改对象中的任何位,它将获得一个新的,不同的哈希ID。 2 Hence, when you want to go from: 2因此,当你想要去的时候:

       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 . B之后的东西不能A ,它必须是一个不同的提交,只是闻起来像A We can call this commit A' to tell them apart: 我们可以将此提交A'称为分开:

...--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' . 但是如果我们将A复制到一个新的,更新鲜的(但相同的树) A' ,它的历史中不再有中间的东西 - 也就是说, A'直接连接到B我们必须复制第一个提交后的 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

1 Psychologists like to say that change is hard , but for Git, it's literally impossible! 1心理学家喜欢说改变很难 ,但对于Git来说,这几乎是不可能的! :-) :-)

2 Hash collisions are technically possible , but if they occur, they mean that your repository stops adding new things. 2 Hash冲突在技术上是可行的 ,但如果它们发生,则意味着您的存储库停止添加新内容。 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! 也就是说,如果你设法提出一个类似旧的提交但是有你想要的更改并且具有相同的哈希ID的新提交,Git会禁止你添加它!


Using git rebase -i 使用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 . 复制这样提交的标准命令是git rebase However, rebase deals very poorly with merge commits like A . 但是,对于像A这样A合并提交,rebase交易非常糟糕。 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, ie, nothing in X depends on Y or vice versa, a simple git rebase -i <hash-of-B> may suffice. 现在,如果合并提交A进展顺利,即X任何内容都不依赖于Y ,反之亦然,那么简单的git rebase -i <hash-of-B>就足够了。 You can change all but the first one of the pick s 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. 你可以改变除了提交XY的第一个pick的所有提交 - 实际上可能是许多提交 - squash并且所有事情都进展顺利并且你完成了:Git完全支持单个XY'组合XY'提交,它具有您的合并提交A具有的相同树。 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. 如果我们称之为XY' A' ,然后忘记自己原来的散列的ID删除所有刻度线,我们可以得到你想要什么。


Using git replace 使用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. 但是,如果合并很困难,那么你想要的是保留合并中的 ,同时删除所有的XY提交。 Here git replace is the (or a) right solution . 这里git replace是(或者)正确的解决方案 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的替换有点复杂,但你可以指示Git创建一个新的提交A' ,它“像A一样A但是B是它的单父哈希ID”。 Git will now have this commit graph structure: Git现在将具有此提交图结构:

       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' . 这个特殊的refs/replace名称告诉Git,当它执行诸如git log和其他使用提交ID的命令之类的事情时,Git应该将其隐喻的眼睛从提交A转移而不是提交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; 由于A'是否则的副本 Agit checkout <hash of A>使得GIT中看看A' ,并检查了相同的树; and git log shows the same log message when it looks aside at A' instead of A . 当它看起来放在A'而不是A时, git log显示相同的日志消息。

Note that both A and A' exist in the repository at this point. 请注意,此时存储库中都存在AA' 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. 它们是并排的,Git只是向你显示A'而不是A除非你使用特殊的--no-replace-objects标志。 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 . 一旦Git向你显示(并使用) A'代替A ,它就会跟随从A'B的向后链接,跳过XY所有。

Making the replacement permanent, shedding X and Y entirely 使替换永久化,完全脱落XY

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. 您可以使用git filter-branch执行此操作,它只是复制提交。 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. 它从一些起点开始复制并在历史上向前移动,与Git的正常向后相反“从今天开始,在历史中向后工作”的方式。

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. 当filter-branch正在制作它的副本时 - 以及它的复制内容列表 - 它通常会像Git的其余部分一样完成另一个避开眼睛的事情。 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: 因此,如果我们有上面显示的历史记录,并且我们告诉filter-branch在branch上结束并在提交B之后启动,它将收集现有的提交列表,如下所示:

E, D, C, A'

and then reverse the order. 然后颠倒顺序。 (In fact, we could stop at A' if we like, as we'll see.) (事实上​​,如果我们愿意,我们可以停在A' ,正如我们所看到的那样。)

Next, filter-branch will copy A' to a new commit. 接下来,filter-branch将A'复制到新的提交。 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' . 这种新的承诺将B作为其母公司,相同的日志消息A'同一棵树,同样的作者和日期,邮票等,总之,它简直等同于A' So it will get the same hash ID as A' , and actually be commit A' . 因此它将获得与A'相同的哈希ID,并且实际上是提交A'

Next, filter-branch will copy C to a new commit. 接下来, filter-branchC复制到新的提交。 This new commit will have A' as its parent, the same log message as C , and the same tree and so on. 这个新提交将使用A'作为其父级,与C相同的日志消息以及相同的树等等。 This is slightly different from the original C , whose parent is A , not A' . 这与原始C略有不同,原始C的父级是A ,而不是A' So this new commit gets a different hash ID: it becomes commit C' . 所以这个新的提交获得了一个不同的哈希ID:它变成了提交C'

Next, filter-branch will copy D . 接下来, filter-branch将复制D This will become D' , in the same way C 's copy was C' . 这将成为D' ,就像C的副本是C'

Finally, filter-branch will copy E to E' and make branch point to E' , giving us this: 最后, filter-branchE复制到E'并使branch指向E' ,给我们这样:

       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 . 我们现在可以删除refs/replace/ name以及refs/heads/branch的备份副本,filter-branch用来保存原始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. 这正是我们想要(并获得)使用git rebase -i ,但无需再次进行合并。

The mechanics of filter-branch 过滤器分支的机制

To tell git filter-branch where to stop , use ^<hash-id> or ^<name> . 要告诉git filter-branch停止的位置 ,请使用^<hash-id>^<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. 否则git filter-branch将不会停止列出提交,直到它用完提交:它将跟随提交B到其父级,并且跟随父级的父级,依此类推,直到历史记录为止。 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; 这些提交的副本将与原始文件一点一点地相同,这意味着它们实际上是原件,相同的哈希ID和所有; 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 . 由于我们可以停在<hash-id-of-B>甚至<hash-id-of-A'> ,我们可以使用^refs/replace/<hash>来识别提交A Or we can just use ^<hash-id> , which is probably actually easier. 或者我们可以使用^<hash-id> ,这可能实际上更容易。

Furthermore, we can write either ^<hash> branch or <hash>..branch . 此外,我们可以编写^<hash> branch<hash>..branch Both mean the same thing (see the gitrevisions documentation for details). 两者意味着相同的事情(详情请参阅gitrevisions文档 )。 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. 如果一切顺利,请删除git filter-branch文档末尾附近显示的refs/original/ reference,并删除替换引用,然后就完成了。


Using cherry-pick 使用樱桃挑选

As an alternative to git replace , you can also use git cherry-pick to copy commits. 作为git replace ,您还可以使用git cherry-pick来复制提交。 See ElpieKay's answer for details. 有关详细信息,请参阅ElpieKay的答案 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. 这基本上与以前的想法相同,但使用“复制提交”工具而不是“rebase复制提交然后隐藏原件”工具。 It has one tricky step, using git reset --soft to get the index set up to match commit A to make commit A' . 它有一个棘手的步骤,使用git reset --soft来设置索引以匹配提交A以进行提交A'

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM