Is there a way to squash a number of commits non-interactively?

50,257

Solution 1

Make sure your working tree is clean, then

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

Solution 2

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>

Solution 3

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.

Solution 4

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" :)

Solution 5

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

Share:
50,257
Phillip
Author by

Phillip

Updated on July 28, 2022

Comments

  • Phillip
    Phillip almost 2 years

    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?

  • Phillip
    Phillip over 12 years
    @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?
  • KingCrunch
    KingCrunch over 12 years
    If its just the number of lines: git reset --soft HEAD~3 && git commit -m "my message"
  • Lily Ballard
    Lily Ballard over 12 years
    @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.
  • Phillip
    Phillip over 12 years
    Thanks Greg; by 'discards' do you mean that those intermediate commits are subject to cleanup by the git gc?
  • Greg Bacon
    Greg Bacon over 12 years
    @Phillip Yes, the intermediate commits become garbage as well as the old HEAD because it is rewritten to have HEAD~4 as its parent.
  • Asclepius
    Asclepius about 10 years
    @KevinBallard, just to clarify, I think that by mu you meant my.
  • Asclepius
    Asclepius about 10 years
    How do I automate HEAD~3 to reflect the last pushed commit? I don't want to have to type 3, etc.
  • JuJoDi
    JuJoDi about 10 years
    I found it more understandable to use the SHA hash of the commit I wanted to squash to instead of HEAD~3
  • Admin
    Admin almost 10 years
    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.
  • n8tr
    n8tr almost 10 years
    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.
  • Minthos
    Minthos almost 10 years
    I tried this and it reads my commit message as squash count and fails because it's not an integer.
  • Minthos
    Minthos almost 10 years
    The solution was to append - after /squash.sh \$1 \$2'
  • n8tr
    n8tr almost 10 years
    Minthos Yes and that is already in the solution above--see this line: git config --global alias.squash "!sh -c 'sh <path to scripts directory>/squash.sh \$1 \$2'"
  • abergmeier
    abergmeier over 9 years
    Sadly this does not work for me on Windows. Still get the interactive editor.
  • sebnukem
    sebnukem about 9 years
    this answer leads to a "! [rejected] master -> master (non-fast-forward)" error...
  • René Link
    René Link almost 9 years
    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 stackoverflow.com/a/27697274/974186
  • physicalattraction
    physicalattraction over 8 years
    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
    physicalattraction over 8 years
    If the previous commits were already pushed, should I forcefully push the created commit here?
  • n8tr
    n8tr over 8 years
    Thanks 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.
  • avmohan
    avmohan about 8 years
    @sebnukem - That's when we try to push the branch and the remote is configured to reject force pushes.
  • Rick-777
    Rick-777 about 8 years
    You didn't specify what OS/shell is being used here. This solution will probably not work for all the other desktops people use.
  • brauliobo
    brauliobo about 8 years
    yep, you should use linux/bash|zsh for this
  • Landon Kuhn
    Landon Kuhn over 6 years
    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.
  • Maëlan
    Maëlan over 5 years
    --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.
  • Abhishek
    Abhishek almost 5 years
    if these are merge commits it would loose all notion of parents would it not?
  • li ki
    li ki almost 3 years
    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
  • netimen
    netimen over 2 years
    neither worked for me on Mac
  • ruslaniv
    ruslaniv over 2 years
    Too bad this solution does not retain commit messages from squashed commits, otherwise it's pretty good. Would it be possible to revise it?
  • n8tr
    n8tr over 2 years
    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.