Lesson 5 of 5 in Power Tools
Under the hood
Story: The archive files every record under a fingerprint of what's in it. Time to read the filing system itself.
Every commit, every folder and every version of every file in a repository is stored in .git as an object. Each object has an id, 40 characters worked out from exactly what's inside it. You've seen the first 7 characters of commit ids in git log --oneline. This Lesson opens the objects up, with four commands that only read: they change nothing, so they're free to run.
There are four kinds of object:
| Kind | What it holds |
|---|---|
| blob | A file's content, and nothing else: no name, no date. |
| tree | A folder: the name and id of each file and folder in it. |
| commit | The id of a tree, which is the project's top folder as it was; the ids of its parent commits; who made it and when; and the message. |
| tag | An annotated tag, made with git tag -a: the id it points at, who made it, and its message. |
From a name to an id: git rev-parse
git rev-parse prints the full id a name stands for:
$ git rev-parse main
7741b9b61cf17ef391ffc8c7a4998cfb67ad5efa
It takes anything that names a commit: HEAD, a branch, main~2, or the start of an id. Wherever git asks for an object, the first 7 characters of its id are usually enough. Any start of at least 4 characters works, as long as no other object's id starts the same way.
What is this object? git cat-file
git cat-file -t prints an object's kind, and git cat-file -p prints what's in it:
$ git cat-file -t main
commit
$ git cat-file -p main
tree e2971965114d12bd8da3e3c18d85d689d7b3d56d
parent 580cae6f376891fcea72ca0c207e2275084b89bc
author You <you@example.com> 1723108500 +0000
committer You <you@example.com> 1723108500 +0000
Fix the phone number
That's the whole commit:
treeis the snapshot: the project's top folder as it was in this commit.parentis the commit before it. The very first commit has noparentline, and a merge commit has one for each commit it joins: usually two.authoris who wrote the change, andcommitteris who made this commit. Each comes with a time, in seconds since 1970, and a time zone. They're usually the same. The commits thatgit commit --amend,git rebaseandgit cherry-pickmake are different: they keep the author and the author's time, and record you as the committer, with the time now.- After a blank line comes the message.
Trees and blobs
Follow the tree's id:
$ git cat-file -p e297196
100644 blob 4467cbe714b2158b803763271702b266d3fe8c2d README.md
040000 tree 3686b543b2f949be94d0dd648988c37d8f54bff0 css
100644 blob 2270e427c119678f66cb89d09ed3592c7342ca60 index.html
There's one line for each file and folder: its mode, its kind, its id and its name. 100644 is an ordinary file, and 100755 a file you can run. 040000 is a folder, which is another tree. A submodule shows up as 160000 commit, with the id of the commit it's pinned at.
A blob is the file's content:
$ git cat-file -t 2270e42
blob
$ git cat-file -p 2270e42
<!doctype html>
<title>The Corner Bakery</title>
<link rel="stylesheet" href="css/style.css">
<h1>The Corner Bakery</h1>
<p>Fresh bread every morning.</p>
<p>Open 7 to 3, Monday to Saturday.</p>
<p>Call us on 555 0124.</p>
A blob has no name: the name is in the tree that lists it. So renaming a file gives its folder a new tree, but the blob stays the same.
Same content, same id: git hash-object
git hash-object works out the id a file in your folder would get as a blob, without storing anything:
$ git hash-object index.html
2270e427c119678f66cb89d09ed3592c7342ca60
That's the id in main's tree, so index.html hasn't changed since the last commit. A blob's id depends only on the content, so the same content gets the same id on any computer, in any repository. Change one character, and the id is completely different.
That's how git keeps every version without copying every file into every commit. Here is the commit before main, and its tree:
$ git cat-file -p 580cae6
tree 5aef19f17e1bcbf7804a6b282d833bd1098585b9
parent 9c66f89f8229290b40935434b9d7118a1d4afa7b
author You <you@example.com> 1723028400 +0000
committer You <you@example.com> 1723028400 +0000
Add the phone number
$ git cat-file -p 5aef19f
100644 blob 4467cbe714b2158b803763271702b266d3fe8c2d README.md
040000 tree 3686b543b2f949be94d0dd648988c37d8f54bff0 css
100644 blob d9cbdfb374155750e4e4875c7c3eacb617f329f1 index.html
README.md and the css folder have the same ids in both trees: one blob and one tree, stored once and listed twice. Only index.html, which "Fix the phone number" changed, has a new blob.
Ids build on each other. Change a file, and its content gets a new blob id. Its folder's tree lists that id, so the tree gets a new id, and so does every folder above it. The commit names the new top tree, so it gets a new id, and the next commit names it as its parent. Two things follow:
- A changed commit is a new commit. A commit's tree, parents, message, author, committer and times are all part of it, so changing any of them, even just the message, gives a new id. No commit is edited in place: when
git commit --amendorgit rebasechanges a commit, it makes a new one, and leaves the old one as it was. - The same tree id means the same files. Two commits whose
treelines match hold exactly the same files, whatever their messages say. Comparing two ids is quicker, and surer, than comparing every file by eye.
The staging area: git ls-files
The staging area, also called the index, is the list of files the next commit will hold. git ls-files lists them:
$ git ls-files
README.md
css/style.css
index.html
These are the files git tracks. A new file you haven't added isn't listed, and neither is a file .gitignore ignores. But .gitignore only stops git from adding a file. A file that was committed before the rule was written is still tracked, and git ls-files still lists it.
| Command | What it does |
|---|---|
git rev-parse <name> |
Prints the full id a name stands for, such as a branch, HEAD or main~2. |
git cat-file -t <id> |
Prints an object's kind: blob, tree, commit or tag. |
git cat-file -p <id> |
Prints what's in an object. |
git hash-object <file> |
Prints the id a file would get as a blob. |
git ls-files |
Lists the files in the staging area: the files git tracks. |
On your own computer
git cat-file -p main:index.html prints a file as it is in a commit, without following the trees by hand, and git rev-parse main^{tree} prints a commit's tree id. git ls-files -s adds each staged file's blob id, and git hash-object -w stores a file as a blob. This Terminal doesn't support those. Inside .git/objects, git keeps the objects compressed and filed by their ids.
New commands
git rev-parsegit cat-file -tgit cat-file -pgit hash-objectgit ls-files
Explainer: What a commit is made of
Graph as text
project: your repo, you are here
- HEAD → main at e08e58b "Say we're closed on Sundays"
Branches and tags
- main at e08e58b "Say we're closed on Sundays"
Commits, newest first
- e08e58b "Say we're closed on Sundays"; parent 7741b9b; refs: HEAD → main
- 7741b9b "Fix the phone number"; parent 580cae6
- 580cae6 "Add the phone number"; parent 9c66f89
- 9c66f89 "Add the opening hours"; parent a208d6d
- a208d6d "Add a stylesheet"; parent f1729b8
- f1729b8 "Add the home page"; no parent
Challenge 1 of 3
Story: One of the archive's old logs is still filed with every report, Agent. Take it off the list, but don't shred it.
This project's .gitignore tells git to ignore every .log file. Yet git still tracks one of the three log files in the folder, because it was committed before that rule was written.
Find out which one. Then stop tracking it, but keep it in your folder, and commit the change.
Unlocks after Rewriting History.
Terminal
Unlocks after Rewriting History.
This Lesson can be played once Rewriting History is complete. Its text and Explainer are open to everyone.