Skip to content

Lab: Clean Up a Branch with Interactive Rebase

Lesson 4 of 4Intermediate4 min readHands-On Git & GitHub Labs · Workflow Labs
Time15 minutes
LevelIntermediate
You needLocal Git only — no GitHub account needed

The feature works. The history looks like this:

typo fix / docstring
remove debug print
add test
wip
Add discount function

A reviewer has to read “wip” and “remove debug print” and work out that they cancel out. The branch does two things — add a function, add its test — and should say so in two commits.

Use interactive rebase to fold the noise into the commit it belongs to, reword a commit message, and verify with git diff that the branch’s result is byte-for-byte unchanged.

  • Interactive rebase — the todo list and its commands
  • This branch has not been pushed or reviewed. That is the one precondition for rewriting it.
Terminal window
mkdir -p /tmp/lab-cleanup && cd /tmp/lab-cleanup
git init -q -b main .
git config user.email "lab@example.com"
git config user.name "Lab User"
echo "# Shop" > README.md && git add . && git commit -q -m "Initial commit"
git switch -qc feature/discounts
echo "def discount(price, pct): return price * (1 - pct)" > discounts.py
git add . && git commit -q -m "Add discount function"
echo "print('debug')" >> discounts.py
git commit -qam "wip"
echo "def test_discount(): assert discount(100, 0.1) == 90" > test_discounts.py
git add . && git commit -q -m "add test"
sed -i '/debug/d' discounts.py
git commit -qam "remove debug print"
printf 'def discount(price, pct):\n """Apply a percentage discount."""\n return price * (1 - pct)\n' > discounts.py
git commit -qam "typo fix / docstring"
git log --oneline main..
  1. Record what the branch does before touching history — this is the check you will repeat at the end:

    Terminal window
    git diff --stat main...HEAD
  2. Plan the todo list. Decide, on paper, which commits fold into which. Rule: fixup and squash fold a commit into the one above it in the list.

  3. Open the rebase:

    Terminal window
    git rebase -i main

    Edit the list so it reads (your SHAs will differ):

    pick 8fa04ce Add discount function
    fixup c057cf2 wip
    fixup 6cb6117 remove debug print
    fixup dfcaffb typo fix / docstring
    pick 1de55ac add test

    Note that add test has moved below the three fixups. Save and close.

  4. Check the result:

    Terminal window
    git log --oneline main..
    git diff --stat main...HEAD

    Two commits. Same diff.

  5. Reword the test commit so it reads like the first one. Open the rebase again and change pick to reword on add test; in the editor that follows, make the message Add discount test:

    Terminal window
    git rebase -i main
    git log --oneline main..
  6. Confirm nothing was lost:

    Terminal window
    cat discounts.py
    git reflog | head -5

git log --oneline main.. shows exactly two commits, Add discount test above Add discount function. git diff --stat main...HEAD is identical before and after. discounts.py has the docstring and no debug print.

Step 2. Three commits are noise around the first one: the debug print and its removal cancel out, and the docstring belongs with the function. The test is a separate, meaningful change.

Step 3. Reordering is just moving lines. Moving add test below the fixups is what lets the fixups reach the commit above them. If a fixup ends up first in the list, Git refuses — there is nothing above it to fold into.

Step 3, alternative. fixup discards the folded commit’s message; squash keeps it and opens an editor to combine. For noise, fixup.

Step 5. reword stops only to edit the message; the content is untouched.

Step 1:

discounts.py | 3 +++
test_discounts.py | 1 +
2 files changed, 4 insertions(+)

Step 3, on saving the todo:

Successfully rebased and updated refs/heads/feature/discounts.

Step 4:

0e6a05d add test
a4c76ca Add discount function
discounts.py | 3 +++
test_discounts.py | 1 +
2 files changed, 4 insertions(+)

Step 5:

0834e69 Add discount test
a4c76ca Add discount function

Step 6:

def discount(price, pct):
"""Apply a percentage discount."""
return price * (1 - pct)

The reflog still lists all five original commits; they are unreachable, not gone.

Rebase rewrites; it does not have to change content. The diff between main and the branch tip was identical before and after because every change was kept — only the grouping into commits changed. git diff --stat main...HEAD before and after is the proof, and the habit.

Fold noise into what it belongs to. A debug print and its removal are not two events a reviewer needs; they are zero events. The docstring is part of adding the function. fixup expresses exactly that.

One meaningful change per commit is what makes main bisectable and revertable later. Two commits here can each be reverted on their own; five could not.

Do this before review, never during. Review comments attach to commits. Rewriting after a review orphans them. The when-not-to-rebase rule applies with full force.

Git says “cannot ‘fixup’ without a previous commit”. A fixup is first in the list. Move a pick above it.

A conflict during the rebase. Reordering put a change before something it depends on. Resolve, git add, git rebase --continue — or git rebase --abort and reorder differently. The repair-broken-rebase lab covers this fully.

The editor did not open, or opened the wrong one. git config core.editor — set it to something you can use (nano, code --wait, vim).

Terminal window
cd /tmp && rm -rf lab-cleanup

Return to the labs overview — or continue with the 30-Day Modern Git & DevOps Challenge, which uses this lab on day 2.

Choose a learning pathA sequenced route through the curriculum for wherever you are now.