MHauge | Security Engineer

Go back

Stop using git rebase for all your squashing

What is squashing?

Squashing commits is the process of merging changes from one or more commits into a single commit instead. This allows us to rewrite the history of our git repository retroactively, allowing us to appear more organized than we actually are. It also has the benefit that if you squash changes on sensible intervals, it can make it easier to roll back features / changes using git revert.

Preface

In a scenario where you’re not specifically looking to clean up your recent history before you continue working, but just want to merge your work into another branch e.g. main, you can save yourself some time and do the following instead:

git checkout main
git fetch && git pull
git merge --squash feature-branch
git commit -m "feat: Added Domain Engine to project"

This will squash all commits from the branch named “feature-branch” into a single commit on the main branch.

The scenario at hand

You’ve been working on your feature on the branch named ‘feature-branch’ for some time now, committing as you go along. Our history looks like this:

mha@hakkeboksen:~/dev/git-example$ git log
commit 88be53edac2b01af33b3136e3133064fe9b46b87 (HEAD -> feature-branch)
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:22:21 2023 +0100

    test: Wrote tests for domain engine

commit e3a2fd05d30f5efb6cbb9b19bfd246ebfcc77cff
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:22:14 2023 +0100

    chore: Done with domain engine, missing tests

commit 58ef93a86d2d441ec5aae32b33e1ef8e5a257d59
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:22:07 2023 +0100

    chore: Rewriting domain engine...

commit 6396166205f8a4a4a8b46d68201805fc2611fe7e
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:21:56 2023 +0100

    chore: WIP. Committing to save current state

commit 7d5401f5b7404db86ba7930d98af6dad42990294
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:21:41 2023 +0100

    fix: Resolved an issue where users were unable to delete comments

commit 9a92dcfad0ca66f418d40b7920312e45fa0dd71f (main)
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:21:13 2023 +0100

    chore: Initial commit

You’re finished with a large portion of the work for the feature, and want to squash all of your recent chagnes into a single commit, such as “feat: Added Domain Engine to project”, before you continue working on the next part of the feature in your current branch.

At this point our sources on the internet recommends that we make an offline backup of our entire project, since we are about to do an interactive rebase, and chances are high we are going to irreparably mess up our project. Instead of doing this I recommend the following:

  1. Grab the hash of the last commit you want to appear in the history before your soon-to-exist squash commit. In our case this is 9a92dcfad0ca66f418d40b7920312e45fa0dd71f with the text chore: Initial commit since I want my squash-commit to appear directly after this.

  2. Run git status to quickly verify that we have no unstaged changes or unpushed commits:

    mha@hakkeboksen:~/dev/git-example$ git status
    On branch feature-branch
    nothing to commit, working tree clean
  3. Run git reset --soft 9a92dcfad0ca66f418d40b7920312e45fa0dd71f

  4. Run git commit -m "feat: Added Domain Engine to project"

Note that it is also possible to use relative commits, such as HEAD~3, but I find that this is often more work than just grabbing the hash directly from git log.

Why did this work?

When you ran git reset --soft you told git to point your HEAD at the commit you specified, without touching the index or your working tree. As a result, all the files that have been edited between HEAD and the specified commit are now automatically staged with the content they had before you issued the reset, and the commits between them have been “undone”. This allows us to create a new commit using standard git commit syntax. Similarly to a rebase, since we have rewritten the commit history, we now need to perform a force push to have our changes reflected upstream.

Essentially, to git everything appears as if you made all your changes without making any commits in between and now want to make a huge commit.

git push -f origin feature-branch

Let’s look at the commit history after running our commands:

mha@hakkeboksen:~/dev/git-example$ git log
commit 0b73c96db879796645c1d6e28566670fecfd23b0 (HEAD -> feature-branch)
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:37:57 2023 +0100

    feat: Added Domain Engine to project

commit 9a92dcfad0ca66f418d40b7920312e45fa0dd71f (main)
Author: Morten Hauge <morten.hauge97@gmail.com>
Date:   Sat Nov 4 18:21:13 2023 +0100

    chore: Initial commit

Note that the trick above only works if you want to squash all commits between HEAD and the commit you specified, but I find that this is the case more often than not.