Git Repository Structure: What Is Inside the .git Directory
Every concept in this cluster — commits, branches, HEAD, the index, the object database — is a file or
directory inside .git. This final lesson opens it and connects each piece back to what you already
know.
.git is not a black box. It is a small, comprehensible directory, and reading it is the fastest way
to convert an understanding of Git’s model into an understanding of Git’s behaviour.
What is not always there
Section titled “What is not always there”Before the tour, the most useful correction: .git does not have a fixed contents list. Files
appear as the operations that create them are performed.
Immediately after git init, before any commit:
Directory.git/
- HEAD
- config
- description
Directoryhooks/
- …
Directoryinfo/
- …
Directoryobjects/
- …
Directoryrefs/
- …
Note what is missing. There is no index, because nothing has been staged. There is no logs, because
no ref has moved yet. There are no files under refs/heads/, because main does not exist as a ref
until a commit gives it something to point at — even though HEAD already names it.
After the first commit, index, logs/ and refs/heads/main all exist. Later operations add more:
ORIG_HEAD after a reset, MERGE_HEAD during an unfinished merge, packed-refs and
objects/pack/ after garbage collection, refs/remotes/ after a fetch.
A repository with some history
Section titled “A repository with some history”Here is a repository with two branches, a tag, a remote and a few commits:
Directorymy-project/
- README.md
Directorysrc/
- …
Directory.git/
- HEAD which branch you are on
- config this repository’s settings
- index the staging area
- COMMIT_EDITMSG last commit message buffer
- description legacy, used only by gitweb
- ORIG_HEAD where HEAD was before the last big move
- packed-refs refs consolidated into one file
Directoryobjects/
Directory0d/
- …
Directory33/
- …
Directoryinfo/
- …
Directorypack/
- …
Directoryrefs/
Directoryheads/
- …
Directorytags/
- …
Directoryremotes/
- …
Directorylogs/
- HEAD
Directoryrefs/
- …
Directoryhooks/
- …
Directoryinfo/
- exclude
We will take these roughly in order of how often they matter.
A one-line text file naming your current branch:
cat .git/HEADref: refs/heads/mainThe ref: prefix makes it a symbolic reference — a pointer to another ref rather than to an object.
In detached HEAD state it instead holds a commit ID directly:
83a232882cf16f99552b6c02632b2e65e39b3219This is the file that answers “where am I?”, and git switch rewrites it.
config
Section titled “config”The repository’s own configuration, in INI format. This is the local scope — the most specific of
Git’s three, overriding your global ~/.gitconfig and the system config.
cat .git/config[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true[remote "origin"] url = https://example.com/demo.git fetch = +refs/heads/*:refs/remotes/origin/*The [core] section is written by git init. The [remote "origin"] section appeared when a remote
was added — this file is where remotes actually live, which is why git remote -v needs no network.
The staging area: a binary file holding one entry per tracked path, each with a mode, an object ID, a stage number and cached filesystem metadata.
It does not exist until something is first staged, and it is rewritten by add, rm, restore,
commit, checkout and merge. Inspect it with git ls-files -s, never with a text editor.
objects/
Section titled “objects/”The object database — every blob, tree, commit and annotated tag the repository contains.
.git/objects/├── 0d/│ └── 3f8a1c9e4b2d7f6a8c5e0b1d2f3a4b5c6d7e8f├── 33/│ └── 2f4ee605310ac48e2e23fb563a55970cd2176e├── info/└── pack/Each loose object is one zlib-compressed file, named by its 40-character object ID split into a two-character directory and a 38-character filename. The split keeps any single directory from holding hundreds of thousands of entries.
pack/ holds packfiles, created by git gc, which consolidate many objects into one file and may
delta-compress similar objects against each other:
.git/objects/pack/├── pack-e6f5dd7a492df926ea5b55be88e9c1f189fcb56e.pack├── pack-e6f5dd7a492df926ea5b55be88e9c1f189fcb56e.idx└── pack-e6f5dd7a492df926ea5b55be88e9c1f189fcb56e.revThe .pack holds the objects; the .idx lets Git find one without scanning; auxiliary files such as
.rev and .mtimes support specific optimisations and are not always present.
info/ may hold packs (a list of available packfiles) and, in some configurations, alternates —
paths to other object databases this repository may borrow objects from.
To see the loose/packed balance:
git count-objects -vcount: 0size: 0in-pack: 11packs: 2size-pack: 3Lesson 11 explores the objects themselves.
Where branches, tags and remote-tracking branches live. Every file here contains one 40-character object ID and nothing else.
.git/refs/├── heads/│ ├── main│ └── feature├── tags/│ └── v1└── remotes/ └── origin/ ├── HEAD └── maincat .git/refs/heads/mainff3c99c8a1b2c3d4e5f60718293a4b5c6d7e8f90That is a branch. The entire implementation.
| Path | Holds |
|---|---|
refs/heads/ | Local branches |
refs/tags/ | Tags — a commit ID for lightweight, a tag object ID for annotated |
refs/remotes/ | Remote-tracking branches, created by fetch; absent until you fetch |
refs/stash | The stash, if you have used git stash |
packed-refs
Section titled “packed-refs”Thousands of tiny ref files are inefficient, so Git periodically consolidates them into one:
cat .git/packed-refs# pack-refs with: peeled fully-peeled sorted4ff2767a422691863b00b07ee6e51de7a65b1919 refs/heads/mainaad719b54d64b1940226c8f3f889921f1ddb6ef7 refs/tags/v0.1.0^4ff2767a422691863b00b07ee6e51de7a65b19194ff2767a422691863b00b07ee6e51de7a65b1919 refs/tags/v0.1.0-lightThe ^ line is a peeled tag: aad719b… is the annotated tag object, and 4ff2767… is the commit
it ultimately points at. Storing both lets Git resolve the tag without reading the tag object.
The reflog: a record of every value each ref has held.
.git/logs/├── HEAD└── refs/ └── heads/ ├── main └── featurecat .git/logs/HEAD0000000000000000000000000000000000000000 a936f7ce… Ada Lovelace <ada@example.com> 1787403557 +0000 commit (initial): Add project READMEa936f7ce… 4ff2767a… Ada Lovelace <ada@example.com> 1787403569 +0000 commit: Add greeting moduleEach line is: old value, new value, who, when, and what operation caused the move. The all-zeros value means “did not exist before”.
This file is why lost work is usually recoverable. Read it with:
git refloghooks/
Section titled “hooks/”Scripts Git runs at defined points in its operations — before a commit is created, after a merge completes, before a push is sent.
git init populates this directory with disabled samples:
.git/hooks/├── pre-commit.sample├── commit-msg.sample├── pre-push.sample├── prepare-commit-msg.sample└── … several moreEvery file ends in .sample, which is precisely why none of them run: Git executes a hook only if a
file with the exact hook name exists and is executable. Renaming pre-commit.sample to pre-commit
and making it executable activates it.
Repository-local metadata that is not part of the project’s content.
info/exclude works exactly like .gitignore, with one important difference: it is not committed, so
it is not shared:
cat .git/info/exclude# git ls-files --others --exclude-from=.git/info/exclude# Lines that start with '#' are comments.Use .gitignore for patterns everyone on the project should share (build output, dependency
directories). Use info/exclude for patterns that are yours alone — a personal scratch file, or your
editor’s local settings — where adding them to the project’s .gitignore would be imposing your setup
on everyone else.
The small files
Section titled “The small files”| File | When it appears | What it is |
|---|---|---|
COMMIT_EDITMSG | After the first commit | A scratch buffer holding the last commit message. Editing it does nothing |
ORIG_HEAD | After reset, merge, rebase | Where HEAD pointed before that operation — git reset --hard ORIG_HEAD undoes it |
MERGE_HEAD | During an unfinished merge | The commit being merged in. Its presence is how Git knows a merge is in progress |
MERGE_MSG | During an unfinished merge | The prepared merge commit message |
FETCH_HEAD | After git fetch | What the last fetch retrieved |
description | Always | Legacy; used only by the gitweb browser. Safe to ignore |
shallow | In a shallow clone | Marks where truncated history ends |
branches/ | Older Git versions | A directory for configuring remotes, deprecated since 2005 and scheduled for removal in Git 3.0. Recent versions may not create it; ignore it |
How it all connects
Section titled “How it all connects”A five-step chain through the .git directory. Step one, .git/HEAD contains the text ref colon refs slash heads slash main. Step two, .git/refs/heads/main contains a commit object ID. Step three, that ID locates a commit object under .git/objects, which names a tree. Step four, the tree object lists entries with blob IDs. Step five, a blob object under .git/objects holds the file content. A note explains that the index, at .git/index, holds a parallel listing used to compare the working tree against this committed state.
Trace one file’s content from scratch:
- Read
.git/HEAD→ref: refs/heads/main. - Read
.git/refs/heads/main→ a commit ID. (Or find it inpacked-refs.) - Read that commit object → it names a root tree.
- Read the tree → it names a blob for each file.
- Read the blob → your file’s content.
Meanwhile .git/index holds a parallel listing of object IDs, and git status reports the differences
between it, the commit at the end of that chain, and the actual files on disk. That is the three-state
model from Lesson 3, expressed as files.
Bare repositories
Section titled “Bare repositories”A bare repository has no working tree. Its contents are what would normally be inside .git, placed
at the top level:
git clone --bare https://example.com/demo.gitdemo.git/├── HEAD├── config├── objects/├── refs/└── …There is no index either, because there is nothing to stage.
This is the form used on servers: a repository meant to be pushed to rather than worked in. Pushing to a
non-bare repository would update its history while leaving its working tree stale and inconsistent,
which is why Git refuses to do so by default. The .git suffix on the directory name is a convention
signalling that it is bare.
Where .git might not be a directory
Section titled “Where .git might not be a directory”Two cases where the layout differs from everything above:
Worktrees. git worktree add creates an additional working directory attached to the same
repository. In it, .git is a file containing a path back to the real repository:
gitdir: /home/you/my-project/.git/worktrees/feature-branchSubmodules. A submodule’s .git is also a file, pointing into the parent repository’s
.git/modules/ directory.
In both cases, Git follows the pointer. Tooling that assumes .git is always a directory can be caught
out by this.
Common mistakes
Section titled “Common mistakes”Editing files inside .git by hand. Several files must stay consistent with each other. Use Git
commands.
Deleting .git to “clean up”. It is the repository. Removing it destroys every commit, branch and
tag irreversibly, leaving only the current files.
Committing .git into another repository. Nesting one repository inside another without using
submodules produces confusing behaviour. The inner .git is not tracked by the outer repository.
Assuming hooks are shared. They live in .git and are never cloned. Use core.hooksPath with a
committed directory.
Expecting every listed file to exist. index, logs, packed-refs, ORIG_HEAD, MERGE_HEAD and
refs/remotes/ all appear only after the operations that create them.
Reading refs/heads/ to list branches. After gc they may be in packed-refs. Use git show-ref
or git branch.
Mental Model
Section titled “Mental Model”
.gitis a small filing system.
HEADis the bookmark saying which drawer you have open.refs/are labelled tabs, each holding one object ID.objects/is the archive itself, filed by content hash.indexis the tray of material you are preparing to file next.logs/is the sign-out sheet recording every time a tab moved.Nothing in the archive is ever edited — only added. Everything else is pointers into it.
What You Learned
Section titled “What You Learned”.gitis the repository; the files beside it are the working tree.- Its contents are conditional —
index,logs/,packed-refs,ORIG_HEADandrefs/remotes/appear only when the relevant operation creates them. HEADnames the current branch through a symbolic reference.configholds the local scope, including remote definitions.objects/stores loose objects in hash-split directories, and packfiles underpack/.refs/holds one object ID per file;packed-refsconsolidates them, and a loose file wins.logs/is the reflog: local, expiring, and the primary recovery tool.hooks/ships disabled.samplefiles and is never cloned;core.hooksPathenables sharing.info/excludeis a private, uncommitted.gitignore.- Bare repositories have no working tree and no index; worktrees and submodules make
.gita file.
Try It Yourself
Section titled “Try It Yourself”In a disposable repository. Everything here is read-only.
- Run
git initin a fresh directory and list.gitwithls -a .git. Predict first: is there anindexfile? Alogsdirectory? A file underrefs/heads/? - Create and commit a file, then list
.gitagain. What appeared? - Run
cat .git/HEAD, thencat .git/refs/heads/main. Compare the second againstgit rev-parse HEAD. - Run
git cat-file -p $(cat .git/refs/heads/main)and read the commit object. - Run
find .git/objects -type fand count the objects. For a one-file commit there should be three — name them. - Run
git gc, thenfind .git/objects -type fandls .git/refs/heads/again. Where did everything go? - Run
git show-refand confirm the branch still exists.
Step 1 is the point: index, logs/ and refs/heads/main are all absent in a repository with no
commits. Step 5’s three objects are the blob, the tree and the commit.
You have finished Cluster 1
Section titled “You have finished Cluster 1”Across twelve lessons you have gone from “what is version control” to reading a commit object out of
.git/objects by hand. You now know:
- What Git is, and how it differs from GitHub.
- The three-state model: working tree, index, repository.
- How to install and configure Git on Ubuntu, Windows and macOS.
- How to build a repository and read its history.
- What the working tree, the index and HEAD each are, precisely.
- How the object database stores blobs, trees, commits and tags.
- Where every one of those things lives on disk.
That is a genuine foundation. Everything else in Git — branching strategies, merging, rebasing, remotes, workflows, recovery — builds on exactly these pieces.
What comes next
Section titled “What comes next”The next cluster of the Git Fundamentals pillar covers everyday commands and branching in depth. It is
not published yet. In the meantime, the most valuable thing you can do is use what you have learned on
a real project: initialise a repository, commit deliberately, and read git status as the three
comparisons it actually is.