Refs, HEAD, and the index file
Objects are content. Refs are names. The reason commits get human-readable names like main or v2.4.0 is the refs/ hierarchy under .git/: small files containing one hash each, plus packed-refs for older entries. The reason Git knows where you currently are is HEAD, a special pointer pointing at a ref (or, when detached, directly at a commit). Together these three pieces — refs, HEAD, and the index — are the bridge between the object database and your daily commands.
Refs live in three sub-trees. refs/heads/<name> are local branches. refs/remotes/<remote>/<branch> are remote-tracking branches — read-only-from-your-perspective shadows of what the remote had at last fetch. refs/tags/<name> are tags (lightweight tags are direct hashes; annotated tags point at tag objects). git for-each-ref walks all of them in one query, useful for housekeeping.
HEAD is the only pointer Git modifies on every operation. Its file content is usually ref: refs/heads/main — a symbolic ref to a branch. When you commit, Git updates the commit object, then updates the branch ref to point at the new commit, leaving HEAD pointing at the same branch (now at a new tip). When you switch branches, HEAD's symbolic-ref content changes. When you check out a specific commit, HEAD becomes a direct hash — that is detached HEAD.
The index is the third structure. Stored at .git/index, it is a binary file describing what the next commit's tree will look like — every path Git tracks, with mode, blob hash, timestamps, and stage information for in-progress merges. git ls-files --stage reads it. git diff compares working tree to index; git diff --staged compares index to HEAD's tree. The index exists because of git add — without it, every commit would have to be all-or-nothing.