Skip to content

Rebasing

Rewriting history to keep it clean: Rebasing is like rewriting a history book to remove digressions — instead of a messy merge commit that says “combined two timelines,” you get a clean linear narrative. The end result is the same, but the history is easier to follow.

Why it matters: Clean history makes debugging easier (you can git bisect effectively), code review simpler (changes are in logical order), and onboarding faster (new team members can follow the story).

The key insight: Never rebase commits that have been pushed to a shared repository — rewriting published history forces everyone else to reconcile conflicting versions. Rebase only your local, unpushed commits.

Rebasing is the process of replaying a series of commits onto a new base commit. Unlike merging, which creates a new commit with two parents, rebasing rewrites history by creating new commit objects with the same changes but different parent pointers (and therefore different SHA-1 hashes).

Before (both branches diverge from commit B):

gitGraph
    commit id: "B (base)"
    checkout main
    commit id: "D (main)"
    checkout feature
    commit id: "C"
    commit id: "E"

After git merge feature (non-linear history):

gitGraph
    commit id: "B (base)"
    checkout main
    commit id: "D (main)"
    checkout feature
    commit id: "C"
    commit id: "E"
    checkout main
    merge feature id: "F (merge)"

After git rebase main (linear history):

gitGraph
    commit id: "B (base)"
    commit id: "D (main)"
    commit id: "C" (rebased)"
    commit id: "E' (rebased)"

Note that C' and E' are new commits with different hashes than C and E. The original commits still exist in the object store (reachable via the reflog) but are no longer on any branch.

The git rebase operation performs the following steps:

  1. Find the merge base: Identify the common ancestor between the current branch and the target branch.
  2. Save the current state: Record the current HEAD and branch reference.
  3. Move HEAD to the target branch (e.g., main).
  4. Replay each commit: For each commit in the original branch (from oldest to newest), apply its diff onto the new base, creating a new commit object.
  5. Move the branch pointer to the tip of the new commit chain.
flowchart TD
    A["Find merge base (commit B)"] --> B["Move HEAD to main (commit D)"]
    B --> C["Cherry-pick commit C onto D → create C'"]
    C --> D["Cherry-pick commit E onto C' → create E'"]
    D --> E["Move feature branch to E'"]

    style A fill:#fff3e0
    style E fill:#e8f5e9

Each “replayed” commit is effectively a cherry-pick: Git computes the diff introduced by the original commit and applies it to the new base. This means:

  • If a commit’s changes cleanly apply to the new base, the rebase succeeds.
  • If there are conflicts, Git pauses and asks you to resolve them before continuing.
Terminal window
## Rebase the current branch onto main
$ git switch feature-auth
$ git rebase main
## Rebase a specific branch onto main (without switching)
$ git rebase main feature-auth

If conflicts become unresolvable:

Terminal window
$ git rebase --abort
# Restores the branch to its original state before the rebase

After resolving a conflict:

Terminal window
$ git add <resolved-file>
$ git rebase --continue

If a commit’s changes are no longer relevant (e.g., a fix that was already applied upstream):

Terminal window
$ git rebase --skip

Interactive rebase (git rebase -i) is one of Git’s most powerful features. It allows you to rewrite the commits on your branch: reorder, edit, squash, split, or drop commits.

Terminal window
# Rebase the last 5 commits interactively
$ git rebase -i HEAD~5
# Rebase all commits since diverging from main
$ git rebase -i main

Interactive rebase opens an editor with a todo list:

pick a3f2b1c Add user model
pick b7e9d4f Add authentication middleware
pick c1d2e3f Add login endpoint
pick d4e5f6a Fix auth token expiry
pick e5f6a7b Update tests
# Rebase a3f2b1c..e5f6a7b onto a3f2b1c (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]

The default action. The commit is replayed onto the new base without modification.

Stops at the commit and opens an editor with the current message for editing. The commit’s content (diff) is unchanged.

Stops at the commit, allowing you to:

  • Modify files (git add / git restore)
  • Amend the commit (git commit --amend)
  • Split the commit into multiple commits
  • Continue the rebase (git rebase --continue)
Terminal window
# Example workflow:
# 1. Rebase opens with "edit c1d2e3f Add login endpoint"
# 2. Git stops at this commit
$ git log --oneline -3 # Verify you're at the right point
# 3. Make changes
$ echo "new code" >> src/login.c
$ git add src/login.c
$ git commit --amend # Amend the commit
# 4. Continue
$ git rebase --continue

Melds the commit into the previous commit, combining their diffs and prompting for a new combined message:

# This is a combination of 2 commits.
# This is the 1st commit message:
Add authentication middleware
# This is the commit message #2:
Add login endpoint
# TODO: Edit the combined message
  • Merging: Alternative integration strategy to rebasing, preserving full branch history.
  • Conflict Resolution: Handles merge conflicts that arise during both merging and rebasing.
  • Branching: Branch creation and management fundamentals that underpin rebasing workflows.