Repository Structure
A Git repository looks like an ordinary folder on disk — but it is hiding a complete database in a subfolder called .git/. Understanding that subfolder demystifies almost everything Git does. You do not need to memorise its contents, but a quick tour gives you a feel for what is real and what is just commands.
A repo at a glance
A typical project after a few commits
my-project/
├── .git/ ← Git's database — the actual repository
├── .gitignore ← (optional) which files to ignore
├── README.md
├── package.json
├── src/
│ └── index.js
└── tests/
└── index.test.jsEverything outside .git/ is your working tree — the files you edit. Everything inside .git/ is the repository itself. Delete .git/ and you have an ordinary folder with no history; delete the rest and Git can recreate it from history.
Inside .git/
What lives in .git
.git/
├── HEAD ← pointer to the current branch
├── ORIG_HEAD ← previous HEAD before a destructive op
├── FETCH_HEAD ← latest fetched commit
├── config ← repo-level git config
├── description ← only used by GitWeb
├── index ← the staging area (binary file)
├── packed-refs ← compressed ref store
├── hooks/ ← scripts that run on Git events
├── info/
│ └── exclude ← like .gitignore but private to this clone
├── logs/ ← reflogs for branches and HEAD
│ └── HEAD
├── objects/ ← THE DATABASE — every commit, tree, and blob
│ ├── 1f/
│ │ └── 9ab2c...
│ ├── pack/
│ └── info/
└── refs/
├── heads/ ← local branch pointers
│ └── main
├── tags/ ← tag pointers
└── remotes/
└── origin/
└── mainThe interesting files explained
HEAD — a tiny text file containing
ref: refs/heads/main(or whichever branch you are on). When youcat .git/HEADyou see exactly which branch is current.index — a binary file that is the staging area. When you
git add, Git updates the index. When yougit commit, Git turns the index into a new commit.config — INI-format file with repo-level settings (the remote URLs, user name overrides, etc.).
objects/ — the heart of Git. Every commit, every directory snapshot, and every file content lives here as a content-addressed blob.
refs/heads/ — one tiny file per local branch. The file content is a 40-character commit hash — the tip of that branch.
refs/tags/ — same idea, but for tags.
refs/remotes/ — pointers to where Git thinks each remote branch is. Updated by
git fetch.hooks/ — sample scripts you can rename and make executable to run on
commit,push, etc.logs/ — the reflog — a per-branch log of every move HEAD or each branch has made. This is how
git reflogrecovers lost commits.
How a branch is stored
Look inside .git
cat .git/HEAD # ref: refs/heads/main cat .git/refs/heads/main # 1f9ab2c0e8b4... (a 40-char SHA-1) # That is literally the entire 'main' branch: # - HEAD points to refs/heads/main # - refs/heads/main points to a commit hash # - the commit lives in .git/objects/1f/9ab2c0e8b4...
Branches really are just 41-byte files. That is why creating one is instantaneous.
The four object types
blob — the contents of a file (no name, no permissions, just bytes).
tree — a snapshot of a directory: a list of names mapping to blobs and other trees, with permissions.
commit — a snapshot reference (one tree), plus parent commit(s), plus metadata (author, date, message).
tag — an annotated tag — a named pointer to a commit with its own message.
Peek at any object
# What type is this object? git cat-file -t 1f9ab2c # commit # Show its contents git cat-file -p 1f9ab2c # tree 5b88e9... # parent c204c1... # author You <you@example.com> 1700000000 +0000 # committer You <you@example.com> 1700000000 +0000 # # Initial commit
The working tree vs the repository
Working tree — your real files. Edit them in your editor.
Repository (
.git/) — Git’s database. Modified only by Git commands.Files only become part of history once you
git addthem (into the index) andgit committhem (into the objects database).
The staging area / index
The index (the file .git/index) is a curated picture of what your next commit will look like. It sits between the working tree and the repo.
Three places at once
┌────────────┐ git add ┌────────────┐ git commit ┌────────────┐ │ Working │ ──────────▶ │ Index │ ─────────────▶ │ Repository │ │ Tree │ ◀────────── │ (staging) │ ◀───────────── │ (.git) │ └────────────┘ git restore └────────────┘ └────────────┘
Bare repositories look different
A bare repo (e.g., the one GitHub hosts on its servers) has no working tree. Its top-level layout is just the contents of .git/:
A bare repo
my-project.git/ ├── HEAD ├── config ├── description ├── hooks/ ├── info/ ├── objects/ └── refs/
Inspecting your repo
Useful inspections
# Top-level repo location git rev-parse --show-toplevel # Where is .git for this repo? git rev-parse --git-dir # How big is the repo? git count-objects -vH # Are we inside a worktree or a bare repo? git rev-parse --is-inside-work-tree git rev-parse --is-bare-repository
find .git -type f | head -20 inside one of your repos. The file names will start to make sense. After two weeks they will feel obvious. That moment is when Git really clicks.