Git: how to squash all commits between two commits into a single commit

I have a branch office that I have been working on on multiple computers over the past few months. The result is a long chain of history that I want to clean up before merging it into the master branch. Ultimately, the goal is to get rid of all these mistakes that I often make when working with server code.

Here is a screenshot of the gitk history rendering:

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

The path at the bottom of this point is where I separated from the master. The master has changed since I started this branch, but the changes were non-overlapping, so merging should be a piece of cake. Usually my workflow is to reinstall the master and then crush the dirt.

I tried to follow a simple

git rebase -i master

      

and i edited commits in sqush.

It seemed like it started out well, but then it failed and I wanted me to face a conflict. However, it didn't seem like there was a good way to handle this by looking at the differences. Each part was using variables that were undefined in scope, so I wasn't sure how to resolve them.

I also tried using git rebase -i -s recursive -X theirs master

, which did not result in a conflict, but it changed the HEAD state from the revised branch (I want to edit history in such a way that the final HEAD result does not change).

I believe these conflicts arise from the parts of the chain where you can see the diamond picture. (for example, between reworeked classifiers ... and the Merge iccv branch).


To frame my question better, let's A

say = "Merge branch iccv" and B

= "redesigned classifiers" refer to the example in the image. And between them there will be X

and Y

.

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

      

I want to rewrite history so that the state is A

exactly what it is and efficiently destroy intermediate views X

and Y

so the resulting history looks like this:

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

      

Is there a way to cut the resolved state A

, X

and Y

into a single commit in the middle of a story chain like this?

If A

u B

are the SHAIDs of the commits, is there a simple command I can run (or possibly a script) that achieves the result I want?

If there A

was HEAD I suppose I could do

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

      

to create a new chapter, but how can I do that if I'm A

in the middle of a story chain like this. I want to keep this history of all nodes that appear after it.

+3


source to share


2 answers


It can be done, but it is from pain, to pain, with the usual mechanisms.

The main problem is that you have to copy commits to new (slightly different) commits when you want to change something. The reason is that no amount of committing can ever change. 1 The reason is that the hash ID of a commit is a commit in a very real sense: Git hash IDs is how Git finds the underlying object. Change any bit inside the object and it will get a new, different hash ID. 2 Hence, if you want to go from:

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

      

to what looks like:

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

      

the thing after B

cannot be A

, it must be another fixation 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 (but the same tree) A'

, which no longer has intermediate material in its history, i.e. A'

connects directly to B

-then we also have to copy the first message after A'

. Once we do that, we have to copy the commit after that, etc. Result:

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

      


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

2 Hash conflicts are technically possible , but if they happen, they mean your repository stops adding new things. That is, if you were able to come up with a new commit that was similar to the old one but had the desired change and had the same hash ID, Git would prevent you from adding it!


Using git rebase -i

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

The standard command that copies such entries is git rebase

. However, the iteration operation is very bad since the merge happens like A

. In fact, he usually throws them away entirely, preferring to linearize everything instead:

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

      

eg.

Now if the merge commit A

went well, i.e. nothing X

depends on Y

or vice versa, simple enough git rebase -i <hash-of-B>

. You can change everything but the first one pick

for commits X

and Y

, which can be really significant for commits-to squash

, and everything is simple and you're done: Git drops X

and Y'

completely in favor of a single merged commit XY'

that has the same tree as at your merge A

. Result:

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

      

and if we name XY'

A'

and then discard all labels, forgetting their original hash ids, we get exactly what you wanted.


Using git replace

If the merger has been difficult, you want to keep the tree from the merger, while all the tags X

and Y

committed. Here git replace

is (or) the right solution
. Git replace is a little more complicated, but you can tell Git to make a new commit A'

that is "similar to A

but has B

as its only parent identity hash identifier." Git will now have this commit build structure:

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

      



This special refs/replace

name tells Git that when it does things like git log

other commands that use commit IDs, Git should turn its metaphorical eyes off commit A

and look at commit instead A'

. Since it A'

is otherwise a copy A

, git checkout <hash of A>

makes Git look A'

and check out the same tree; and git log

shows the same log message if looking away A'

instead of A

.

Note that both repositories A

and A'

. They are kind of side-by-side, and Git shows you A'

instead A

, unless you use a special flag --no-replace-objects

. Once Git shows you (and uses) A'

instead A

, it follows the reverse connection from A'

to B

, skipping right through all X

and Y

.

Make the replacement permanent, shed X

and Y

completely

Once you are happy with the replacement, you can make it permanent. You can do this with git filter-branch

, which just copies the commits. It copies, starting at some starting point and moving forward in history, backwards. Git normal backwards "starts today and works backwards in history."

When a filter branch makes copies of itself - and its list of what to copy - it usually does the same thing for the eyes as the rest of Git does. So if we have the history shown above and we push the filter-branch to branch

and start right after commit B

, it will collect the existing commit list as:

E, D, C, A'

      

and then change the order. (Actually, we could stop at A'

if we want, as we'll see.)

The filter branch will then copy A'

to the new commit. This new commit will have B

as its parent the same log message as A'

, same tree, same author and datestamps, and so on. - in short, it will be literally identical A'

. This way it will get the same hash id as and A'

will actually be commit A'

.

Then it filter-branch

will copy C

to a new commit. This new commit will have A'

as its parent the same log message as C

, the same tree, etc. This is slightly different from the original C

, of which it is a parent A

and not A'

. So this new commit gets a different hash ID: it becomes commit C'

.

Then it filter-branch

will copy D

. It will become D'

the same way C

copy was C'

.

Finally, it filter-branch

will copy E

to E'

and make a branch

pointer to E'

, giving us the following:

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

      

We can now remove the name refs/replace/

and the backup refs/heads/branch

so that the filter branch retains the original one E

. When we do this, the names are out of the way and we can redraw our graph:

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

      

what we wanted (and got) out of use git rebase -i

, but without the need to re-merge.

Branch filter mechanics

git filter-branch

Use ^<hash-id>

or to tell where to stop ^<name>

. Otherwise, will git filter-branch

not stop listing commits to copy until the commit ends: it will follow commit B

to its parent and parent parent and so on, right down to history. The copies of these commits will be bit-by-bit identical to the originals, which means they will actually be the originals, the same hash ids, and all; but they will take a long time.

Since we can stop at <hash-id-of-B>

or even <hash-id-of-A'>

, we can use ^refs/replace/<hash>

commit to define A

. Or we can just use ^<hash-id>

, which is probably actually easier.

Alternatively, we can write either ^<hash> branch

or <hash>..branch

. Both mean the same (see gitrevisions documentation for details ). So:

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

      

filtration is enough to cement the replacement in place.

If all went well, remove the link refs/original/

as shown at the end of the documentationgit filter-branch

, and remove the replacement as well, and you're done.


Using cherry pick

Alternatively 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 "reinstall to copy and then hide originals". It has one tricky step, using git reset --soft

to set the index to match commit A

to make commit A'

.

+2


source


First, make the current working tree clean and then run the following commands:

#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

S

- squash X,Y,A

. M'

equivalent to M

and N'

before N

. If you want to restore the original state, run

git checkout thesis4
git reset backup --hard

      

+5


source







All Articles