Every commit has a full copy of every file—or more precisely, every file that it has, but stated this way, it sounds redundant. What this means is that if you check out some commit C
and begin working, and one of the things you do is delete some file path/to/file
and eventually run git commit
to make new commit D
, commit D
has all the files it has, which means that it omits path/to/file
. But path/to/file
remains in commit C
, since no commit can ever be changed, by any power anywhere, once it's made.
What this in turn means is that, if file path/to/file
isn't changed in commit C
vs some earlier commits B
and A
, you can get the file path/to/file
out of any of those three commits. The file in all three commits is just a duplicate (and in fact, Git de-duplicated it, storing the file only once despite its appearance in all three commits). The mechanism behind this is very clever and elegant, and completely irrelevant to your purposes. All that matters to you is the fact that you can get the file from any commit that has the copy you want back.
While keeping that in mind, if it's easiest to find commit C
specifically, rather than commit A
or B
, we can just do that—find commit C
. If it's easiest to find commit B
or A
, and you're sure the copy in those two commits is just as good, consider using commit B
or A
to get the file back.
The first command that Florian Endrich showed in his answer, git log --diff-filter=D --summary
, is a way to find the commit I'm calling D
above—the one in which the file you care about was deleted, and thereby to find the full path name of the file. If you already know the file's path, it's simpler to just use the next command:
git log -n 1 -- path/to/file
which is a simplified version: we just need to get the raw hash ID of commit D
. Once we have that, it's a trivial matter to name commit C
, as commit C
is the parent commit of commit D
. Here's an example, trimmed slightly and with one @
changed to a space to (perhaps) cut down a bit on spam:
git log -n 1 -- compat/gmtime.c
commit 84b0115f0dc9483dbc7f064b46afaddc4d94db92
Author: Carlo Marcelo Arenas Belón <carenas gmail.com>
Date: Thu May 14 12:18:54 2020 -0700
compat: remove gmtime
ccd469450a (date.c: switch to reentrant {gm,local}time_r, 2019-11-28)
removes the only gmtime() call we had and moves to gmtime_r() which
doesn't have the same portability problems. ...
This particular commit, to the Git repository for Git, removes the file compat/gmtime.c
. So that file exists in this commit's parent commit, in the form it had just before the removal. What commit is the parent of commit 84b0115f0dc9483dbc7f064b46afaddc4d94db92
? Git can tell us:
$ git rev-parse 84b0115f0dc9483dbc7f064b46afaddc4d94db92^
7397ca33730626f682845f8691b39c305535611e
(note the caret, ^
, character at the end of the rev-parse
argument; you can use ~
if your command line interpreter likes to gnaw on carrots, er, carets).
The parent of 84b0115f0d...
is therefore 7397ca3373...
, which means if we want compat/gmtime.c
back, we can simply ask Git to look at, or extract, the version of that file as it appears in commit 7397ca3373...
:
$ git show 7397ca3373:compat/gmtime.c
#include "../git-compat-util.h"
#undef gmtime
#undef gmtime_r
[snipped]
We don't really have to find C's hash ID either, because we can add that ^
suffix to D
's hash ID to mean "commit C
" in general. So git show 84b0115f0d^:compat/gmtime.c
would work just as well.
Given that you want the file back, the command you would want to use is the new git restore
in Git 2.23:
git restore --source=<commit> --staged --worktree -- path/to/file
where the <commit>
part is the correct hash ID for C
, or the hash ID for D
followed by a caret ^
or tilde ~
character, or whatever.
If you have an easy-to-use, easy-to-recall name for a commit A
or B
that precedes commit C
, but has the right file in it, you can use that here too:
git restore --source=dev --staged --worktree -- path/to/file
if the copy of the file in the last commit on branch dev
is a good copy.
If you don't have Git 2.23 and cannot upgrade, you can use:
git checkout <commit> -- <path>
instead of git restore
with --source=... --staged --worktree
. In fact, if you do have Git 2.23 or later, you can still use this kind of git checkout
, which still works.
Summary
Find a commit with a good copy of the file. Any commit will do: commits only hold full snapshots of files, so any commit with the right snapshot in it is a fine source for that copy of that file.
Use git restore
or git checkout
to extract the file from that commit. Using --staged --worktree
tells git restore
to extract the file to both the staging area and your working tree, so that the file is ready to go. Using git checkout
always does this as well: with git restore
you can choose not to have the file already staged, for instance, so git restore
is more flexible, at the cost of requiring more typing.