Rebasing and History

Rebase vs merge, interactive rebase for cleanup, and inspecting history with log and diff.

Rebase vs. merge

Both git merge and git rebase solve the same underlying problem — combining the work from two branches — but they produce very different history shapes.

Merge preserves exactly what happened, including a merge commit with two parents (as covered in Branching and Merging):

Plaintext
A---B---E---F  (merge commit, main)
     \       /
      C-----D

Rebase instead rewrites history: it takes your branch's commits and replays them, one at a time, on top of the latest commit of the branch you're rebasing onto — as if you'd started your work later than you actually did:

Plaintext
Before rebase:              After: git rebase main (run on feature branch)
main                         main
 |                            |
 A---B---E                    A---B---E
      \                                \
       C---D                            C'---D'
    feature                          feature (rebased)
Bash
$ git switch feature/user-login
$ git rebase main
Successfully rebased and updated refs/heads/feature/user-login.

Note that C' and D' are new commits with new hashes — they contain the same changes as C and D, but they're not the same commits, because their parent changed. This is the core trade-off to understand: rebase gives you a clean, linear history with no merge commits, but it does so by rewriting commits that already existed.

The rule that matters: never rebase shared history

Because rebase creates new commits and abandons the old ones, rebasing commits that other people have already pulled causes real problems — their local history now disagrees with yours, and reconciling it is painful. The standard guidance:

  • Safe to rebase: commits that exist only on your local branch and haven't been pushed, or a feature branch nobody else has based work on.
  • Don't rebase: main, or any shared branch that others have already pulled from.
git merge git rebase
History Preserves exactly what happened, including merge commits Rewrites commits onto a new base — linear, no merge commits
Commit hashes Unchanged Changed for every replayed commit
Safe on shared branches? Yes, always Only for your own unpushed/private commits
Result Accurate but can look "noisy" with many merge commits Clean, easy-to-read linear history

Interactive rebase: cleaning up commits before sharing

git rebase -i (interactive) is one of Git's most useful features for tidying up your own messy commit history before pushing or opening a pull request — squashing "fix typo" commits into the real commit they belong with, reordering, or rewriting messages.

Bash
$ git rebase -i HEAD~3

This opens an editor listing the last 3 commits, oldest first:

Plaintext
pick a1b2c3d Add login form
pick e4f5g6h fix typo
pick i7j8k9l Add password validation

# Commands:
# p, pick <commit> = use commit as-is
# r, reword <commit> = use commit, but edit the message
# s, squash <commit> = meld into previous commit
# f, fixup <commit> = like squash, but discard this commit's message
# d, drop <commit> = remove commit entirely

To fold the "fix typo" commit into the one before it, change pick to squash (or fixup to also discard its message):

Plaintext
pick a1b2c3d Add login form
fixup e4f5g6h fix typo
pick i7j8k9l Add password validation

Saving and closing the editor replays the commits according to your instructions, resulting in two clean commits instead of three — the "fix typo" commit's changes are folded into "Add login form" without a separate, noisy entry in the history.

Inspecting history: git log and git diff

Bash
# Compact, one-line-per-commit view
$ git log --oneline
i7j8k9l Add password validation
a1b2c3d Add login form
3f2a1b9 Add initial README

# Show a branching graph across all branches
$ git log --oneline --graph --all

# See exactly what changed in one commit
$ git show a1b2c3d

# Compare working directory against the last commit
$ git diff

# Compare the staging area against the last commit
$ git diff --staged

# Compare two branches directly
$ git diff main feature/user-login

git diff with no arguments only shows unstaged changes — a common point of confusion is running git diff after git add and seeing nothing, because the change is now staged; git diff --staged (or --cached) is what shows staged-but-uncommitted changes.

Common mistakes

  • Rebasing a branch that others have already pulled from, forcing everyone else to reconcile diverged history (git push --force on a shared branch is a strong warning sign this happened).
  • Treating rebase and merge as interchangeable — they produce different history shapes, and a team should generally agree on one convention (e.g., "rebase feature branches before opening a PR, merge PRs with a merge commit") rather than mixing styles inconsistently.
  • Forgetting that a rebase can hit the same conflicts a merge would — git rebase pauses on each conflicting commit exactly like a merge, with git rebase --continue (after resolving) or git rebase --abort (to bail out) as the way forward.

Interview questions

Q: When would you use rebase instead of merge, and why? Rebase is best for cleaning up your own local, unpushed feature branch before merging or opening a pull request — it produces a linear history with no merge commits. It should never be used on commits that have already been pushed and potentially pulled by others, because rebase rewrites commit hashes, and rewriting shared history forces everyone else to reconcile diverged histories.

Q: What does interactive rebase (git rebase -i) let you do? It lets you rewrite your recent, unpushed commit history before sharing it — reordering commits, rewording messages, squashing/fixing up multiple small commits into one clean commit, or dropping a commit entirely. It's commonly used to turn a messy sequence of "wip," "fix typo," and "actually fix it" commits into one clean, reviewable commit.

Q: Why might git diff show nothing right after you've made changes, even though git status shows modified files? git diff with no flags only shows unstaged changes against the last commit. If the changes were already staged with git add, they won't appear there — git diff --staged (or --cached) shows the diff between the staging area and the last commit instead.