Skip to content
Codeloom
Git

Git Rebasing Strategies for Clean Team Histories

Advanced git rebase strategies for teams: interactive rebase workflows, rebase --onto, autosquash, and policies that keep shared branches clean without rewriting public history.

·7 min read · By Codeloom
Advanced 11 min read

What you'll learn

  • How rebase --onto transplants branches between base points
  • Autosquash workflows with fixup and amend commits
  • Rebase policies that work for teams without causing conflicts
  • How to recover from a rebase gone wrong using reflog
  • When to choose rebase-and-merge vs squash-and-merge in PRs

Prerequisites

None — this post is self-contained.

Most teams learn git rebase as “replay my commits on top of main.” That understanding is enough for solo work, but it falls apart when multiple developers share feature branches, when you need to move a branch from one base to another, or when your commit history looks like a stream of “WIP” messages. This guide covers the rebase strategies that keep histories clean in real team environments.

Rebase —onto for Branch Transplants

Standard rebase replays your branch on top of a new base. rebase --onto does something more precise: it moves a specific range of commits to a new base, discarding the original starting point.

Consider this scenario. You branched feature-b off feature-a, but feature-a was abandoned. You want to move feature-b so it starts from main instead.

# Before: main -> feature-a -> feature-b
# After:  main -> feature-b

git rebase --onto main feature-a feature-b

The three arguments are: new base, old base (exclusive), and the branch to move. Git replays every commit between feature-a and feature-b on top of main.

Another common use case is dropping a range of commits from the middle of a branch:

# Remove commits between abc123 and def456
git rebase --onto abc123 def456 HEAD

This is cleaner than interactive rebase when the commits you want to remove form a contiguous block.

Autosquash for Incremental Fixes

When working on a feature branch, you often need to go back and fix something in an earlier commit. Instead of making a standalone “fix typo” commit, use --fixup or --squash to mark the commit for automatic folding.

# Make a fixup commit that targets an earlier commit
git commit --fixup=abc123

# Later, run interactive rebase with autosquash
git rebase -i --autosquash main

Git reorders the fixup commits to sit directly below their targets and marks them with fixup instead of pick. You just save and close the editor.

For a smoother workflow, enable autosquash globally:

git config --global rebase.autoSquash true

Now every git rebase -i will automatically reorder fixup and squash commits. This single setting transforms how you iterate on feature branches.

The difference between --fixup and --squash is that --squash opens the editor to let you merge commit messages, while --fixup silently discards the fixup commit’s message.

Team Rebase Policies

The golden rule of rebase is well-known: never rebase commits that have been pushed and shared. But teams need more nuanced policies than that binary rule.

Rebase Before Push

The simplest team policy is “rebase onto main before pushing your feature branch.” This keeps the branch history linear from the reviewer’s perspective.

# Before pushing or updating a PR
git fetch origin
git rebase origin/main
git push --force-with-lease

Use --force-with-lease instead of --force. It refuses to push if the remote branch has commits you have not fetched, preventing you from overwriting a teammate’s push.

Rebase on Shared Branches

When two developers work on the same feature branch, rebasing becomes dangerous. Developer A rebases and force-pushes, then Developer B pulls and gets duplicate commits or conflicts.

The safer approach is to designate one person as the branch maintainer who rebases, or to avoid rebasing shared branches entirely and use merge commits instead. If you must rebase a shared branch, coordinate explicitly:

# Developer A rebases and pushes
git rebase origin/main
git push --force-with-lease

# Developer B resets to the rebased branch
git fetch origin
git reset --hard origin/feature-branch

This works only when Developer B has no local commits that are not yet pushed.

PR Merge Strategies

Most Git hosting platforms offer three merge strategies for pull requests:

  • Merge commit: Preserves all branch commits and adds a merge commit. Full history, noisy log.
  • Squash and merge: Combines all branch commits into one. Clean log, lost granularity.
  • Rebase and merge: Replays branch commits on top of main without a merge commit. Linear history, preserved granularity.

For teams that value both clean history and meaningful commits, rebase-and-merge works best when combined with the autosquash workflow. Developers squash their WIP commits during development, then the final branch has only intentional, well-described commits that get replayed onto main.

Handling Rebase Conflicts Efficiently

Rebase replays commits one at a time, so you might face the same conflict multiple times across different commits. Two tools help.

Rerere (Reuse Recorded Resolution)

Enable rerere to let Git remember how you resolved a conflict and apply the same resolution automatically next time:

git config --global rerere.enabled true

The first time you resolve a conflict, Git records it. The next time the same conflict appears during a rebase, Git resolves it automatically. This is especially useful when you repeatedly rebase a long-lived branch onto a moving target.

Abort and Retry

When a rebase produces unexpected results, you have escape hatches:

# During a rebase, if things go wrong
git rebase --abort

# After a rebase is complete, undo it
git reflog
# Find the commit hash before the rebase started
git reset --hard HEAD@{5}

The reflog records every HEAD movement. After a bad rebase, find the entry labeled “rebase (start)” and reset to the commit just before it.

Practical Rebase Workflow

Here is a complete workflow that combines these strategies for a feature branch:

# Start feature branch
git checkout -b feature/payment-flow main

# Work and commit incrementally
git commit -m "Add payment form component"
git commit -m "Add Stripe integration"
git commit -m "Add error handling for declined cards"

# Realize the form component needs a fix
git commit --fixup=<hash-of-form-commit>

# Before opening a PR, clean up
git fetch origin
git rebase -i --autosquash origin/main

# The fixup commit is automatically folded in
# Review the remaining commits, reword if needed
# Force push the clean branch
git push --force-with-lease origin feature/payment-flow

The result is a branch with three clean, intentional commits that tell a coherent story, rebased on the latest main.

When Not to Rebase

Rebase is the wrong tool when the merge history itself carries meaning. Release branches, hotfix branches merged back to main, and long-running integration branches all benefit from explicit merge commits that document when and where code was integrated.

Rebase is also dangerous when commits have been tagged or referenced in external systems like issue trackers. Rebasing changes commit hashes, breaking those references permanently.

Use merge when you need traceability. Use rebase when you need clarity.

Key Takeaways

Rebase is not a single operation but a family of strategies. Use --onto to transplant branches, --autosquash to fold incremental fixes, --force-with-lease to push safely, and rerere to avoid resolving the same conflict twice. Establish a team policy early: who rebases, when, and which PR merge strategy to use. The goal is not a perfectly linear history for its own sake, but a history that is easy to read, bisect, and revert when something breaks at 2 AM.