Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reverting one of the commits in a squashed commit?

Tags:

git

github

I have a pull request with 250 files changed, only 5~10 of which I changed.

It's a squash of 4 commits, and one of them is a revert of a merge with another branch, and this is the one that contains ~240 files changed.

It looks like this

<some commits I made after this>
<squashed commit of:

minor change

working on abcd..

Revert "Merge branch 'settlement-statement' into rents-proration"

This reverts commit 0e44eae, reversing
changes made to a39e7fd.

some more change>

I want to only revert

Revert "Merge branch 'settlement-statement' into rents-proration"

This reverts commit 0e44eae, reversing
changes made to a39e7fd.

I thought merging settlement-statement branch back to my branch would fix it, but it didn't.

How can I fix this, without looking at all the files in files changed and picking my changes?

like image 752
Eric Na Avatar asked Nov 16 '25 07:11

Eric Na


2 Answers

Update - I wrote the original answer in two phases, and as such it's a little confusing. The information remains the same, but I'm cleaning it up a bit...


Depending on how you squashed the commits, you have one of two very similar situations. Roughly speaking:

1) If you squashed from one branch to another (e.g. by git merge --aquash), you have a graph like

x -- AB!MCD -- E <--(branch_for_pr)

A -- B -- !M -- C -- D <--(work_branch)

where !M reverted some previous merge, and AB!MCD is a "squash" of A, B, !M, C, and D.

2) If you squashed the commits "in place" on a branch, e.g. git rebase -i, the graph is more like

x -- AB!MCD -- E <--(branch_for_pr)

A -- B -- !M -- C -- D <--(branch_for_pr@{2})

Here branch_for_pr@{2} is a reflog entry. The tools don't show reflog entries as visibly as current refs, so it might appear as though A..D are "gone", but (at least on the local clone where the squash occurred) they hopefully aren't. (It is true that reflog entries eventually expire - by default after about 3 months, or when you explicitly expire them. After the reflog entry expires, gc could delete the old commits unless, in the mean time, you've done something that keeps them "reachable". Also, reflogs aren't shared on push or fetch.)

You would find the reflog entry with a command like

git reflog branch_for_pr

and could then put a new branch or tag on it until you know you no longer need it, to remove the risk that reflog expiry would hide it from you. If you do that, the result would look like the first diagram (from (1) above).

The rest of this entry assumes an actual branch points to D. So... how to proceed?

The easiest thing to do is to revert !M on branch_for_pr. This may seem counter-intuitive, but you can revert just about anything; it doesn't have to be an ancestor of HEAD. So you need an expression that resolves to !M - which can be its commit ID, or in this example something like work_branch~2.

git checkout branch_for_pr
git revert work_branch~2

A few points of background to know here:

A commit is just a snapshot of a project with a small amount of metadata. The metadata includes a "parent" list, which places the snapshot relative to other snapshots in history. Many git commands operate on a commit's patch - i.e. the difference between that commit and its parent (or one of its parents, in the case of merges). A commit's metadata does not include any special relationship to "commit reverted by this commit", or "commit(s) squashed into this commit"; that information is essentially lost.

As implied above, a "revert" is just a short-hand way to assemble a commit which relates to its parent in a certain way, governed by another commit's relationship with its parent. git does not memorialize that this particular commit was generated by reverting another commit (much less what that other commit was).

Likewise, a "squash" is just a shorthand way of specifying a group of changes you want to apply to HEAD to create a new commit. The resulting commit doesn't "know" that it is a squash.

So those things might change the way you think about what you're trying to do, and affect how you approach this question. And yet a more immediate problem for the proposed solution of "re-doing the merge" is, git is a little weird about reverts of merges. Once you've reverted a merge, it' snot so easy to re-merge it. You either have to regenerate the branch from new commits (to make a re-merge possible), or just revert the revert - which is what I recommend doing above, directly on your final branch.

There are many variations on this idea, which may produce "cleaner" histories; but this is the simplest and avoids possibly muddying things even more.

like image 186
Mark Adelsberger Avatar answered Nov 18 '25 00:11

Mark Adelsberger


You cannot pull a single commit out of a squash. You got your wish, they're squashed into one.

Fortunately Git takes a few weeks to throw anything away. Your unsquashed branch is probably still there. Your repository hopefully looks like this.

A - B - C - D [master]
            |\
            | E [feature]
            \
             F - G - H - I

feature is your branch. F - G - H - I are your original, unsquashed commits. E is F - G - H - I all squashed together. We want to put feature back on I.

I is unreferenced by a branch or tag. We can find it with the reflog. This is a history of every time HEAD has moved. Every checkout, rebase, and reset. You're looking for something like this.

c6a7e90 (HEAD -> master) HEAD@{0}: rebase -i (finish): returning to refs/heads/master
c6a7e90 (HEAD -> master) HEAD@{1}: rebase -i (squash): second
40c7031 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
4c79819 HEAD@{3}: rebase -i (start): checkout HEAD^^^
7fd34b9 HEAD@{4}: rebase -i (finish): returning to refs/heads/master
7fd34b9 HEAD@{5}: rebase -i (start): checkout master

7fd34b9 is I, the commit ID of your branch before you squashed. c6a7e90 is E, the commit after you squashed. You can then git reset --hard 7fd34b9 to put feature back before you squashed.

More information can be found in the Git Book about Maintenance and Data Recovery and Reset Demystified.


This is why one doesn't squash entire feature branches. Important detail is lost both to history and the reviewers. Why did you change that line? It could be one of 4 mixed together reasons. OTOH you can get a "squashed" view of an unsquashed branch eash enough; Github will provide one for your PR, or you can do git diff master. But you can't go the other way around, you can't get unsquashed changes from a squashed branch. Vital information is lost.

Squashing should be reserved for eliminating uninteresting changes like typo fixes and "I want to do that change three commits ago differently".

like image 44
Schwern Avatar answered Nov 17 '25 23:11

Schwern