Git Internals
Git through content-addressed snapshots, refs, staging, and history transport.
Git is a distributed version control system. The important word is distributed. A clone is not just a working copy of files. It also contains its own local repository with commits, branches, tags, and object history. That design is why many Git operations are fast and work offline.
Inside a repository, Git mainly stores four object types. A blob stores file contents. A tree stores a directory snapshot that maps names to blobs or other trees. A commit points to one top-level tree and records metadata such as author, timestamp, parent commits, and message. A tag can point at a commit and attach a stable name. These objects are content addressed, so their identifiers are derived from their content. If a file snapshot changes, its blob ID changes too.
Git does not think in terms of diff files as its primary storage model. Conceptually, each commit is a snapshot of the project tree. Diffs are derived later when Git compares one snapshot with another. This is a useful mental model because commands like checkout, reset, and merge are really about moving or reconciling snapshots.
There are three important local states. The working tree is the actual files on disk that you edit. The index, also called the staging area, is a structured snapshot of what will go into the next commit. The local repository under .git stores committed objects and references. On top of that, you may have remote repositories such as origin, which are just other Git repositories reachable over a transport protocol.
git add copies the current contents of selected files from the working tree into the index. It does not create a commit. git commit reads the index, writes any new blobs and trees, then creates a commit object that points to that tree and to its parent commit or parents. A branch name such as main is only a movable reference pointing at a commit. HEAD usually points to the current branch, which is why new commits advance that branch automatically.
Remote operations add another layer. git fetch downloads commits and refs from a remote into your local repository without changing your working tree. git pull is usually fetch plus a merge or rebase. git push asks the remote to move one of its refs to commits you already have locally. Pushes can fail if the remote branch advanced and your update is not a fast-forward. That failure is a safety feature because it prevents accidental history loss.
Merging and rebasing often confuse new users because both integrate work, but in different ways. A merge creates a new commit with multiple parents and preserves the original branch structure. A rebase rewrites commits so they appear to have been created on top of a different base. Rebasing can produce a clean linear history, but it changes commit IDs, so rebasing shared public history is dangerous unless everyone coordinating with that branch knows what is happening.
Git is efficient partly because loose objects are later compressed into packfiles, which reduces storage and network transfer. It is also safe partly because history is immutable at the object level. You do not edit an old commit in place. You create new objects and move refs.
The main failure modes are practical, not magical. Staging the wrong files, resetting to the wrong commit, force pushing rewritten history, or resolving a merge conflict incorrectly can all lose work. The good news is that Git usually moves references rather than instantly deleting data, so recovery is often possible through the reflog as long as garbage collection has not pruned the unreachable objects.
So Git works by storing content-addressed snapshots locally, using refs as movable names, and letting commands move data between the working tree, the index, the local repository, and remotes. Once that model is clear, the command set stops feeling arbitrary and starts looking like a consistent way to create, name, compare, and transport project history.