2

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.

Xilexio
  • 1,178
  • 19
  • 40

2 Answers2

2

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

eftshift0
  • 26,375
  • 3
  • 36
  • 60
  • Why not just a `reset --soft X` and recommit? I can't believe I'm asking that to "Mr Reset Soft" himself :-D (and it's a genuine question, I wonder if there's a difference I'm missing) – Romain Valeri Feb 15 '23 at 10:09
  • @RomainValeri Woudn't that squash the wrong half of the commits? Notice that the presented graph is "upside down" (compared to what `git log --graph` would show). – j6t Feb 15 '23 at 10:10
  • If I didn't do it, it's for a reason @RomainValeri :-) The chart is upside down. The root is R and the tip is Y. How is it going? – eftshift0 Feb 15 '23 at 10:15
  • @eftshift0 **That**'s what I was missing. D'oh. The direction of the chart. Sorry for the noise.... ^^ – Romain Valeri Feb 15 '23 at 10:23
  • Wonderful idea. However, it does not seem to work as-is: `option orphan requires a value` and `-b, -B and --orphan are mutually exclusive`. Still, I'll experiment with this idea. – Xilexio Feb 15 '23 at 12:21
  • My bad. Try without the `-b`. – eftshift0 Feb 15 '23 at 12:23
  • 1
    Still does not work because `--orphan` requires a value - it should probably go right after `checkout`. EDIT: `git checkout --orphan X temp` works! – Xilexio Feb 15 '23 at 12:31
  • For some reason I am getting conflicts (merge conflict, modify/delete, rename/rename) when doing cherry-pick `X..Y` from an orphaned branch that starts from `X`. Any idea where I messed up? Should it have been `(X+1)..Y`? – Xilexio Feb 15 '23 at 12:35
  • Nope.... because X will be discarded, it has to be `X..Y`. Are you sure it is a straight line from `X` to `Y`? – eftshift0 Feb 15 '23 at 12:59
  • What was the problem with the conflicts? – eftshift0 Feb 17 '23 at 10:43
  • Works! Thank you so much. Earlier conflicts were probably caused by some leftover junk when experimenting on the old repo such as branch with the same name as a commit hash, but everything works correctly on a fresh clone. – Xilexio Feb 17 '23 at 10:44
1

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.

hlovdal
  • 26,565
  • 10
  • 94
  • 165
  • Thank you for the help and new insights into the problem! I understand the general idea behind these commands, particularly the `diff | apply` trick, but this solution does not work because the repo has some non-text assets: `error: cannot apply binary patch to '(...).png' without full index line` and `error: (...).png: patch does not apply`. Maybe some more tweaking is needed to make it work. The other answer circumvents that by creating an orphaned branch, though I don't know if your answer with it in the place of the `diff | apply` trick would work. – Xilexio Feb 17 '23 at 10:53
  • Ah, binary files. Yes, those will not be created with `apply`. To have such files included, cherry pick the commit(s) that introduces those files (or the last commit to modify the files) right before doing `diff | apply`. Maybe the cherry-picks will include text files as well, but that does not significant (if this creates conflicts you can choose the easy way out and just delete them). Then proceed with `diff | apply` as described. You will then end up with intermediate "add image" commits `R'--I1--I2--I3--X'--P'--Q'--Y'` which you then can reduce to `R'--X''--P'...` with interactive rebase. – hlovdal Feb 17 '23 at 11:23