Interactive Rebase in Practice: Cleaning Up Commits
AI generated
git
HEAD
Git · Interactive Rebase · Commit History · Code Review
Interactive Rebase in Practice
Cleaning up commits before the pull request

Submitting a feature branch with a chaotic commit history for review costs the team time and burns trust. This guide walks step by step through using git rebase -i to reorder, combine, rename, or remove commits, resolve conflicts safely, and undo a botched rebase at any point using reflog.

13 min. read pick · squash · fixup · reword · drop git rebase -i · reflog · conflict resolution

1. Why a messy commit history becomes a problem before the pull request

A feature branch with ten commits like fix, wip, another attempt, and asdf is an ordeal for reviewers. Opening a pull request without cleaning up the history first forces colleagues to wade through intermediate states that never worked on their own and carry no meaningful information. Every commit should compile on its own, be testable, and describe a traceable change, otherwise git blame and git bisect become worthless during the next debugging session because individual commits no longer have any useful granularity.

Interactive rebase before opening a pull request is therefore not optional polish, it is lived practice in virtually every professional Git workflow. The important boundary: as long as a branch exists only locally or belongs exclusively to you, rewriting its history is completely unproblematic. Once other developers have already checked out the same branch, a rebase becomes risky and should be coordinated with the team beforehand, because every rebased commit gets a new checksum and existing local copies end up diverging.

2. Starting interactive rebase: git rebase -i HEAD~n and reading the todo list

git rebase -i HEAD~n starts an interactive rebase over the last n commits, counted from the current HEAD. In practice, the alternative git rebase -i main is often more convenient because it automatically captures exactly the commits that differ from the base branch, without having to count commits beforehand. After invoking it, Git opens the configured editor with a todo list where each commit appears as its own line, sorted chronologically ascending: the oldest commit is at the top, the newest at the bottom.

Each line starts by default with the command pick, followed by the abbreviated commit ID and the first line of the commit message. Below the todo list, Git inserts a detailed comment block listing every available command with its short form and explanation; this block is automatically stripped when saving and does not affect the rebase. The key thing to understand: the file is a list of instructions, not history. Any change, whether deleting a line, changing a command, or swapping the order, is executed directly once the editor is closed.


# Inspect the last 5 commits on the feature branch
git log --oneline -5

a1b2c3d fix typo
e4f5g6h wip: cart totals
h7i8j9k add discount calculation
k1l2m3n wip
n4o5p6q feat: add loyalty discount to cart

# Start an interactive rebase covering these 5 commits
git rebase -i HEAD~5

# Or rebase interactively against the base branch instead
git rebase -i main

# The editor opens a todo list, oldest commit first
pick n4o5p6q feat: add loyalty discount to cart
pick k1l2m3n wip
pick h7i8j9k add discount calculation
pick e4f5g6h wip: cart totals
pick a1b2c3d fix typo

# Comment block below lists every available command
# p, pick <commit>   = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit>   = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit>  = like squash, but discard this commit's log message
# d, drop <commit>   = remove commit

3. The pick command and reordering commits

pick is the default command and simply means: apply this commit unchanged. A rebase where every line stays pick and the order is untouched changes nothing in the end except the commit hashes, because every commit is reapplied against a potentially new base. The real usefulness of pick only becomes apparent combined with reordering: since the line order in the todo list directly determines the application order, it's enough to swap two lines in the editor to swap the order of two commits in the resulting log.

Care is needed when reordering commits that depend on each other content-wise. If a commit is moved ahead of another commit whose changes it actually depends on, for example because it modifies the same line in the same file, Git produces a conflict when applying it that then has to be resolved manually. A good rule of thumb: commits touching different files or clearly separated areas of functionality can usually be reordered safely, while changes that build on each other within the same code section are better left in their original order.

4. squash versus fixup: combining commits meaningfully

Both squash and fixup merge a commit with the one directly above it, that is, the previous one in the todo list. The difference lies in how the commit message is handled: squash opens an editor after merging where both messages are combined and can be freely edited, while fixup silently discards the message of the commit being merged and keeps only the message of the previous commit. squash is therefore appropriate when both commits each contain relevant information for the final message, fixup is the right choice for commits like wip or fix typo whose message offers no value anyway.

A productive workflow combines fixup with git commit --fixup <commit>, which automatically generates a commit message in the fixup! format that points to the target commit. During the subsequent git rebase -i --autosquash, Git automatically sorts these fixup commits directly under their target commit in the todo list and already marks them with fixup, so you no longer need to adjust the order manually. This saves considerable time on larger feature branches with many small correction commits.


# Before: five commits, several are noise
pick n4o5p6q feat: add loyalty discount to cart
pick k1l2m3n wip
pick h7i8j9k add discount calculation
pick e4f5g6h wip: cart totals
pick a1b2c3d fix typo

# After: fold noise commits into their predecessor
pick n4o5p6q feat: add loyalty discount to cart
fixup k1l2m3n wip
squash h7i8j9k add discount calculation
fixup e4f5g6h wip: cart totals
fixup a1b2c3d fix typo

# Result after saving and closing the editor:
git log --oneline -1
d9e8f7g feat: add loyalty discount to cart

# Shortcut: mark a fixup target at commit time, then autosquash later
git commit --fixup h7i8j9k
git rebase -i --autosquash main

5. reword: fixing commit messages without changing the diff

reword pauses the rebase process at exactly this commit and opens only the editor for the commit message, the actual diff of the commit remains completely untouched. This is the ideal command for fixing typos in older commit messages, retroactively adapting a message to Conventional Commits conventions like feat: or fix:, or simply replacing a message that was too terse with a more descriptive one, without touching the code itself again.

The key difference from git commit --amend: amend only works for the very last commit on the current branch, whereas reword allows rewriting the message of any commit in the todo list, regardless of how many further commits follow it. After closing the message editor, the rebase automatically continues with the next todo entry without a manual rebase --continue being necessary, as long as no conflict occurs.

6. drop to remove and edit to split commits

drop removes a commit completely from the history, its changes disappear entirely from the branch. The same can be achieved by simply deleting the corresponding line from the todo list, but the explicit drop command makes the intent more readable for later traceability. Caution is needed if later commits build on the dropped commit content-wise: in that case, applying the following commits produces conflicts, because the expected intermediate state in the code is missing.

edit, on the other hand, does not remove a commit but pauses the rebase at exactly that point, with the working directory in the state after applying that commit. That's the entry point for splitting an overly large commit into several smaller ones afterward: git reset HEAD^ turns the changes back into unstaged changes while the commit itself disappears, git add -p then allows selectively staging individual hunks for several new, topically cleanly separated commits. A final git rebase --continue resumes the rebase with the remaining todo entries.


# Original order (oldest first) before editing the todo list
pick n4o5p6q feat: add loyalty discount to cart
pick k1l2m3n wip
pick h7i8j9k add discount calculation
pick e4f5g6h wip: cart totals
pick a1b2c3d fix typo
pick b2c3d4e refactor: extract price helper

# Edited todo list: reordered, reworded, squashed, fixed up, dropped
pick n4o5p6q feat: add loyalty discount to cart
pick b2c3d4e refactor: extract price helper
reword h7i8j9k add discount calculation
fixup k1l2m3n wip
squash e4f5g6h wip: cart totals
drop a1b2c3d fix typo

# "edit" pauses the rebase at a chosen commit to split it
edit h7i8j9k add discount calculation

# After the rebase stops on that commit:
git reset HEAD^
git add -p
git commit -m "feat: calculate discount percentage"
git add -p
git commit -m "test: cover discount calculation edge cases"
git rebase --continue

7. Resolving conflicts during rebase: continue, skip, abort

Unlike a single merge, a rebase with many commits can produce a conflict at every single commit, because each commit is applied individually against the new base. Git stops the process at the first conflict, marks the affected files with the familiar conflict markers <<<<<<<, =======, and >>>>>>>, and reports the affected commit hash. git status reliably shows which files are affected, git diff helps understand exactly which two versions are colliding.

After manually resolving the conflict markers, the affected files are marked with git add and the rebase is continued with git rebase --continue, Git then automatically applies the remaining commits from the todo list. If the current commit is redundant or not worth the effort anyway, git rebase --skip skips it entirely and discards its changes. Anyone who notices the situation has become too confusing can abort the entire rebase at any time with git rebase --abort, Git then restores the exact state of the branch before the rebase started.


# Interactive rebase stops on a commit that no longer applies cleanly
git rebase -i main
# ...
# CONFLICT (content): Merge conflict in src/Model/Discount.php
# error: could not apply h7i8j9k... add discount calculation

git status
# Unmerged paths: both modified: src/Model/Discount.php

# Open the file, resolve the <<<<<<< / ======= / >>>>>>> markers
git diff src/Model/Discount.php

# Stage the resolved file and continue the rebase
git add src/Model/Discount.php
git rebase --continue

# Skip this commit entirely instead (drops its changes)
git rebase --skip

# Or abandon the whole rebase and restore the branch as it was
git rebase --abort

8. Reflog to the rescue: undoing a botched rebase

git rebase --abort only works as long as a rebase is still actively running. If a rebase has already completed fully and the result only turns out to be wrong afterward, for example because the wrong commit was accidentally dropped or an important change was lost while squashing, git reflog helps. Reflog locally records every movement of HEAD, including every single intermediate step during an interactive rebase, and it remains available even when those commits are no longer referenced by any branch.

git reflog shows a chronological list of all HEAD positions with entries like rebase (start) or rebase (pick); you typically look for the entry immediately before the rebase started. With git reset --hard HEAD@{n} or directly with the matching commit hash, the branch can be reset exactly to that state, as if the rebase had never happened. Important to know: reflog entries are valid for 90 days by default before they can be removed by the garbage collector, which is more than enough time to rescue a fresh rebase mistake.


# Something went wrong mid-rebase, HEAD is in a confusing state
git rebase --abort
# but --abort only works while a rebase is still in progress

# After a rebase already finished with the wrong result, check reflog
git reflog
a1b2c3d HEAD@{0}: rebase (finish): returned to refs/heads/feature/cart
a1b2c3d HEAD@{1}: rebase (pick): feat: add loyalty discount to cart
c4d5e6f HEAD@{2}: rebase (start): checkout main
n4o5p6q HEAD@{3}: commit: feat: add loyalty discount to cart

# n4o5p6q is the tip of the branch just before the rebase started
git reset --hard HEAD@{3}

# Or reference it directly by hash once identified
git reset --hard n4o5p6q

# Reflog entries expire eventually, default 90 days for reachable commits
git config gc.reflogExpire

9. Common pitfalls: risky versus safe practices

The most common mistake with interactive rebase is running it on a branch that other developers have already checked out, without coordinating beforehand. Since every rebased commit gets a new checksum, all existing local copies end up in a diverging state, which leads to confusing merge conflicts on the next pull. Equally risky is a force push with git push --force after the rebase, because that command overwrites uncompromisingly, even if new commits from colleagues have been added to the remote branch in the meantime. The table below contrasts risky habits with the safe alternatives that have proven themselves in daily practice.

Situation Risky practice Safe practice
Push after rebase git push --force git push --force-with-lease
Target branch Rebase on a shared team branch Rebase only on your own local feature branch
On conflicts Immediately --abort without understanding the cause Resolve the conflict deliberately, then --continue
Cleaning up history Squash all commits into one mega commit Squash/fixup deliberately by logical unit
Before rebasing Just dive in without knowing about reflog Know the reflog rescue path, back up the branch if unsure

Anyone who internalizes these practices uses interactive rebase for what it's meant to be: a precise tool for cleaning up your own, not-yet-shared history, not a means of retroactively changing commits that have already been published. When in doubt, the rule of thumb is that rebasing local feature branches before the first pull request is safe, while shared branches like main or develop should never be rebased.

Mironsoft

Git workflows, code reviews, and CI/CD for Magento and Hyva teams

Ready to set up a professional Git workflow and code quality process?

We set up clear Git workflows for your Magento and Hyva team, from branching strategy through commit conventions to an automated code review pipeline, so pull requests get merged faster and more safely.

Git workflow audit

Putting branching strategy, commit conventions, and the review process to the test

Team coaching

Teaching interactive rebase, clean commits, and conflict resolution hands on

CI/CD integration

Setting up automated checks and pipelines around pull requests

10. Summary

Interactive rebase solves a very concrete problem: a commit history that inevitably grows chaotic during development can be turned into a clear, traceable sequence of clean changes before the pull request. pick keeps commits unchanged, reordering only changes the sequence, squash and fixup combine related changes, reword corrects messages without touching the code, drop removes what's redundant, and edit makes it possible to split an overly large commit into smaller, reviewable units.

Conflicts during a rebase are no reason to panic as long as you're confident with git status, git rebase --continue, --skip, and --abort. And even if a rebase goes completely wrong, git reflog almost always offers a way back to the original state, as long as you don't wait too long and can find the right reflog entry. Anyone who masters these tools opens pull requests with a history reviewers can actually understand in a few minutes, instead of fighting through a dozen meaningless wip commits.

Interactive Rebase in Practice, the Essentials at a Glance

pick & order

pick applies commits unchanged, the line order in the todo list directly determines the order in the resulting log.

squash vs. fixup

squash combines messages manually, fixup discards them automatically. With --fixup and --autosquash, Git even sorts them in for you.

Resolving conflicts

git status shows affected files, git add marks resolutions, git rebase --continue, --skip, or --abort control how it proceeds.

Reflog rescue

git reflog records every HEAD movement. git reset --hard on the matching entry undoes a botched rebase.

11. FAQ: Interactive Rebase in Practice

1What is the difference between git rebase and git rebase -i?
git rebase reapplies commits automatically. git rebase -i additionally opens an editable todo list where each commit can be reordered, renamed, merged, or removed.
2When should I use squash instead of fixup?
squash when the message of the commit being merged still contains relevant information. fixup for commits like wip or fix typo whose message is worthless anyway.
3What happens if I don't change anything with reword?
Nothing problematic. The commit keeps its original message, only the commit hash changes because it gets reapplied as part of the rebase.
4Can I clean up commits that have already been pushed with rebase?
Technically yes, in practice only without other checkouts by teammates. A force push is needed afterward, ideally with git push --force-with-lease instead of plain --force.
5How do I safely abort an interactive rebase?
With git rebase --abort, as long as the rebase is still running. The branch is reset exactly to the state before the rebase started.
6What is the difference between rebase --skip and --abort?
--skip only skips the conflict-causing commit and moves on. --abort cancels the entire rebase and restores the original state.
7How do I restore the previous state after a botched rebase?
Look through git reflog for the entry before the rebase started, usually rebase (start), and reset with git reset --hard to that entry or commit hash.
8What exactly does the edit command do in the todo list?
edit pauses the rebase after this commit. With git reset HEAD^ and git add -p, the commit can then be split into several smaller ones.
9Why does my reflog eventually disappear?
Reflog entries are valid for 90 days by default, configurable via gc.reflogExpire. For rescuing a recent mistake, this window is more than enough in practice.
10Is interactive rebase dangerous for teamwork?
Only on shared branches like main or develop. On your own feature branch not yet checked out by others, before the first pull request, it's standard practice.