Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 012 / 100

Interactive rebase: Git & GitHub lessons from production

Clean up commits before sharing.

Expona Admin7 min read

You've shipped a feature, but when your team reviews the pull request, they see forty commits with messages like "wip", "fix typo", and "actually works now". Interactive rebase: Git & GitHub lessons from production teaches you to clean up that mess before anyone sees it. In this lesson, we'll walk through how to rewrite your commit history, combine related changes, and present work that tells a coherent story.

Why interactive rebase matters in production teams

Interactive rebase is a powerful tool that lets you edit, combine, and reorder commits before merging to main. Unlike a regular rebase that simply replays commits on top of a branch, interactive rebase stops at each commit and asks what you want to do with it. This is invaluable in production environments where:

  • Your team reviews pull requests and expects clean, logical commits that explain why changes were made
  • Debugging requires reading commit history, and a messy history wastes hours
  • You need to bisect to find which commit introduced a bug
  • Release notes and changelogs are derived from commit messages

The cost of not cleaning up commits is paid repeatedly: every developer who reads your history, every deploy that gets rolled back and investigated, every code review that gets derailed by noise.

Starting with git rebase -i HEAD~5

Let's say you've been working on a payment feature for the last few hours. You've made five commits, but only the first and fourth are truly meaningful—the rest are adjustments, experiments, and fixes. Time to clean them up.

The command to start an interactive rebase on your last five commits is:

bash
git rebase -i HEAD~5

This opens your editor (usually Vim or Nano, configured in your Git settings) with a list like this:

bash
pick 1a2b3c4 Add payment processing endpoint
pick 5d6e7f8 Fix import statement
pick 9g0h1i2 Update validation logic
pick 3j4k5l6 Typo fix in error message
pick 7m8n9o0 Refactor payment handler

# Rebase abc1234..7m8n9o0 onto abc1234 (5 commands)
#
# Commands:
# p, pick   = use commit
# r, reword = use commit, but edit the 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 reordered; they are executed from top to bottom.

The number after HEAD~ is important: it tells Git how many commits back to go. If you want to rebase everything since you branched from main, use:

bash
git rebase -i main

This rebases all commits that are in your current branch but not in main.

Squash and fixup: Combine small commits into meaningful ones

In that payment feature example, "Fix import statement", "Typo fix in error message", and one or two other commits are really just noise. They're not mistakes—they're necessary corrections—but they don't add information when someone reads the history. They should be absorbed into the commit that introduced them.

This is where squash and fixup come in. Both merge a commit into the previous one, but they differ in how they handle the commit message:

  • squash (or s): Combines the commit and includes its message, allowing you to edit the combined message
  • fixup (or f): Combines the commit but discards its message entirely, keeping only the previous commit's message

For our example, we'd edit the rebase list to:

bash
pick 1a2b3c4 Add payment processing endpoint
fixup 5d6e7f8 Fix import statement
pick 9g0h1i2 Update validation logic
fixup 3j4k5l6 Typo fix in error message
pick 7m8n9o0 Refactor payment handler

When you save and exit, Git will:

  1. Keep "Add payment processing endpoint" as-is
  2. Merge "Fix import statement" into it, discarding that commit's message
  3. Keep "Update validation logic" as-is
  4. Merge "Typo fix in error message" into it, discarding that message
  5. Keep "Refactor payment handler" as-is

The result is three commits instead of five, each with a clear purpose. When a teammate runs git log, they see the story of the feature, not the messy process of building it.

Real-world scenario: In an e-commerce system, a developer adds a discount calculation endpoint, then fixes a bug in the validation, then adjusts the response format. Using fixup, these become a single, cohesive commit rather than three separate ones. When the team does a post-mortem on a discount bug months later, they find one commit to investigate instead of hunting across three.

Reword: Fix commit messages

Interactive rebase also lets you fix commit messages without changing the code. Maybe you made a typo, or the message wasn't descriptive enough, or you want to follow the team's commit convention now.

Use the reword (or r) command:

bash
pick 1a2b3c4 Add payment processing endpoint
pick 9g0h1i2 Update validation logic
r 7m8n9o0 Refactor payment handler

When Git reaches the reworded commit, it stops and opens your editor so you can change the message. For example, you might change "Refactor payment handler" to "Refactor payment handler to reduce complexity in charge calculation" to be more specific about why you refactored.

You can also do this for a single commit without interactive rebase:

bash
git commit --amend

But interactive rebase is better when you want to fix messages in multiple commits at once or need to reword something from a few commits ago.

Autosquash: Let Git combine commits automatically

As you work, you sometimes realize you need to fix something in an earlier commit. You could use interactive rebase to squash it manually, but there's a smarter way: use git commit --fixup and let rebase handle it.

Say you're working on that payment feature, you've already committed "Add payment processing endpoint", and now you notice the validation is too strict. Instead of amending the original commit, create a fixup commit:

bash
git commit --fixup 1a2b3c4

Git creates a commit with the message "fixup! Add payment processing endpoint", automatically referencing the commit hash you want to fix. When you're ready to clean up, use:

bash
git rebase -i --autosquash main

The --autosquash flag tells Git to automatically reorder and mark commits for squashing based on their messages. Your rebase list will look like:

bash
pick 1a2b3c4 Add payment processing endpoint
fixup 2b3c4d5 fixup! Add payment processing endpoint
pick 9g0h1i2 Update validation logic
pick 7m8n9o0 Refactor payment handler

Git did the reordering for you, and fixup is already set. Just save and exit.

You can also use --squash if you want to merge the commit while keeping its message for review:

bash
git commit --squash 1a2b3c4

Production benefit: In a fast-paced SaaS team, developers often push small fixes during code review. Instead of telling them to amend and force-push, they create a fixup commit and use autosquash in the final cleanup. It's faster, less error-prone, and keeps the workflow smooth.

Common mistakes

Rebasing shared branches: Never rebase a branch that other developers are working on. If you rebase main or develop and force-push it, you'll corrupt everyone's local copies. Rebase only your own feature branches. As a rule: if it's in a shared repository and other people have pulled from it, don't rebase it.

Using rebase when you meant merge: If your branch has diverged from main (you have commits, and main has new commits), rebasing will replay your commits on top of main. Merging instead will create a merge commit. Both are valid; use rebase for a linear history in feature branches, merge for integration branches. Know which you want before you run the command.

Forgetting to force-push after rebasing: When you rebase locally and try to push, Git will reject it because the remote history doesn't match. You need git push --force-with-lease (safer than --force, because it aborts if someone else has pushed). If you forget this step, you'll be stuck until you remember.

Overusing rebase for cleanliness: Squashing every small commit into bigger ones can hide important intermediate states. If a commit represents a logical unit of work that someone might need to understand, keep it. The goal is readability, not minimal commit count.

Losing work mid-rebase: If you make a mistake during interactive rebase, you can abort with git rebase --abort. You're back to where you started. Git also keeps a log of rebase operations, so you can recover with git reflog if needed.

Key takeaways

  • Interactive rebase with git rebase -i HEAD~N or git rebase -i main lets you edit, reorder, and combine commits before merging
  • Use fixup to silently merge small commits into a previous one, keeping the combined commit's message clean
  • Use reword to fix commit messages without changing code, improving clarity for future readers
  • Combine git commit --fixup with git rebase -i --autosquash to automatically organize and merge related commits
  • Only rebase branches you own; never rebase shared branches like main or develop
  • Always use git push --force-with-lease after rebasing, never --force
  • Clean commit history is a form of communication: each commit should tell part of the story of why a feature was built the way it was
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Interactive rebase | Git & GitHub | Expona Engineering Blog