18

Here's what I did:

  1. Coded 8 hours worth of changes.
  2. git status displays all my changes.
  3. git add -A
  4. git commit -m "Foo". A pre-commit git hook fires with husky and lint-staged.
  5. I remember that there's a TypeScript typing error I failed to fix, so I press Ctrl+C to cancel.
  6. Absentmindedly, I run git commit -m "Foo" again and immediately cancel.
  7. Changes are gone! The files are reverted, git status is clean, git log and git reflog do not show a new commit.

Why did my changes get reverted? How do I recover them?

step 1 step 2

Andrey Mikhaylov - lolmaus
  • 23,107
  • 6
  • 84
  • 133
  • 1
    Do two things, first immediately check the history of each file in VS Code to see if you can recover your work from the IDE. Second, figure out what this hook is doing. I saw a "cleanup" task mentioned. Could it actually wipe your working directory and stage? Not sure, but you need to investigate this. – Tim Biegeleisen Feb 21 '20 at 08:01
  • Use `git reflog` to see if the commit was done. As you have run `git add -A`, the contents of the changed files (whose names are in green) are stored in blobs. Don't remove `.git` and you can finally find them back. https://stackoverflow.com/questions/9560184/recover-dangling-blobs-in-git/9560317 may help if the commit was not done. – ElpieKay Feb 21 '20 at 08:06
  • 2
    I'd guess the problem here is lint-staged, not git. – jonrsharpe Feb 21 '20 at 08:13
  • 2
    `lint-staged` has a long history of [developers losing work](https://github.com/okonet/lint-staged/issues?q=is%3Aissue+lost+is%3Aclosed) - my personal recommendation would be to not use it; a pre-commit hook should do nothing more than validate and exit. – AD7six Feb 21 '20 at 09:00

2 Answers2

40

OK, it was lint-staged to blame. It stashed my changes.

So running git stash apply recovered them!


⚠ Do not attempt running multiple instances of lint-staged in parallel on the same git repo (e. g. on each project in a monorepo). This will destroy your changes without any way to recover them.

If you work on a monorepo, either lint projects sequentially or give up lint-staged and lint the whole codebase, including unstaged files.

Andrey Mikhaylov - lolmaus
  • 23,107
  • 6
  • 84
  • 133
  • 1
    It probably uses `git stash -k`. This is a tempting approach in writing pre-commit hooks, but I think it has way too many failure modes. You've just encountered one of the worse ones. – torek Feb 21 '20 at 09:00
  • 1
    Cool. Stash is evil and you should try to avoid it, use `git worktree instead` https://stackoverflow.com/questions/38133883/how-to-work-on-the-multiple-different-branches-where-i-can-switch-easily-between/38134131#38134131 – CodeWizard Feb 21 '20 at 09:01
  • 2
    @CodeWizard it is not the OP's choice how lint-staged works. Not sure why you are recommending a pre-commit hook creates a new worktree though? – AD7six Feb 21 '20 at 09:16
  • Thank you so much, i thought i lost my 1 month changes , tq so much – niyamxsept Dec 11 '20 at 12:23
  • Haha, no prob, don't forget to upvote the question and the answer. – Andrey Mikhaylov - lolmaus Dec 11 '20 at 13:35
  • this answer saved me from a meltdown. thank you – tsiege Jun 11 '21 at 18:43
  • why lint-staged do this though. I was stunned when I see all my changes gone. – Phuc Vuong Sep 24 '21 at 17:01
  • 1
    @PhucVuong, `lint-staged` needs to remove unstaged edits from the codebase, so that the linter only sees staged edits. `git stash` is a natural way of hiding edits and restoring them later.. – Andrey Mikhaylov - lolmaus Sep 27 '21 at 07:47
  • 1
    Thanks! I faced the same situation: failed lint-staged git hook (because of the missed `npm i` on another machine) and all my work has disappeared without new commit created. I already started to think that all the work is lost, but your answer saved me. – Kirill Nov 15 '21 at 17:16
  • THANK YOU LIFE SAVIOUR I WAS GOING NUTS FOR TWO HOURS! thought i have lost 10h of work.. bloody lint – Kerim092 Mar 23 '22 at 21:38
  • Many thanks! My head almost became grey when the code disappered – MyName Mar 14 '23 at 23:31
5

For all of you who are: TL;DR
- Option 1 - which you mentioned you already did: use git reflog && git reset
- Option 2 - Use your editor history
- Option 3 - If you added those files grab them from the staging area but you will need to find them

# Find all dangling files
git fsck --all

## Now use git cat-file -p to print those hashes
git cat-p <SHA-1>


Full answer:

Before answering, let's add some background, explaining what this HEAD is.

First of all what is HEAD?

HEAD is simply a reference to the current commit (latest) on the current branch.
There can only be a single HEAD at any given time (excluding git worktree).

The content of HEAD is stored inside .git/HEAD and it contains the 40 bytes SHA-1 of the current commit.


detached HEAD

If you are not on the latest commit - meaning that HEAD is pointing to a prior commit in history it's called detached HEAD.

Enter image description here

On the command line, it will look like this - SHA-1 instead of the branch name since the HEAD is not pointing to the tip of the current branch:

Enter image description here

Enter image description here


A few options on how to recover from a detached HEAD:


git checkout

git checkout <commit_id>
git checkout -b <new branch> <commit_id>
git checkout HEAD~X // x is the number of commits t go back

This will checkout new branch pointing to the desired commit.
This command will checkout to a given commit.
At this point, you can create a branch and start to work from this point on.

# Checkout a given commit.
# Doing so will result in a `detached HEAD` which mean that the `HEAD`
# is not pointing to the latest so you will need to checkout branch
# in order to be able to update the code.
git checkout <commit-id>

# Create a new branch forked to the given commit
git checkout -b <branch name>

git reflog

You can always use the reflog as well.
git reflog will display any change which updated the HEAD and checking out the desired reflog entry will set the HEAD back to this commit.

Every time the HEAD is modified there will be a new entry in the reflog

git reflog
git checkout HEAD@{...}

This will get you back to your desired commit

Enter image description here


git reset --hard <commit_id>

"Move" your HEAD back to the desired commit.

# This will destroy any local modifications.
# Don't do it if you have uncommitted work you want to keep.
git reset --hard 0d1d7fc32

# Alternatively, if there's work to keep:
git stash
git reset --hard 0d1d7fc32
git stash pop
# This saves the modifications, then reapplies that patch after resetting.
# You could get merge conflicts if you've modified things which were
# changed since the commit you reset to.
  • Note: (Since Git 2.7) you can also use the git rebase --no-autostash as well.

git revert <sha-1>

"Undo" the given commit or commit range.
The reset command will "undo" any changes made in the given commit.
A new commit with the undo patch will be committed while the original commit will remain in the history as well.

# Add a new commit with the undo of the original one.
# The <sha-1> can be any commit(s) or commit range
git revert <sha-1>

This schema illustrates which command does what.
As you can see there, reset && checkout modify the HEAD.

Enter image description here

CodeWizard
  • 128,036
  • 21
  • 144
  • 167