Skip to main content

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

SymptomCauseFix
Pipeline checks out the wrong commitShallow clone (--depth 1) doesn't include the history a step expectsConfigure 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 branchCI systems commonly check out a specific commit rather than a branch by designExplicitly 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 configuredUse a pipeline-scoped access token or deploy key with write access, configured explicitly for that step
Tag-triggered pipeline doesn't fireTag 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

SituationCommand
Undo an unpushed reset --hardgit refloggit reset --hard <hash>
Recover a deleted local branchgit reflog or git fsck --lost-foundgit branch <name> <hash>
Undo a pushed mergegit revert -m 1 <merge-hash>
Back out of an in-progress mergegit merge --abort
Back out of an in-progress rebasegit rebase --abort
Recover from detached HEAD with new commitsgit switch -c recovered-work
Push safely to a shared branchgit push --force-with-lease (never plain --force)
Committed secretRotate the credential first, then git filter-repo if history needs cleaning
Find unreferenced commit objectsgit 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.