Squash the first two commits in Git? [duplicate]
Asked Answered
S

9

685

With git rebase --interactive <commit> you can squash any number of commits together into a single one.

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

Are there any ways 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?

Stilwell answered 28/2, 2009 at 20:55 Comment(3)
Related: Edit the root commit in Git?.Itu
one-liner: git squash 2 with the alias squash = !"f() { NL=$1; GIT_EDITOR=\"sed -i '2,$NL s/pick/squash/;/# This is the 2nd commit message:/,$ {d}'\"; git rebase -i HEAD~$NL; }; f". See https://mcmap.net/q/12060/-how-do-i-squash-my-last-n-commits-together.Faldstool
Of the 'squash' script: with two commits, the HEAD~2 that this script creates doesn't exist, so in this one specific case it doesn't fly.Circumstance
E
960

Update July 2012 (git 1.7.12+)

You now can rebase all commits up to root, and select the second commit Y to be squashed with the first X.

git rebase -i --root master

pick sha1 X
squash sha1 Y
pick sha1 Z
git rebase [-i] --root $tip

This command can now be used to rewrite all the history leading from "$tip" down to the root commit.

See commit df5df20c13 (rebase -i: support --root without --onto, 2012-06-26) on GitHub from Chris Webb (arachsys).

As noted in the comments, a git push --force-with-lease (safer than --force, as Mikko Mantalainen reminds us) would be needed after any rebase operation, if you need to publish that rework in a remote repository.


Original answer (February 2009)

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)

(That last sentence is no longer true with git rebase -i --root <aBranch>)

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

  1. Go back to the last commit that we want to form the initial commit (detach HEAD):

    git checkout <sha1_for_B>
    
  2. Reset the branch pointer to the initial commit, but leaving the index and working tree intact:

    git reset --soft <sha1_for_A>
    
  3. Amend the initial tree using the tree from 'B':

    git commit --amend
    
  4. Temporarily tag this new initial commit (or you could remember the new commit sha1 manually):

    git tag tmp
    
  5. Go back to the original branch (assume master for this example):

    git checkout master
    
  6. Replay all the commits after B onto the new initial commit:

    git rebase --onto tmp <sha1_for_B>
    
  7. 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)

Encyclopedic answered 28/2, 2009 at 22:18 Comment(19)
Great tip. Will keep it in mind. Alas, I tried it on a "git svn" repo and that did break the connection to the svn. No worries, I had a backup...Lauraine
This doesn't work for me. When I then go on with git push, I get an error message saying that I need to git pull first. If I do that and then push, I end up repeating several of the commits instead of reducing the number of commits.Fourflush
@MattHuggins but if you rebase, you have then to push --force, you cannot just push. The history has been changed (different SHA1), so the push is no longer fast-forward. I confirm that if you pull, then push, you end up with duplicate commits. See https://mcmap.net/q/21271/-git-duplicate-commits-after-local-rebase-then-pull/6309Encyclopedic
@MattHuggins and not that if your upstream repo (the one you are pushing to) is locally accessible, others won't know that you did a push --force ;) See https://mcmap.net/q/21272/-how-can-i-find-out-who-force-pushed-in-git/6309Encyclopedic
Thanks, I figured out that I needed to --force the push :)Fourflush
I got a lot of conflicts doing your steps. The conflicts like this: Failed to merge in the changes. Patch failed at 0093 Email wording updatesAppleby
@Appleby that happens, where there are a lot of commits to rebase. In that case, I would recommend an incremental rebase: github.com/mhagger/git-imerge (softwareswirl.blogspot.fr/2013/05/…). That way, you can resolve step by step the conflicts, with the possibility of stopping and resuming later that rebase.Encyclopedic
I have a lot of commits made by other devs. I need just clear the repo by leaving only some last months of commits. and apparently it is not so easy. If I skip this conflicts, it leaves the difference on the final result :(Appleby
@Appleby that sounds more like a git reset than a git rebase then.Encyclopedic
git rebase -i --root worked like a charm. my git log looks like a cute baby now. thanks!Millstream
I was working on splitting a git repo into two repos and I ended up with one repo having an empty first commit. This commit shows up in git log but doesn't show up in git rebase -i root. anybody else getting this error?Kliber
@Kliber strange. I t would be best to ask a separate question, with as much details as possible on the specifics of your repo.Encyclopedic
Weird, magically disappeared the next morning. Maybe some late garbage collection or something…Kliber
Is there some explanation somewhere that breaks down why rebasing commits onto the initial commit is different than rebasing onto any other commit?Inclinatory
@DerekGreer as mention in github.com/git/git/commit/…, when rebasing on root, you need to create a create a sentinel commit with an empty tree (see https://mcmap.net/q/12221/-is-git-39-s-semi-secret-empty-tree-object-reliable-and-why-is-there-not-a-symbolic-name-for-it) to rebase onto. And the, you need to automatically squash the sentinel with any commits rebased directly onto it, so they end up as root commits in their own right and retain their authorship and commit message. You don't need those steps when rebasing onto an existing commit.Encyclopedic
This does not work if there's only 2 commits - git opens up a blank file for rebase - git 2.17Predestine
I would recommend using --force-with-lease instead of --force while pushing the changes to remote server.Lime
@MikkoRantalainen Thank you, good point. I have included your comment in the answer for more visibility, as well as a link to the difference between --force and --force-with-lease.Encyclopedic
This is useful even in 2022. perhaps we all should leave 1st commit to be empty so this don't need to happen and everyone can do HEAD~1 to make their life easier.Forster
T
36

If you simply want to squash all commits into a single, initial commit, just reset the repository and amend the first commit:

git reset hash-of-first-commit
git add -A
git commit --amend

Git reset will leave the working tree intact, so everything is still there. So just add the files using git add commands, and amend the first commit with these changes. Compared to rebase -i you'll lose the ability to merge the git comments though.

Telltale answered 12/5, 2014 at 8:57 Comment(0)
C
33

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
Cockswain answered 18/4, 2010 at 13:5 Comment(2)
+1 for creating it. You should mention thought that it doesn't work for rebasing commits somewhere inside the history, only recent commits.Christianize
See also this answer.Itu
V
24

For what it's worth, I avoid this problem by always creating a "no-op" first commit, in which the only thing in the repository is an empty .gitignore:

https://github.com/DarwinAwardWinner/git-custom-commands/blob/master/bin/git-myinit

That way, there's never any reason to mess with the first commit.

Vying answered 7/10, 2011 at 20:30 Comment(2)
git should do this automatically, if it were less insane. There's a nice way to INSERT such an initial commit to an existing repo... #645950Obsecrate
git commit --allow-empty -m empty often is my first commit. This even avoids to "pollute" the commit with a .gitignore file. Please note that some older tools had trouble viewing empty trees like this.Paunchy
S
5

This will squash second commit into the first one:

A-B-C-... -> AB-C-...

git filter-branch --commit-filter '
    if [ "$GIT_COMMIT" = <sha1ofA> ];
    then
        skip_commit "$@";
    else
        git commit-tree "$@";
    fi
' HEAD

Commit message for AB will be taken from B (although I'd prefer from A).

Has the same effect as Uwe Kleine-König's answer, but works for non-initial A as well.

Sharpsighted answered 16/3, 2012 at 10:14 Comment(2)
@Anothony - Hi, as a novice git user I am not sure if this suits my needs but it looks promising. Could you possibly explain a little more? I am try to squash all my git commits into one for cherry picking into an existing project (leaving the initial commit there is fine). I need something scriptable however, as there are many projects, git rebase -i is not. Will this command work for me? Do I specify the hash for the first commit(A), where C is HEAD? Any further explanation you could offer would be great! Many thanks!Hinch
Squashing all commits into one is generally not needed for merging two projects. Explain why you need it in a separate question. "How do I specify the hash for the first commit(A), where C is HEAD?" is also a separate question. git rev-list --reverse HEAD|head -n1 could be the answerSharpsighted
J
3

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.

Jaimie answered 1/3, 2009 at 14:41 Comment(1)
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.Encyclopedic
H
3

You can use git filter-branch for that. e.g.

git filter-branch --parent-filter \
'if test $GIT_COMMIT != <sha1ofB>; then cat; fi'

This results in AB-C throwing away the commit log of A.

Hatchel answered 17/1, 2011 at 18:22 Comment(3)
This did not work for me. git filter-branch said that the branch was unchanged.Almsman
@Leo: did you substitute <sha1ofB> by the actual hashid?Delossantos
This needs more up votes! Rebase didn't work for me because of my complex git history, but this did the trick.Nablus
R
-1

You could use rebase interactive to modify the last two commits before they've been pushed to a remote

git rebase HEAD^^ -i
Resistor answered 27/12, 2011 at 18:48 Comment(3)
True, but kch asked about squashing the first two commits, not the last (most recent) two commits.Barnum
Like this it's super simple, you just have to use the commit's hash you want to merge into and use that instead of HEAD^^Wallis
@SebastianBlask, I don't believe it's that simple. If you use the SHA1 of the first commit, then you'll only be starting from the second commit. It's not possible to squash/fixup that commit unfortunately.Futilitarian
M
-6

There is an easier way to do this. Let's assume you're on the master branch

Create a new orphaned branch which will remove all commit history:

$ git checkout --orphan new_branch

Add your initial commit message:

$ git commit -a

Get rid of the old unmerged master branch:

$ git branch -D master

Rename your current branch new_branch to master:

$ git branch -m master
Manna answered 25/10, 2013 at 21:15 Comment(1)
and then you lose your entire commit history?Stilwell

© 2022 - 2024 — McMap. All rights reserved.