Ran Wei/Git Series
中文
Git and Version Control — Ran Wei

How Git works inside

Look inside the .git folder: the blobs, trees and commits that hold your history, the hashes that name them, and the small files that make branches and HEAD.

2 hours2 study sessions4 command sessionsGit 2.50.1 (Apple Git-155)

By the end you can

  • Name Git's three main kinds of object, blob, tree and commit, and say what each holds.
  • Look inside any object with git cat-file.
  • Explain how hashes are calculated from content, and why changing a commit changes every later hash.
  • Find where objects, branches and HEAD are stored in the .git folder.
  • Explain why branches are cheap, and what a detached HEAD is in these terms.

Before you start

Modules 2 to 8. Nothing here is needed for everyday work, but it explains why Git behaves as it does.

Contents

Study plan

2 hours

Times are estimates and include typing the commands. Progress is saved in this browser and shared by both language editions.

Session 160 min
Objects
Session 260 min
Storage, refs and practice
1

Objects and hashes

You can use Git for years without knowing how it stores your work. But a look inside explains much of what you have learned: why commits have hashes, why a branch costs nothing, why rewriting a commit changes every commit after it, and why a detached HEAD is nothing to fear. This module opens the .git folder. Everything in it is read-only for you: you only look.

At heart, Git is a small database of objects. Each object is stored under a name calculated from its content, its hash, a 40-character hexadecimal number such as 8bf3c2d58c2d8d9c541c166d6e80435faab5756a. Three kinds of object hold your history:

  • A blob holds the contents of one file: just the bytes, without the file’s name.
  • A tree holds the contents of one folder: a list of names, each with the hash of a blob (for a file) or of another tree (for a subfolder).
  • A commit holds the hash of the tree for the whole project, the hash of its parent commit (two for a merge, none for the first commit), the author, the committer, the dates and the message.

So a commit is a snapshot: its tree describes every file in the project at that moment. A fourth kind, the annotated tag, appears in Module 10.

Two commits, their trees and their blobs commitAdd a pancake recipecommitAdd a READMEparenttreetreeblobREADME.md contentsblobpancakes.md contentsA commit points to a tree and to its parent; a tree points to blobs. An unchanged file is stored once.
Figure 9.1

Two commits from the recipes repository. Each points to its own tree, and the newer one to its parent. Both trees point to the same README blob, because the README did not change between them.

2

Looking inside a commit

git cat-file shows any object: -t prints its type, and -p (“pretty-print”) its contents. You can name an object by its full hash, or by the first few characters as long as no other object starts with them; seven usually suffice.

In the session, you follow the newest commit of the recipes repository down to a file. To follow along in your own repository, start from a hash that git log --oneline prints, then copy each hash you see into the next command.

Try itLook inside a commit
git log --oneline
Output
8bf3c2d Add a pancake recipe
907a979 Add a README
git cat-file -t 8bf3c2d
Output
commit
git cat-file -p 8bf3c2d
Output
tree 063e1373abab86d50db53df21ded863f36cfc794
parent 907a97958867987ca6ef7be665cd4b3f86bb08d6
author Alex Smith <alex@example.com> 1767604020 +0000
committer Alex Smith <alex@example.com> 1767604020 +0000

Add a pancake recipe
git cat-file -p 063e137
Output
100644 blob 2ecc1f2d65cf3b272e009b477014267d15171467	README.md
100644 blob 871d830f4cec82e60e46ad67ec042c33f215a834	pancakes.md
git cat-file -p 871d830
Output
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
git cat-file -p 907a979
Output
tree 38a015c41d763382717c898da156ce660f860bea
author Alex Smith <alex@example.com> 1767603900 +0000
committer Alex Smith <alex@example.com> 1767603900 +0000

Add a README
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.
  • git cat-file -t 8bf3c2d says that 8bf3c2d is a commit.
  • -p shows the whole commit object, and it is short. tree gives the hash of the project’s tree, and parent the hash of the commit before it, 907a979. author and committer give the name and email, then the date as seconds since 1 January 1970 (UTC), then the time zone, +0000. After a blank line comes the message. That is all a commit is.
  • git cat-file -p 063e137 shows that tree. Each line has a mode (100644 means an ordinary file; 100755 an executable one, and 040000 a subfolder, which is another tree), a type, a hash and a name.
  • git cat-file -p 871d830 shows the blob for pancakes.md: exactly the file’s contents, with no name and no date. The name lives in the tree.
  • The first commit, 907a979, has a tree but no parent line: that is what makes it the first.

The author and the committer are usually the same person. They differ when someone applies another person’s work, for example when a rebase replays a colleague’s commits: the author wrote the change, the committer made this commit of it.

3

Hashes come from content

Git calculates an object’s hash from its content, with a short header that gives the object’s type and size. The same content always gives the same hash, on every computer, and any change, however small, gives a completely different one. git hash-object <file> calculates the hash a file would have as a blob, without storing anything.

In the session, you compare the hash of pancakes.md with that of an identical copy, and then with the copy after changing one character, a full stop. To follow along, use any file in your repository.

Try itSame content, same hash
git hash-object pancakes.md
Output
871d830f4cec82e60e46ad67ec042c33f215a834
Create or edit pancakes-copy.md so that it contains:
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
git hash-object pancakes-copy.md
Output
871d830f4cec82e60e46ad67ec042c33f215a834
Create or edit pancakes-copy.md so that it contains:
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk.
git hash-object pancakes-copy.md
Output
797b0e6c8204d8a9fce80e8b1c8254eb2f060d10
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

pancakes.md hashes to 871d830f…, the same blob hash the tree listed in Section 2. The identical copy gives the same hash, so Git would store it only once, under that name. Adding one full stop gives a hash with nothing in common with the first.

This explains several things you have seen:

  • Unchanged files cost nothing. A new commit’s tree refers to the same blobs as before for every file that did not change. Only changed files add new blobs.
  • History cannot change unnoticed. A commit contains its tree’s hash and its parent’s hash. Change anything in an old commit, even a typo in its message, and its hash changes; the next commit names a different parent, so its hash changes too, and so on to the newest commit. This is why amending and rebasing make new commits (Modules 3 and 8), and why others notice when shared history is rewritten.
  • Copies agree. Two clones that have a commit with the same hash have exactly the same history up to that commit.
Note — SHA-1 and SHA-256

Git’s hashes are calculated with SHA-1, in a hardened form that detects known attacks. Recent versions of Git can also create repositories that use the stronger SHA-256 (git init --object-format=sha256), whose hashes are 64 characters long. Hosting services do not yet generally support them, so stay with the default for now.

4

Where objects live

Everything Git knows about a repository is in its .git folder. In the session, you list it, find the objects, and pick out one by its hash. To follow along, run the same commands in your own repository; your folder names under objects will differ.

Try itWhere the objects live
ls .git
Output
COMMIT_EDITMSG  HEAD  config  description  hooks  index  info  logs  objects  refs
ls .git/objects
Output
06  2e  38  87  8b  90  info  pack
ls .git/objects/87
Output
1d830f4cec82e60e46ad67ec042c33f215a834
git cat-file -t 871d830f4cec82e60e46ad67ec042c33f215a834
Output
blob
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

The entries of .git, in the order ls lists them:

  • COMMIT_EDITMSG holds the message of the last commit you made.
  • HEAD records where you are (Section 5), and config holds the repository’s settings, including its remotes.
  • description is used only by an old web viewer, and hooks holds sample scripts that Git can run at certain moments.
  • index is the staging area from Module 2: a list of the files and blobs the next commit will contain.
  • info holds exclude, a private .gitignore for this clone only.
  • logs holds the reflog from Module 8.
  • objects and refs hold the objects and the branches.

Under objects, each object is a file named after its hash: the first two characters name a folder, and the other 38 the file. So blob 871d830f… is the file 1d830f4cec… in the folder 87. The six folders correspond to the six objects of the two commits: two commits, two trees and two blobs. The README’s blob is shared by both trees, as the figure in Section 1 shows.

The object files are compressed, so you cannot read them with cat; use git cat-file. As a repository grows, Git packs many objects into a few files under objects/pack, storing similar objects as differences from each other. That is why a repository with a long history usually takes less space than you might expect.

Pitfall

Never edit or delete files inside .git by hand. A damaged object or reference can make commits unreadable. Everything this module shows has a Git command that does it safely. The one exception is info/exclude, which you may edit like a .gitignore.

5

Branches and HEAD

A branch is not an object. It is a ref, a reference: a name that points at a commit, stored in .git/refs/heads/ as a small file containing the commit’s hash. HEAD is a ref too, usually a symbolic one: instead of a hash, it names the branch you are on.

In the session, you read HEAD and main, create a branch, switch to it, and watch the files change. To follow along, run the same commands; your hashes will differ, and ls .git/refs/heads lists all your branches. Afterwards, run git switch main.

Try itBranches and HEAD are small files
cat .git/HEAD
Output
ref: refs/heads/main
ls .git/refs/heads
Output
main
cat .git/refs/heads/main
Output
8bf3c2d58c2d8d9c541c166d6e80435faab5756a
git branch starters
ls .git/refs/heads
Output
main  starters
cat .git/refs/heads/starters
Output
8bf3c2d58c2d8d9c541c166d6e80435faab5756a
git switch starters
Output
Switched to branch 'starters'
cat .git/HEAD
Output
ref: refs/heads/starters
git rev-parse HEAD
Output
8bf3c2d58c2d8d9c541c166d6e80435faab5756a
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.
  • .git/HEAD contains ref: refs/heads/main: you are on main. .git/refs/heads/main contains one line, the hash of the newest commit, 8bf3c2d….
  • git branch starters creates .git/refs/heads/starters, with the same hash. That one small file is the whole branch, which is why creating a branch is instant and copies nothing (Module 4).
  • git switch starters changes .git/HEAD to ref: refs/heads/starters. When you commit, Git writes the new commit’s hash into the file of the branch that HEAD names, which is how the branch label moves forward.
  • git rev-parse HEAD turns any name into the hash it stands for. It is the safe way to read refs from scripts.

Other refs follow the same pattern. Remote-tracking branches such as origin/main live under refs/remotes/, and tags under refs/tags/. A detached HEAD (Module 3) is simply a .git/HEAD that contains a commit’s hash directly, instead of the name of a branch, as Exercise 4 shows.

Git does not always keep one file per ref. It can collect many refs in a single file, .git/packed-refs, and newer versions offer another storage format altogether. So read refs with commands such as git rev-parse and git branch, not by opening files.

6

Exercises

Exercise 1★★★conceptual3 min

Which object? For each, say whether it is stored in a blob, a tree or a commit: the text of pancakes.md; the name pancakes.md; the commit message; the hash of the parent commit; the fact that pancakes.md is an ordinary file, not an executable one.

Show solution

The text: a blob. The name: a tree. The message: a commit. The parent’s hash: a commit. The file mode (100644): a tree, next to the name.

Exercise 2★★★coding4 min

The first commit’s tree. List the files in the tree of the first commit of your recipes repository, using only git log and git cat-file.

Show solution

Find the first commit, read its tree’s hash, then show the tree:

SolutionThe tree of the first commit
git log --oneline
Output
8bf3c2d Add a pancake recipe
907a979 Add a README
git cat-file -p 907a979
Output
tree 38a015c41d763382717c898da156ce660f860bea
author Alex Smith <alex@example.com> 1767603900 +0000
committer Alex Smith <alex@example.com> 1767603900 +0000

Add a README
git cat-file -p 38a015c
Output
100644 blob 2ecc1f2d65cf3b272e009b477014267d15171467	README.md
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

The first tree lists only README.md. Its blob, 2ecc1f2, is the same as in the newer tree of Section 2: the README did not change.

Exercise 3★★★conceptual3 min

One letter. You correct a typo in the message of a commit that has three commits after it, with reword in an interactive rebase. Why do all four commits get new hashes, although only one message changed?

Show solution

The changed commit’s content differs, so its hash changes. The next commit contains its parent’s hash, which is now different, so its content and hash change as well, and so on for each later commit. Each commit’s hash covers the whole history before it.

Exercise 4★★★coding4 min

A detached HEAD on disk. Detach HEAD at the commit before the newest one, and look at .git/HEAD. Then switch back to main and look again.

Show solution
SolutionWhat HEAD holds when it is detached
git switch --detach HEAD~1
Output
HEAD is now at 907a979 Add a README
cat .git/HEAD
Output
907a97958867987ca6ef7be665cd4b3f86bb08d6
git switch main
Output
Previous HEAD position was 907a979 Add a README
Switched to branch 'main'
cat .git/HEAD
Output
ref: refs/heads/main
Output from git version 2.50.1 (Apple Git-155), run while this page was built. Other versions of Git may word some messages differently.

Detached, .git/HEAD holds a commit hash; on a branch, it holds ref: refs/heads/main. A commit made while detached moves only HEAD, and no branch file records it, which is why such commits are easy to lose (Module 3).

Exercise 5★★★conceptual3 min

Count the objects. A new repository gets two commits. The first adds a.txt and b.txt. The second changes only b.txt. How many blobs, trees and commits does the repository hold?

Show solution

Three blobs (a.txt, the first b.txt, the second b.txt), two trees (one per commit, both naming the same a.txt blob) and two commits: seven objects.

Exercise 6★★★conceptual3 min

Cheap branches. Explain, in terms of what is stored, why creating a branch takes no time, and why deleting a merged branch loses nothing.

Show solution

A branch is one small file holding a commit’s hash; creating it writes that file, and copies no objects. Deleting a merged branch deletes only that file. The commits are objects, and the branch they were merged into still reaches them through its own commits’ parents.

7

Self-check quiz

1
What does a blob contain?
2
What does a commit object contain?
3
Two files in different folders have exactly the same contents. How many blobs does Git store for them?
4
Why does changing an old commit change the hashes of all later commits?
5
What does .git/refs/heads/main usually contain?
6
What does .git/HEAD contain when you are on main?
7
Which command safely shows the contents of any object?
8

Further reading