The confusing bit is here:
Git never ever sees those as individual files. Git thinks everything as the full content.
Git often uses 160 bit hashes in place of objects in its own repo. A tree of files is basically a list of names and hashes associated with the content of each (plus some metadata).
But the 160 bit hash uniquely identifies the content (within the universe of the git database). So a tree with hashes as content includes the content in its state.
If you change the state of the content of a file, its hash changes. But if its hash changes, the hash associated with the file name's content also changes. Which in turn changes the hash of the "directory tree".
When a git database stores a directory tree, that directory tree implies and includes all of the content of all of the subdirectories and all of the files in it.
It is organized in a tree structure with (immutable, reusable) pointers to blobs or other trees, but logically it is a single snapshot of the entire content of the entire tree. The representation in the git database isn't the flat data contents, but logically it is all of its data and nothing else.
If you serialized the tree to a filesystem, deleted all .git folders, and told git to add the tree back into its database, you'd end up with adding nothing to the database -- the element would already be there.
It may help to think of git's hashes as a reference counted pointer to immutable data.
If you built an application around that, a document is a bunch of pages, which have layers, which have groups, which have objects.
When you want to change an object, you have to create a completely new group for it. If you want to change a group, you have to create a new layer, which needs a new page, which needs a new document.
Every time you change a single object, it spawns a new document. The old document continues to exist. The new and old document share most of their content -- they have the same pages (except 1). That one page has the same layers (except 1). That layer has the same groups (except 1). That group has the same objects (except 1).
And by same, I mean logically a copy, but implementation-wise it is just another reference counted pointer to the same immutable object.
A git repo is a lot like that.
This means that a given git changeset contains its commit message (as a hash code), it contains its work tree, and it contains its parent changes.
Those parent changes contain their parent changes, all the way back.
The part of the git repo that contains history is that chain of changes. That chain of changes it at a level above the "directory" tree -- from a "directory" tree, you cannot uniquely get to a change set and the chain of changes.
To find out what happens to a file, you start with that file in a changeset. That changeset has a history. Often in that history, the same named file exists, sometimes with the same content. If the content is the same, there was no change to the file. If it is different, there is a change, and work needs to be done to work out exactly what.
Sometimes the file is gone; but, the "directory" tree might have another file with the same content (same hash code), so we can track it that way (note; this is why you want a commit-to-move a file separate from a commit-to-edit). Or the same file name, and after checking the file is similar enough.
So git can patchwork together a "file history".
But this file history comes from efficient parsing of the "entire changeset", not from a link from one version of the file to another.