Skip to content

How Git Works

What You'll Learn

  • How Git stores your project as content-addressed objects
  • Why a commit is a snapshot, not a diff
  • What the working tree, staging area, and repository each hold
  • Why branches are cheap, what HEAD points to, and how remotes fit in

Mental Model

Git is a database of immutable snapshots linked into a graph. Branches are movable name tags on that graph. HEAD says which tag you're standing on. Almost every Git command either adds snapshots or moves tags.

Once this clicks, "dangerous" commands like reset and rebase become understandable: they move tags and create new snapshots, and the old snapshots are still there for a while.

The Object Database

Everything Git stores lives in .git/objects, named by a hash of its content.

Object Stores Points to
blob The contents of one file (no name, no permissions) Nothing
tree A directory listing: names, modes, and the hashes of blobs and subtrees Blobs and trees
commit Author, committer, message, timestamp One tree (the snapshot) and zero or more parent commits
tag (annotated) A named, optionally signed, pointer with a message Usually a commit
flowchart LR
  C2["commit b7e41c<br/>Add retries"] -->|parent| C1["commit 3fa09d<br/>Initial API"]
  C2 --> T2["tree"]
  T2 --> B1["blob: README.md"]
  T2 --> T3["tree: src/"]
  T3 --> B2["blob: app.py (new version)"]
  C1 --> T1["tree"]
  T1 --> B1
  T1 --> T4["tree: src/"]
  T4 --> B3["blob: app.py (old version)"]

Unchanged files are shared between snapshots — README.md is stored once. That's why snapshots are cheap.

See it yourself

mkdir git-internals && cd git-internals && git init -q
echo "hello" > greeting.txt
git add greeting.txt
git commit -qm "Add greeting"

git cat-file -p HEAD                  # the commit object
tree 6d0cf2a3c1b56a1c3a0f4a0d0c7f3f3c5c0a4a2e
author Priya Shah <priya@example.com> 1757840400 +0000
committer Priya Shah <priya@example.com> 1757840400 +0000

Add greeting
git cat-file -p 'HEAD^{tree}'         # the tree: one blob named greeting.txt
git cat-file -p HEAD:greeting.txt     # the blob: hello

Because an object's name is the hash of its content, changing anything — a file, a message, a parent — produces a different hash. That's what makes history tamper-evident, and why rewriting one old commit changes every commit after it.

Those hashes are SHA-1 (40 hex characters) in almost every repository today. Git also supports SHA-256 repositories (git init --object-format=sha256), and the Git project plans to make SHA-256 the default in Git 3.0, alongside main as the default branch name. Existing repositories keep working either way; the change matters mostly for tools that assume a 40-character hash.

Three Places Your Changes Live

flowchart LR
  W["Working tree<br/>files on disk"] -->|"git add"| I["Staging area (index)<br/>the next snapshot"]
  I -->|"git commit"| R["Repository<br/>committed snapshots"]
  R -->|"git restore / git switch"| W
  I -->|"git restore --staged"| W
Area Holds Inspect with
Working tree Files as you're editing them git status, git diff
Staging area Exactly what the next commit will contain git diff --staged
Repository Every commit git log

The staging area lets you commit part of your changes:

git add -p app.py        # stage selected hunks interactively
git commit -m "Add request timeout"
# the rest of your edits stay uncommitted

Branches Are Pointers

A branch is a file containing one commit hash:

$ cat .git/refs/heads/main
b7e41c9a0f6d2e8c3b1a4f5d6e7c8b9a0d1e2f3a

Creating a branch writes a new 41-byte file. Committing on a branch moves that pointer to the new commit.

gitGraph
  commit id: "3fa09d"
  commit id: "b7e41c"
  branch feature/retry
  checkout feature/retry
  commit id: "c91d02"
  commit id: "e44a7b"
  checkout main
  commit id: "f10b3c"
  • main points to f10b3c.
  • feature/retry points to e44a7b.
  • Both share history up to b7e41c, their merge base.

HEAD is a pointer to what you have checked out — normally a branch, not a commit:

$ cat .git/HEAD
ref: refs/heads/feature/retry

When you commit, Git writes the commit, then moves the branch HEAD points to.

Detached HEAD

If you check out a commit or tag directly, HEAD points at a commit instead of a branch:

git switch --detach v2.3.0

You can look around and even commit, but no branch moves with you. When you switch away, those commits aren't on any branch. Keep them by creating one:

git switch -c hotfix/from-v2.3.0

Remotes and Remote-Tracking Branches

A remote is another copy of the repository, usually named origin.

git remote -v
git branch -a
* feature/retry
  main
  remotes/origin/main
  remotes/origin/feature/retry

origin/main is your local record of where main was on the remote the last time you talked to it. It only updates when you fetch (or pull).

Command Does
git fetch Download new commits and update origin/*. Never touches your branches or files.
git pull fetch, then merge or rebase origin/<branch> into your current branch
git push Upload your commits and move the branch on the remote

Fetching is always safe, so git fetch followed by a look at git log origin/main is a good habit before changing anything.

Tags

git tag -a v2.4.0 -m "Release 2.4.0"   # annotated: author, date, message; can be signed
git push origin v2.4.0

A tag is a pointer that doesn't move. Use annotated tags for releases, and never move a published release tag — CI systems, caches, and people depend on it meaning one exact commit.

What Most Engineers Miss

  • A branch is a 41-byte file. cat .git/refs/heads/main prints a commit hash and a newline (or the ref lives in .git/packed-refs). Creating a branch costs nothing, which is why short-lived branches are cheap.
  • Git stores snapshots, then compresses them. Each commit points to a full tree; packfiles later store most objects as deltas against similar ones. "Git stores diffs" is wrong at the model level and right only inside packfiles.
  • SHA-1 is hardened, not trusted. After Google and CWI published the first practical SHA-1 collision in 2017 (SHAttered), Git 2.13 switched to a SHA-1 implementation that detects collision attacks. Repositories can also use SHA-256 (git init --object-format=sha256), but most hosting services, including GitHub, don't support SHA-256 repositories yet.
  • Git 3.0 changes defaults. The project's published breaking-changes plan makes SHA-256 the default for new repositories and main the default branch name in Git 3.0, and makes Rust a required part of the build. No release date is set; set init.defaultBranch explicitly today so nothing depends on the default.
  • The empty tree has a famous hash. git hash-object -t tree /dev/null prints 4b825dc642cb6eb9a060e54bf8d69288fbee4904 in every SHA-1 repository, handy for git diff 4b825dc HEAD to see a whole project as one diff.

Common Mistakes

  • Thinking of commits as diffs, and being confused when a rebase "changes every commit hash".
  • Believing git fetch changes your working files, or that origin/main updates itself.
  • Committing in detached HEAD state and losing track of the work when switching branches.
  • Staging everything with git add . and committing unrelated changes, secrets, or build output together.
  • Moving or deleting published tags that pipelines and deployments reference.

Check Yourself

Deleting a branch

You delete a local branch that was never merged or pushed. What happened to its commits?

  • They were deleted with the branch
  • They still exist as unreachable objects until garbage collection prunes them, and the reflog can find them
  • They moved to main

A branch is only a pointer. git branch -D removes the pointer; the reflog still knows the hash.

Interview Questions

  • What are the four types of Git objects, and how do they relate?
  • What is a branch in Git, physically?
  • What does HEAD point to, and what is a detached HEAD?
  • What's the difference between main and origin/main?
  • Why does rewriting an old commit change the hashes of every later commit?

Next

Continue to Everyday Workflow.