Alexander Mills
Alexander Mills

Reputation: 100446

un-deleting file after running "git reset" after running "git rm"

I accidentally ran git rm file

to undo that I ran git reset HEAD which didn't restore the file, so then I ran git reset HEAD~1 which didn't seem to restore the file either (this should be easy...)

Now I see this:

Unstaged changes after reset:
M       test/.suman/logs/runner-debug.log
M       test/.suman/logs/test-debug.log
M       test/.suman/logs/test-output.log
M       test/.suman/logs/tests/test.test-src.alphabet.test.js.log
M       test/.suman/logs/tests/test.test-src.four.test.js.log
M       test/.suman/logs/tests/test.test-src.one.test.js.log
M       test/.suman/logs/tests/test.test-src.three.test.js.log
M       test/.suman/logs/tests/test.test-src.two.test.js.log
M       test/example.js
D       test/test-src/one.test.js
D       test/test-src/test-sort.js
M       test/test-src/z.test.js

what I am trying to do is restore

 D       test/test-src/one.test.js

how can I do that?

Upvotes: 0

Views: 367

Answers (2)

torek
torek

Reputation: 489928

mkrieger1's answer is correct (and upvoted), but you have a Situation now, so more is required—probably, git reset @{1}—first. Here's the explanation.

When you ran git rm path, Git removed the given path from both the index (the next commit you're building now) and the work-tree.

When you subsequently ran git reset HEAD, Git made two changes, the first one of which was a no-op. But then you ran a second git reset that also made two changes, and this time the first change was not a no-op.

What git reset does by default

  • Whatever the current branch is, its commit is changed to the HEAD commit. That is, if you're on branch master, Git resolves HEAD to some commit ID by reading master, then writes the commit ID it just produced into master. Obviously, reading the ID, then writing it back unchanged, leaves it unchanged. So this is kind of dumb, but is done because if you said git reset HEAD~1 for instance, Git would convert HEAD~1 to a commit ID, then write that commit ID into master—which is what it did for your second command.

    Once the current branch is adjusted, HEAD now resolves to the new commit ID. So for your first reset command, HEAD did not change at all, and for your second one, HEAD stepped back one commit.

  • Then, since you let git reset do a --mixed (default) reset, Git copies all the files from the adjusted HEAD into the index.

This second step is part of what you wanted, when you were undoing your git rm. Your git rm removed the file from the index and from the work-tree. Your git reset HEAD put the file back into the index, first "changing" HEAD to itself in the process. But the file is still missing from your work-tree—and then you did the second git reset, which made things bad,

Recovering most of the way

Here's a crude text drawing of what you had before the two git resets, in terms of the commit graph, all assuming you're on branch master:

...--A--B--C   <-- master

The first git reset moved master to point to commit C. It already pointed to C so that's no change. But, the second git reset moved master to point to commit B, leaving C dangling:

...--A--B    <-- master
         \
          C

To get C back, if you knew its commit ID, you would run:

git reset <commit-id>

This would tell Git to point the current branch (master) to commit C again:

...--A--B
         \
          C   <-- master

The tricky part is: where can you find the ID of commit C? The answer is to use the reflogs. There's one reflog for HEAD, and one reflog for each branch. The one for HEAD gets a new entry every time you do something that changes which commit HEAD points to, and the one for the branch gets a new entry every time you change which commit the branch-name points to. Since git reset adjusts both, they'll be found in both reflogs, probably as HEAD@{1} and @{1}. (If you have done some additional resets you may need @{2} or higher. Also, be sure your shell does not eat the braces—some do; if yours does, you may need to use, e.g., "@{1}".)

Hence:

git reset @{1}

or perhaps some higher number, perhaps with some extra quotes. This should get your branch pointed back to the correct commit, and fill in the index. (Or, you can use the raw hash ID, if you have that. Git takes either hash IDs, or anything that converts to a hash ID. To see what hash ID something converts to, use git rev-parse. Try git rev-parse HEAD, for instance, and git rev-parse master.)

Getting the rest of the way

Now that you have the branch pointed back to the correct commit—that is, now that you're back to:

...--A--B--C   <-- master

or similar—the git reset, doing a --mixed reset, still leaves the desired file stored in the index, but not in the work-tree.

There are several ways to get the file back, but the simplest is to copy it from the index to the work-tree:

git checkout -- path

You can also use this, even if you hadn't run any git reset commands at all:

git checkout HEAD -- path

The key difference is that this version says: copy the given path from the HEAD commit into the index, then copy the same path from the index to the work-tree.

Upvotes: 1

mkrieger1
mkrieger1

Reputation: 23261

You were correct in running git reset HEAD first. This un-marked the file from deletion, after which it counts as a change in the working directory.

It is always helpful to run git status:

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

To restore the file, i.e. to discard the change of the content of the file being deleted:

$ git checkout -- test/test-src/one.test.js

Upvotes: 2

Related Questions