Git Internals for Engineers

When Something Goes Wrong

It is 11 PM. You have been heads-down on a feature for six hours. You just ran a command, saw a scary message, and something is gone. Your heart rate is up. The natural impulse is to try "fixing" with more commands — git reset, git checkout, git clean -fd — and make it worse. The senior engineer's rule for this moment is: stop typing. Git almost never loses data, but git clean, git stash drop, and git reset --hard can make recoverable situations unrecoverable. Breathe. Run git reflog. You are probably fine.

This lesson is the playbook for specific "oh no" scenarios. Each situation has a known recovery path, usually reflog-based. Read this once; bookmark it; keep breathing when the panic hits.


The Universal First Step

Whatever happened, run this first:

git reflog | head -30
git status

reflog shows every recent HEAD change. status shows what Git thinks the current state is. Between them, you usually have enough to diagnose.

Before running any additional command: know what it will do, and whether it is destructive. Prefer creating a backup branch (git branch backup-now) as your first defensive move. Branches are ~41 bytes; you can never have too many in a crisis.

KEY CONCEPT

The single highest-value habit in Git recovery: before running any command that might rewrite or delete state, create a branch at your current position. git branch safety-$(date +%s) takes 2 seconds and guarantees you can always come back. Many "I lost work" stories would have been prevented by this one line.


Scenario 1: "I Committed to the Wrong Branch"

You meant to commit to feature, but you were on main. Your commit is now at the tip of main.

If you have NOT pushed

# Current state:
# main:  A---B---C---D (with D being your accidental commit)
# feature: A---B---C (unchanged)

# Step 1: create feature at the current main tip (capturing your commit)
git branch feature-fixed HEAD    # D is at HEAD

# Step 2: move main back to C (remove the accidental D)
git reset --hard HEAD~1

# Step 3: switch to your new branch and continue
git checkout feature-fixed
# Your commit is here, safely on the intended branch
# main is clean

Alternatively, the shorter path with interactive manipulation:

# 1. Note the SHA of the accidental commit
git log -1 --format=%H
# abc123...

# 2. Reset main
git reset --hard HEAD~1

# 3. Cherry-pick onto feature
git checkout feature
git cherry-pick abc123

If you have ALREADY pushed to main

Now the fix is less forgiving; pushing to main means the commit might be in others' clones. Options:

  • Revert it on main (creates an undo commit) then cherry-pick the original onto feature.
  • Force-push main back (dangerous; only if you own main and no one has pulled yet).
# Revert on main (safe for shared branches)
git revert HEAD
git push origin main

# Apply the original commit to feature
git checkout feature
git cherry-pick abc123     # the original commit
git push origin feature

Main now shows: A-B-C-D-R (where R reverts D). Feature has D's content. Everyone is OK.


Scenario 2: "I Lost a Stash"

git stash drop
# Now realize: "wait, that was important"

Stashes are commits on a special refs/stash ref. When you drop, the ref entry is removed. But the underlying commit object still exists in .git/objects/ until GC runs (~2 weeks for unreachable).

Recovery

# Find unreachable stash commits
git fsck --unreachable --no-reflogs | grep commit
# unreachable commit abc123...
# unreachable commit def456...

# Inspect each to find your stash
git show abc123
# commit abc123
# Merge: def456 ...
# WIP on main: ghi789 add feature foo   <- the stash message format

git branch recovered-stash abc123
# Recover it as a branch; inspect and continue from here
# Or apply it as a stash:
git stash apply abc123

Stash commits have a distinctive Merge line (they record the working dir AND index as two parents). That is how you distinguish stashes from other unreachable commits.

If git fsck does not show it

You may have already GC'd. Default retention for unreachable objects is 2 weeks. If it is been longer, the commit is gone.

Prevention: extend gc.reflogExpireUnreachable (Lesson 4.1). Also, prefer git stash push -m "descriptive" over git stash drop — named stashes are easier to review before deletion.


Scenario 3: "I Force-Pushed Over Someone's Work"

This is the classic team disaster. You force-pushed feature and your teammate had commits on their local feature that you did not know about.

Recovery approach

  1. Stop all pushes from the team to the affected branch.
  2. Find the lost commits. Ask the teammate to run git reflog on their clone — their local feature ref still points at their state before your force-push.
  3. Get their SHAs. Have them share the tip SHA of their pre-force-push state.
  4. Reconstitute. On your machine, fetch their work:
# Your collaborator gives you their SHA: abc123
# You don't have it locally. Options:

# Option A: they push it to a temp branch
# (them) git push origin abc123:refs/heads/collaborator-rescue
# (you) git fetch
#       git log origin/collaborator-rescue

# Option B: if the server still has the old commits reachable via the reflog
# (many managed platforms retain force-pushed refs briefly in their logs)
# Contact platform support / use admin-level tools

# Now either:
# - Merge their commits into your branch and push again (safe)
# - Force-push their SHA to reinstate their work (if your force-pushed work is disposable)
  1. Communicate. Tell the team what happened. They may need to reset their clones.

Prevention

  • Always git push --force-with-lease instead of --force. It fails if the remote has moved; you catch divergence before clobbering.
  • Configure branch protection rules on important branches (main, develop, release branches). Most platforms disallow force-pushes to protected branches.
  • Ask before force-pushing a shared branch. "Hey, I'm about to force-push feature — anyone have uncommitted local changes?"

Scenario 4: "I Deleted a Branch I Needed"

git branch -D feature
# Now realize: "wait, that was my only branch with three weeks of work"

Recovery

git reflog | grep feature
# OR walk through all entries looking for when you were on feature:
git reflog
# abc123 HEAD@{5}: checkout: moving from feature to main
#         ← feature was at abc123 when you left it

git branch feature abc123
# Branch restored at that SHA. Your work is back.

If you have no reflog entries mentioning the branch (you never checked it out; you only did operations on it from somewhere else), look for the SHA in other ways:

# Recent commits, hopefully yours
git log --all --oneline --reflog

# Or find unreachable commits that look like your work
git fsck --lost-found
# Dangling commit abc123
# Dangling blob xyz789

Recover by creating a branch at the SHA. You are never more than one command from restoring a deleted branch within the reflog window.


Scenario 5: "I Committed Secrets"

You committed an API key, password, or private key. If the commit is unpushed, rewrite before pushing. If pushed, you have a problem.

Before push

# Option 1: remove the file entirely and amend
git rm --cached secrets.env
git commit --amend --no-edit
# If the secret is within a file you want to keep, edit the file
# to remove the secret, then:
git add <file>
git commit --amend --no-edit

# CAUTION: the secret is still in .git/objects/ until GC
# If you have not pushed, you can force GC:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Now the secret is physically gone from your local .git/

After push (the bad case)

The moment the commit is on the remote, assume the secret is compromised.

  1. Rotate the secret immediately. Revoke the API key, change the password, regenerate the certificate. This is more important than removing it from Git.
  2. Remove from history. Use git filter-repo (the modern tool — git filter-branch is deprecated):
# Install git-filter-repo (pip install git-filter-repo or platform package)

# Remove a specific file from all history
git filter-repo --path secrets.env --invert-paths

# Remove a specific string from all files (leaving the file but scrubbing the secret)
git filter-repo --replace-text <(echo 'SECRET_KEY==>REDACTED')

# Force-push the rewritten history
git push origin --force --all
git push origin --force --tags
  1. Notify the team. Everyone must re-clone (their local clones still have the secret via reflog).
  2. Assume the secret is compromised anyway. Scanners pull from GitHub as files are pushed; the secret may have been indexed.
WARNING

Secrets in commits are only truly fixed by rotating the secret. History rewrites clean up your repo but do not retroactively remove what search engines, archives, or attackers may already have. Treat "I committed a secret" as "the secret is now public" — rotate first, clean second.


Scenario 6: "git clean -fd Deleted Untracked Files I Wanted"

git clean -fd removes untracked files and directories. If you had new files Git did not know about, they are now gone.

Git does not have them in .git/objects/. Recovery options:

  1. Editor autosave / IDE local history. IntelliJ's "Local History" feature has full file timeline. VS Code has "Timeline" view with recent saves.
  2. OS backup. macOS Time Machine, Windows File History, Linux snapshots.
  3. Filesystem undelete. Some filesystems support recovery if the inodes have not been overwritten. Mostly impractical.

Prevention: git clean -n (dry run) before git clean -f. Or use git stash --include-untracked to save untracked files as a stash, then clean.


Scenario 7: "My Rebase Corrupted Things"

You rebased, and now the code looks wrong — maybe a commit's changes got dropped, or a merge got resolved badly.

# First check: is the rebase mid-flight or finished?
git status
# If it says "rebase in progress," you're still in it
git rebase --abort

# If it's finished, use reflog
git reflog | grep -E '^\S+ HEAD@\{\d+\}: rebase'
# Find the entry RIGHT BEFORE the rebase started
# It will look like:  abc123 HEAD@{15}: checkout: moving from main to feature
#                    (the last pre-rebase state)

git reset --hard HEAD@{15}
# or use the exact SHA from the reflog entry

Better prevention: backup branch before rebase.

git branch backup-YYYYMMDD-HHMMSS
git rebase -i origin/main
# If it goes bad:
git reset --hard backup-YYYYMMDD-HHMMSS

Scenario 8: "The Repo is Corrupt"

git fsck reports errors. Objects are missing. Clones fail.

First: diagnose

git fsck --full --strict
# error: sha1 mismatch abc123
# missing blob abc123
# error: object abc123 is corrupt

Possible causes:

  • Disk corruption (bad sector, power loss mid-write).
  • Concurrent writes from a tool racing with Git.
  • Filesystem sync issue (especially on NFS or FUSE mounts).
  • Manual edits to .git/ that broke invariants.

Recovery paths

# Option 1: re-clone from remote
git remote -v              # confirm remote
cd ..
git clone <remote-url> myrepo-fresh
# Compare to salvage anything local that the remote lacks

# Option 2: copy-in missing objects from another clone
# If a teammate has a healthy clone, scp the missing objects
cd /another/healthy/clone
git cat-file -p <missing-sha> > /tmp/obj.content   # won't work for tree/commit naturally
# More practical:
git bundle create /tmp/everything.bundle --all
# Transfer bundle
cd /your/broken/clone
git fetch /tmp/everything.bundle '+refs/*:refs/*'

# Option 3: if it's a single object, git cat-file + redundant local clones
# (Advanced — rarely the right answer)

Prevention: do not store repos on unstable filesystems; ensure clean Git operations (no killing mid-commit).


Scenario 9: "I'm Stuck in a Weird State"

Symptoms:

  • git status says you are in an unexpected state.
  • Commands fail with cryptic errors.
  • You cannot check out branches.

First, check for in-flight operations:

ls .git/ | grep -i MERGE
# If you see MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD, you are mid-operation
# Abort:
git merge --abort
git cherry-pick --abort
git rebase --abort
git revert --abort

# For rebase specifically:
ls .git/rebase-merge/   # interactive rebase state
ls .git/rebase-apply/   # old-style rebase state
# If these exist, `git rebase --abort` usually handles it

If nothing works:

# Nuclear option: reflog + reset to a known-good state
git reflog | head -20
git reset --hard <good-sha>
# Start over

Scenario 10: "I Pushed a Huge File by Mistake"

Your repo is now 500 MB bigger than it should be.

Remove from history

# Use git filter-repo
git filter-repo --path huge-file.bin --invert-paths

# Force-push
git push origin --force --all
git push origin --force --tags

# Everyone re-clones (important — they still have the file locally)

For preventing this in the future: Git LFS for anything > 100 KB binary, git hooks to reject large files pre-commit, CI checks on the repo size.

Size audit

# Find the biggest files in history
git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  awk '/^blob/ {print $3, $4}' |
  sort -n |
  tail -20
# Lists the top 20 biggest blobs with their paths

The Panic Checklist

When something goes wrong:

  1. Do not type anything else. Take a breath.
  2. Make a backup. git branch panic-backup-$(date +%s).
  3. git reflog | head -30 — understand what just happened.
  4. git status — know the current state.
  5. Identify the pre-mistake SHA from the reflog.
  6. Decide the right recovery command. Think before typing.
  7. Execute. Usually git reset --hard HEAD@{N} or git cherry-pick.
  8. Verify. git log, git status, git diff confirm state.
  9. Post-mortem. What habit or config would have prevented this?

The backup branch (step 2) is cheap insurance. Even if you mess up recovery, you can always go back to panic-backup-....

WAR STORY

A senior engineer at a large company had an unwritten rule: before every git reset, git rebase, git filter-branch, or git push --force, they would run git branch before-risky-$(date +%s). The habit seemed paranoid to juniors. One day, a rebase went wrong in a way the reflog could not recover (the backup branch was saved; the reflog got confused by many cascading operations). The backup branch was the only thing that saved that day. The habit became team standard. Branches are free. Panic recovery sessions are not.


General Recovery Tools

# Find lost commits
git fsck --lost-found

# Find unreachable objects
git fsck --unreachable --no-reflogs

# Inspect any object
git show <sha>
git cat-file -p <sha>
git cat-file -t <sha>

# Walk history including reflog
git log --all --reflog --graph --oneline

# Extract a file from any historical commit
git show <sha>:<path/to/file>

# Package a repo state into a transferable bundle
git bundle create /tmp/backup.bundle --all

# Recover from a bundle
git fetch /tmp/backup.bundle '+refs/*:refs/*'

Key Concepts Summary

  • Always run git reflog before anything else when something goes wrong.
  • Make a backup branch as your first defensive move. git branch panic-backup-now.
  • Committed to wrong branch: create branch at current HEAD, reset origin branch back.
  • Lost stash: git fsck --unreachable finds it, git stash apply <sha> restores.
  • Force-pushed over others' work: collaborators' local reflogs have the SHAs; reconstitute from them.
  • Deleted branch: reflog shows last SHA; git branch <name> <sha> revives.
  • Committed secret: rotate the secret, then rewrite history (git filter-repo), then force-push. Do not rely on history-rewriting alone.
  • git clean -fd is irreversible for untracked files. Use -n dry-run first.
  • Corrupt repo: re-clone or import from a bundle. Git checksums protect against silent corruption.
  • Stuck states (MERGE_HEAD, REBASE_HEAD) need the matching --abort command.
  • Huge files: git filter-repo removes; prevent with LFS and pre-commit hooks.

Common Mistakes

  • Panicking and running more commands without thinking. Each command can make recovery harder.
  • Using git clean -f without -n first. Untracked work is non-recoverable.
  • Relying on the reflog for work that was only in the working directory. Reflog = committed; working dir = ephemeral.
  • git stash drop on a stash you might need — use git stash list + git stash show to verify first.
  • Force-pushing without --force-with-lease. Overrides collaborators' work silently.
  • Using git filter-branch (deprecated, buggy). Use git filter-repo instead.
  • Committing secrets and assuming history rewrite "fixes" it. Rotate the secret; assume the old value is compromised.
  • Not setting up branch protection on main. One mistaken force-push destroys shared history.
  • Over-relying on hard resets as a cleanup tool. Each hard reset is a potential incident. Prefer soft/mixed or revert.
  • Skipping the backup-branch habit. 2 seconds of cost for hours of safety net.

KNOWLEDGE CHECK

You just ran `git push --force origin main` and overwrote three commits that your teammate Alice had pushed earlier today. Alice hasn't noticed yet but will pull soon. What is the correct recovery sequence to preserve her work?