The .git folder

Everything Git knows about a project lives in the hidden .git folder at its top. Here it is straight after git init, and again after the first commit:

git init -q net-tools
ls net-tools/.git
printf '# Network tools\n' > net-tools/README.md
git -C net-tools add README.md
git -C net-tools commit -q -m "Add README"
echo "--- after the first commit"
ls net-tools/.git
HEAD
branches
config
description
hooks
info
objects
refs
--- after the first commit
COMMIT_EDITMSG
HEAD
branches
config
description
hooks
index
info
logs
objects
refs
Entry What it holds
objects/ The object database: every version of every file, folder and commit
refs/ Branches (refs/heads/), tags (refs/tags/) and remote-tracking branches (refs/remotes/)
HEAD Which branch is checked out
index The staging area, a binary file (created by the first git add)
config The local configuration of this repository
logs/ The reflog: where each branch and HEAD pointed over time (see Undoing Things)
hooks/ Sample scripts that Git can run at events such as pre-commit; inactive until renamed
COMMIT_EDITMSG The message of the last commit you wrote
info/, description, branches/ A local exclude file, a description used by some web viewers, and a legacy folder that Git 3.0 plans to drop

The object database

The object database is the store in .git/objects where Git keeps all content and history as objects, each one named by the hash of its content. It holds only four kinds of object: blobs, trees, commits and annotated tags. Make one commit with a README.md and a file in a configs folder, and five objects appear:

git init -q net-tools
cd net-tools
mkdir configs
printf '# Network tools\n' > README.md
printf 'hostname R1\n' > configs/r1.cfg
git add .
git commit -q -m "Add README and R1 config"
find .git/objects -type f | sort
git cat-file -t HEAD
git cat-file -p HEAD
.git/objects/03/7acefe82c92ee6002835c088dac5ab484d641c
.git/objects/06/5bb8206b8c5a1dff0993ed9bc0612b4f3ac46a
.git/objects/21/e39505034afc806dbf4b6e798f65fdadb659a3
.git/objects/87/feea5e57670f811429150943919bf08403e3df
.git/objects/af/a136fadb7d81b6f2ddf6c15ede937ae067c5f4
commit
tree 21e39505034afc806dbf4b6e798f65fdadb659a3
author Sam Rivera <sam@example.com> 1772442000 +0000
committer Sam Rivera <sam@example.com> 1772442000 +0000

Add README and R1 config

Each object is a compressed file whose path is its hash: the first two hexadecimal characters become a folder name (so no folder gets too large), the other 38 the file name. git cat-file is the tool to look inside: -t prints the type of an object, -p pretty-prints its content. Any revision works as the name — here HEAD, the current commit.

Commit objects

The commit is a short text: the hash of one tree (the snapshot of the whole project), the author and the committer with a time stamp in seconds since 1 January 1970 and a time zone offset (1772442000 is 2 March 2026, 09:00 UTC), a blank line, and the message. A commit after the first one also has one or more parent lines — more on those below. The author wrote the change; the committer recorded it. They differ when, for example, someone applies a colleague's patch or a commit is rebased.

Trees and blobs

A tree object is a folder listing: for each entry, a file mode, an object type, a hash and a name. A blob (binary large object) holds the content of one file — only the bytes, without the file name or any date. Names live in the tree that points to the blob. The ^{tree} suffix asks for the tree of a commit, and HEAD:path names the object at a path in that commit:

git init -q net-tools
cd net-tools
mkdir configs
printf '# Network tools\n' > README.md
printf 'hostname R1\n' > configs/r1.cfg
git add .
git commit -q -m "Add README and R1 config"
git cat-file -p 'HEAD^{tree}'
git cat-file -p HEAD:configs
git cat-file -t HEAD:configs/r1.cfg
git cat-file -p HEAD:configs/r1.cfg
100644 blob 87feea5e57670f811429150943919bf08403e3df	README.md
040000 tree 065bb8206b8c5a1dff0993ed9bc0612b4f3ac46a	configs
100644 blob afa136fadb7d81b6f2ddf6c15ede937ae067c5f4	r1.cfg
blob
hostname R1

Mode 100644 is an ordinary file, 100755 an executable file, 040000 a subfolder (another tree) and 120000 a symbolic link. Git stores no other permissions, no owner and no modification time. It also does not store empty folders: a folder exists only as a tree with at least one entry, which is why projects add a placeholder file such as .gitkeep to keep an empty folder.

HEAD points to the branch main, which points to commit 037acef. The commit points to tree 21e3950, the top folder. That tree points to blob 87feea5 under the name README.md and to tree 065bb82 under the name configs. Tree 065bb82 points to blob afa136f under the name r1.cfg. HEAD main commit 037acef "Add README and R1 config" tree 21e3950 top folder README.md configs blob 87feea5 # Network tools tree 065bb82 r1.cfg blob afa136f hostname R1
The objects of the example commit, with their real short hashes. File and folder names are stored in the trees, not in the blobs.

Content-addressable storage

Git is built on content-addressable storage: each object is stored and found under a name computed from its content (its hash), instead of under a name or location someone chose. Two consequences follow directly. The same content always gets the same name, so it is stored only once. And if one byte changes, the name changes.

git hash-object computes the name Git would give a file's content, without storing it. Two files with the same content get the same hash — the one we saw for r1.cfg above — and one changed character gives a completely different one:

printf 'hostname R1\n' > r1.cfg
printf 'hostname R1\n' > backup-of-r1.cfg
git hash-object r1.cfg backup-of-r1.cfg
printf 'hostname R2\n' | git hash-object --stdin
printf 'blob 12\0hostname R1\n' | sha1sum
afa136fadb7d81b6f2ddf6c15ede937ae067c5f4
afa136fadb7d81b6f2ddf6c15ede937ae067c5f4
5e1f4bd672583f3ddfbf1dff2d4b747bd2dd9bb2
afa136fadb7d81b6f2ddf6c15ede937ae067c5f4  -

The last line shows there is no magic: an object's name is the SHA-1 hash of a short header — the type, a space, the size in bytes and a zero byte — followed by the content. The file name is not part of it, which is why a renamed file needs no new blob, and why a hundred copies of the same config in a repository cost the space of one.

Content addressing also gives Git its integrity. A commit names its tree by hash, the tree names its blobs by hash, and a commit names its parent by hash. A single commit hash therefore vouches for the complete snapshot and all history before it: change any byte anywhere, and the hashes no longer match. Git notices corruption on disk or in transfer, and git fsck checks a whole repository.

Commit hashes, SHA-1 and SHA-256

A commit hash is the identifier of a commit: the hash of the commit object, written as 40 hexadecimal characters with SHA-1 (64 with SHA-256). Because the commit object includes the tree, the parents, author, committer, both time stamps and the message, changing any of them — even only the message — gives a different commit with a different hash. You can abbreviate a hash to any unique prefix; Git shows seven characters by default and uses more when a repository needs them to stay unambiguous.

Git has used SHA-1, a hash function from 1995, since the start. In 2017 researchers produced the first practical SHA-1 collision (two different files with the same hash). Git responded by switching to a hardened SHA-1 implementation that detects that kind of attack, and by adding a second object format:

  • Git 2.29 (2020) added SHA-256 repositories (git init --object-format=sha256), marked experimental.
  • Git 2.42 (2023) toned the warning down: SHA-256 repositories are not expected to change incompatibly.
  • Since Git 2.45, work has been under way to let SHA-1 and SHA-256 repositories exchange history; as of 2026 that interoperability is not finished.
  • Git 3.0 (no release date yet) plans to make SHA-256 the default for new repositories.

Until then, a SHA-256 repository cannot push to or fetch from a SHA-1 repository, and support among hosting services is not universal. For everyday work today, SHA-1 repositories remain the norm; what matters is knowing that hashes are content fingerprints, not random IDs.

Refs and HEAD

Hashes are not made for people. A ref (reference) is a human-readable name that points to an object — almost always a commit. A branch is a ref under refs/heads/, a tag a ref under refs/tags/. In the default storage format a ref is literally a small text file containing a hash:

git init -q net-tools
cd net-tools
mkdir configs
printf '# Network tools\n' > README.md
printf 'hostname R1\n' > configs/r1.cfg
git add .
git commit -q -m "Add README and R1 config"
cat .git/HEAD
cat .git/refs/heads/main
git rev-parse main
git rev-parse --short HEAD
ref: refs/heads/main
037acefe82c92ee6002835c088dac5ab484d641c
037acefe82c92ee6002835c088dac5ab484d641c
037acef

The branch main is a 41-byte file holding the hash of the latest commit. That is why creating a branch in Git is instant: it writes one small file. When you commit, Git creates the new commit object and then simply overwrites the branch file with the new hash. git rev-parse translates any revision into its full hash and is the safe way to read refs: Git often moves refs into a single packed-refs file (git gc and git clone do this), and reftable, a new binary format available with git init --ref-format=reftable since Git 2.45, is planned as the Git 3.0 default, so reading files directly is not reliable.

HEAD is the special ref that records what is currently checked out. Normally it is a symbolic ref: it does not hold a hash but the name of a branch (ref: refs/heads/main), so "HEAD" means "the latest commit of the current branch". A new commit becomes the child of the commit HEAD resolves to, and moves that branch forward. Switching branches rewrites HEAD to name another branch. When HEAD holds a commit hash instead of a branch name, you are in a detached HEAD state — see Branches.

Make a second commit and look at it. It has a parent line with the hash of the first commit:

git init -q net-tools
cd net-tools
mkdir configs
printf '# Network tools\n' > README.md
printf 'hostname R1\n' > configs/r1.cfg
git add .
git commit -q -m "Add README and R1 config"
printf 'hostname R2\n' > configs/r2.cfg
git add configs/r2.cfg
git commit -q -m "Add R2 config"
git cat-file -p HEAD
git cat-file -p HEAD:configs
git log --oneline
tree 297fbb67a66aad594cdbd33124bcdb65398c9223
parent 037acefe82c92ee6002835c088dac5ab484d641c
author Sam Rivera <sam@example.com> 1772442000 +0000
committer Sam Rivera <sam@example.com> 1772442000 +0000

Add R2 config
100644 blob afa136fadb7d81b6f2ddf6c15ede937ae067c5f4	r1.cfg
100644 blob 5e1f4bd672583f3ddfbf1dff2d4b747bd2dd9bb2	r2.cfg
048f22e Add R2 config
037acef Add README and R1 config

Three things to notice:

  • The history is a chain of parent links running backward in time. git log starts at HEAD and follows parent lines until it reaches a commit without one (the root commit). A merge commit has two or more parents.
  • Each commit points to a complete snapshot, not to a list of changes. The diff you see in git show is computed on the fly by comparing a commit's tree with its parent's.
  • The snapshot is cheap: r1.cfg did not change, so the new configs tree points to the very same blob afa136f. Unchanged files and folders are shared between commits, never copied.

Because each commit contains its parent's hash, changing an old commit — even its message — produces a new hash for it and for every commit after it. This is why "rewriting history" means creating new commits, and why rewriting commits that others already have causes trouble (see Rebasing and Rewriting History).

Packfiles and garbage collection

New objects are first written as loose objects: one zlib-compressed file each, as above. That is fast to write but wasteful for thousands of versions of the same files. Periodically Git packs them into a packfile: one file in .git/objects/pack/ where similar objects are stored as deltas (differences) against each other, with an index file for fast lookup. Packfiles are also what Git sends over the network during clone, fetch and push.

git init -q net-tools
cd net-tools
mkdir configs
printf '# Network tools\n' > README.md
printf 'hostname R1\n' > configs/r1.cfg
git add .
git commit -q -m "Add README and R1 config"
printf 'hostname R2\n' > configs/r2.cfg
git add configs/r2.cfg
git commit -q -m "Add R2 config"
git count-objects -v | grep -E '^(count|in-pack|packs):'
git gc -q
git count-objects -v | grep -E '^(count|in-pack|packs):'
count: 9
in-pack: 0
packs: 0
count: 0
in-pack: 9
packs: 1

Two commits produced nine objects (two commits, four trees, three blobs); git gc moved all nine into one pack. gc stands for garbage collection: besides packing, it removes objects that nothing refers to any more. The reflog counts as a reference, so commits you "lost" recently stay recoverable for at least 30 days by default. After that, unreferenced loose objects get a further two-week grace period (gc.pruneExpire) before they are deleted. You rarely need to run it: Git runs git gc --auto by itself after some commands, and git maintenance (added in Git 2.29; scheduled background runs with git maintenance start since 2.30) can schedule this and other housekeeping in the background for large repositories.