git: See changes to a specific file by a commit
eftshift0's answer is correct (and I've upvoted it). Here's why, though—along with the one other thing that can go wrong here.
In most cases, git show commit -- path
would be correct and would show you:
- the log message for the specified commit, and
- a patch for that particular file, as produced by comparing that commit with its parent.
The patch would be the same as that produced by git diff commit^1 commit -- path
. (Note that the ^1
suffix here is literal text while the commit
part is a hash ID that you replace. The suffix syntax means "find the first parent". You can add this suffix to most commit selectors, although not to selectors that use certain search patterns. See the gitrevisions documentation.)
There are two important exceptions. Only one of these applies here, because git blame
normally does not blame a merge, it tries to trace the source of the change that fed into the merge. Still, I want to mention it, because git show
's behavior on merges is ... interesting. :-)
If you look at a merge commit with git show
, you will see, by default, a combined diff ("all parents" vs the merge commit's content). In this case you may wish to fall back directly on git diff
, so that you can specify the parent you want to compare (^1
, ^2
, and even more if this is an octopus merge). The reason is that combined diffs deliberately omit any file that matches the version in one of the parent commits: this means that whatever is in the repository from that point forward, it came from one of the two (or N if N > 2) "sides" of the merge.
With git blame
, you are looking for "who changed what". The person who did the merge is often not the person who made the change—so you should keep going "behind the merge" to find who really changed it.
The second exception is the one that caused a problem for you here, and it's really more a case of the way git blame
works when files get renamed during development.
When git blame
is analyzing changes to a file, such as include/svl/itemset.hxx
, it steps back one commit at at time, starting from whichever commit you specify. If you don't select your own starting point, it starts from from HEAD
, i.e., the current commit. It then looks at the parent commit(s) (as if via git show
for instance). For instance, if the current commit 922e935c8812
is an ordinary commit and its parent is 22c6554c98e2
, it compares commit 922e935c8812
to 22c6554c98e2
. If 22c6554c98e2
has a file of the same name, that's probably the same file ... but if not, Git tries to figure out which file in 22c6554c98e2
is the same file as include/svl/itemset.hxx
.
In this case, that exact thing happens at commit b9337e22ce1d
. There is a file named include/svl/itemset.hxx
in commit b9337e22ce1d
, but in commit b9337e22ce1d^
or f4e1642a1761
, the file is named svl/inc/svl/itemset.hxx
. Git detects this rename when stepping back from commit b9337e22ce1d
to commit f4e1642a1761
, and git blame
then carries the new name back with it from commit f4e1642a1761
to commit d210c6ccc3
.
When you run git show d210c6ccc3
, however, Git jumps directly to d210c6ccc3
(and its parent 7f0993d43019
). It no longer knows that the file named include/svl/itemset.hxx
in HEAD
is named svl/inc/svl/itemset.hxx
in d210c6ccc3
. So you must discover this, and pass the earlier name to Git.
You might wonder how you can find this. The answer is to use git log --follow
. The --follow
code for git log
is not great,1 but it's the same code that git blame
uses, so it produces the same answers, at least. Here is what I did:
$ git log --oneline --follow --name-status include/svl/itemset.hxx
00aa9f622c29 Revert "used std::map in SfxItemSet"
M include/svl/itemset.hxx
afaa10da2572 make SfxItemSet with SAL_WARN_UNUSED
M include/svl/itemset.hxx
[snip]
a7724966ab4f Bin comments that claim to say why some header is included
M include/svl/itemset.hxx
b9337e22ce1d execute move of global headers
R100 svl/inc/svl/itemset.hxx include/svl/itemset.hxx
There's a second rename even earlier. Here's another, shorter way to find only the renames:
$ git log --oneline --follow --diff-filter=R --name-status include/svl/itemset.hxx
b9337e22ce1d execute move of global headers
R100 svl/inc/svl/itemset.hxx include/svl/itemset.hxx
e6b4345c7f40 #i103496#: split svtools in two libs, depending on whether the code needs vcl or not
R100 svtools/inc/svtools/itemset.hxx svl/inc/svl/itemset.hxx
If d210c6ccc3
came "before" e6b4345c7f40
, you would have to use the even-earlier path name—but commit d210c6ccc3
is a descendant of (comes after) e6b4345c7f40
, not an ancestor.
1When changes occur at merges, this really, in a fundamental sense, requires following both (or all) input commits simultaneously. However, Git is not (currently?) capable of doing this: both git log --follow
and git blame
really only traverse whichever parent the final version of the file "came from". That is, if we look at a typical merge commit M
containing merged file F
, there are two inputs M^1:F
and M^2:F
, and one output, M:F
. If M:F
is the same as M^1:F
, we should ignore M^2:F
entirely: all the contents of M:F
are those from whoever provided M^1:F
. If M:F
is the same as M^2:F
, we should ignore M^1:F
entirely: all the contents of M:F
are those from whoever provided M^2:F
.
Note that this only works for one file at a time, and even then, it only works if the file exactly matches one of the two inputs. Otherwise, we should look at the combined diff to see how the file got modified from both inputs. This is the logic behind both combined diffs and History Simplification. But it's over-simplified in some rare cases, and hence sometimes gets things wrong.
You can use
git diff d210c6ccc3^ d210c6ccc3 include/svl/itemset.hxx
to view the changes made to a file in commit (d210c6ccc3
) compared to it's predecessor (d210c6ccc3^
).
Where d210c6ccc3^
is the 'base' and d210c6ccc3
is the commit you want to compare to the base.
If as mentioned in comments on your question, you are looking at the wrong commit then replace the SHA above.
This will give a diff
output of the contents of the files, the git show
command you gave will output the commit message - only if that file was modified in the commit specified.
You have to provide the path in that revision:
git show d210c6ccc3 -- svl/inc/svl/itemset.hxx
That should do