Why bother
A branch that took two days usually contains "wip", "fix typo", "actually fix it" and a commit that only deletes a stray console.log. None of that helps the person who runs git log next year, and none of it helps a bisect find the commit that broke the build.
Rewriting turns that into two or three commits that each do one thing and each build. It is polish, applied to your own unpushed work.
Squashing with reset --soft
--soft moves the branch pointer and touches nothing else, so all three commits' changes are sitting in the index, ready to be committed as one. It is the least surprising way to squash.
git reset --soft HEAD~3 # undo three commits, keep everything staged
git commit -m "Fix the discounted total"
git log --oneline # one commit where three were
Interactive rebase, for everything else
pick a1b2c3 Add the discount field
squash d4e5f6 wip
reword 7g8h9i fix typo
drop j1k2l3 remove debug logging
# reorder the lines to reorder the commits
# pick keep as is
# reword change the message
# edit stop here so you can change the commit
# squash fold into the one above, combining messages
# fixup fold in, discard this message
# drop delete the commit entirely
Interactive rebase needs an editor session, which is not something a lab can honestly simulate — so this small git leaves it out rather than faking it. The lab below squashes with reset --soft, which does the same job and is easier to reason about anyway.
cherry-pick: one commit, somewhere else
It applies the change, not the snapshot, so it makes a new commit with a new id. Use it for a hotfix that has to land on both main and a release branch — and not as a substitute for merging, or the same change ends up in history twice with different ids.
git switch main
git cherry-pick 832dbe3 # apply that commit's change here, as a new commit
git cherry-pick a1b2c3..d4e5f6 # a range
Force-pushing, carefully
git push --force-with-lease origin fix-total
--force-with-lease refuses if the remote moved since your last fetch, so you cannot silently delete work that arrived while you were rewriting. Plain --force has no such check. Neither belongs anywhere near main — that is what protected branches are for.
The line that must not be crossed
Rewrite freely: commits only you have. Rewrite carefully, with a warning: a pull-request branch other people are reviewing. Never rewrite: main, a release branch, or anything a colleague has pulled. The cost is not yours to pay — it lands on everyone whose history no longer matches.