views:

7864

answers:

4

With git rebase --interactive <commit> you can squash any number of commits together into a single one. It's an OCD heaven.

And that's all great unless you want to squash commits into the initial commit. That seems impossible to do.

Any way to achieve it?


Moderately related:

In a related question, I managed to come up with a different approach to the need of squashing against the first commit, which is, well, to make it the second one.

If you're interested: git: how to insert a commit as the first, shifting all the others?

A: 

Have you played around with --onto?

-- MarkusQ

MarkusQ
+20  A: 

I believe you will find different recipes for that in the SO question "How do I combine the first two commits of a git repository?"

Charles Bailey provided there the most detailed answer, reminding us that a commit is a full tree (not just diffs from a previous states).
And here the old commit (the "initial commit") and the new commit (result of the squashing) will have no common ancestor.
That mean you can not "commit --amend" the initial commit into new one, and then rebase onto the new initial commit the history of the previous initial commit (lots of conflicts)

Rather (with A the original "initial commit", and B a subsequent commit needed to be squashed into the initial one):

# Go back to the last commit that we want to form the initial commit (detach HEAD)
git checkout <sha1_for_B>

# reset the branch pointer to the initial commit,
# but leaving the index and working tree intact.
git reset --soft <sha1_for_A>

# amend the initial tree using the tree from 'B'
git commit --amend

# temporarily tag this new initial commit
# (or you could remember the new commit sha1 manually)
git tag tmp

# go back to the original branch (assume master for this example)
git checkout master

# Replay all the commits after B onto the new initial commit
git rebase --onto tmp <sha1_for_B>

# remove the temporary tag
git tag -d tmp

That way, the "rebase --onto" does not introduce conflicts during the merge, since it rebases history made after the last commit (B) to be squashed into the initial one (which was A) to tmp (representing the squashed new initial commit): trivial fast-forward merges only.
That works for "A-B", but also "A-...-...-...-B" (any number of commits can be squashed into the initial one this way)

VonC
+1  A: 

Squashing the first and second commit would result in the first commit being rewritten. If you have more than one branch that is based off the first commit, you'd cut off that branch.

Consider the following example:

a---b---HEAD
 \
  \
   '---d

Squashing a and b into a new commit "ab" would result in two distinct trees which in most cases is not desirable since git-merge and git-rebase will no longer work across the two branches.

ab---HEAD

a---d

If you really want this, it can be done. Have a look at git-filter-branch for a powerful (and dangerous) tool for history rewriting.

hillu
Good point. +1. I guess you would need to branch from ab, and rebase a---d onto that branch in order to replay a-d from the new common point ab. And then remove the a-d branch, useless at that point.
VonC
+2  A: 

I've reworked VonC's script to do everything automatically and not ask me for anything. You give it two commit SHA1s and it will squash everything between them into one commit named "squashed history":

#!/bin/sh
# Go back to the last commit that we want to form the initial commit (detach HEAD)
git checkout $2

# reset the branch pointer to the initial commit (= $1),
# but leaving the index and working tree intact.
git reset --soft $1

# amend the initial tree using the tree from $2
git commit --amend -m "squashed history"

# remember the new commit sha1
TARGET=`git rev-list HEAD --max-count=1`

# go back to the original branch (assume master for this example)
git checkout master

# Replay all the commits after $2 onto the new initial commit
git rebase --onto $TARGET $2
fonsinchen