Git Troubleshooting
Volume 1 — Core Developer Skills
The Git Reference chapter's troubleshooting table covers quick day-to-day fixes. This chapter is different: it's the set of deeper recovery procedures for when something has actually gone wrong — a bad reset, a force-push that overwrote work, a merge that shouldn't have happened, or a secret that made it into history.
1. The Golden Rule: Git Rarely Deletes Anything Immediately
The single most useful fact in Git recovery: almost nothing is truly gone until Git's garbage collector actually runs (which happens automatically only periodically, and won't run mid-incident). Commits that seem "lost" after a reset, a deleted branch, or an amend are usually still sitting in Git's object database — they're just no longer referenced by anything you can see with git log.
This means: stop, don't panic, don't run more destructive commands trying to fix it. The recovery tool for almost everything in this chapter is git reflog.
2. git reflog: Your Undo History
reflog records every place HEAD has pointed, on this machine, for the last ~90 days by default — every commit, checkout, reset, rebase, and merge.
git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
e4f5g6h HEAD@{1}: commit: Add leave balance validation
i7j8k9l HEAD@{2}: checkout: moving from main to feature/WAY-16
Every line is a point you can jump back to:
git reset --hard HEAD@{1} # go back to exactly how things looked before the reset
Important: reflog is local to your machine — it is not pushed, not pulled, and not shared. If a teammate needs to recover a commit that only ever existed on their machine, they need to run reflog themselves; you can't recover it for them from your clone.
3. Recovering a "Lost" Commit After reset --hard
This is the single most common panic scenario.
git reset --hard HEAD~3 # oops — meant HEAD~1
Recovery:
git reflog
# find the entry right before the reset, e.g.:
# e4f5g6h HEAD@{1}: commit: Add leave balance validation
git reset --hard e4f5g6h
You're back exactly where you were. This works because reset --hard only moves what HEAD and the branch pointer reference — the actual commit objects stay in Git's database until garbage collection eventually cleans up anything truly unreferenced.
4. Recovering a Deleted Branch
git branch -D feature/WAY-16-leave-management
# realize the branch had unmerged work
git reflog | grep "feature/WAY-16"
# or, more reliably, look for the last commit that was checked out on it:
git reflog
# find: checkout: moving from feature/WAY-16-leave-management to main
git branch feature/WAY-16-leave-management <commit-hash-from-reflog>
If you don't remember the branch name to grep for, git fsck --lost-found lists all commit objects that currently aren't reachable from any branch — the deleted branch's last commit will be in that list.
5. Recovering Uncommitted Work You Never Staged
reflog only helps with commits — if you lost changes that were never committed (e.g. git checkout . wiped uncommitted edits, or a hard reset ran before you committed), reflog won't have them. Your remaining option is git fsck, which can sometimes recover "dangling" blobs left behind by git add (staging creates an object even before commit):
git fsck --no-reflog | grep dangling
git show <blob-hash>
This is a much weaker safety net than reflog and won't always find anything — which is exactly why the habit worth building is: commit early and often on a branch, even with a rough message like "wip". A committed-but-not-pushed mistake is always recoverable via reflog; uncommitted work is not guaranteed to be.
6. Undoing a Bad Merge
If the merge hasn't been pushed yet:
git reflog
# find the commit right before the merge
git reset --hard <commit-before-merge>
If the merge has already been pushed and others may have pulled it:
git revert -m 1 <merge-commit-hash>
The -m 1 tells Git which parent to treat as "mainline" when reverting a merge commit — for a normal merge into main, that's parent 1 (the branch you merged into). This creates a new commit that undoes the merge's changes without rewriting history, safe for a branch others are working from.
7. Force-Push Incident Recovery
Someone force-pushed and main (or your branch) now looks wrong — commits are missing, or history has been rewritten out from under you.
If you have a local clone with the old history:
git reflog show origin/main # only works if you've fetched recently and have a local reflog for it
More reliably, if you or anyone still has a local branch with the old commits:
git push --force-with-lease origin old-local-branch:main
Only do this after confirming with whoever force-pushed and the rest of the team — recovering by force-pushing again is itself a repeat of the same risky operation and needs the same care.
Prevention going forward: default to git push --force-with-lease instead of git push --force on any branch shared with others. --force-with-lease refuses to push if the remote has commits you haven't seen yet — it protects against exactly this scenario by failing safely instead of silently overwriting someone else's work.
git push --force-with-lease origin feature/WAY-16-leave-management
8. Merge Conflict Resolution, in Depth
Beyond hand-editing the <<<<<<</=======/>>>>>>> markers described in the Git Reference chapter:
git mergetool # opens a configured visual diff/merge tool for each conflicted file
Resolving in favor of one side entirely, when you know the whole file should come from one branch:
git checkout --ours path/to/file.cs # keep your current branch's version entirely
git checkout --theirs path/to/file.cs # keep the incoming branch's version entirely
git add path/to/file.cs
Backing out of a merge entirely if it's turning into more than expected:
git merge --abort
This returns you exactly to the pre-merge state — always safe, always available as long as you haven't already committed the merge.
Same idea during a rebase:
git rebase --abort
9. Detached HEAD Recovery
git checkout a1b2c3d
# Note: switching to 'a1b2c3d'. You are in 'detached HEAD' state...
You're not on a branch anymore — new commits made here won't belong to any branch and can be lost the moment you check out something else.
If you haven't made any new commits yet:
git switch main # just leave, nothing to lose
If you've already made commits you want to keep:
git switch -c recovered-work # create a branch pointing at exactly where you are right now
10. Removing a Committed Secret from History
If a secret was committed and pushed, the file's removal in a later commit does not remove it from history — anyone can still find it by checking out an earlier commit.
Step one, always: rotate the credential immediately. Treat it as compromised the moment it was pushed, regardless of whether anyone is known to have seen it. Rewriting history does not undo exposure that already happened.
Then, to clean the history itself (only necessary/worthwhile before wide collaboration on the repo, since rewriting history breaks everyone else's clones):
git filter-repo --path appsettings.Development.json --invert-paths
(git filter-repo is the current recommended tool for this — the older git filter-branch and BFG Repo-Cleaner both still work but are slower and, in filter-branch's case, officially discouraged.)
After rewriting history, everyone with an existing clone needs to re-clone rather than pull — the commit hashes have all changed.
11. Common CI/Pipeline Git Issues
| Symptom | Cause | Fix |
|---|---|---|
| Pipeline checks out the wrong commit | Shallow clone (--depth 1) doesn't include the history a step expects | Configure the pipeline's checkout step to fetch full history, or increase depth, if the step genuinely needs prior commits |
| "detached HEAD" errors in a pipeline step that expects a branch | CI systems commonly check out a specific commit rather than a branch by design | Explicitly git checkout $(BRANCH_NAME) in the step if branch context is actually needed, rather than relying on the default checkout |
| Pipeline can't push back to the repo (e.g., auto-versioning, changelog commits) | Checkout was shallow/anonymous, no push credentials configured | Use a pipeline-scoped access token or deploy key with write access, configured explicitly for that step |
| Tag-triggered pipeline doesn't fire | Tag wasn't pushed (git push doesn't push tags by default) | git push origin --tags, or push the specific tag explicitly |
12. Decision Tree: What Recovery Path Do I Need?
13. Recovery Quick Reference
| Situation | Command |
|---|---|
Undo an unpushed reset --hard | git reflog → git reset --hard <hash> |
| Recover a deleted local branch | git reflog or git fsck --lost-found → git branch <name> <hash> |
| Undo a pushed merge | git revert -m 1 <merge-hash> |
| Back out of an in-progress merge | git merge --abort |
| Back out of an in-progress rebase | git rebase --abort |
| Recover from detached HEAD with new commits | git switch -c recovered-work |
| Push safely to a shared branch | git push --force-with-lease (never plain --force) |
| Committed secret | Rotate the credential first, then git filter-repo if history needs cleaning |
| Find unreferenced commit objects | git fsck --lost-found |
Part of the Traxs Engineering Handbook — Volume 1: Core Developer Skills. Companion chapters in this volume: Bash Reference, Git Reference, SSH & Key Management.
Volume 1 — Core Developer Skills is now complete: Bash Reference, Git Reference, SSH & Key Management, and Git Troubleshooting.