Nobody's first draft of history is pretty. You end up with wip, fix, fix fix, actually works now, and one commit titled asdf. Squashing folds a run of commits into a single one with a message worth reading.
Two ways to get there — and a fork in the road depending on whether you've pushed.
Way 1: interactive rebase (local, before pushing)
git rebase -i HEAD~4
Your editor opens with the last four commits listed, oldest first, each marked pick. Leave the first one as pick, change the rest to squash (or fixup if the commit messages deserve to die). Save, and git walks you through writing the combined message.
squash= fold in, keep the message for editingfixup= fold in, throw the message away
The result: four commits become one, with new SHAs. That's the important part — the old commits are replaced, not edited. Which leads to the rule:
Never rebase commits you've already pushed and someone might have pulled. You'd be swapping out commits their history depends on, and git will repay you with diverged-branch hell.
Way 2: squash-and-merge on GitHub (whole PRs)
GitHub's "Squash and merge" button folds an entire PR into one commit on the target branch. Zero rebase skills required, no force push, and the PR itself keeps the full messy history for archaeology.
The limitation: it's all-or-nothing per PR. You can't squash just the last six commits, and you can't squash a range sitting in the middle of a branch. For that you're back to rebase — or the tool below.
Squash with a safety net
Pick the range of commits, write the new message, and git.tools opens a pull request containing the single squashed commit. Your original branch stays untouched until you merge — if the result looks wrong, just close the PR.
Open Squash →When not to squash
Squashing is for noise, not for signal. A PR that fixes three distinct bugs is better as three commits — future you, bisecting a regression at 2 AM, will bless every clean boundary you kept. Squash the stumbles; keep the story.
Also know that squashing erases individual commit signatures and collapses authorship. If a teammate's work is in the range, keep their name on the result with a Co-authored-by: line. It's ten seconds and it's the decent thing to do.
The escape hatch
Did a rebase go sideways mid-squash? git rebase --abort takes you back to before you started. Finished the rebase and hate the result? The pre-rebase state is one git reflog lookup away. And if you'd rather not need escape hatches: a squashed PR you can review before merging is the version of this operation where nothing is ever at risk.