Git — Rebasing & Interactive Rebase
Rebasing is one of Git's most powerful — and controversial — features. It allows you to rewrite commit history by moving, combining, or modifying commits. When used correctly, rebasing produces a clean, linear project history that is easier to review, bisect, and understand.
This guide covers everything from basic rebasing to interactive rebase workflows, the golden rule, and how to handle conflicts when they arise.
Rebasing is the process of moving a branch to a new base commit. Instead of creating a merge commit that ties two branch histories together, rebasing replays each commit from your current branch onto the tip of another branch, one at a time.
The result is a linear history with no merge bubbles. Each commit in the rebased branch gets rewritten with new parent pointers and potentially new content if conflicts are resolved during the replay.
| 1 | # Before rebase: |
| 2 | # A---B---C feature |
| 3 | # / |
| 4 | # D---E---F---G main |
| 5 | # |
| 6 | # git rebase main (from feature branch): |
| 7 | # A'--B'--C' feature |
| 8 | # / |
| 9 | # D---E---F---G main |
| 10 | # |
| 11 | # The feature commits are replayed on top of G |
| 12 | # A', B', C' are NEW commits (different hashes) |
The standard rebase command replays commits from your current branch onto the tip of another branch. Here are the most common invocations:
| 1 | # Rebase current branch onto another branch |
| 2 | git rebase main |
| 3 | |
| 4 | # Rebase onto a specific commit (by hash or reference) |
| 5 | git rebase <commit-hash> |
| 6 | |
| 7 | # Rebase but preserve merge commits (--preserve-merges, deprecated) |
| 8 | # Use --rebase-merges instead |
| 9 | git rebase --rebase-merges main |
| 10 | |
| 11 | # Abort a rebase that went wrong |
| 12 | git rebase --abort |
| 13 | |
| 14 | # Skip the current commit during rebase |
| 15 | git rebase --skip |
| 16 | |
| 17 | # Continue rebase after resolving conflicts |
| 18 | git rebase --continue |
| 19 | |
| 20 | # Pull with rebase instead of merge (config) |
| 21 | git pull --rebase |
| 22 | # Or set it permanently: |
| 23 | git config --global pull.rebase true |
info
Both rebase and merge integrate changes from one branch into another, but they produce different histories. Understanding when to use each is key to maintaining a clean project history.
| Aspect | Merge | Rebase |
|---|---|---|
| History shape | Non-linear (branch bubbles) | Linear (no forks) |
| Commit hashes | Preserved (original commits stay) | Rewritten (new hashes for replayed commits) |
| Merge commit | Creates a merge commit | No merge commit |
| Conflict resolution | Done once in the merge commit | Done per commit being replayed |
| Safety | Safe for shared branches | Dangerous on shared branches |
| Use case | Integrating finished features | Updating feature branch with latest main |
| 1 | # Merge approach: |
| 2 | git checkout feature |
| 3 | git merge main |
| 4 | # Creates: D---E---F---G main |
| 5 | # # A---B---C Merge commit |
| 6 | |
| 7 | # Rebase approach: |
| 8 | git checkout feature |
| 9 | git rebase main |
| 10 | # Creates: D---E---F---G main |
| 11 | # # A'--B'--C' feature (linear) |
best practice
Interactive rebase (git rebase -i) opens an editor where you can modify commits before replaying them. This is the cornerstone of craft Git history. You can squash, fixup, reword, reorder, edit, and drop commits.
| 1 | # Start interactive rebase for the last N commits |
| 2 | git rebase -i HEAD~5 |
| 3 | |
| 4 | # Or rebase everything since branching off main |
| 5 | git rebase -i main |
| 6 | |
| 7 | # This opens your editor with a todo list like: |
| 8 | # pick a1b2c3d Add login form |
| 9 | # pick e4f5g6h Add validation |
| 10 | # pick i7j8k9l Fix typo in validation |
| 11 | # pick m0n1o2p Add tests |
| 12 | # pick q3r4s5t Update README |
Each commit in the todo list can be prefixed with a command:
| Command | Short | Effect |
|---|---|---|
| pick | p | Use the commit as-is |
| reword | r | Edit the commit message only |
| edit | e | Stop to amend the commit (content + message) |
| squash | s | Combine with previous commit, merge messages |
| fixup | f | Combine with previous commit, discard message |
| drop | d | Remove the commit entirely |
Squash & Fixup
Squash combines a commit with its predecessor and lets you merge the commit messages. Fixup does the same but discards the squashed commit's message — perfect for cleaning up typos and review fixes.
| 1 | # Before: messy history with "fix" commits |
| 2 | # pick a1b2c3d Add login form |
| 3 | # pick e4f5g6h Fix password validation |
| 4 | # pick i7j8k9l Fix styling on login page |
| 5 | # pick m0n1o2p Add tests |
| 6 | # pick q3r4s5t Fix test assertion |
| 7 | |
| 8 | # After squashing fixes into the relevant commits: |
| 9 | # pick a1b2c3d Add login form |
| 10 | # squash e4f5g6h Fix password validation ← merged into login form |
| 11 | # pick i7j8k9l Fix styling on login page ← keep (different concern) |
| 12 | # pick m0n1o2p Add tests |
| 13 | # fixup q3r4s5t Fix test assertion ← merged, message discarded |
pro tip
Reword & Reorder
Reword lets you fix typos or improve clarity in commit messages. Reorder changes the sequence of commits, which is useful for grouping related changes before creating a pull request.
| 1 | # Before reorder: |
| 2 | # pick a1b2c3d Add login form |
| 3 | # pick m0n1o2p Add tests |
| 4 | # pick e4f5g6h Add validation |
| 5 | # pick q3r4s5t Add test for validation |
| 6 | |
| 7 | # After reorder (logical grouping): |
| 8 | # pick a1b2c3d Add login form |
| 9 | # pick e4f5g6h Add validation ← moved up |
| 10 | # pick m0n1o2p Add tests ← tests for login form |
| 11 | # pick q3r4s5t Add test for validation ← tests for validation |
warning
Rebasing rewrites commit hashes. If someone else has based work on the original commits, their history will diverge from yours, and they will encounter duplicate commits and merge conflicts when they pull.
The golden rule: only rebase commits that exist solely in your local repository. Once a branch is shared via push or pull request, use merge instead.
| 1 | # SAFE to rebase: local commits not yet pushed |
| 2 | git commit -m "WIP: add feature" |
| 3 | git rebase main |
| 4 | git push |
| 5 | |
| 6 | # UNSAFE to rebase: commits already pushed |
| 7 | git push -u origin feature |
| 8 | # ... another developer pulls this branch ... |
| 9 | git rebase main # DO NOT DO THIS — rewrites public history |
| 10 | git push --force # Forces everyone to recover |
| 11 | |
| 12 | # Safe alternative: use merge instead |
| 13 | git merge main |
| 14 | git push |
The most common rebase workflow is keeping a feature branch up to date with main. This avoids a merge commit and ensures your feature can be merged with a fast-forward merge.
| 1 | # Start a new feature |
| 2 | git checkout -b feature/payments main |
| 3 | |
| 4 | # Make some commits |
| 5 | git commit -m "Add payment model" |
| 6 | git commit -m "Add payment controller" |
| 7 | git commit -m "Add payment tests" |
| 8 | |
| 9 | # Meanwhile, main has advanced. Update your branch: |
| 10 | git fetch origin |
| 11 | git rebase origin/main |
| 12 | |
| 13 | # If conflicts occur, resolve them and continue |
| 14 | git add resolved-file.js |
| 15 | git rebase --continue |
| 16 | |
| 17 | # Push the rebased branch (force push required!) |
| 18 | git push --force-with-lease origin feature/payments |
| 19 | |
| 20 | # Create PR — merge will be fast-forward |
| 21 | |
| 22 | # After PR is merged, delete the branch |
| 23 | git branch -d feature/payments |
warning
Unlike merge conflicts (which are resolved once), conflicts during rebase must be resolved for each commit being replayed. Git pauses after each conflicting commit and asks you to resolve it before continuing.
| 1 | # During rebase, Git stops at the conflicting commit: |
| 2 | git rebase main |
| 3 | # Auto-merging file.js |
| 4 | # CONFLICT (content): Merge conflict in file.js |
| 5 | # error: could not apply a1b2c3d... Add payment model |
| 6 | |
| 7 | # Check what's going on |
| 8 | git status |
| 9 | # You are in rebase — you are currently rebasing branch 'feature' |
| 10 | # (fix conflicts and then run "git rebase --continue") |
| 11 | |
| 12 | # Resolve conflicts in your editor, then: |
| 13 | git add file.js |
| 14 | git rebase --continue |
| 15 | |
| 16 | # Git applies the next commit. Repeat if needed. |
| 17 | |
| 18 | # To see which commit is currently being applied: |
| 19 | git log -1 |
| 20 | |
| 21 | # To skip the problematic commit entirely: |
| 22 | git rebase --skip |
| 23 | |
| 24 | # To abort everything and return to original state: |
| 25 | git rebase --abort |
info
Git provides a powerful workflow for fixing up commits without manually editing the interactive rebase todo list. The --fixup and --squash flags create specially-named commits that --autosquash can recognize and arrange automatically.
| 1 | # Make a commit |
| 2 | git commit -m "Add user authentication" |
| 3 | |
| 4 | # Oops, forgot to include the JWT library. Create a fixup commit: |
| 5 | git add src/auth/jwt.js |
| 6 | git commit --fixup HEAD |
| 7 | |
| 8 | # Later, more fixes: |
| 9 | git add src/auth/session.js |
| 10 | git commit --fixup HEAD |
| 11 | |
| 12 | # Start interactive rebase with autosquash: |
| 13 | git rebase -i --autosquash HEAD~4 |
| 14 | |
| 15 | # Git automatically reorders and marks the fixup commits: |
| 16 | # pick a1b2c3d Add user authentication |
| 17 | # fixup d4e5f6g fixup! Add user authentication |
| 18 | # pick g7h8i9j Add tests |
| 19 | # fixup j0k1l2m fixup! Add tests |
| 20 | |
| 21 | # You can also create squash commits: |
| 22 | git commit --squash HEAD |
| 23 | # Works the same way but uses "squash" instead of "fixup" |
pro tip
Rebase Before PR, Merge Into Main
Rebase your feature branch onto the latest main before creating a pull request, then use a regular merge (or squash merge) to bring the feature into main. This gives you a linear, conflict-free history without rewriting public commits.
Squash Before Merging
Use interactive rebase to squash WIP commits, fixups, and trivial changes into logical, well-described commits before merging. Each commit should represent one complete, testable change — not a stream of consciousness.
Never Rebase Shared Branches
If a branch has been pushed to a remote repository and other developers may have pulled it, do not rebase it. Use merge instead. The golden rule protects your team from history chaos.
Use --force-with-lease
When pushing a rebased branch, always use git push --force-with-lease instead of --force. It ensures you don't accidentally overwrite commits that someone else pushed since your last fetch.
Configure pull.rebase
Set git config --global pull.rebase true to rebase local commits on top of remote changes when pulling. This keeps your local history linear and avoids unnecessary merge commits.