Xilexio
Xilexio

Reputation: 1238

Squashing first few dozens of git commits that contain merge commits

I have an existing repository that has root R, then a few dozens of commits including multiple merges, up to X, and then linear history up to Y. I'd like to squash everything from R to X into a single commit and force push it. How can I do it without a lot of effort involving re-resolving merges?

Alternatively, this problem could be phrased as changing the root commit from R to X and cutting off the graph before X.

Here is an illustration simplifying the commit graph:

R           ---- I want to squash from here...
|
A
|\
B C
| |
D E
| |\
F G H
| |/
I J
|\ \
K L M
|/  |
N  /
|/
O 
|
X           ---- to here.
|
P
|
Q
|
Y

Squashing everything with regular rebase would require re-resolving multiple merge commits. I am aware of git rerere, but I do not know how to use it in this situtation. It was not enabled when commiting all that.

Upvotes: 2

Views: 169

Answers (2)

eftshift0
eftshift0

Reputation: 30242

This can be done with rebasing but it's much easier to do by hand

git checkout --orphan temp X
# now you are on a brand new branch with no history, and your working tree is just like X
git commit -m "Single shot"
# now let's carry over X up to Y
git cherry-pick X..Y

If you like the result, set your branch over here and live happily ever after.

Update: If the history after D is complex, it might be better to run a rebase for the last step:

git rebase --rebase-merges X Y --onto temp

And if that history is complex and it includes merges with conflicts, I can offer this script to take care of that last step to avoid having to redo the merges with conflicts:

https://github.com/eantoranz/git-replay

Upvotes: 2

hlovdal
hlovdal

Reputation: 28198

Preface: I am not a fan of squashing and would generally advice against it. There can be situations where you want to do this for good reasons (like undoing awful parallel mirror branch merges that some graphical tools/frontends (like Visual Studio) produces), but please make really sure you are not squashing commits as a deodorant to cover up for poor version control hygiene instead of using and filtering out temporary commits properly.


That said, here is how to do what's asked, completely conflict free:

git status              # Make sure you start with no untracked files.
git branch new_branch1 commit_R
git branch new_branch2 commit_Y
git switch new_branch1
git diff HEAD commit_X | git apply - # This makes the worktree identical to X, in
                                     # effect "fast-forward merge" of R..X
git add .
git commit -m "Some message describing the R..X changes"
git rebase --onto new_branch1 commit_X new_branch2
git branch --delete new_branch1
# Now you're done, new_branch2 is now R'--X'--P'--Q'--Y'


# If you want to (forcibly) change the original branch that pointed to Y do the
# following:
git switch the_original_branch
git reset --hard new_branch2
git branch --delete new_branch2

Since new_branch1 and commit_X have the exact same content (the very goal of the diff...apply commands) when running the rebase command, there will be zero conflicts because applying P..Y on top of either is exactly the same.

Upvotes: 1

Related Questions