How Git works inside
Expert Core Git
Why this matters
Git's behaviour looks arbitrary until you see the storage, and then it is all consequence. Branches are cheap because a branch is a file with a hash in it. Commits cannot be edited because their name is derived from their content. Rewriting history changes every hash downstream for the same reason.
Four object types
| Object | Contains |
|---|---|
| blob | File content. No name, no date, no permissions |
| tree | A directory listing: names, modes, and the hash of each entry |
| commit | One tree, zero or more parents, an author, a committer, a message |
| tag | An annotated tag: a target, a name, a tagger and a message |
You can read all of them. This is a commit:
$ git cat-file -p HEADtree b40140b83e8499b80e282838afe2a397f8193192
parent b81702c523ed562a905c6f45eb009117991dc57f
author You <you@example.com> 1788352800 +0200
committer You <you@example.com> 1788352800 +0200
chore: add CI configuration and contributing guideA commit is that: a pointer to a tree, a pointer to its parent, two people with timestamps, and a message. Nothing else.
Its tree:
$ git cat-file -p HEAD^{tree}040000 tree eb55cebc2898675748a0421acdbb5c492cbafc97 .github
100644 blob 87ba6593dc4417da144f052d0082adb0a0d2b7c8 .gitignore
100644 blob 6d4763d8ef9235aa6f44b025cd4f97e3c1a4fada .gitlab-ci.yml
100644 blob 6ab7bd17cd9610c883e76d401fc8108269c68a58 .markdownlint.json
100644 blob 811e0c8cf23844d5e45e6a7715e10cc0243362c9 CONTRIBUTING.md
100644 blob 19ed53648d0a3042cfbaf91e822c0ef799a38ae6 README.md
040000 tree 2dcb616dc344ac2a526d55b161b29ef200393146 data
040000 tree beb14f09f34979d1a5b9cde851e102030ce79c57 docsNames live in trees, not in blobs. That is why renaming a file creates no new content: the blob is unchanged and only the tree entry differs, which is how Git detects renames without recording them.
And a blob:
$ git cat-file -t 19ed53648d0a3042cfbaf91e822c0ef799a38ae6blob$ git cat-file -s 19ed53648d0a3042cfbaf91e822c0ef799a38ae61554
Content addressing
Every object's name is the hash of its content. Three consequences worth internalising:
- Identical content is stored once. The same paragraph in ten files is one blob. Copying a whole directory adds trees, not content.
- Nothing can be edited. Change any byte and the hash changes, so it is a different object. "Editing a commit" always means creating a new one, which is why amending and rebasing produce new hashes (lesson 12.3).
- History is tamper-evident. A commit's hash covers its tree and its parent, whose hash covers its parent, and so on. Altering anything old changes every hash after it, which everyone else's clone would notice.
Git uses SHA-1 today, with SHA-256 available for new repositories, and the transition is slow precisely because hashes are how everything is named.
The .git folder
$ ls .gitCOMMIT_EDITMSG
HEAD
config
description
hooks
index
info
logs
objects
refs| Entry | Is |
|---|---|
objects/ |
Every object, by hash; later packed into packfiles |
refs/heads/ |
One file per local branch, containing a commit hash |
refs/remotes/ |
Remote-tracking branches, the same way |
refs/tags/ |
Tags |
HEAD |
A file naming the current branch, or a hash when detached |
index |
The staging area (lesson 2.2) |
config |
This repository's configuration (lesson 18.2) |
logs/ |
The reflog (lesson 12.1) |
hooks/ |
Scripts Git runs at certain moments (lesson 18.4) |
A branch is one line of text: .git/refs/heads/main contains a commit hash and nothing else. That is why creating a branch is instant regardless of project size, and why deleting one deletes nothing but a name (lesson 6.1).
HEAD usually contains ref: refs/heads/main, which is the whole of "you are on a branch". Detached HEAD is the same file containing a hash instead (lesson 12.5).
Packfiles
Storing every version of every file as a whole object would be wasteful, so Git periodically packs objects into a packfile, compressed and stored as deltas against similar objects. git gc does this, and it happens automatically.
Two practical consequences: a repository's size on disk is not the sum of its file versions, and git count-objects -vH reports what is loose and what is packed, which is where a size investigation starts (lesson 18.9).
What this explains
| Behaviour | Because |
|---|---|
| Branching is instant | A branch is a file containing a hash |
| Amending changes the hash | The hash covers the content, so a change is a different object |
| Rebase rewrites every commit | Each new parent changes each child's hash |
| A deleted branch loses no commits | It removed a name; the objects remain (lesson 12.1) |
| Copying a file costs nothing | The blob already exists; only the tree changes |
| Old history cannot be quietly altered | Every later hash would change |
How to do it
The commands for looking inside:
$ git cat-file -p HEAD # read any object
$ git cat-file -t <hash> # its type
$ git rev-parse HEAD:README.md # the blob hash of a file
$ git ls-tree HEAD # the tree, more readably
$ git count-objects -vH # loose and packed sizesNone of them changes anything, so this is a safe afternoon.
Nothing exposes the object model; the integrated terminal is the way in (lesson 15.9).
The same: use the terminal (⌥F12). The Log shows commits and trees as a user interface, not as objects.
Every commit page has its hash, and the API can return the tree, but the object model is not exposed as such. What the platform does show is the consequence: a tag or branch is a name pointing at a commit.
The same.
Common mistakes
- Thinking a commit stores a diff. It stores a tree, a complete snapshot; diffs are computed on demand.
- Thinking file names live in blobs. They live in trees.
- Editing files under
.gitby hand. Read them freely; write with commands. - Running
git gc --prune=nowwhile recovering something, which removes exactly the unreachable objects you are looking for. - Assuming a large
.gitmeans large files now. It usually means large files once, still in history (lesson 18.9).
Try it yourself
Goal: read your own repository's objects.
- Run
git cat-file -p HEADand identify the tree, the parent and the author. - Run
git cat-file -p HEAD^{tree}and find your README's blob hash. - Run
git cat-file -p <that blob hash>and see the file content come back. - Run
cat .git/HEADandcat .git/refs/heads/main, and confirm the second matchesgit rev-parse HEAD. - Copy a file to a new name, commit it, and compare
git rev-parse HEAD:<old>withHEAD:<new>.
Expected result: you have read a commit, a tree and a blob by hand, seen that a branch is a file containing a hash, and watched two names share one blob.
Show solution
Step 5 is the demonstration that content addressing is real: the two paths have the same blob hash, because the content is identical, and Git stored it once. Every claim in this lesson can be checked the same way, which is unusual and reassuring for something that is often described as magic.