Skip to main content
TellaDev
Learn Git Git Rebase: Clean History Without the Mess
intermediate Git

Git Rebase: Clean History Without the Mess

Learn when and how to use git rebase, interactive rebase, and squashing to keep your commit history clean.

Biplab Adhikari 838 words
git rebase interactive squash history
Git Rebase: Clean History Without the Mess

Rebase vs Merge

Both git merge and git rebase integrate changes from one branch into another — but they produce very different histories.

Merge preserves the exact history, including when branches diverged and rejoined:

      A─B─C  feature
     /       \
D─E─F─────────G  main (merge commit G)

Rebase replays your commits on top of the target branch, producing a linear history:

      A─B─C  feature (before)
     /
D─E─F  main

        A'─B'─C'  feature (after rebase onto main)
              /
D─E─F────────  main

The result looks as if you started the feature branch from the latest main, even if you branched off much earlier.


Basic Rebase: Updating a Feature Branch

The most common use: bring your feature branch up to date with main before opening a PR.

# You're on feature/user-auth, main has moved forward
git fetch origin
git rebase origin/main

# If conflicts occur:
# 1. Fix the conflicts in the affected files
# 2. Stage the resolved files
git add conflicted-file.ts
# 3. Continue the rebase
git rebase --continue

# To abort and go back to the original state:
git rebase --abort

After rebasing, your branch has the same commits but with new hashes — you’ll need to force-push if you’ve already pushed:

git push origin feature/user-auth --force-with-lease
# --force-with-lease is safer than --force: it fails if someone else pushed

Interactive Rebase: Rewriting History

git rebase -i (interactive rebase) lets you edit, reorder, squash, and delete commits before they go into the shared history.

# Interactively rebase the last 4 commits
git rebase -i HEAD~4

This opens your editor with a list like:

pick a1b2c3 Add login form HTML
pick d4e5f6 Add form validation
pick 7890ab Fix typo in placeholder text
pick cdef01 Add CSS for login button

Available commands:

CommandWhat it does
pickKeep the commit as-is
rewordKeep the commit but edit its message
editPause and let you amend the commit
squashMeld into the previous commit (keep both messages)
fixupMeld into the previous commit (discard this message)
dropDelete this commit entirely

Squashing Commits Before a PR

A messy work-in-progress history (WIP, typo fix, oops) is fine locally, but a clean PR history is easier to review and revert. Squash it before you push.

# Squash the last 4 commits into 1
git rebase -i HEAD~4

Change pick to squash (or s) for all but the first:

pick a1b2c3 Add login form HTML
squash d4e5f6 Add form validation
squash 7890ab Fix typo in placeholder text
squash cdef01 Add CSS for login button

Git opens a new editor window to write the final combined commit message. Write a clear, descriptive message, save, and close — you now have one clean commit.

# Verify the result
git log --oneline -5
# → a7f3d21 Add login form with validation and styling

The Golden Rule

Never rebase a branch that other people have pushed to or are working on.

Rebasing rewrites commit hashes. If a teammate has your branch checked out and you force-push a rebased version, their local copy diverges from the remote — causing confusion and potential lost work.

Safe to rebase:

  • Your own local feature branches that you haven’t shared yet
  • Your own feature branches where you’re the only contributor

Never rebase:

  • main or master
  • develop in GitFlow
  • Any shared branch with multiple contributors

Resolving Rebase Conflicts

Conflicts during rebase are resolved commit by commit (not all at once, like in a merge):

git rebase origin/main

# Conflict! Git pauses at the first conflicting commit.
# Your editor shows conflict markers:
# <<<<<<< HEAD (main)
# const timeout = 5000;
# =======
# const timeout = 3000;
# >>>>>>> a1b2c3 (your commit)

# 1. Edit the file to resolve
# 2. Stage the resolution
git add src/config.ts

# 3. Continue to the next commit
git rebase --continue
# Repeat for each conflicting commit

If you get confused mid-rebase:

git rebase --abort   # Returns to the pre-rebase state, no changes made

After a clean rebase, run your tests before force-pushing to confirm nothing broke during the replay.

When Rebase Is The Right Tool

Use rebase when you want to replay your private work on top of a newer base branch. This is common when main moved forward while you were working. Rebasing keeps the history linear and makes the final pull request easier to review.

Use merge when the branch is shared or when preserving the exact history of integration matters. A merge commit says, “these lines of work came together here.” That can be useful for release branches, long-running team branches, or situations where multiple people are already basing work on the branch.

The practical rule is simple: rebase your own local branch, be careful rebasing anything someone else has pulled.

Interactive Rebase For Cleanup

Interactive rebase is where the command becomes really useful. Before opening a pull request, you can squash noisy checkpoint commits, reword vague messages, and reorder commits into a clearer story.

git rebase -i main

In the editor, pick keeps a commit, reword changes its message, squash combines it with the previous commit, and drop removes it. Use this to make review easier, not to hide important context. If a messy debugging commit explains a tricky decision, keep the idea in the final commit message or pull request description.

Force Push Safely

After rebasing a branch that already exists on the remote, use:

git push --force-with-lease

This is safer than plain --force because it refuses to overwrite remote work you do not have locally. It protects you from accidentally deleting a teammate’s pushed commit.

What I Would Do In Practice

I would rebase local feature branches often enough that conflicts stay small. Before a pull request, I would use interactive rebase to turn noisy commits into a reviewable sequence. After rebasing, I would run tests, inspect the diff against main, and push with --force-with-lease.

Rebase is not about making history look perfect. It is about making history useful. If the cleaned-up history helps a reviewer understand the change, it was worth doing.

Rebase Troubleshooting

If a rebase goes wrong, remember that Git gives you escape hatches. git rebase --abort returns the branch to the state before the rebase started. Use it when the conflict set looks wrong, when you realize you rebased onto the wrong branch, or when you need to regroup.

During conflicts, resolve one file at a time, run tests when possible, stage the resolved files, then continue with git rebase --continue. Do not rush through conflict markers. They are asking you to choose the final code, not simply delete the markers.

After a complicated rebase, inspect the result with git log --oneline --decorate and git diff main...HEAD. The history should be cleaner, but the actual code change should still be the change you intended to submit.