---
title: 'Interactive Rebase'
source: 'https://academia.sh/en/courses/advanced-git/interactive-rebase'
course: 'Advanced Git'
language: en
updated: '2026-08-17T18:10:43+00:00'
license: 'CC BY-SA 4.0'
---

# Interactive Rebase

Changing a commit changes the identity of every commit after it; rewriting from the middle touches 252, 362, 3002, and 11322 objects in 50-, 200-, 1000-, and 4000-commit histories respectively, and the ratio stays at roughly half at every scale.

The previous course read history with a single measure: an integration format's value was
not the code it produced but the number of questions the history it left behind could
answer. Four formats produced the same code, and the six questions were answered five,
three, two, and three times respectively. In that course, history was a **given record**:
questions were asked of it, answers were counted. Once the record was written, changing
it was not on the table.

This course treats the same record as an object. History can be rewritten: a commit
message can be fixed, two commits can be reduced to one, the order can be changed, a
commit can be dropped entirely. The measure changes along with it. There, what was
measured was the **answered question**; here, what is measured is the **touched
object**. This lesson's question is this: how much of history does going back and
changing a commit touch, and does that ratio drop as history grows?

## Identity Propagates Through the Chain

A commit's identity is derived from its content. The fields that go into the identity
are fixed: the file tree at that point, the author, the committer, the timestamp, the
message, and the **parent's identity**. This last field determines this entire course.
When a parent's identity changes, the child's identity is forced to change too, because
the child's content carries the parent's identity. This is unavoidable because identity
is produced from content: there is no way to keep the same identity while changing the
content, because if there were, an identity would not be an identity.

The consequence can be written in one sentence: **changing a commit changes the identity
of every commit after it.** In this respect there is no difference between fixing a
single typo and removing an entire file from history; both regenerate everything from
that point onward. The **size** of the change does not determine the cost — its
**location** does.

The old commits are not deleted at this point. They stay in the repository; because no
branch or tag points to them anymore, they become **unreachable**. Being unreachable is
not the same as ceasing to exist, and this distinction is the entire subject of this
topic's fourth lesson. For now, this much is enough: rewriting is not a deletion but a
**duplication** — the old chain stays in place, a new chain is written beside it, and the
branch tip points to the new one.

## Interactive Mode

Rebase was established in the previous course as an **integration format**: the
branch's commits are turned into patches and applied in order onto a new base. That
comparison is not repeated here. What is new is the same family's **interactive** mode,
which never appeared there.

In interactive mode, the tool does not apply the patches directly. It first puts the
commits to be staged in front of you as a list; each line in the list carries a commit
and the action to be applied to it. Once the list is saved and closed, the tool executes
the lines **top to bottom**.

```text
# taught command and example dump — not executed

$ git rebase -i HEAD~3
pick 9c4d7e1 metrics: print threshold value
pick 4a5b6c7 report: fix field width
pick 5b6c7d8 report: add summary line

# r, reword = keep the commit, edit its message
# s, squash = merge the commit into its predecessor
# d, drop   = drop the commit
```

Four actions form this lesson's measurement axis. **Reword** keeps the commit and
changes only its message. **Reorder** changes the lines' positions; the order in the
list becomes the order in history. **Squash** merges a commit into its predecessor and
reduces the resulting commit count by one. **Drop** removes the commit entirely, and
that too reduces the count by one.

The four do very different jobs: one only fixes a piece of text, another removes a
change from history entirely. What the measurement asks is whether this difference is
reflected in the **cost**.

One of these actions was already established on its own in the Introduction to Version
Control course: the ability to amend the last commit in place produces a new commit from
the staging area's current state and leaves the old one unreachable. That, too, was a
rewrite, only it was the **shallowest** kind — it sat at the tip of history and had no
descendant. Interactive mode does the same job anywhere in history; when it is done on a
commit that has descendants, the cost comes along with it.

A patch may fail to apply while the plan is being executed. The mechanics of this case
were already established with the rebase format in the previous course and are not
repeated here. Two points are specific to interactive mode: on a squash line, the tool
puts the merged commits' messages in front of you in a single text and waits for you to
fix it; and if the operation is canceled midway, the branch tip returns to where it was
before entering the plan. Canceling is the one intact exit available while the plan is
running.

## Where the Cost Starts

Interactive mode asks how many commits you will put on the list. If you plan the last
three commits, the tool shows three lines and the rewrite starts from the third commit.
If you want to fix a commit in the middle of history, you are forced to put **everything**
written since that commit into the list — even if only one line needs fixing.

The cost is therefore measured not by the number of actions in the plan but by **where
the plan starts**. The measurement asks this in two separate ways: four different
actions at the same position, and the same action at four different depths.

Two limits of the plan should be stated here. First, commits below the listed lines
cannot be touched; once a starting point is chosen, anything before it is outside the
plan, and going further back means a new plan. Second, if there is a fork edge in
history, a linear plan does not carry it as it is: merge commits are flattened unless
requested otherwise, and the two-parent structure collapses to a single parent. The
measurement's setup is linear, so this second limit does not enter the measurement; but
it needs to be known before building a plan in a repository whose history has a fork.

The measurement's assumptions:

- **RH1** — The setup is a single history built at four scales: **50, 200, 1000, 4000**
  commits. Each commit touches one of five files; one in eleven also carries a binary
  asset, and every version of that asset stays in history as a separate object.
- **RH2** — A commit's object cost is **2** for the tree and blob; if it carries a binary
  asset, **40** is added to that. The count is the repository's object count, not its
  file count.
- **RH3** — Changing a commit changes the identity of every commit after it. The
  touched set is the closed interval from the changed position to the tip of history.
- **RH4** — "Rewriting from the middle" means the position `n // 2`; the rule does not
  change even as the scale changes.
- **RH5** — Interactive mode's four actions differ only in **the length of the
  result**: squash and drop remove one commit, reword and reorder do not. The touched
  set is the same in all four.
- **RH6** — This lesson's unit of cost is the **touched object**. The number of commits
  whose identity changes is not a cost, it is the width of the touched set; the step
  unit is not used in this lesson.
- **RH7** — If the number of commits staged is `k`, the rewrite starts at position
  `n - k + 1`.
- **RH8** — The set's resolution at the four scales is **1/50, 1/200, 1/1000, 1/4000**
  respectively.

## Measurement

```python
"""Interactive rebase: the cost of rewriting from the middle.

Part 1 - rewriting from the middle at four scales: commits, objects, ratio.
Part 2 - interactive mode's four actions at the same position.
Part 3 - the number of commits staged determines the cost.
"""
SEED = 20260814
FILES = ("metrics.py", "report.py", "identity.py", "config.py", "document.md")
SCALES = (50, 200, 1000, 4000)
ACTIONS = ("reword", "reorder", "squash", "drop")


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def history(n, seed=SEED):
    """n-commit linear history; each commit touches one file."""
    draw, commits = rng(seed), []
    for i in range(n):
        file = FILES[draw(5)]
        binary = draw(11) == 0
        commits.append({"no": i + 1, "file": file, "binary": binary,
                         "objects": 2 + (40 if binary else 0)})
    return commits


def rewrite(t, position):
    """Changing a commit changes the identity of every commit after it."""
    after = [x for x in t if x["no"] >= position]
    return len(after), sum(x["objects"] for x in after)


def clone_cost(t):
    return sum(x["objects"] for x in t)


def interactive(t, position, action):
    """Four actions: only the result's length changes, the touched set does not."""
    changed, touched = rewrite(t, position)
    return len(t) - (1 if action in ("squash", "drop") else 0), changed, touched


print(f"{'commits':>7s} {'rewritten':>16s} {'touched objects':>16s}"
      f" {'ratio':>6s} {'object ratio':>12s}")
for n in SCALES:
    t = history(n)
    changed, touched = rewrite(t, n // 2)
    print(f"{n:7d} {changed:16d} {touched:16d} {changed / n:6.3f}"
          f" {touched / clone_cost(t):12.3f}")

print()
T = history(1000)
print(f"{'action':<18s} {'result commits':>14s} {'identity changed':>17s}"
      f" {'touched objects':>16s}")
for a in ACTIONS:
    remaining, changed, touched = interactive(T, 500, a)
    print(f"{a:<18s} {remaining:14d} {changed:17d} {touched:16d}")

print()
print(f"{'staged':>13s} {'starting position':>17s} {'identity changed':>17s}"
      f" {'touched objects':>16s} {'ratio':>6s}")
for k in (3, 10, 50, 500):
    changed, touched = rewrite(T, len(T) - k + 1)
    print(f"{k:13d} {len(T) - k + 1:17d} {changed:17d} {touched:16d}"
          f" {changed / len(T):6.3f}")
```

```
commits        rewritten  touched objects  ratio object ratio
     50               26              252  0.520        0.663
    200              101              362  0.505        0.393
   1000              501             3002  0.501        0.532
   4000             2001            11322  0.500        0.501

action             result commits  identity changed  touched objects
reword                       1000               501             3002
reorder                      1000               501             3002
squash                        999               501             3002
drop                          999               501             3002

       staged starting position  identity changed  touched objects  ratio
            3               998                 3                6  0.003
           10               991                10               20  0.010
           50               951                50              260  0.050
          500               501               500             3000  0.500
```

## Rewriting Does Not Get Proportionally Cheaper

The top table does the same operation at four scales: changing the commit in the middle
of history. Touched objects climb from **252** to **11322** — forty-five times. The
ratio column on the right does not budge: **0.520 / 0.505 / 0.501 / 0.500**. As history
grows eightyfold, rewriting from the middle still touches roughly **half** of the
commits.

This is a counterintuitive result. One might expect that fixing a single commit in a
growing history would become **proportionally** smaller work; the opposite happens.
Because the ratio stays constant, the absolute cost grows linearly with history.
**Rewriting does not get proportionally cheaper;** the only thing that gets cheaper is
staying close to the tip.

The object-ratio column carries a separate warning. While the commit ratio approaches
0.500, the object ratio bounces around as **0.663 / 0.393 / 0.532 / 0.501**. At small
scales, where the commits carrying a binary asset happen to fall decides the result: in
the fifty-commit history most of those commits fell in the second half, in the two
hundred-commit one most fell in the first half. Counting commits and counting objects
are not the same thing, and this distinction is itself the measure of the
`large-repositories` topic. Here only this is recorded: **the ratio depends on the unit
it is counted in**, and this lesson's unit is the object.

The middle table answers the second question. When the four actions are applied at the
same position, **identity changed is 501**, **touched objects is 3002** — the same in
all four. The one column that diverges is the resulting commit count: squash and drop
give **999**, the other two give **1000**. So fixing a typo in a message and removing a
commit from history entirely have **the same cost**. The tool does not care which action
you chose; it cares where you started.

The bottom table measures this from the other direction. When **3** commits are staged,
touched objects are **6**; at **10**, **20**; at **50**, **260**; at **500**, **3000**. A
fifty-commit plan's object cost is thirteen times a ten-commit plan's, while its commit
count is only five times as large — the difference comes from the binary assets that
fall within the staged range. Commits near the tip are therefore cheap, and this is
exactly where interactive mode's everyday use lies: tidying up the last few commits that
have not yet been shared.

Read together, the three tables give a single rule: **keep the plan close to the tip.**
The rule has both a cost side and a risk side. The cost side has been measured — a
three-commit plan touches six objects, a five-hundred-commit plan touches three thousand.
The risk side has not been measured yet: the further back the staged range reaches, the
higher the chance that the commits in that range have already been picked up by others.
Both sides point in the same direction and say the same thing; the second one is
measured in this topic's third lesson.

## Before Rewriting

Rewriting is not a reversible operation; the old chain staying unreachable does not mean
there is no way to bring it back, but it does mean you will go **looking** for one.
Three habits make that search unnecessary.

**Backup branch.** Opening a branch that points at the branch's current tip before the
operation is a single line, and it reduces recovery to rolling back one pointer. A
backup branch is not a local record; it is an ordinary branch, and it stays until
deleted.

**Dry run.** There are comparison abilities that show how far two chains have diverged
before the plan is applied. Placing the rewritten chain next to its backup side by side
shows which commit actually changed and which one only changed identity.

**Separate clone.** If a bulk transformation is going to be tried, it is tried on a
separate copy of the repository. This is the next lesson's subject, and there it becomes
a required step.

```text
# taught command and example dump — not executed

$ git branch backup/metrics metrics
$ git rebase -i --onto main 9c4d7e1 metrics
$ git range-diff main...backup/metrics main...metrics
```

The fourth safeguard is the **reflog**: the tool writes every move of a branch tip to a
local log, and the old tip can be read from there. The log has two limits — it is local,
and it is not kept indefinitely. This topic's fourth lesson measures exactly that limit:
how many commits stay reachable after a rewrite, and when the recovery window closes.

## Summary

- A commit's identity contains its parent's identity; this is why changing a commit
  changes the identity of **every commit after it**. What determines the cost is not
  the size of the change but its **location**.
- Interactive mode puts commits into a plan list; the four actions are reword, reorder,
  squash, and drop.
- All four actions at the same position change the identity of **501** commits and
  touch **3002** objects; the only thing that diverges is the resulting commit count
  (**999** or **1000**).
- Rewriting from the middle touches **252 / 362 / 3002 / 11322** objects at four scales
  and the ratio stays at **0.520 / 0.505 / 0.501 / 0.500**: rewriting does not get
  proportionally cheaper.
- The number of commits staged directly determines the cost: **3 / 10 / 50 / 500**-commit
  plans touch **6 / 20 / 260 / 3000** objects respectively.
- Rewriting cannot be undone; a backup branch, a dry run, and a separate clone are the
  three safeguards.

## Next Step

In this lesson rewriting stayed within a single branch's last few commits, and even its
deepest attempt started from the middle of history. Sometimes what is needed is broader
than that: removing every version of a file from history, fixing an author field across
every commit, moving a directory to the root. What these jobs share is that they are
forced to start from the **first** commit of history. The next lesson measures the cost
of this expansion: in a thousand-commit history, how many identities does a
transformation starting from the first commit change, how many objects does it touch,
and what happens to the repository's copies once the operation completes.
