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

Tags:

Git