Categories: Tools

How to squash git commits

If you use git to secure the outcome of your work frequently, the code responsible for one functionality may be scattered  across many commits within a feature branch. You can still commit a unit of work as a whole even if you pushed it to the origin in several stages.

Requirements

  • The familiarity with Gitflow Workflow – we are working on a local feature_branch, all work is done only within this one branch and there are no commits or partial merges on the develop.

Squash commits during a pull request

When using a tool like BitBucket, commits will be automatically squashed during a pull request with a message that lists all integrated commits.

It may be just enough if you didn’t have to push the work half done at the end of your work day and complete it in another commit. We want to be able to keep the app working properly even if there is a need to revert some commits. We should never encounter the moment in the git history when the app is broken.

Do not rewrite the git history if there are other programmers working on the branches affected.

Squash latest commits

If you want to compress 3 latest commits on your local feature_branch, reset the HEAD:

$ git reset --soft HEAD~3

Now all the changes from those 3 commits awaits to be aggregated into one:

$ git commit -m "The united message for all three commits."

To fix the inconsistency between local and origin push the repo with the --force-with-lease option to be sure that you will not interfere with somebody else’s work:

$ git push --force-with-lease origin feature_branch

The feature_branch has squashed commits and the git history is the same in both local and remote repositories. You can continue your work on the branch and merge it with develop when you are done.

Squash only selected commits

You are on the local feature_branch, it’s late Friday afternoon and there is no way you will finish the task. You want to save what you did so far, even though the app crashes, so you commit and push on the remote:

$ git add -A
$ git commit -m "Work - part 1."
$ git push origin feature_branch

On Monday you add the missing code and the app is again working properly, so you make another commit:

$ git commit -m "Work - part 2."

And then you add something more to the branch and commit it as well:

$ git commit -m "Distinct changes that you want to keep in a separate commit."

The history of the branch looks like this:

$ git log
commit Distinct changes that you want to keep in a separate commit.
commit Work - part 2.
commit Work - part 1.
commit Initial commit.

If necessary, you need to be able to revert commits safely – it happens only when every one of them keeps the app in a working condition. Therefore you need to squash Work - part 1 and Work - part 2 into one commit and keep the rest as it is.

We want to keep all commits before Work-part 1 intact, so we will rewrite the history only within the three latest commits:

$ git rebase -i HEAD~3

The rebase editor displays specified commits ordered from the oldest to the newest:

pick d5f8d35 Work - part 1.
pick 78488a2 Work - part 2.
pick dfaae62 Distinct changes that you want to keep in a separate commit.

# Rebase f3f29ed..dfaae62 onto f3f29ed (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.

Rebase will squash a newer commit with the older one. Just write squash instead of pick in the lines of commits you want to integrate with the preceding one. In our example we want to keep together Work - part 1 and Work - part 2 so we will put squash only in the line with Work - part 2:

pick   d5f8d35 Work - part 1.
squash 78488a2 Work - part 2.
pick   dfaae62 Distinct changes that you want to keep in a separate commit.

Save changes and exit the rebase editor. After that you will be able to write a new commit message for both squashed commits in a commit editor:

# This is a combination of 2 commits.
# This is the 1st commit message:

Work - part 1.

# This is the commit message #2:

Work - part 2.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Thu Aug 23 14:33:41 2018 +0200
#
# interactive rebase in progress; onto f3f29ed
…

Specify the new message and comment the one that is no longer needed:

# This is a combination of 2 commits.
# This is the 1st commit message:

Work - part 1 and 2 combined.

# This is the commit message #2:

#Work - part 2.
…

Save the changes and exit. Now the git history looks like this:

$ git log
commit Distinct changes that you want to keep in a separate commit.
commit Work - part 1 and 2 combined.
commit Initial commit.

If you want to keep the original commit message “Work – part 2.” use fixup instead of squash:

pick   d5f8d35 Work - part 1.
fixup  78488a2 Work - part 2.
pick   dfaae62 Distinct changes that you want to keep in a separate commit.

and the git history will look like this:

$ git log
commit Distinct changes that you want to keep in a separate commit.
commit Work - part 2.
commit Initial commit.

Too keep the local feature_branch compatible with the origin, push the branch with the --force-with-lease option to be sure that you will not interfere with somebody else’s work:

$ git push --force-with-lease origin feature_branch

You can find useful info in this git ready post.

Forgot to add something to the latest commit?

Instead of committing the forgotten changes and squashing the commits, you can change the latest commit with --amend. Just add the remaining files with:

  •  $ git add -A (all remaining files)
  • or $ git add file_name  (a specified file)

and run:

$ git commit --amend --no-edit

The latest commit contains the previously added files and the new ones like they were committed together in one action. The --no-edit option leaves the commit message unmodified.

You should always have a very good reason to rewrite the git history and be sure that nobody from the team will be affected by conflicts when you decide to do this.

Merge feature branch to the develop in one commit

If you are going to merge the feature_branch to the develop branch, you can make it in one commit – use the --squash option:

$ git checkout develop
$ git merge --squash feature_branch
$ git commit -m "The united message for all commits from the feature_branch."

The feature_branch still has multiple commits, but, if you delete branches after they are merged – there is no need to fix the inconsistency.

Photo by Jason Blackeye on StockSnap

little_pinecone

Recent Posts

Simplify the management of user roles in Spring Boot

Spring Security allows us to use role-based control to restrict access to API resources. However,…

3 years ago

Create a custom annotation to configure Spring Boot tests

A custom annotation in Spring Boot tests is an easy and flexible way to provide…

3 years ago

Keycloak with Spring Boot #4 – Simple guide for roles and authorities

Delegating user management to Keycloak allows us to better focus on meeting the business needs…

3 years ago

Keycloak with Spring Boot #3 – How to authorize requests in Swagger UI

Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…

3 years ago

Keycloak with Spring Boot #2 – Spring Security instead of Keycloak in tests

Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…

3 years ago

Keycloak with Spring Boot #1 – Configure Spring Security with Keycloak

Keycloak provides simple integration with Spring applications. As a result, we can easily configure our…

3 years ago