Intermediate Git
Before you start
You need: Articles 01–06.
Time: about 55 minutes, at the keyboard. Cause each mistake deliberately and recover from it.
Learning objective
Undo any Git mistake safely, and use blame and bisect to find where a defect came from.
Topics
reset— soft, mixed, hardrevert— undoing in publiccherry-pickreflogas a safety netblamebisect- Interactive rebase
clean
reset
git reset --soft HEAD~1 # undo the commit; changes stay staged
git reset --mixed HEAD~1 # undo the commit; changes stay in the working directory (default)
git reset --hard HEAD~1 # undo the commit and DISCARD the changes
Commit: A---B---C (HEAD)
--soft HEAD~1: A---B (HEAD) C's changes staged
--mixed HEAD~1: A---B (HEAD) C's changes unstaged, still in files
--hard HEAD~1: A---B (HEAD) C's changes gone
| Flag | Repository | Staging | Working directory |
|---|---|---|---|
--soft | Moved | Unchanged | Unchanged |
--mixed | Moved | Cleared | Unchanged |
--hard | Moved | Cleared | Overwritten |
--soft is the everyday one: you committed too early, or want to combine the last three commits into one.
git reset --soft HEAD~3
git commit -m "Add student search with tests"
--hard is the only Git command that destroys uncommitted work with no recovery. Committed work it discards is recoverable from reflog; uncommitted changes in your files are simply overwritten.
git stash # if there is any chance you want it back
git reset --hard HEAD~1
Reset rewrites history. Safe on unpushed commits, destructive on pushed ones — after pushing, use revert.
revert
git revert a3f9c1d
git revert HEAD
git revert --no-commit a3f9c1d # stage the inverse without committing
git revert -m 1 a3f9c1d # a merge commit; 1 = keep the first parent
revert creates a new commit that undoes an old one. History is unchanged, so everyone else's clone stays valid.
Before: A---B---C---D (D broke production)
After: A---B---C---D---D' (D' undoes D)
reset | revert | |
|---|---|---|
| History | Rewritten | Preserved |
| Safe after pushing | No | Yes |
| Record of the mistake | Erased | Kept |
| Needs a force push | Yes | No |
Rule: reset before pushing, revert after.
Reverting a merge needs -m to say which parent is the mainline — normally 1. And a reverted merge cannot simply be re-merged later; you must revert the revert. Worth knowing before you revert a merge on a Friday.
cherry-pick
git cherry-pick a3f9c1d
git cherry-pick a3f9c1d 8c2e4f1 # several
git cherry-pick a3f9c1d..8c2e4f1 # a range
git cherry-pick -n a3f9c1d # apply without committing
git cherry-pick --continue / --abort
Cherry-pick copies one commit's changes onto the current branch, creating a new commit with a different hash.
Legitimate uses:
- A hotfix made on
mainthatdevelopalso needs - One commit from an abandoned branch
- Backporting a fix to a maintenance release
Cherry-picking a commit that will later be merged creates a duplicate, and duplicated changes are a reliable source of conflicts. If the whole branch is coming anyway, wait and merge it.
reflog
git reflog
git reflog show main
git reflog --date=iso
a3f9c1d HEAD@{0}: reset: moving to HEAD~1
8c2e4f1 HEAD@{1}: commit: Add fee report
1a9b3c5 HEAD@{2}: checkout: moving from main to feature/fees
Reflog records every movement of HEAD — commits, checkouts, resets, merges, rebases — including states that no branch points to any more.
git reset --hard 8c2e4f1 # go back to before the bad reset
git branch rescue 8c2e4f1 # or recover onto a new branch
This is the safety net under everything else in this article. A bad reset --hard, a deleted branch, a rebase that went wrong — all recoverable, because Git does not delete commit objects until garbage collection, and reflog holds the hashes.
It is local — it does not clone, does not push, and does not help a colleague. And entries expire (90 days by default, 30 for unreachable commits).
What reflog cannot recover: anything never committed.
blame
git blame src/Services/FeeService.cs
git blame -L 40,60 src/Services/FeeService.cs
git blame -w src/Services/FeeService.cs # ignore whitespace changes
git blame -C src/Services/FeeService.cs # follow code moved between files
a3f9c1d8 (Ravi Kumar 2026-03-14 11:02:44 +0530 42) decimal total = ...
8c2e4f1a (Priya Sharma 2026-04-02 16:31:09 +0530 43) if (payment.IsCancelled)
git log -p -L 42,45:src/Services/FeeService.cs # full history of those lines
git show a3f9c1d # the commit and its message
Blame is for understanding, not attribution. The question is "what was this commit trying to do?" — answered by reading the message, not by finding someone to hold responsible. A team that uses blame to assign fault stops getting useful commit messages.
-w matters in practice. Without it, a reformat makes one person the author of an entire file, and blame becomes useless.
bisect
Binary search through history for the commit that introduced a bug.
git bisect start
git bisect bad # current commit is broken
git bisect good v2.3.0 # this release worked
# Git checks out a commit halfway between
# test it, then:
git bisect good # or: git bisect bad
# repeat
git bisect reset # return to where you started
Ten steps search a thousand commits. That is the entire value: a bug someone introduced "sometime last month" becomes a specific commit, with a message explaining the intent, in about ten tests.
Automated:
git bisect start HEAD v2.3.0
git bisect run dotnet test --filter FullyQualifiedName~FeeCalculationTests
bisect run uses the exit code — zero is good, non-zero is bad — and finds the commit unattended.
Bisect is the strongest argument for small commits. When it lands on a 2,000-line commit, you know the day but not the change. When it lands on a 20-line commit, you have the answer.
Interactive rebase
git rebase -i HEAD~4
pick a3f9c1d Add search endpoint
squash 8c2e4f1 Fix typo
reword 1a9b3c5 Add tests
drop 5d7e9f2 Debug logging
| Command | Effect |
|---|---|
pick | Keep |
reword | Keep, edit the message |
squash | Combine into the previous, merge messages |
fixup | Combine into the previous, discard the message |
edit | Stop to amend |
drop | Remove |
reorder lines | Reorder commits |
Use it to clean a branch before opening a pull request — three "fix typo" commits become one, and a stray debug commit disappears.
It rewrites every commit from that point. Only ever on unpushed commits, or on a branch nobody else uses.
Interactive rebase is not available in this environment's terminal, and many editors handle the todo list badly. git commit --fixup plus git rebase --autosquash achieves the common case with less ceremony.
clean
git clean -n # dry run — always start here
git clean -f # delete untracked files
git clean -fd # and untracked directories
git clean -fdx # and ignored files (bin/, obj/, node_modules/)
git clean -fdx deletes files Git never tracked, so there is no copy anywhere. It removes appsettings.Development.json, .env, local certificates and anything else ignored but needed.
Always -n first. Read the list, then run it.
-fdx is genuinely useful for "clean build, no stale artefacts" — just know exactly what it will take.
Diagnosing
| Problem | Fix |
|---|---|
| Committed too early | git reset --soft HEAD~1 |
| Bad commit, already pushed | git revert <hash> |
Bad reset --hard | git reflog, then reset to the earlier hash |
| Deleted branch | git reflog, git branch <name> <hash> |
| Committed to the wrong branch | git reset --soft HEAD~1, switch, commit |
| Need one commit from another branch | git cherry-pick <hash> |
| "This used to work" | git bisect |
| "Why is this line here?" | git blame -w, then git show |
| Messy branch before a PR | git rebase -i |
| Stale build artefacts | git clean -n, then -fdx |
Committing to the wrong branch, in full:
git reset --soft HEAD~1
git stash
git switch correct-branch
git stash pop
git commit -m "..."
Errors you will hit
| Situation | What happened | Recovery |
|---|---|---|
reset --hard on committed work | Commits unreachable | git reflog, then reset to the hash |
reset --hard with uncommitted work | Gone | Unrecoverable |
Deleted a branch with -D | Pointer removed, objects remain | git reflog; git branch <name> <hash> |
| Committed to the wrong branch | — | reset --soft, stash, switch, commit |
| Pushed a bad commit | History is public | git revert, never reset |
clean -fdx removed .env | Untracked and ignored files deleted | Unrecoverable — always -n first |
Two rows here are unrecoverable, and both involve uncommitted work. Everything committed can be found again.
Common mistakes
reset --hardwith uncommitted workreseton pushed commits, then a force push- Not knowing reflog exists, and redoing lost work
clean -fdxwithout-n, losing.env- Cherry-picking commits that will be merged anyway
- Reverting a merge without
-m - Interactive rebase on shared commits
- Blame used for fault rather than context
- Blame without
-wafter a reformat - Not using bisect on a regression
Practice
The course exercise is recover from a mistake.
- Commit, then
reset --soft HEAD~1. Confirm the changes are staged. - Repeat with
--mixedand--hard, and record the difference in each area. reset --hardwith an uncommitted change. Confirm it cannot be recovered.reset --hard HEAD~3, then recover with reflog.- Push a commit, then revert it. Compare the history with what a reset would have produced.
- Revert a merge commit and read the error you get without
-m. - Cherry-pick a hotfix from
mainontodevelop. - Cherry-pick a commit and then merge its branch. Observe the duplication.
- Delete a branch, recover it from reflog.
- Commit to the wrong branch, then move the commit using soft reset and stash.
- Run
git blameon a real file, pick a confusing line, and read the commit behind it. - Compare
git blamewith and without-won a file that has been reformatted. - Introduce a bug ten commits back, then find it with
git bisect. - Do the same with
git bisect runand a test command. - Clean up a four-commit branch with
git rebase -i— squash two, reword one, drop one. - Run
git clean -nin a repository with build output. Read the list before running anything.
Exercises 4, 9 and 13 are the ones that change how you work: two make mistakes reversible, one makes regressions findable.
You can now
- Choose between
reset,revertandrestore - Recover lost commits from reflog
- Move a commit to the right branch
- Find a regression with
git bisect - Say what Git cannot recover
Review questions
- What is the difference between
--soft,--mixedand--hard? - When must you use
revertrather thanreset? - What can reflog recover, and what is permanently outside its reach?
- Why does bisect reward small commits?