TL;DR
You probably (probably, this can get complicated) want a particular mode of git checkout
or, in Git 2.23 or later, git restore
, here. You can extract particular files from particular commits this way, without actually changing commits. The details get a little sticky, although the new git restore
probably does exactly what you want, right out of the box, as it were:
git restore <commit-hash> -- path/to/filename1 path/to/filename2
Long
Each commit in a Git repository is (or holds, really) a snapshot of all of your files.
When you use:
git checkout <commit-hash>
(or since Git 2.23, the same thing with git switch
), you ask Git to switch to the given commit as a detached HEAD. This reads the chosen commit into the index—the index, or staging area, holds your proposed next commit so switching to a commit requires filling it in from that commit—and in the process, adjusts the contents of your work-tree to match the commit you've switched-to.
When you use git checkout branch-name
( or again git swtich
), Git does the same thing, except that now the special name HEAD
is attached to the branch name. Git now remembers which branch you're on, and making a new commit will write the new commit's hash ID into that branch name.
Well, that's all well and good, but now you want some particular file out of one particular commit, without switching commits at all. This is where Git 2.23 and later are better, because there's a separate command for this, git restore
. We'll get to that in a moment, but first let's talk more about git checkout
.
The git checkout
command is absurdly complex. It has, depending on how you count, something like four to seven different modes of operation. The one we care about here is the one that checks out particular files from particular commits. That is, we want:
git checkout <commit-hash> -- <paths>
It's important to remember Git's index here, because this kind of git checkout
first writes the files to the index. The paths
argument you give, after the --
, determines which files come out of the chosen commit. The --
itself is only there to make sure that these file names don't look like flags or branch names or whatever. The commit-hash
part can be anything acceptable to git rev-parse
, as long as it names a commit or tree object internally; a commit hash is good here.
Having copied the file(s) you named from the commit you named into the index, this git checkout
goes on to write the files into your work-tree. Note that unlike a regular, safe git checkout
, this particular mode will destroy the current contents of the named files even if they are not saved anywhere else. So if you have used git checkout
a lot and are happy with the fact that it will tell you: no, I can't do that, I'll lose some files you might have forgotten to save, just remember that this form of git checkout
is quite ruthless.
In Git 2.23 and later, there is a separate command, git restore
, that takes over this job. The new restore
command can copy the file separately to the index, or to your work-tree, or both. The default is just to copy to the work-tree, without affecting the index (and also without checking whether you've saved the work-tree file anywhere, so be careful with it!). So:
git restore <commit-hash> -- path/to/filename1 path/to/filename2
will copy those files from that commit into your work-tree. The updated files won't be staged for commit, as your index has not been altered at all. (As before, the --
is just in case a file name is something weird like --staged
. If it's not anything like this, you can omit the --
. It's a good habit to get into though.) If everything looks good, you can git add
and git commit
as usual.
(If, after this, you want to use interactive rebase, or a soft reset and commit, to squash away some of the extra intermediate commits, that's a topic for another question—there are already a lot of answers to those questions.)
git revert
does not accept file names; you're reverting an entire commit, or not. There are many ways to achieve what you want, depending on what exactly it is you want...