Scenario
Section titled “Scenario”The feature works. The history looks like this:
typo fix / docstringremove debug printadd testwipAdd discount functionA 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.
Objective
Section titled “Objective”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.
Prerequisites
Section titled “Prerequisites”- Interactive rebase — the todo list and its commands
- This branch has not been pushed or reviewed. That is the one precondition for rewriting it.
Starting state
Section titled “Starting state”mkdir -p /tmp/lab-cleanup && cd /tmp/lab-cleanupgit 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/discountsecho "def discount(price, pct): return price * (1 - pct)" > discounts.pygit add . && git commit -q -m "Add discount function"echo "print('debug')" >> discounts.pygit commit -qam "wip"echo "def test_discount(): assert discount(100, 0.1) == 90" > test_discounts.pygit add . && git commit -q -m "add test"sed -i '/debug/d' discounts.pygit commit -qam "remove debug print"printf 'def discount(price, pct):\n """Apply a percentage discount."""\n return price * (1 - pct)\n' > discounts.pygit commit -qam "typo fix / docstring"
git log --oneline main..-
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 -
Plan the todo list. Decide, on paper, which commits fold into which. Rule:
fixupandsquashfold a commit into the one above it in the list. -
Open the rebase:
Terminal window git rebase -i mainEdit the list so it reads (your SHAs will differ):
pick 8fa04ce Add discount functionfixup c057cf2 wipfixup 6cb6117 remove debug printfixup dfcaffb typo fix / docstringpick 1de55ac add testNote that
add testhas moved below the three fixups. Save and close. -
Check the result:
Terminal window git log --oneline main..git diff --stat main...HEADTwo commits. Same diff.
-
Reword the test commit so it reads like the first one. Open the rebase again and change
picktorewordonadd test; in the editor that follows, make the messageAdd discount test:Terminal window git rebase -i maingit log --oneline main.. -
Confirm nothing was lost:
Terminal window cat discounts.pygit reflog | head -5
Validation
Section titled “Validation”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.
Solution
Section titled “Solution”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 testa4c76ca Add discount function discounts.py | 3 +++ test_discounts.py | 1 + 2 files changed, 4 insertions(+)Step 5:
0834e69 Add discount testa4c76ca Add discount functionStep 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.
Explanation
Section titled “Explanation”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.
Troubleshooting
Section titled “Troubleshooting”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).
Clean up
Section titled “Clean up”cd /tmp && rm -rf lab-cleanupRelated lessons
Section titled “Related lessons”Next lab
Section titled “Next lab”Return to the labs overview — or continue with the 30-Day Modern Git & DevOps Challenge, which uses this lab on day 2.