Learning Objectives

By the end of this module, you will be able to:

  1. Use git rebase -i to intercept and modify commits before they are replayed
  2. Apply all interactive rebase commands: pick, reword, edit, squash, fixup, drop
  3. Squash messy WIP commits into clean, logical commits before creating a PR
  4. Split a single large commit into multiple focused commits using edit
  5. Leverage --autosquash and fixup!/squash! prefixes for streamlined workflows

1. What Is an Interactive Rebase?

In Module 9, you learned that a regular rebase replays commits onto a new base — automatically, without intervention. An interactive rebase gives you a pause point: before the replay begins, Git opens your editor with a list of commits, and you get to decide what happens to each one.

Think of it as intercepting the rebase to:

  • Reorder commits
  • Drop (delete) commits
  • Squash multiple commits into one
  • Reword commit messages
  • Edit a commit (pause to make additional changes)
  • Split one commit into several

All rebases are interactive behind the scenes. A non-interactive rebase is just an interactive rebase where every commit is set to pick.


2. Launching an Interactive Rebase

git rebase -i <base>

The <base> determines which commits appear in the editor — all commits after <base> up to HEAD.

Common Targets

TargetMeaningWhen to use
git rebase -i mainAll commits since you branched off mainRewriting before merging into main
git rebase -i HEAD~3The last 3 commitsQuick fixups to recent work
git rebase -i HEAD~5The last 5 commitsCleaning up a chunk of history
git rebase -i --rootEvery commit in the repoRewriting from the very beginning (rare)

The most common workflow: you've been working on a topic branch, rebasing onto main regularly to stay current. When you're ready to create a PR, you run git rebase -i main to clean up your commits.

What the Editor Shows

When you run git rebase -i HEAD~4, your editor opens with something like:

pick a1b2c3d Add user model
pick e4f5g6h Add validation — WIP
pick i7j8k9l Fix typo in validation
pick m0n1o2p Add user controller

# Rebase abc1234..m0n1o2p onto abc1234 (4 commands)
#
# Commands:
# p, pick   = use commit
# r, reword = use commit, but edit the commit message
# e, edit   = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup  = like "squash" but discard this commit's message
# d, drop   = remove commit
# x, exec   = run command (the rest of the line) using shell
# b, break  = stop here (continue rebase later with 'git rebase --continue')
#
# These lines can be re-ordered; they are executed from top to bottom.
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.

Key observations:

  • Commits are listed oldest first (top to bottom) — the reverse of git log
  • Each line starts with a command (pick by default), followed by the short SHA and message
  • Lines starting with # are comments (instructions for you, ignored by Git)
  • Deleting all non-comment lines aborts the rebase

Configuring Short Commands

By default, Git shows the full command names (pick, reword, etc.). You can switch to abbreviations:

git config --global rebase.abbreviateCommands true

Now the editor shows p, r, f, etc. — faster to read and edit once you know them.

Configuring the Sequence Editor

You can use a different editor just for interactive rebase (while keeping your regular editor for commit messages):

git config --global sequence.editor "code --wait"

3. The Commands in Detail

pick (p) — Use the Commit As-Is

pick a1b2c3d Add user model

This is the default. The commit is replayed without changes. You only need to change this if you want to do something different.

reword (r) — Change the Commit Message

reword a1b2c3d Add user model

Git replays the commit, then opens your editor so you can rewrite the message. The code changes stay the same — only the message changes.

Use this to:

  • Fix typos in commit messages
  • Add ticket numbers you forgot
  • Rewrite unclear messages before a PR review

edit (e) — Pause at This Commit

edit a1b2c3d Add user model

Git replays the commit, then stops. You're dropped into the terminal at that point in history. You can:

  • Make additional changes and git commit --amend
  • Split the commit into multiple commits (see Section 6)
  • Run tests to verify the commit works in isolation

When you're done, run git rebase --continue to proceed.

squash (s) — Meld into Previous Commit (Keep Messages)

pick   a1b2c3d Add user model
squash e4f5g6h Add validation — WIP

The second commit is fused into the first. Git opens your editor showing both commit messages, allowing you to combine or rewrite them.

# This is a combination of 2 commits.
# This is the 1st commit message:
Add user model

# This is the commit message #2:
Add validation — WIP

# Please enter the commit message for your changes.

fixup (f) — Meld into Previous (Discard Message)

pick  a1b2c3d Add user model
fixup e4f5g6h Add validation — WIP

Same as squash, but the second commit's message is discarded. No editor opens — the result simply uses the first commit's message. This is the most common squash variant.

Important: You cannot fixup or squash the first commit in the list — there's no "previous commit" to meld into. The first commit must be pick (or reword).

drop (d) — Remove the Commit

drop i7j8k9l Fix typo in validation

The commit is thrown away entirely. Equivalent to deleting the line. The changes from this commit will not appear in the result.

exec (x) — Run a Shell Command

pick a1b2c3d Add user model
exec npm test
pick e4f5g6h Add validation
exec npm test

After replaying each commit, Git runs the specified command. If the command fails (non-zero exit code), the rebase pauses so you can investigate. Useful for verifying that each commit passes tests independently.

break (b) — Pause Here

pick a1b2c3d Add user model
break
pick e4f5g6h Add validation

Git stops after replaying the first commit. You can inspect the state, run tests, or make changes. Continue with git rebase --continue.


4. Common Scenarios

Scenario 1: Squash WIP Commits

You've been committing "work in progress" saves throughout the day:

pick a1b2c3d WIP: start user auth
pick e4f5g6h WIP: more auth work
pick i7j8k9l WIP: auth almost done
pick m0n1o2p WIP: fix tests
pick q3r4s5t WIP: final cleanup

You want one clean commit:

pick   a1b2c3d WIP: start user auth
fixup  e4f5g6h WIP: more auth work
fixup  i7j8k9l WIP: auth almost done
fixup  m0n1o2p WIP: fix tests
fixup  q3r4s5t WIP: final cleanup

After saving, Git melds all five into one commit with the first message. Since the message is "WIP: start user auth," you'll probably want reword instead of pick on the first line:

reword a1b2c3d WIP: start user auth
fixup  e4f5g6h WIP: more auth work
fixup  i7j8k9l WIP: auth almost done
fixup  m0n1o2p WIP: fix tests
fixup  q3r4s5t WIP: final cleanup

Now Git pauses to let you write a proper message like "Add user authentication with JWT."

Scenario 2: Reorder Commits

Your commits tell the story in the wrong order:

pick a1b2c3d Add API endpoint
pick e4f5g6h Add database migration
pick i7j8k9l Add data model

Logically, the model should come first, then the migration, then the endpoint:

pick i7j8k9l Add data model
pick e4f5g6h Add database migration
pick a1b2c3d Add API endpoint

Just rearrange the lines. Git replays them in the new order. Conflicts may arise if the commits modify the same files.

Scenario 3: Drop an Accidental Commit

You accidentally committed a debug log statement:

pick a1b2c3d Add search feature
pick e4f5g6h Add console.log everywhere for debugging
pick i7j8k9l Remove most console.logs
pick m0n1o2p Fix edge case in search

Drop the debug commits:

pick a1b2c3d Add search feature
drop e4f5g6h Add console.log everywhere for debugging
drop i7j8k9l Remove most console.logs
pick m0n1o2p Fix edge case in search

Or simply delete those two lines from the editor.

Scenario 4: Craft a Story from Chaos

Your actual work was messy, but you want the PR to tell a clear story:

# Actual history (messy):
pick a1b Add user model
pick b2c Fix model typo
pick c3d Add controller
pick d4e WIP
pick e5f Fix controller bug
pick f6g Add tests
pick g7h Fix flaky test

# Rewritten (clean story):
pick   a1b Add user model
fixup  b2c Fix model typo
pick   c3d Add controller
fixup  d4e WIP
fixup  e5f Fix controller bug
pick   f6g Add tests
fixup  g7h Fix flaky test

Result: three clean commits — "Add user model," "Add controller," "Add tests."


5. --autosquash and Magic Prefixes

Manually editing the interactive rebase TODO list works, but Git offers a shortcut for the most common operation (fixup).

The fixup! Prefix

When you realize you need to fix something from a previous commit, create a new commit with a message starting with fixup! followed by the original commit's message:

# Original commit was: "Add user model"
# You notice a bug in the model. Fix it, then:
git commit -m "fixup! Add user model"

Now when you run git rebase -i --autosquash, Git automatically rearranges the TODO list:

pick   a1b2c3d Add user model
fixup  x9y0z1a fixup! Add user model    ← moved here automatically
pick   e4f5g6h Add controller
pick   m0n1o2p Add tests

No manual editing needed. Save and exit.

The squash! Prefix

Same as fixup! but preserves the commit message (like squash vs fixup):

git commit -m "squash! Add user model"

Making Autosquash the Default

git config --global rebase.autoSquash true

With this set, every git rebase -i automatically reorders fixup! and squash! commits. You can disable it for a specific rebase with --no-autosquash.

git commit --fixup

Instead of manually typing the fixup! prefix, use:

git commit --fixup <commit-sha>

Git automatically creates a commit with the message fixup! <original message>. Combined with --autosquash, this makes incremental fixes almost effortless:

# Fix something from commit abc1234
git add fixed-file.py
git commit --fixup abc1234
# Later:
git rebase -i --autosquash main
# The fixup is automatically placed after abc1234

6. Splitting a Commit with edit

Sometimes a commit does too much. You want to split it into two or more focused commits.

Step by Step

# 1. Start interactive rebase
git rebase -i HEAD~3
 
# 2. Mark the commit to split as 'edit':
#    edit a1b2c3d Add model and controller (too big!)
#    pick e4f5g6h Add tests
 
# 3. Save and exit. Git replays up to the marked commit and stops.
 
# 4. Undo the commit but keep all changes staged:
git reset HEAD~1
 
# 5. Now selectively stage and commit:
git add model.py
git commit -m "Add user model"
 
git add controller.py
git commit -m "Add user controller"
 
# 6. Continue the rebase:
git rebase --continue

The single commit is now two separate commits in your history.

Before:
  ... ← [Add model and controller] ← [Add tests]

After:
  ... ← [Add user model] ← [Add user controller] ← [Add tests]

7. --autostash: Rebase with Dirty Working Directory

Normally, Git refuses to rebase if you have uncommitted changes. --autostash tells Git to stash your changes automatically, rebase, then pop the stash:

git rebase -i main --autostash

Or set it as the default:

git config --global rebase.autoStash true

With this, you never need to manually stash before rebasing. Git handles it transparently.


8. The gwip / gunwip Pattern

A practical workflow from the transcript: use "work in progress" commits throughout the day, then clean up before the PR.

gwip — Save Everything as WIP

# Alias: stages everything and commits with "WIP [skip ci]"
git add -A && git commit -m "--wip-- [skip ci]"

The [skip ci] tag tells CI servers not to run builds on this throwaway commit. Push freely — even if code doesn't compile. Your branch is your scratch pad.

gunwip — Undo the Last WIP Commit

# If the last commit message contains "--wip--":
git log -1 --format='%s' | grep -q '\-\-wip\-\-' && git reset HEAD~1

This removes the last commit but keeps all changes in your working directory, ready for proper staging and committing.

The Full Workflow

# Throughout the day:
gwip           # Save current state
git push       # Back up to remote
 
gwip           # Save again later
git push       # Back up again
 
# When ready to create PR:
gunwip                        # Undo last WIP
git reset main                # Collapse ALL commits since main (soft reset)
                              # All changes are now unstaged
git add model.py
git commit -m "Add user model"
git add controller.py tests/
git commit -m "Add controller with tests"
git push --force-with-lease   # Replace messy WIP history with clean commits

Why Not Just Interactive Rebase?

You can use git rebase -i main with fixups instead of git reset main. Both achieve the same result. The reset approach is faster when you want to completely restructure your commits from scratch. Interactive rebase is better when you want to keep most commits and only tweak a few.


Command Reference

CommandDescription
git rebase -i <base>Start interactive rebase
git rebase -i HEAD~NInteractively rebase last N commits
git rebase -i --autosquashAuto-arrange fixup!/squash! commits
git rebase -i --autostashAuto-stash dirty working directory
git commit --fixup <sha>Create a fixup commit targeting <sha>
git commit --squash <sha>Create a squash commit targeting <sha>
git rebase --continueContinue after resolving conflict or edit
git rebase --abortCancel the rebase, restore original state
git rebase --skipSkip current commit
git config rebase.abbreviateCommands trueShow short command names (p, r, f, etc.)
git config rebase.autoSquash trueAlways auto-arrange fixup!/squash! commits
git config rebase.autoStash trueAlways auto-stash before rebase
git config sequence.editor "<editor>"Set editor for interactive rebase only

Hands-On Lab: From Chaos to Clean History

Setup — Create a Messy Branch

mkdir rebase-interactive-lab && cd rebase-interactive-lab
git init
git switch -c main

Create the base:

echo "# My App" > README.md
git add README.md
git commit -m "Initial commit"

Create a messy feature branch:

git switch -c feature-messy
 
echo "class User:" > user.py
echo "    pass" >> user.py
git add user.py
git commit -m "WIP: start user class"
 
echo "    def __init__(self, name):" >> user.py
echo "        self.name = name" >> user.py
git add user.py
git commit -m "WIP: add constructor"
 
echo "console.log('debug');" > debug.txt
git add debug.txt
git commit -m "Temporary debug file"
 
echo "    def greet(self):" >> user.py
echo "        return f'Hello, {self.name}'" >> user.py
git add user.py
git commit -m "WIP: add greet method"
 
sed -i '' 's/pass/# User model/' user.py
git add user.py
git commit -m "Fix: remove pass statement"
 
echo "import unittest" > test_user.py
echo "class TestUser(unittest.TestCase):" >> test_user.py
echo "    def test_greet(self):" >> test_user.py
echo "        u = User('Alice')" >> test_user.py
echo "        self.assertEqual(u.greet(), 'Hello, Alice')" >> test_user.py
git add test_user.py
git commit -m "Add user tests"
 
echo "    def test_name(self):" >> test_user.py
echo "        u = User('Bob')" >> test_user.py
echo "        self.assertEqual(u.name, 'Bob')" >> test_user.py
git add test_user.py
git commit -m "WIP: another test"
 
echo "# User Management Module" > README.md
git add README.md
git commit -m "Update README"

Step 1 — Inspect the mess

git log --oneline

Checkpoint: You should see 8 commits on the feature branch (plus the initial commit on main). The history is cluttered with WIP messages, a debug file, and scattered changes.

Part A: Drop the Debug Commit

Step 2 — Start interactive rebase

git rebase -i main

Step 3 — In the editor, find the "Temporary debug file" commit and change pick to drop (or delete the line entirely). Save and exit.

git log --oneline
ls

Checkpoint: The commit is gone. debug.txt no longer exists. You should now have 7 commits.

Part B: Squash the WIP Commits

Step 4 — Rebase again to squash the user.py WIP commits

git rebase -i main

Edit the TODO list to consolidate:

pick   <sha> WIP: start user class
fixup  <sha> WIP: add constructor
fixup  <sha> WIP: add greet method
fixup  <sha> Fix: remove pass statement
pick   <sha> Add user tests
fixup  <sha> WIP: another test
pick   <sha> Update README

Save and exit.

git log --oneline

Checkpoint: You should now have 3 commits. The WIP commits are gone. The user.py commit contains all the user model code.

Part C: Reword a Message

Step 5 — The first commit still says "WIP: start user class" — let's fix it

git rebase -i main

Change the first line from pick to reword. Save and exit. When the editor opens for the message, change it to:

Add User model with greeting functionality
git log --oneline

Checkpoint: Three clean commits with clear messages.

Part D: Use --autosquash

Step 6 — Practice the fixup! prefix workflow

# Add a bug to user.py
echo "    # TODO: add validation" >> user.py
git add user.py
git commit -m "Add TODO comment to User"
 
# Now fix it using --fixup
sed -i '' '/# TODO/d' user.py
git add user.py
git commit --fixup HEAD~1
git log --oneline

You should see a fixup! Add TODO comment to User commit.

Step 7 — Run autosquash

git rebase -i --autosquash main

Checkpoint: The editor should show the fixup commit already moved next to its target. Just save and exit — the fixup is applied automatically.

Part E: Split a Commit with edit

Step 8 — Suppose the "Add user tests" commit should be two separate commits (one per test)

git rebase -i main

Change the test commit line from pick to edit. Save and exit. Git stops at that commit.

Step 9 — Split it

# Undo the commit, keeping changes staged
git reset HEAD~1
 
# Check what's there
git status
git diff test_user.py

Now create two commits from the changes:

# Stage only the first test (lines with test_greet)
git add -p test_user.py
# Select the hunk containing test_greet, skip test_name
 
git commit -m "Add greet test"
 
git add test_user.py
git commit -m "Add name test"
 
git rebase --continue
git log --oneline

Checkpoint: The single test commit is now two separate commits.

Cleanup

cd ..
rm -rf rebase-interactive-lab

Challenge

Create a branch with 10+ commits (mix of WIP saves, typo fixes, debug additions, and actual feature work). Using a single git rebase -i, transform it into exactly 3 commits:

  1. "Add data model"
  2. "Add API endpoint"
  3. "Add tests"

Use a combination of pick, fixup, reword, drop, and reordering.


Common Pitfalls & Troubleshooting

PitfallSymptomFix
Fixup/squash on first lineEditor highlights line in red, rebase failsFirst commit must be pick or reword — it needs something to meld into
Deleting all lines"Nothing to do" — rebase abortsThis is actually the intended abort mechanism; re-run if accidental
Reordering causes conflictsConflicts on every commitCommits that touch the same file in sequence may conflict when reordered; resolve or reconsider the order
Forgetting --continue after editStuck in detached-looking stategit status will tell you; run git rebase --continue
Losing a commit by dropping itChange is gone from historyUse git reflog to find the SHA, then git cherry-pick it back
Huge interactive rebase listEditor shows 100+ commitsUse HEAD~N to limit scope, or rebase in stages
Autosquash doesn't workfixup! commits not rearrangedEither pass --autosquash explicitly or set rebase.autoSquash true

Pro Tips

  1. Set autosquash globally — It's always helpful, never harmful:

    git config --global rebase.autoSquash true
  2. Use git commit --fixup religiously — When you notice a bug in a previous commit, don't just commit a fix with a generic message. Use --fixup <sha> and let autosquash handle the rest.

  3. The fastest way to squash everything: If you want to collapse all commits on your branch into one, git reset is faster than interactive rebase:

    git reset main          # Undo all commits, keep changes
    git add -A
    git commit -m "Add complete feature"
  4. Use exec to verify each commit builds:

    git rebase -i main --exec "make test"

    This runs make test after every replayed commit. If any commit breaks the build, the rebase pauses.

  5. VS Code has interactive rebase support — Set sequence.editor to code --wait and you get a visual interface with dropdowns for each command instead of text editing.

  6. Commit early, commit often, rewrite later — Interactive rebase exists so you don't have to write perfect commits on the first try. Treat your local branch as a scratchpad. Clean it up before sharing.


Quiz / Self-Assessment

Q1: What is the difference between squash and fixup in an interactive rebase?

Answer

Both meld a commit into the previous one. squash opens your editor showing both commit messages so you can combine them. fixup silently discards the melded commit's message and keeps only the previous commit's message. fixup is more common because you usually want to keep the original message.

Q2: Why can't you use fixup or squash on the first commit in the TODO list?

Answer

fixup and squash meld a commit into the previous one. The first commit in the list has no previous commit within the rebase scope, so there's nothing to meld into. The first commit must be pick, reword, or edit.

Q3: What happens if you delete all lines from the interactive rebase editor?

Answer

The rebase is aborted. Git treats an empty TODO list as a signal that you changed your mind. Your branch is left exactly as it was before the rebase started.

Q4: Describe how --autosquash works with fixup! prefixed commits.

Answer

When you create a commit with a message starting with fixup! followed by an existing commit's message (e.g., fixup! Add user model), Git recognizes this during git rebase -i --autosquash. It automatically reorders the TODO list so the fixup commit is placed immediately after its target commit, and sets its command to fixup. You just save and exit — no manual editing needed.

Q5: How do you split a single commit into multiple commits during an interactive rebase?

Answer
  1. Mark the commit as edit in the interactive rebase TODO list
  2. When Git stops at that commit, run git reset HEAD~1 to undo the commit but keep changes in the working directory
  3. Selectively stage and commit changes in whatever groupings you want (use git add -p for partial staging)
  4. Run git rebase --continue when done

Q6: What does git rebase -i main --exec "npm test" do?

Answer

It starts an interactive rebase against main and inserts exec npm test after every commit in the TODO list. As Git replays each commit, it runs npm test afterwards. If any test fails, the rebase pauses so you can investigate. This ensures every commit in your history independently passes tests.

Q7: You have 15 WIP commits on your feature branch. What is the fastest way to collapse them all into a single commit?

Answer

Use git reset instead of interactive rebase:

git reset main          # Undo all commits, keep changes unstaged
git add -A
git commit -m "Add complete feature"

This is faster than opening the interactive rebase editor and marking 14 commits as fixup. However, if you want to keep some commits separate (e.g., 3 logical groups), interactive rebase gives you more control.

Q8: What does rebase.autoStash true do?

Answer

When set, Git automatically runs git stash before starting a rebase (if you have uncommitted changes) and git stash pop after the rebase completes. This means you don't need to manually stash/unstash your work-in-progress changes before rebasing.

Q9: In the interactive rebase editor, commits are listed oldest-first (top to bottom). Why is this the opposite of git log?

Answer

Because the editor represents the execution order. Git replays commits from top to bottom — the oldest commit is replayed first, then the next, and so on. git log shows the most recent commit first because you typically care about what happened recently. The rebase editor shows the execution order because you're programming a sequence of actions.

Q10: You accidentally dropped a commit during an interactive rebase and already completed the rebase. Can you recover it?

Answer

Yes. The commit still exists in Git's object database (it just has no branch pointing to it). Use git reflog to find the SHA of the dropped commit, then git cherry-pick <sha> to bring it back. Reflog entries are kept for a configurable period (default: 90 days), so act before garbage collection removes the orphaned commit.