Interactive rebase lets you rewrite a string of recent commits before anyone else sees them: combine ten messy work-in-progress commits into one clean one, fix a typo in a commit message, or reorder commits so the story reads in the right sequence. The command is git rebase -i, and the most common job is squashing, folding several commits into a single tidy one before you open a pull request.
The one thing to internalize first: interactive rebase rewrites history. That is exactly what makes it powerful, and exactly why you only run it on commits you have not shared yet. Squash your own local branch all you like; never rebase commits other people have already pulled.
The quick version
You are on a feature branch with four commits that should really be one. Run this:
git rebase -i HEAD~4Git opens an editor listing those four commits, oldest at the top. Change every line except the first from pick to squash (or s), save, close the editor, and Git folds all four into a single commit and lets you write one combined message. Done.
The rest of this article explains what each of those words means, how to pick the right number after HEAD~, and what to do when it goes sideways.
What git rebase -i actually shows you
HEAD~4 means "the last four commits, counting back from where I am now." When you run the command, Git drops you into your configured editor with a to-do list that looks like this:
pick a1b2c3d Add login form markup
pick d4e5f6a Wire up the submit handler
pick 7g8h9i0 fix typo
pick j1k2l3m forgot the validation, oops
# Rebase 9f8e7d6..j1k2l3m onto 9f8e7d6 (4 commands)
#
# Commands:
# 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 commitA few things to notice before you touch anything:
- The list is in chronological order, oldest at the top. This is the reverse of
git log, which shows newest first. It trips up everyone the first time. - Each line starts with an action (
pick) followed by the short hash and the subject line. - You edit this file like any text file: change the action word at the start of a line, save, and quit. Git then replays the commits top to bottom according to your instructions.
The four actions you will actually use
There are six commands, but four of them cover almost everything. Here is what each one does and when to reach for it.
| Action | Short | What it does |
|---|---|---|
pick | p | Keep the commit exactly as is. This is the default for every line. |
reword | r | Keep the commit and its changes, but stop to let you edit just the message. |
squash | s | Combine this commit into the one above it, and merge both messages so you can edit the result. |
fixup | f | Same as squash, but throw this commit's message away and keep only the one above. |
Two more exist for completeness: edit (e) pauses the rebase at that commit so you can amend the actual content, and drop (d) deletes the commit entirely. You will reach for squash, fixup, and reword far more often.
The mental model for squash and fixup: each one melds the line it is on into the line above it. So you always leave the top commit as pick and squash the later ones up into it.
Squashing a messy branch before a pull request
Say your branch history reads like a diary of false starts:
git log --onelinej1k2l3m forgot the validation, oops
7g8h9i0 fix typo
d4e5f6a Wire up the submit handler
a1b2c3d Add login form markupNobody reviewing your pull request needs to see "fix typo" and "oops." Clean this up before you open a pull request, so the diff people review tells one coherent story. Squash all four into one clean commit:
git rebase -i HEAD~4In the editor, leave the first line as pick and change the rest to squash (or s):
pick a1b2c3d Add login form markup
squash d4e5f6a Wire up the submit handler
squash 7g8h9i0 fix typo
squash j1k2l3m forgot the validation, oopsSave and quit. Git replays the commits, then opens a second editor so you can write the combined message. By default it stacks all four original messages; delete the noise and write one good subject line and body:
Add login form with submit handler and validation
Builds the login form markup, wires up the submit handler,
and validates required fields before posting.Save and quit again. Your four commits are now one. If you want to keep the first commit's message verbatim and silently discard the other three messages, use fixup instead of squash on lines two through four; Git skips the message-editing step entirely. A common real-world pattern is pick on the meaningful first commit and fixup on every follow-up tweak.
Writing a good combined message matters more than the squash itself. If you are not sure what a clean commit message looks like, I keep a short guide on Git commit message best practices that covers the subject-line-plus-body convention I used above.
Just want to fix one message? Use reword
You do not need a full squash to fix a single bad commit message. Start the same interactive rebase, then change only the line you care about to reword:
pick a1b2c3d Add login form markup
reword d4e5f6a Wire up the submit handler
pick 7g8h9i0 Add field validationGit keeps every commit's changes untouched and only stops to let you rewrite that one message. This is much safer than the alternatives when all you want is wording. For fixing just the most recent commit, you do not even need rebase, git commit --amend does it; see how to undo the last Git commit for that shortcut and a few others.
Picking the right number after HEAD~
The number in HEAD~N is how many commits back you want to edit. Count the commits you want to touch and use that number. If your feature branch has five commits since it split off main, HEAD~5 covers exactly them. The cleaner way to express "everything on this branch since it diverged from main" is to rebase against the branch point instead of counting:
git rebase -i mainThat puts every commit unique to your branch into the to-do list, no counting required. Just make sure your local main is current first. If you are fuzzy on how branches diverge and merge back, Git branching explained for beginners walks through the model that makes this click.
If it goes wrong: abort
This is the safety net, so commit it to memory. If you get partway through a rebase and the editor looks wrong, or a conflict appears that you do not want to deal with right now, you can bail out completely and end up exactly where you started:
git rebase --abortThat throws away the in-progress rebase and restores your branch to its pre-rebase state. Nothing is lost. Use it freely; it is the reason interactive rebase is low-risk on a local branch.
If a rebase stops on a merge conflict (because you reordered or squashed commits that touch the same lines), you have three choices:
# fix the conflicted files, stage them, then continue
git add path/to/file
git rebase --continue
# skip the commit currently being applied
git rebase --skip
# give up entirely and restore the original branch
git rebase --abortNote that git add is doing the same job here as in a normal commit: it stages the resolved files so the replayed commit picks them up. If that step feels opaque, how the staging area feeds each commit explains the index that sits between your working tree and the commit. Resolving the conflict itself is the same skill as any merge; my walkthrough on resolving merge conflicts in Git applies here unchanged. And if you somehow rebase yourself into a state you regret after the rebase has finished, the commits are not gone, the reflog still has them. Recovering lost commits with git reflog shows how to find the pre-rebase hash and reset back to it.
Rebase vs squash-on-merge
You do not always have to squash by hand. GitHub, GitLab, and most hosts offer a "Squash and merge" button on pull requests that collapses the whole branch into one commit at merge time, leaving your branch history alone. The trade-off:
| Approach | Where it happens | When to use it |
|---|---|---|
| Interactive rebase | Locally, before you push | You want fine control: squash some commits, reword others, reorder, drop. |
| Squash and merge | On the host, at merge time | You just want one commit on main and do not care about the intermediate ones. |
Both are valid. I tend to use interactive rebase when the branch history needs real editing (a couple of meaningful commits plus some fixups) and the host's squash button when the branch is short and I just want it flattened. In practice the choice is often a team convention rather than a personal one: whether you squash commits with interactive rebase as part of a team workflow or lean on the host's merge button is the kind of thing a team settles once and applies to every branch. If you are weighing rebase against a plain merge more broadly, git merge vs git rebase covers that decision in full.
The one rule that keeps you out of trouble
Never interactive-rebase commits you have already pushed to a branch other people use. Rewriting shared history changes commit hashes, which forces everyone else into a painful reconciliation and can lose work. Squash freely on your own unpushed branch; once it is shared, treat history as immutable and undo with git revert instead. One thing to expect even on your own branch: after a rebase you have to force-push, and if you already pushed the branch the normal git push gets rejected as non-fast-forward. If you hit that, push the cleaned-up branch covers why the rejection happens and the safe way past it.
If you are still getting your footing with Git generally, start at my Git for beginners guide, which threads together staging, branching, and the everyday commands that interactive rebase builds on.
Sources
Authoritative references this article was fact-checked against.
- git-rebase: official Git documentationgit-scm.com
- Pro Git: Rewriting Historygit-scm.com
- GitHub Docs: About pull request mergesdocs.github.com





