147

I'm trying to squash a range of commits - HEAD to HEAD~3. Is there a quick way to do this, or do I need to use rebase --interactive?

Karl Bielefeldt
  • 47,314
  • 10
  • 60
  • 94
Phillip
  • 5,366
  • 10
  • 43
  • 62
  • 5
    Similar question: http://stackoverflow.com/questions/5189560/how-can-i-squash-my-last-x-commits-together-using-git – koppor Jul 14 '12 at 10:05

8 Answers8

182

Make sure your working tree is clean, then

git reset --soft HEAD~3
git commit -m 'new commit message'
Bucket
  • 7,415
  • 9
  • 35
  • 45
wilhelmtell
  • 57,473
  • 20
  • 96
  • 131
  • @wilhelmtell: Great! Now would I be able to construct a git alias, e.g. "mysquash 3 'some message'", to cut this down to one line? – Phillip Sep 01 '11 at 19:53
  • 7
    If its just the number of lines: `git reset --soft HEAD~3 && git commit -m "my message"` – KingCrunch Sep 01 '11 at 20:16
  • 9
    @Phillip: You can embed a shell function in the git alias. `git config alias.mysquash '!f(){ git reset --soft HEAD~$1 && git commit ${2:+-m "$2"}; };f'`. `git mysquash 3 'some message'` will work, but I also tweaked it so `git musquash 3` will omit the -m flag entirely so you'll get the interactive `git commit` UI in that case. – Lily Ballard Sep 01 '11 at 20:16
  • @KevinBallard, just to clarify, I think that by `mu` you meant `my`. – Asclepius Apr 22 '14 at 19:15
  • How do I automate `HEAD~3` to reflect the last pushed commit? I don't want to have to type `3`, etc. – Asclepius Apr 22 '14 at 19:22
  • I found it more understandable to use the SHA hash of the commit I wanted to squash to instead of HEAD~3 – JuJoDi May 14 '14 at 13:41
  • this answer leads to a "! [rejected] master -> master (non-fast-forward)" error... – sebnukem Mar 19 '15 at 17:54
  • 5
    Just to make it clear: It is not the same as a squash. A squash will also merge the commit messages. If you do a soft reset you will lose all messages of the commits. If you want to squash try http://stackoverflow.com/a/27697274/974186 – René Link Aug 03 '15 at 13:21
  • 1
    @sebnukem - That's when we try to push the branch and the remote is configured to reject force pushes. – avmohan Feb 17 '16 at 10:47
  • if these are merge commits it would loose all notion of parents would it not? – George Mauer Aug 01 '19 at 22:22
39

I personally like wilhelmtell's solution:

git reset --soft HEAD~3
git commit -m 'new commit message'

However, I made an alias with some error checking so that you can do this:

g.squash 3 'my commit message'

I recommend setting up aliases that actually run scripts so that it is easier to (a) code up your scripts and (b) do more complex work with error checking. Below is a script that does the work of squashing. I put that in a scripts folder in my HOME path.

Script for squashing (squash.sh)

#!/bin/bash
#

#get number of commits to squash
squashCount=$1

#get the commit message
shift
commitMsg=$@

#regular expression to verify that squash number is an integer
regex='^[0-9]+$'

echo "---------------------------------"
echo "Will squash $squashCount commits"
echo "Commit message will be '$commitMsg'"

echo "...validating input"
if ! [[ $squashCount =~ $regex ]]
then
    echo "Squash count must be an integer."
elif [ -z "$commitMsg" ]
then
    echo "Invalid commit message.  Make sure string is not empty"
else
    echo "...input looks good"
    echo "...proceeding to squash"
    git reset --soft HEAD~$squashCount
    git commit -m "$commitMsg"
    echo "...done"
fi

echo
exit 0

Then to hook up that squash.sh script to an alias, I add the following to my .zprofile:

export PATH="$PATH:$HOME/scripts" # Add scripts folder to PATH
...
alias g.squash='function _gSquash(){ sh squash.sh $1 $2; };_gSquash'
...

Note: You can make your alias anything you want. I have my a lot of my git shortcuts as g.<myCommand>

n8tr
  • 5,018
  • 2
  • 32
  • 33
  • 4
    You can also put it in `$PATH` named `git-squash.sh` and it will be automatically aliased as `git squash`. I didn't change your answer, just in case there's a reason to use the `create-aiases.sh` script that I'm not aware of. –  Jun 12 '14 at 13:19
  • I basically use the create_aliases.command script so that even our PMs and designers can easily get set up. Simply a double-click on the script and they are all set (especially since I have the setup script in our repo and the relative path is known). Then they don't even need to restart terminal. – n8tr Jun 12 '14 at 20:28
  • 1
    I tried this and it reads my commit message as squash count and fails because it's not an integer. – Minthos Aug 12 '14 at 09:46
  • 1
    The solution was to append - after /squash.sh \$1 \$2' – Minthos Aug 12 '14 at 09:52
  • [Minthos](http://stackoverflow.com/users/197197/minthos) Yes and that is already in the solution above--see this line: git config --global alias.squash "!sh -c 'sh /squash.sh \$1 \$2'" – n8tr Aug 12 '14 at 12:52
  • 1
    I like the idea of the solution, but the comment above is not yet taken into account in the solution. There needs to be a minus sign between the single and the double quote. – physicalattraction Oct 27 '15 at 15:48
  • If the previous commits were already pushed, should I forcefully push the created commit here? – physicalattraction Oct 27 '15 at 15:53
  • Thanks [physicalattraction](http://stackoverflow.com/users/1469465/physicalattraction) updated the have the `-`. Also, yes you'll have to force push if you've already pushed the previously un-squashed commits. I recommend to only force push to your own branches, and not do that to a master branch. – n8tr Oct 27 '15 at 20:24
  • Too bad this solution does not retain commit messages from squashed commits, otherwise it's pretty good. Would it be possible to revise it? – ruslaniv Dec 27 '21 at 08:38
  • I purposely do not retain all the commit messages and force typing in a new one. My rationale is that when you squash, you should have a clear explanation of what that commit is about. The previous commits are no longer important and creates noise. If you want to retain all of the commit messages, then you probably shouldn't be squashing, or you should be doing an interactive squash to decide what commits to keep and which to squash. – n8tr Dec 29 '21 at 18:44
27

To add to the answer by wilhelmtell I find it convenient to soft reset to HEAD~2 and then amending the commit of HEAD~3:

git reset --soft HEAD~2
git commit --all --amend --no-edit    

This will merge all commits to the HEAD~3 commit and use its commit message. Be sure to start from a clean working tree.

Andy
  • 17,423
  • 9
  • 52
  • 69
harmonious
  • 521
  • 8
  • 8
  • 12
    This is the correct answer because it squashes a series of commits leading up to head, and it uses the 1st commit's message. It is non-interactive. – Landon Kuhn Sep 29 '17 at 20:02
  • 1
    In this scenario, I think the `--all` parameter in `git commit` is unnecessary because the modified files are already staged after soft resetting. [Doc for `git commit --all`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---all) – li ki Jun 17 '21 at 14:45
11

I used:

EDITOR="sed -i '2,/^$/s/^pick\b/s/'" git rebase -i <ref>

Worked quite fine. Just don't try to have a commit log with a line that starts with "pick" :)

Julien Wajsberg
  • 111
  • 1
  • 2
4

Use the following command to squash the last 4 commits within the last commit:

git squash 4

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"
sq = !git squash $1
sqpsf = !git squash $1 && git psf 

From https://github.com/brauliobo/gitconfig/blob/master/configs/.gitconfig

brauliobo
  • 5,843
  • 4
  • 29
  • 34
2

Here is a one liner to squash the last 2 commits. In this example, the message of second last commit will be retained. You may change the message as you wish.

git commit -am "$(git log -1 --skip=1 --pretty=%B | xargs && git reset --soft HEAD~2)"

This command will be very useful if you create an alias for this command and use the alias instead.

Jasir
  • 677
  • 1
  • 8
  • 26
1

To squash everything since the branch was forked from master:

git reset --soft $(git merge-base --fork-point master) \
  && git commit --verbose --reedit-message=HEAD --reset-author
Andy
  • 17,423
  • 9
  • 52
  • 69
  • `--reedit-message=HEAD` will use the message of the _last commit which is not part of the squashing_. This is likely not the one you want. To rather get the message of the _first commit to be included_, either **(1)** replace `HEAD` with the hash of the commit you want the message of, or **(2)** jump to the first commit to be included and `git commit --amend --reedit-message=HEAD`. This is what harmonious’ answer does. – Maëlan Nov 09 '18 at 12:13
-1

You can get pretty close with

git rebase --onto HEAD~4 HEAD~ master

This assumes you're on master with a linear history. It's not quite a squash because it discards the intermediate commits. You'd need to amend the new HEAD to modify the commit message.

Greg Bacon
  • 134,834
  • 32
  • 188
  • 245
  • Thanks Greg; by 'discards' do you mean that those intermediate commits are subject to cleanup by the git gc? – Phillip Sep 01 '11 at 20:39
  • @Phillip Yes, the intermediate commits become garbage as well as the old HEAD because it is rewritten to have `HEAD~4` as its parent. – Greg Bacon Sep 06 '11 at 01:53