3

I have the following issue:
I have many commits that need to be squashed (several thousand, we had a runaway script). Those commits are in groups between 40 to 200.
Using git rebase -i is not feasible since it would involve too much labor. We have a tool, that can output the first and last commit of such a group relative from the branch HEAD (which can be used to get the actual commit by its reference hash).

So as an example I'm looking for something that can squash HEAD~400 to HEAD~200 into a single commit. And can then be run again (with change arguments) to squash HEAD~100 to HEAD~50 into another single commit.

Edit 1:
I have thought about about creating a "fake" editor, that essentially fakes the interactiveness, by performing the changes to the rebase file. An abstract example script would look like this (which I could loop until all groups have been squashed):

start=$('get start of oldest group')
end=$('get end of oldest group')
git config core.editor "'~/fakeeditor' -start $start -end $end"
git rebase -i
4
  • Just rewiring ancestry is a matter of setting up local grafts and running filter-branch to bake them in.
    – jthill
    Commented Nov 9, 2020 at 20:46
  • 1
    stackoverflow.com/questions/12394166/…
    – phd
    Commented Nov 9, 2020 at 20:46
  • You say you don't want git rebase -i but a command that automatically squashes ranges of commits. Why don't you use git rebase -i and build a command that can automatically mark ranges of commits in that todo list? Commented Nov 10, 2020 at 7:33
  • @NilsWerner which is what I did already propose using that "fake editor" Commented Nov 10, 2020 at 23:53

3 Answers 3

5

My personal favorite is using git reset --soft.

So, say you want to squash from HEAD~1000 up to this point (HEAD~1000 being the last surviving commit that won't be squashed):

git reset --soft HEAD~1000
git commit -m "Squashed a lot of stuff"

That's it. You can use a revision ID instead of using HEAD~n references.

I see that you want to do it like by segments.... so, say.... let's squash HEAD~400 to HEAD~200... then from HEAD~200 to HEAD~100... then from HEAD~100 to HEAD. So, let's create a temp branch where we will do our work.

git checkout -b temp HEAD~200
git reset --soft HEAD~400
git commit -m "squashing first segment"
# next segment
git checkout the-original-branch~100
git reset --soft temp
git commit -m "Second segment"
git branch -f temp #set temp over here
# final segment
git checkout --detach the-original-branch
git reset --soft temp
git commit -m "Final segment"
git branch -f temp #set temp over here
# if you like the result, feel free to move whatever branch over here
git branch -f whatever-branch
# and delete temp if so you want
git branch -D temp

Very easy, I think.

After one day, I just realized (by answering another question) that it can be done in a much simpler way:

git branch -f temp $( git commit-tree -p HEAD~400 -m "first squash" HEAD~200^{tree} )
# that set up temp on the first squash
git branch -f temp $( git commit-tree -p temp -m "second squash" HEAD~100^{tree} )
# temp has move to second squash
git branch -f temp $( git commit-tree -p temp -m "final squash" HEAD^{tree} )

Now temp has the squashed commits the way it was requested in my example. Feel free to do a reset --hard over there (the usual warning when using reset --hard).

1
  • Clearly the tool for the job. Commented Nov 10, 2020 at 8:32
2

First, create a script for manipulate the todo list of git rebase:

squash-it file:

#!/bin/sh
first=$(git rev-parse --short "$1")
last=$(git rev-parse --short "$2")
todo="$3"
lines=$(
sed -n "/^pick $first /,/^pick $last/{s/^pick/squash/p}" "$todo" | sed "1s/squash/pick/"
sed "/^pick $first /,/^pick $last/d" "$todo"
)
echo "$lines" > "$todo"

Then use this script like this:

GIT_SEQUENCE_EDITOR='sh -c "./squash-it HEAD~100 HEAD~50 $1"' GIT_EDITOR=cat git rebase -i HEAD~100^

You can replace squash with fixup if it’s what you want.

You can of course generate this line in a script itself that will replace HEAD~100 and HEAD~50 with your liking in a loop for instance.

2
  • Well, having shebang #!/bin/sh and running the script with bash -c is a bit strange, no? :-) Why not just use shebang #!/usr/bin/env bash and GIT_SEQUENCE_EDITOR='./squash-it ?
    – phd
    Commented Nov 9, 2020 at 22:51
  • Ok I change it to use sh everywhere Commented Nov 10, 2020 at 7:26
0

You can generate the entire list of commands for git rebase -i in advance, and then paste them into an editor. The process would look something like this:

  1. For each batch of commits you want to squash, generate a full list of commit hashes, preceded by the word fixup; assuming your start and end commits are in $start and $end: git log "$start...$end" --pretty='fixup %h'
  2. In between each batch of commits, do the same thing but with pick; you could take the end of one batch of squashes and the parent of the start of the next, then discard the first line: git log "$end...$nextStart^" --pretty='pick %h' | sed -n '2,$p'
  3. Find the commit before the first one you want to squash, and run git rebase -i with that as its argument.
  4. Delete all the lines that git generates, and paste in your prepared todo list.
  5. Save and exit the editor, and git's rebase tooling will work through your list.

Not the answer you're looking for? Browse other questions tagged or ask your own question.