---
title: "Rebase"
description: "Replay commits onto a new base and watch every copied commit receive a new ID."
source: https://vizlearn.app/cs/version-control/rebase
---

# Rebase

Rebase rewrites the current Git branch by replaying its unique commits, oldest first, on a different base. The replacements keep each patch and message, but new parent links give them new object IDs; the target branch never moves. Use it to refresh private feature work, not history teammates already use.

Think of two corrections penciled into an old printing of a book. A new printing arrives with its own edits, so you copy your corrections onto that edition in order. You did not move the old pages. You made replacement pages whose lineage starts from the new printing.

## How rebase works

Suppose `feature` and `main` share a fork point. Git walks from the current `feature` tip back toward that fork point and collects the commits that `main` cannot reach. Walking backward finds the newest commit first, so Git reverses that list before replay. A later patch may depend on a file state created by an earlier one; oldest-first order preserves that dependency.

The replay starts from the target tip. For each original commit, Git computes the change from that commit's parent snapshot to its own snapshot, applies that patch to the temporary replay snapshot, writes a new tree, and writes a new commit. The new commit retains the original author information and message, but its parent is different and its committer information is new. Since parent headers are part of the serialized commit body, the object ID must change even when the patch is byte-for-byte identical.

During this work, `HEAD` is temporarily detached at the target or the latest replayed commit. The named `feature` ref still points to the original tip. Only after every patch succeeds does Git move `feature` once to the rewritten tip and reattach `HEAD`. That delayed ref write is what makes abort sane: the old branch tip remains a clean anchor throughout the attempt.

The original commit objects are not edited or immediately deleted. Once `feature` moves, those objects may become unreachable by branch names, but they can remain in the object database until Git's normal retention and garbage-collection rules remove them.

One real-Git detail matters when the target already contains an equivalent patch. Git may recognize a clean cherry-pick and leave that commit out of the replay queue. `--reapply-cherry-picks` asks it to replay those commits instead. This lesson's scoped engine does not scan upstream history for patch equivalence, so it always queues every current-branch commit after the merge base.

## Rebase step by step

The default trace uses three one-line files so each patch's required before-state and resulting snapshot stay visible: `app.py`, `README.md`, and `config.json`.

1. `commit base` writes the initial snapshot: `app.py` is `print("v1")`, `README.md` is `hello`, and `config.json` is `{"port":3000}`. The new `main` ref advances only after the commit object exists.
2. `branch feature` creates `feature` at the same base commit. `HEAD` remains attached to `main`.
3. `switch feature` attaches `HEAD` to `feature`.
4. `commit feature-app` changes `app.py` from `print("v1")` to `print("feature")`.
5. `commit feature-v2` changes that same line again, from `print("feature")` to `print("feature v2")`. This patch expects the file state created by `feature-app`, so replaying it first would fail immediately.
6. `switch main` reattaches `HEAD` to `main`.
7. `commit main-config` changes only `config.json` to `{"port":8080}`. Now the histories have diverged: `main` owns the config edit, while `feature` owns two other edits.
8. `switch feature` makes `feature` the current branch again.
9. `rebase main` finds the shared base, collects the two `feature` commits, and orders them as `feature-app` then `feature-v2`. A temporary detached `HEAD` starts at `main-config`.

The first replay applies the `app.py` patch to the snapshot under `main-config`. The result keeps main's port `8080`, adds `print("feature")`, and writes a new `feature-app` commit. The second patch can now find the `print("feature")` state it expects, replace it with `print("feature v2")`, and write a new `feature-v2` commit. The order is not bookkeeping: reverse these two corrections and the second one has no matching page to edit.

That is the book analogy doing real work. You copy the first penciled correction onto the new edition before copying a later correction that edits the result of the first.

After the second replay succeeds, `feature` moves once to the rewritten tip. `main` stays on `main-config`, `HEAD` reattaches to `feature`, and the two original feature commits remain stored but no longer branch-reachable. In the visualizer, the old chain fades while the primed chain stays on the new base; the mapping ledger makes each replacement explicit.

## What rebase costs

Let `k` be the number of commits replayed and let `D` be the total amount of file content examined while computing and applying their patches. The history walk costs time proportional to the ancestry traversed, and replay costs roughly `O(D)` for this ordinary single-parent case. Rebase also writes up to `k` new commit objects plus the tree and blob objects required by their resulting snapshots.

The practical cost is rarely the hash calculation. It is conflict work. A clean patch is mechanical; a patch that overlaps a target-side edit stops the sequence and asks you to decide what the file should mean now. Replaying a long branch can therefore turn one large merge conflict into several smaller conflicts spread across the original commits. Whether that is pleasant depends on the quality of those commits. Git cannot rescue a commit titled `stuff` from its author.

The working rule is simple: rebase a private feature branch when you want to update it before review or present a linear series of commits. Prefer a merge, or coordinate explicitly, once teammates have based work on those commit IDs. A tidy graph is not worth ambushing everyone else's clones.

This lesson models a clean local repository, one unique merge base, ordinary non-interactive rebase, and single-parent commits. It uses Git's normal merge-style patch application but omits merge commits, interactive editing, `--onto`, fork-point heuristics, submodules, rename-heavy cases, and conflict continuation. Those features change the planning surface, not the core rule that replacement commits are new objects.

## Rebase edge cases

- **Already based:** if the target tip is already an ancestor of the current tip, there are no commits to transplant. The operation creates zero commits and moves zero refs.
- **Fast-forward boundary:** if the current tip is an ancestor of the target tip, the current branch can move directly to the target. No replacement commit is needed.
- **Conflict on the first patch:** temporary `HEAD` stays at the new base, `REBASE_HEAD` names the failing original commit, and the current branch still names its original tip.
- **Conflict after a successful replay:** temporary `HEAD` points to the last replacement commit. The current branch still has not moved, so `git rebase --abort` can restore the attached original state.
- **Abort and object retention:** abort makes temporary replay commits unreachable; it does not promise to erase their object bytes immediately.
- **Missing or unborn refs:** a target name that does not resolve to a commit, or a current branch with no commit, cannot supply the two tips rebase needs.
- **Clean cherry-pick already upstream:** real Git may drop the equivalent commit from the replay queue; `--reapply-cherry-picks` changes that choice. The visualizer does not model this scan.

## Common Mistakes

- Saying rebase "moves commits." Commit objects are immutable. Rebase reads old commits and writes replacements.
- Expecting the same commit ID because the patch and message match. The parent header changed, so the commit body and hash changed.
- Rebasing the wrong direction. While on `feature`, `git rebase main` rewrites `feature`; it does not move `main` onto `feature`.
- Treating the target branch as an output. A normal branch rebase moves the current branch after success and leaves the target ref alone.
- Assuming a conflict means the branch already points to a partial rewrite. During the stopped state, detached `HEAD` carries partial progress while the branch ref remains at the original tip.
- Rebasing commits that other people already use as shared history, then pushing as if their IDs had not changed. The replacement IDs force everyone else to reconcile two histories that describe similar changes with different objects.
