odCat
odCat

Reputation: 98

Why does git show filename display a diff?

git show filename diplays a diff, while git show branch:path/to/filename displays the content of the file.

I look in the help (git show --help) and what I understood is that it should default to HEAD, i.e. git show HEAD:filename.

But that produces the content of the file as I expected. Does anyone have any insight?

Upvotes: 5

Views: 315

Answers (4)

Steve Summit
Steve Summit

Reputation: 48083

Thanks, @LeGEC, @IMSoP, and @VonC for the additional answers. I have chosen LeGEC's for the bonus, and I'm also going to provide a sort of a "distilled" answer here. (This is an informal answer; see the others for more precise details.)

The confusion is, how could

git show filename

not show you, like, that file? Especially given that

git show revision:filename

does show you the file? And the answer (as is so often the case with git) is that you were thinking too narrowly, there's actually more going on, and once you look at the bigger picture things make more sense.

The first part of the answer is that git show is obviously not just for showing files. If you entered

git show revision

you would clearly want it to show you that revision, and showing diffs is a great and appropriate way to show a revision. So, sometimes git show shows files, and sometimes it shows diffs, and this shouldn't be surprising. (And sometimes it shows other things, like trees.)

And then, the second part of the answer is that, like many git commands, git show has the option to boil down some potentially voluminous output to just the files you care about. As a different example, with git diff, you can say

git diff revision

to show you diffs with respect to a certain revision, or you can say

git diff revision filename

to limit the diffs to just that filename. Similarly, with git show, you can say

git show revision filename

to, again, limit the revision's diffs to just that filename.

But this is then the answer to the original question: When you say

git show filename

it's a shortcut for

git show HEAD filename

and shows you just the diffs in the latest revision that apply to the filename.

Or in other words, the surprising behavior of

git show filename

falls out of git's naming conventions, and the fact that there's an implicit HEAD in there, making this an instance of the git show-showing-revisions form, not git show-showing-files.

Upvotes: 3

LeGEC
LeGEC

Reputation: 52226

I was surprised, by reading git help show, to see no mention of extra [<path>...] arguments. The description mentions git diff-tree, and although git help diff-tree mentions the existence of [<path>...], there is no description of what it does.
This is (IMHO) clearly a shortcoming in git help show.

Nevertheless, much like git diff or git log, git show <object>... can be extended to git show <object>... [<path>...], or the non ambiguous git show <object>... -- <path>..., which applies on commit objects to narrow what is displayed from these commits. Specifying <path>... has no effect on git show when displaying a tree or a blob.

A point that is mentioned in the doc is that when naming a directory (a tree), only the content of that directory is shown (not a diff), and when naming a file (a blob), only the content of that file is shown (also not a diff).

Naming such things is described in git help gitrevision :

# some ways to name a tree:
git show <commit>:path/to/dir  # path from the top-level
git show <commit>:./dir  # path relative to your pwd
git show <commit>:   # the top-level of your repo at <commit>
git show <commit>^{tree}

# some ways to name a blob:
git show <commit>:full/path/to/file
git show <commit>:./rel/path/to/file

One an extra piece of information which, to my knowledge, isn't entirely worded in git doc:

for commands such as git diff, git log, git show tries to guess, from the names you pass on the command line, what is an object and what is a filtering path.

Meaning: when you type git show foo/bar

  1. if foo/bar happens to match a branch or a tag name, it will display that object (and its diff...), and not resort to the default HEAD

  2. if foo/bar matches no branch or tag but matches a path on disk, it will try to treat it as a filtering path on the default HEAD commit, as in git show HEAD foo/bar ; this will show a diff, narrowed to path foo/bar

  3. if the string actually contains a :, as in git show foo:bar or git show :foo/bar, git will unambiguously treat this as a blob or tree name (with foo:bar: object located at bar within commit foo, with :foo/bar, object located at foo/bar within the default HEAD commit) ; this will display the plain content, and no diff

You can remove the guesswork by putting a -- in your command line:

#  left of '--' : object names ('foo' and 'bar')
# right of '--' : filtering paths  ('baz' and 'buzz')
git show foo bar -- baz buzz

If you don't provide a -- in your command line, and a name can be ambiguous, you would see an error message looking like that:

# with a file named 'master' on disk:
$ git show master
fatal: ambiguous argument 'master': both revision and filename
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Back to the OP's question :

  • his first git show filename falls in point 2. above,
  • his second git show HEAD:path/to/filename falls in point 3.

As stated in point 2., the only (small but impactful) thing to correct in the OP's question is that git show filename defaults to git show HEAD -- filename, not to git show HEAD:filename.

Upvotes: 6

IMSoP
IMSoP

Reputation: 98005

tl;dr

  • git show example.txt means git show HEAD -- example.txt
  • The object shown is HEAD, not example.txt
  • The example.txt argument is used to first filter the "list" of revisions, and then filter the diff displayed
  • So the result is either empty (if example.txt was not changed in the latest commit), or a summary of the latest commit with everything other than example.txt ignored

To figure this out, I went to the source. It turns out that git show actually shares a lot of its implementation with git log, including how it processes its command-line arguments.

That means the synopsis from the git log manual page can give us some clues:

git log [<options>] [<revision-range>] [[--] <path>…​]

Note that the revision range and path are both optional, and the -- is only needed if it's ambiguous which is meant. Under <revision-range>, we learn that:

When no <revision-range> is specified, it defaults to HEAD

So assuming there is a file called "example.txt", but no branch or tag called that, the following are all equivalent:

git log HEAD -- example.txt
git log HEAD example.txt
git log -- example.txt
git log example.txt

In all four cases, the <revision-range> is HEAD, and the <path> is example.txt.


In git log, the meaning of <path> is:

Show only commits that are enough to explain how the files that match the specified paths came to be.

In other words, while the <revision-range> decides what branch of history to look through, <path> decides what commits inside that history should be examined.

It also has another effect, which is crucial to understanding the next section: when asked to show changes introduced by a commit, it only considers the changes to <path>. For instance, if you run:

git log --oneline --name-status example.txt

You'll get something like this:

93f11d8429 Do the needful
M       example.txt
e4d79ce24c Make some changes
M       example.txt
2ce2aff50e Reformat all files to use non-breaking spaces lol
M       example.txt

To see all the other files in each commit, you have to pass the --full-diff option.


Now, back to git show. Although it doesn't give the same synopsis, it actually uses the same argument parsing, so for our same hypothetical repo, the following are equivalent:

git show HEAD -- example.txt
git show HEAD example.txt
git show -- example.txt
git show example.txt

So what does this actually do?

  1. It processes the <revision-range> (HEAD) looking for a single commit; but because it's using the machinery from git log, what it actually gets is a list with one item in
  2. It "simplifies" this list based on whether each commit "contributed to the history of example.txt" (the <path> argument).
  3. As long as example.txt was actually changed in the current HEAD commit, it will still have one commit, and proceed with its display, starting with the summary of the commit.
  4. It then prints a diff of the changes introduced by the commit, but only the changes made to <path>

You can demonstrate this is what's happening with a few variations:

  • If you run git show HEAD -- example-2.txt, and example-2.txt exists, but wasn't modified in the most recent commit, you get an empty output: the revision is filtered out at step 2.
  • The same is true if example-2.txt doesn't exist at all - it still processes step 1 successfully, so there's no error. Running git show example-2.txt would instead complain that it can't tell if example-2.txt was intended as a branch or a file, since it can't find either.
  • If you run git show --full-diff example.txt, you'll get the same output as git show HEAD, as long as example.txt was changed in the most recent commit. The option changes step 4 to be "show all changes in the selected revision, regardless of <path>".

Finally, what about the format that does show the content?

git show HEAD:example.txt
git show :example.txt

In this case, HEAD:example.txt or :example.txt is a single argument, which can be interpreted as a <revision-range> according to the general revision parser:

A suffix : followed by a path names the blob or tree at the given path in the tree-ish object named by the part before the colon.

For git log, this is valid but meaningless; but for git show, it leads to a completely different code path:

  1. It looks at the <revision-range>, which is HEAD:example.txt, and decides what type it references.
  2. Seeing that it references a blob, it decides to show the content of that blob.

Upvotes: 3

ElpieKay
ElpieKay

Reputation: 30966

git show <path> is equivalent to git show HEAD <path>.

Without <path>, it prints the log message and the diff of changed files of the head commit.

With <path>, it prints the log message and the diff of the specified path of the head commit if the file is changed in the commit. If not, nothing is printed.

Upvotes: 2

Related Questions