Skip to main content
Published / updated

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, hard
  • revert — undoing in public
  • cherry-pick
  • reflog as a safety net
  • blame
  • bisect
  • 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
FlagRepositoryStagingWorking directory
--softMovedUnchangedUnchanged
--mixedMovedClearedUnchanged
--hardMovedClearedOverwritten

--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)
resetrevert
HistoryRewrittenPreserved
Safe after pushingNoYes
Record of the mistakeErasedKept
Needs a force pushYesNo

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 main that develop also 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
CommandEffect
pickKeep
rewordKeep, edit the message
squashCombine into the previous, merge messages
fixupCombine into the previous, discard the message
editStop to amend
dropRemove
reorder linesReorder 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

ProblemFix
Committed too earlygit reset --soft HEAD~1
Bad commit, already pushedgit revert <hash>
Bad reset --hardgit reflog, then reset to the earlier hash
Deleted branchgit reflog, git branch <name> <hash>
Committed to the wrong branchgit reset --soft HEAD~1, switch, commit
Need one commit from another branchgit cherry-pick <hash>
"This used to work"git bisect
"Why is this line here?"git blame -w, then git show
Messy branch before a PRgit rebase -i
Stale build artefactsgit 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

SituationWhat happenedRecovery
reset --hard on committed workCommits unreachablegit reflog, then reset to the hash
reset --hard with uncommitted workGoneUnrecoverable
Deleted a branch with -DPointer removed, objects remaingit reflog; git branch <name> <hash>
Committed to the wrong branchreset --soft, stash, switch, commit
Pushed a bad commitHistory is publicgit revert, never reset
clean -fdx removed .envUntracked and ignored files deletedUnrecoverable — always -n first

Two rows here are unrecoverable, and both involve uncommitted work. Everything committed can be found again.

Common mistakes

  • reset --hard with uncommitted work
  • reset on pushed commits, then a force push
  • Not knowing reflog exists, and redoing lost work
  • clean -fdx without -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 -w after a reformat
  • Not using bisect on a regression

Practice

The course exercise is recover from a mistake.

  1. Commit, then reset --soft HEAD~1. Confirm the changes are staged.
  2. Repeat with --mixed and --hard, and record the difference in each area.
  3. reset --hard with an uncommitted change. Confirm it cannot be recovered.
  4. reset --hard HEAD~3, then recover with reflog.
  5. Push a commit, then revert it. Compare the history with what a reset would have produced.
  6. Revert a merge commit and read the error you get without -m.
  7. Cherry-pick a hotfix from main onto develop.
  8. Cherry-pick a commit and then merge its branch. Observe the duplication.
  9. Delete a branch, recover it from reflog.
  10. Commit to the wrong branch, then move the commit using soft reset and stash.
  11. Run git blame on a real file, pick a confusing line, and read the commit behind it.
  12. Compare git blame with and without -w on a file that has been reformatted.
  13. Introduce a bug ten commits back, then find it with git bisect.
  14. Do the same with git bisect run and a test command.
  15. Clean up a four-commit branch with git rebase -i — squash two, reword one, drop one.
  16. Run git clean -n in 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, revert and restore
  • 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

  1. What is the difference between --soft, --mixed and --hard?
  2. When must you use revert rather than reset?
  3. What can reflog recover, and what is permanently outside its reach?
  4. Why does bisect reward small commits?

Next: Team repository simulation