Below is the the pushed commits history.
Commit Changed Files
Commit 1| (File a, File b, File c)
Commit 2| (File a, File b, File c)
Commit 3| (File a, File b, File c)
Commit 4| (File a, File b, File c)
Commit 5| (File a, File b, File c)
I want to revert the changes happened to File b of Commit 3. But i want the changes happened to this file in commit 4 and 5.
In Git, each commit saves a snapshot—that is, the state of every file—rather than a set of changes.
However, every commit—well, almost every commit—also has a parent (previous) commit. If you ask Git, e.g., what happened in commit
a123456
?, what Git does is find the parent ofa123456
, extract that snapshot, then extracta123456
itself, and then compare the two. Whatever is different ina123456
, that's what Git will tell you.Since each commit is a complete snapshot, it's easy to revert to a particular version of a particular file in a particular commit. You just tell Git: Get me file
b.ext
from commita123456
, for instance, and now you have the version of fileb.ext
from commita123456
. That's what you seemed to be asking, and hence what the linked question and current answer (as of when I am typing this) provide. You edited your question, though, to ask for something rather different.A bit more background
I now have to guess at the actual hash IDs for each of your five commits. (Every commit has a unique hash ID. The hash ID—the big ugly string of letters and numbers—is the "true name" of the commit. This hash ID never changes; using it always gets you that commit, as long as that commit exists.) But they're big ugly strings of letters and numbers, so instead of guessing, say,
8858448bb49332d353febc078ce4a3abcc962efe
, I'll just call your "commit 1"D
, your "commit 2"E
, and so on.Since most commits have a single parent, which lets Git hop backwards from the newer commits to the older ones, let's arrange them in a line with these backwards arrows:
A branch name like
master
really just holds the hash ID of the latest commit on that branch. We say that the name points to the commit, because it has the hash ID that lets Git retrieve the commit. Somaster
points toH
. ButH
has the hash ID ofG
, soH
points to its parentG
;G
has the hash ID ofF
; and so on. That's how Git manages to show you commitH
in the first place: you ask Git Which commit ismaster
? and it saysH
. You ask Git to show youH
and Git extracts bothG
andH
and compares them, and tells you what changed inH
.What you've asked for
Note that this version of the file probably does not appear in any commit. If we take the file as it appears in commit
E
(your Commit 2), we get one without the changes fromF
, but it does not have the changes fromG
andH
added to it. If we go ahead and do add the changes fromG
to it, that's probably not the same as what's inG
; and if we add the changes fromH
to it after that, that's probably not the same as what's inH
.Obviously, then, this is going to be a little harder.
Git provides
git revert
to do this, but it does too muchThe
git revert
command is designed to do this sort of thing, but it does it on a commit-wide basis. Whatgit revert
does, in effect, is to figure out what changed in some commit, and then (try to) undo just those changes.Here is a fairly good way to think of
git revert
: It turns the commit—the snapshot—into a set of changes, just like every other Git command that views a commit, by comparing the commit to its parent. So for commitF
, it would compare it to commitE
, finding the changes to filesa
,b
, andc
. Then—here's the first tricky bit—it reverse-applies those changes to your current commit. That is, since you're actually on commitH
,git revert
can take whatever is in all three files—a
,b
, andc
—and (try to) undo exactly what got done to them in commitE
.(It's actually a bit more complicated, because this "undo the changes" idea also takes into account the other changes made since commit
F
, using Git's three-way merge machinery.)Having reverse-applied all the changes from some particular commit,
git revert
now makes a new commit. So if you did agit revert <hash of F>
you'd get a new commit, which we can callI
:in which
F
's changes to all three files are backed out, producing three versions that probably aren't in any of the earlier commits. But that's too much: you only wantedF
's changes tob
backed-out.The solution is thus to do a little less, or do too much and then fix it up
We already described the
git revert
action as: find the changes, then reverse-apply the same changes. We can do this manually, on our own, using a few Git commands. Let's start withgit diff
or the short-hand version,git show
: both of these turn snapshots into changes.With
git diff
, we point Git to the parentE
and the childF
and ask Git: What's the difference between these two? Git extracts the files, compares them, and shows us what changed.With
git show
, we point Git to commitF
; Git finds the parentE
on its own, and extracts the files and compares them and shows us what changed (prefixed with the log message). That is,git show commit
amounts togit log
(for just that one commit) followed bygit diff
(from that commit's parent, to that commit).The changes that Git will show are, in essence, instructions: they tell us that if we start with the files that are in
E
, remove some lines, and insert some other lines, we'll get the files that are inF
. So we just need to reverse the diff, which is easy enough. In fact, we have two ways to do this: we can swap the hash IDs withgit diff
, or we can use the-R
flag to eithergit diff
orgit show
. Then we'll get instructions that say, in essence: If you start with the files fromF
, and apply these instructions, you'll get the files fromE
.Of course, these instructions will tell us to make changes to all three files,
a
,b
, andc
. But now we can strip away the instructions for two of the three files, leaving only the instructions we want.There are, again, multiple ways to do this. The obvious one is to save all the instructions in a file, and then edit the file:
git show -R hash-of-F > /tmp/instructions
(and then edit
/tmp/instructions
). There's an even easier way, though, which is to tell Git: only bother showing instructions for particular files. The file we care about isb
, so:git show -R hash-of-F -- b > /tmp/instructions
If you check the instructions file, it should now describe how to take what's in
F
and un-changeb
to make it look like what's inE
instead.Now we just need to have Git apply these instructions, except that instead of the file from commit
F
, we'll use the file from the current commitH
, which is already sitting in our work-tree ready to be patched. The Git command that applies a patch—a set of instructions on how to change some set of files—isgit apply
, so:should do the trick. (Note, though, that this will fail if the instructions say to change lines in
b
that were subsequently changed by commitsG
orH
. This is wheregit revert
is smarter, because it can do that whole "merge" thing.)Once the instructions are successfully applied, we can look over the file, make sure it looks right, and use
git add
andgit commit
as usual.(Side note: you can do this all in one pass using:
git show -R hash -- b | git apply
And,
git apply
has its own-R
or--reverse
flag as well, so you can spell this:git show hash -- b | git apply -R
which does the same thing. There are additional
git apply
flags, including-3
/--3way
that will let it do fancier things, much likegit revert
does.)The "do too much, then back some of it out" approach
The other relatively easy way to deal with this is to let
git revert
do all of its work. This will, of course, back out the changes to the other files, that you didn't want backed-out. But we showed at the top how ridiculously easy it is to get any file from any commit. Suppose, then, that we let Git undo all the changes inF
:git revert hash-of-F
which makes new commit
I
that backs out everything inF
:It's now trivial to
git checkout
the two filesa
andc
from commitH
:git checkout hash-of-H -- a c
and then make a new commit
J
:The file
b
in bothI
andJ
is the way we want it, and the filesa
andc
inJ
are the way we want them—they match the filesa
andc
inH
—so we're now pretty much done, except for the annoying extra commitI
.We can get rid of
I
in several ways:Use
git commit --amend
when makingJ
: this pushesI
out of the way, by making commitJ
use commitH
asJ
's parent. CommitI
still exists, it's just abandoned. Eventually (after roughly a month) it expires and really goes away.The commit graph, if we do this, looks like this:
Or,
git revert
has a-n
flag that tells Git: Do the revert, but don't commit the result. (This also enables doing a revert with a dirty index and/or work-tree, though if you make sure you start with a clean checkout of commitH
you don't need to worry about what this means.) Here we'll start withH
, revertF
, then tell Git get filesa
andc
back from commitH
:git revert -n hash-of-F
git checkout HEAD -- a c
git commit
Since we're on commit
H
when we do this, we can use the nameHEAD
to refer to the copies ofa
andc
that are in commitH
.(Git being Git, there are a half dozen additional ways to do this; I'm just using the ones that we're illustrating in general here.)
Assuming the hash of the commit you want is c77s87:
git checkout c77s87-- file1/to/restore file2/to/restore
The git checkout main page gives more information.