Git Course 0%

How Git works inside

Expert Core Git ≈ 16 min

Before this lesson

What you will learn

  • What a commit, tree, blob and tag object each contain
  • What is in the .git folder, and what a branch really is
  • Why hashes make history tamper-evident and storage cheap

After this lesson you can

  • I can explain what Git stores and why its behaviour follows from it

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

The four object types inside Git A commit object at the top holds a tree hash, a parent hash, an author, a committer and a message. It points to a tree object, which lists names with their modes and hashes: entries for a .gitignore blob, a README.md blob, and a docs subtree. The docs tree in turn points to two blobs, faq.md and getting-started.md. A blob is just file content with no name and no date. Beside them, refs are described as files under .git/refs containing a hash: a branch is a file whose content is a commit hash, and HEAD is a file naming the current branch. A caption says every object is named by the hash of its content, which is why the same content stored twice is stored once. commit d3b92ec tree b40140b… parent b81702c… author You <you@example.com> committer You … a snapshot plus its parent, author and message tree b40140b 100644 blob 87ba659… .gitignore 100644 blob 19ed536… README.md 040000 tree beb14f0… docs a directory listing: names, modes, hashes blob 19ed536 # Trailguide … (1554 bytes) file content only: no name, no date refs are files .git/refs/heads/main → d3b92ec… .git/HEAD → ref: refs/heads/main a branch is one line of text tag objects an annotated tag is a fourth object: a name, a tagger, a message, and a target Every object is named by the hash of its content, so identical content is stored once, however many commits contain it.
A commit points to a tree, a tree lists blobs and other trees, and a blob is content with no name. Refs are files containing a hash.
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:

Terminal
$ git cat-file -p HEAD
tree b40140b83e8499b80e282838afe2a397f8193192
parent b81702c523ed562a905c6f45eb009117991dc57f
author You <you@example.com> 1788352800 +0200
committer You <you@example.com> 1788352800 +0200

chore: add CI configuration and contributing guide

A commit is that: a pointer to a tree, a pointer to its parent, two people with timestamps, and a message. Nothing else.

Its tree:

Terminal
$ 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	docs

Names 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:

Terminal
$ git cat-file -t 19ed53648d0a3042cfbaf91e822c0ef799a38ae6
blob
Terminal
$ git cat-file -s 19ed53648d0a3042cfbaf91e822c0ef799a38ae6
1554

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

Terminal
$ ls .git
COMMIT_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:

Terminal
$ 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 sizes

None of them changes anything, so this is a safe afternoon.

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 .git by hand. Read them freely; write with commands.
  • Running git gc --prune=now while recovering something, which removes exactly the unreachable objects you are looking for.
  • Assuming a large .git means 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.

  1. Run git cat-file -p HEAD and identify the tree, the parent and the author.
  2. Run git cat-file -p HEAD^{tree} and find your README's blob hash.
  3. Run git cat-file -p <that blob hash> and see the file content come back.
  4. Run cat .git/HEAD and cat .git/refs/heads/main, and confirm the second matches git rev-parse HEAD.
  5. Copy a file to a new name, commit it, and compare git rev-parse HEAD:<old> with HEAD:<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.

Check yourself

1. What does a commit object contain?
2. Why can a commit never be edited?
3. What is a branch, on disk?

Key terms

Hash (SHA) Commit HEAD Ref Repository (repo)