Snapshots, not patches
The most common wrong intuition about Git is that it stores diffs — the difference between version N and N+1. That is how SVN and CVS work. Git does something fundamentally different: every commit is a complete snapshot of every file, addressed by hash.
If a file did not change between two commits, Git does not store it twice. It stores a pointer to the same blob. Identical content always produces the same hash, so deduplication is automatic. Any one-bit change produces a completely different hash, so corruption is detectable. This is the whole reason Git is fast on history operations: showing you any version is just loading the snapshot, not replaying every diff since the dawn of the project.
Three object types make this work. A blob is the raw bytes of one file. A tree is a directory listing — names, modes, and the blob hashes they point to. A commit is a small object holding tree hash, parent hash(es), author, committer, and message. Each object lives in .git/objects/, named by its SHA-1 hash. Branches are 41 bytes pointing at a commit. Tags are 41 bytes pointing at any object. The whole graph is content-addressable.
You will not type git cat-file every day, but seeing one blob, one tree, and one commit by hand is the vaccine against the diff-based mental model. Once you have looked, branches stop being magical and merges stop being scary.