---
name: git-history-repair
description: Undo, rewrite, and recover Git history safely — amend, reset vs restore vs revert, interactive rebase (reorder/squash/split/drop), undoing bad merges, recovering "lost" commits via reflog/fsck, and purging secrets or huge files from all history. Use when something in a repo went wrong or history must be reshaped — "undo this commit", "I lost my work", "remove this password/file from history", "clean up my commits before pushing", "botched merge/rebase" — even if the user doesn't say "rewrite history". Also covers finding when/where a bug or change was introduced (bisect, blame, pickaxe). Not for routine branching/merging or resolving fresh merge conflicts.
---

# Git history repair and recovery

Repair mistakes in a Git repository and reshape history without losing work.
Derived from Pro Git, 2nd ed. (Chacon & Straub).

## Safety invariants — check before any destructive step

1. **Anything committed can almost always be recovered** (via reflog, even after
   `--amend` or branch deletion). **Anything never committed is likely gone forever.**
2. The only common data-destroying operations are:
   - `git reset --hard` (overwrites working directory without checking)
   - `git checkout -- <file>` / `git restore <file>` (discards file changes)
   - `git clean` (deletes untracked files, usually unrecoverable)
   - `git stash drop` / losing an unapplied stash
3. **The cardinal rule: never rewrite commits that exist outside your repository
   and that people may have based work on.** Amend, rebase, filter-branch, and
   reset all create *new* commits; rewriting pushed history forces every
   collaborator to re-merge and creates duplicate commits.
4. Before a risky operation on uncommitted work, park it first: commit to a
   temporary branch or `git stash` (see "Parking and discarding work" below).

## Decision table: what went wrong → remedy

| Situation | Remedy |
|---|---|
| Wrong message / forgot file in the **last, unpushed** commit | `git commit --amend` (`--no-edit` to keep message) |
| Staged a file by mistake | `git restore --staged <file>` (Git ≥2.23) or `git reset HEAD <file>` — working dir untouched, safe |
| Want to discard uncommitted edits to a file | `git restore <file>` / `git checkout -- <file>` — DESTRUCTIVE, confirm first |
| Messy series of local commits to clean before sharing | `git rebase -i` (below) or squash via `git reset --soft HEAD~N` + `git commit` |
| Accidental local merge commit, nothing built on it, not pushed | `git reset --hard HEAD~` |
| Bad merge already shared | `git revert -m 1 <merge>` — read the re-merge caveat below |
| "Lost" commits (deleted branch, hard reset) | reflog recovery (below) |
| Secret / huge file must vanish from all history | Read references/purging-history.md |
| Someone force-pushed rebased history under you | `git rebase <their-remote-branch>` or `git pull --rebase` — patch-id lets Git skip your copies of rewritten commits (only works if the rewritten patches are nearly identical) |
| Need to know when/where a bug or line came from | Read references/investigation.md (bisect, blame -C, log -S/-L) |

## Amending

`git commit --amend` **replaces** the previous commit entirely (new SHA-1) — it is
a tiny rebase. Only amend commits that are still local and unpushed. If you
amended the content substantially, update the message too; for trivial fixes use
`--amend --no-edit`.

## Reset, demystified

Git manages three trees: **HEAD** (last commit), **index** (proposed next
commit), **working directory**. `git reset <commit>` works through up to three
steps, stopping where told:

1. Move the branch HEAD points to → stop here with `--soft` (undoes `commit`)
2. Make the index look like HEAD → stop here by default / `--mixed` (also undoes `add`)
3. Make the working directory look like the index → `--hard` (also destroys edits — the **only** dangerous form)

Key distinction: `reset` moves *the branch HEAD points to*; `checkout <branch>`
moves *HEAD itself* and is working-directory-safe (does a trivial merge instead
of blindly overwriting).

With a path (`git reset <file>`, `git reset <commit> -- <file>`) step 1 is
skipped: it just copies that file version into the index — i.e. unstages, or
stages an old version. `checkout <commit> -- <file>` additionally overwrites the
working file — not WD-safe. All of `reset`, `checkout`, `add` accept `--patch`
for hunk-level operation.

Squash trick: `git reset --soft HEAD~2 && git commit` collapses the last two
commits into the state of one new commit.

## Interactive rebase (clean up local commits)

`git rebase -i HEAD~3` — the argument is the **parent** of the oldest commit you
want to edit. The script lists commits **oldest first** (reverse of `git log`).
Change `pick` per line:

- `reword` — edit message only
- `edit` — stop after that commit; then `git commit --amend`, `git rebase --continue`
- `squash` / `fixup` — meld into previous commit (fixup discards its message)
- `drop` (or delete the line) — remove the commit
- reorder lines to reorder commits

To **split** a commit: mark it `edit`, then at the stop run `git reset HEAD^`,
stage and commit the pieces separately, then `git rebase --continue`.

Escape hatches: `git rebase --abort` restores the pre-rebase state; a *finished*
but regretted rebase is recovered through the reflog. Every rewritten commit and
all its descendants get new SHA-1s — never include already-pushed commits in
the range.

## Undoing merges

- **Local-only mistake:** `git reset --hard HEAD~` (ref surgery; don't use if
  others have the commits or newer commits exist on top).
- **Shared history:** `git revert -m 1 <merge-commit>` — `-m 1` keeps the first
  parent (the branch you merged *into*) and undoes what the merged branch
  brought in.
- **Caveat:** after reverting a merge, Git still considers the branch merged.
  Re-merging later brings in nothing, and new work on that branch merges
  without the old work. To truly re-merge, first **revert the revert**, then
  merge again.

## Recovering lost commits

1. `git reflog` (or `git log -g` for full log formatting) — the reflog records
   every HEAD movement for months. Syntax: `HEAD@{5}`, `master@{yesterday}`.
   The reflog is strictly local and empty in a fresh clone.
2. Find the lost commit's SHA-1, then anchor it: `git branch recover-branch <sha>`.
3. If the reflog itself is gone: `git fsck --full` lists *dangling commits*
   (objects unreachable from any ref); branch off the dangling SHA-1 the same way.
4. A deleted remote branch is often recoverable server-side too — the server
   keeps the data until garbage collection runs.

## Parking and discarding uncommitted work

- `git stash` (aka `stash push`) saves modified tracked + staged state;
  `git stash apply` restores (add `--index` to also restore what was staged);
  `pop` = apply + drop. `-u` includes untracked files, `--all` also ignored
  files, `--patch` selects hunks.
- If applying an old stash conflicts with later work: `git stash branch
  <newbranch>` replays it on a branch made from the original commit.
- `git clean` removes untracked files — often unrecoverable. **Always dry-run
  first** with `-n`; then `-f` (`-d` for directories, `-x` to also delete
  ignored files, `-i` interactive). Nested Git repos need `-ff`.
  Safer alternative to clean: `git stash --all`.

## Gotchas

- `git commit --amend` after pushing + force-push breaks collaborators — same
  perils as rebasing published work.
- After a history rewrite, if a collaborator's `git pull` merges old and new
  histories, `git log` shows duplicate commits (same author/date/message):
  recover with `git pull --rebase`, not merge.
- `filter-branch` has many pitfalls; the book itself recommends the external
  `git-filter-repo` tool for serious history filtering (see
  references/purging-history.md for both).
- `branch -vv` ahead/behind counts are from your last fetch — `git fetch --all`
  first for accurate numbers.
- Checking out a tag or commit → detached HEAD; commits made there are
  unreachable except by SHA-1 once you switch away — create a branch
  (`git checkout -b <br> <tag>`) if you intend to commit. Recover forgotten
  detached-HEAD commits via reflog.
- PowerShell: quote `"HEAD@{0}"`; cmd.exe: quote or double the caret (`HEAD^^`).

## References

- Read **references/purging-history.md** when a file, secret, or large blob must
  be removed from all history, or when a repo is bloated after an import.
- Read **references/investigation.md** when you must first *find* the commit
  that introduced a change, bug, or line (bisect, blame, pickaxe, revision
  ranges) before repairing anything.
