troymass
troymass

Reputation: 1062

Git - squash entire branch - one line squash command

Whilst I am working on new code, I make many small commits to track my changes. My company, however, prefers each feature to be committed as a single commit. So the solution is to squash my entire (local) branch down to a single commit.

How do I squash an entire branch without using git rebase --interactive and then changing pick to squash for all the commits?

Upvotes: 30

Views: 16566

Answers (9)

Jay Sullivan
Jay Sullivan

Reputation: 18289

If you want to squash the entire commit history into one commit (I did in my case), then the following works and is actually a one-liner:

git reset --soft $(git rev-list --max-parents=0 HEAD) && git commit --amend --date=now --reset-author -m "" -e

Explanation:

  • git reset --soft ...: point branch to a commit id without actually affecting files
  • git rev-list --max-parents=0 HEAD: get the oldest commit id [1]
  • git commit --amend --date=now --reset-author -m "" -e: amend commit with new date, new author, and new message

Upvotes: 0

onlythefinestwilldo
onlythefinestwilldo

Reputation: 381

Find the hash for the commit just before you started the branch and copy it out into the clipboard. Then do a reset to to that hash.

$ git reset --soft [hash]

Then just re add and re commit the changes in a single message.

$ git add -A
$ git commit -m 'EVERYTHING SQUASHED'

Upvotes: 8

user229044
user229044

Reputation: 239491

My preferred method is a two-liner (excluding steps 1 and 4 below). The benefits are you do not need to know/record any commit IDs, you can write a simple alias to perform all the steps involved, and your actually moving your entire branch onto origin/master so that the actual merge into master can be a fast-forward and there cannot be any conflicts.

First, my assumptions:

  • You're working on a branch called my-feature-branch. This branch has diverged from master by several commits; this is the checked-out branch.
  • Your local master tracks remote branch origin/master
  • You want to squash all of your commits from my-feature-branch into a single commit ontop of the current state of origin/master (not your local master, which may be out of date)
  • All of your changes are committed, you have no unstaged changes (they will be lost during git reset --hard)

My process is as follows:

  1. Fetch, so origin/master is current:

    $ git fetch
    
  2. Throw away all the commits on your local branch by resetting it to point at origin/master

    $ git reset --mixed origin/master
    
  3. Merge all of your old changes from the previous state of your branch into the index

    $ git merge --squash HEAD@{1}
    
  4. Commit your changes - Git will pre-populate your editor with a commit message containing all the commit messages from the squashed commits

The simple alias I mentioned would be:

alias squash="git fetch; git reset --mixed origin/master; git merge --squash HEAD@{1}"

Upvotes: 18

Ulysse BN
Ulysse BN

Reputation: 11414

Just squash from original point in main branch

The way I like to squash is by regrouping commits without rebasing, so I can rebase only afterwards.

# Squash current branch from its departure point in main branch. You must run
# this command when on your branch's last commit.
git-squash() {
    # https://stackoverflow.com/a/48524405/6320039
    local main_commit="$(git merge-base HEAD main)"
    local last_branch_commit="$(git rev-parse HEAD)"
    git reset --soft "$main_commit"
    # Preserve commits
    git commit -em "$(git log --reverse --format=%B ${main_commit}..${last_branch_commit})"
}

Source (and integration in dotfiles). Inspired from this other answer by troymass.

Of course, now you can just rebase with git rebase main.

NOTE: you can replace main here with git_main_branch if you are a zsh user. This will pick either main or master.

Upvotes: 0

ianinini
ianinini

Reputation: 680

This is a perfect use case for git reset --soft.

Assume you have a commit history

D   Your latest patch
C   Your second patch
B   Your first patch
A   Someone else's work

you have no staged changes, and git status, git log or git show tell you are currently at commit D.

Then git reset --soft B will take the cumulative changes of commits C and D and stage them for commit. git commit --amend will then 'merge' these changes into commit B.

Use as follows:

git reset --soft B
git commit --amend

The second command will bring up your editor with a chance to edit the commit message.

Note that if you have staged changes before starting this process (i.e. you have done git add XXX but not followed up with a git commit) then those staged changes will also be merged into the commit.

Upvotes: 23

Giovanni Bassi
Giovanni Bassi

Reputation: 839

The best would be to do a hard reset and merge the previous HEAD with squash. Here is an alias:

[alias]
  squash = "!f() { git reset --hard $1; git merge --squash HEAD@{1}; git commit; }; f"

This way you can call it like so:

git squash master

Or to squash from another branch, like dev:

git squash dev

Upvotes: 0

Yeo
Yeo

Reputation: 11814

I don't think this is the correct answer for this question.

But I usually squash my branch before making the Pull Request to the remote upstream master using the following command:

git rebase -i master

But you will then still have to choose which to pick and squash.

Upvotes: 0

twalberg
twalberg

Reputation: 62479

Probably the best option for this would be to use git merge --squash at merge time. That will leave your branch as it developed, which is quite often a lot easier to troubleshoot with, because you'll have some notion of "I was changing that specific functionality in commit Z", and looking at that specific commit, you have all the context of any changes you made to multiple files - looking at a single commit that is the squashed results of your development path makes it quite a bit harder to remember "Oh, yeah, I had to change this one other thing in a different file, too...". You also have the benefit of using git bisect when you have your entire path available - all it could tell you in the squashed case is "this huge commit here broke something".

The result of using git merge --squash is a single commit on the branch that you are "merging" into that contains the cumulative changes from your branch, but it leaves your original branch alone.

Upvotes: 9

troymass
troymass

Reputation: 1062

Edit your git configuration file ~/.gitconfig and add the following to the alias section

[alias]
    squash = "!f(){ CUR=`git rev-parse HEAD` && git reset --soft ${1} && git commit -m \"$(git log --format=%B ${1}..${CUR})\"; };f"

This alias gets the current HEAD commit hash, resets back to the commit you specify, and creates a new commit preserving all of the commit messages.

Usage:

git squash <refspec>

refspec can be any valid commit reference such as a commit hash, branch name, tag name, HEAD^ HEAD~3

Upvotes: 3

Related Questions