Git Internals
Inspect objects and refs until commit, branch, and tag are locations in a graph rather than product-button names.
Objects are Git, not GitHub
Git Working Tree named three places: working tree, index, object database. This lesson opens the third place. Git Commit already wrote a commit object. Git Branch already treated a branch as a pointer. Internals makes those sentences checkable with git cat-file, git ls-tree, and git rev-parse.
Objects and refs
Blobs, trees, commits, and tag objects live in the repository database. Branch and tag names are refs that point at objects. They travel with a clone.
Not this lesson
Pull requests, Actions checks, and the website file browser are GitHub. A SHA on github.com is a view of a Git object. It is not a different kind of object.
# These are Git. None of them talks to GitHub.
git cat-file -t HEAD
git cat-file -p HEAD
git ls-tree HEAD
git hash-object note.txt
git rev-parse HEADUse a disposable local repository. A journal or feature-branch project folder is fine if you still control it. You do not need a GitHub remote. You do not need gh.
Four object types
commit 9c2e… ← refs/heads/main and HEAD name this
│
├─ parent 4a11… (earlier commit, or none on a root)
├─ tree 7f08… (the snapshot)
├─ author / committer / message
│
▼
tree 7f08…
note.txt blob e4d9… "hello\n"
copy.txt blob e4d9… same content, same blob
src/ tree 1a2b…
A filename lives on the tree entry. The blob does not know it.blob
File content. No path, no author, no newline policy beyond the bytes you stored. git cat-file -t prints blob.
tree
A directory listing: mode, type, object id, name. Nested directories are trees. This is the snapshot Git Commit recorded from the index.
commit
Root tree, zero or more parents, author, committer, dates, message. History is commits pointing at parents, not a folder of copies.
tag
An annotated tag is a fourth object: tagger, message, and a pointer to another object. A lightweight tag is only a ref. Git Tags already split those; here you git cat-file -t v1.4.0 and read tag or commit.
Git Log walks commits. git show often prints a commit plus a patch against its parent. That patch is a convenience. The stored object does not contain a unified diff.
Name an object, then print it
HEAD is a revision. Git resolves it to a commit (when you are on a branch). git cat-file -t prints the type. git cat-file -p pretty-prints the content.
# Disposable local repository. GitHub is not required.
git switch main
git rev-parse HEAD
git cat-file -t HEAD
git cat-file -p HEADTest what you learned
Type the Git command that prints the object type of HEAD.
Test what you learned
Type the Git command that pretty-prints the object HEAD names.
The first lines of a commit are tree and, except on a root commit, parent. Those values are object ids. Copy one and run git cat-file -t on it. You are no longer taking the vocabulary on trust.
git hash-object note.txt hashes the file the same way Git names a blob. If the path is already committed unchanged, that id matches the blob column of git ls-tree. hash-object without -w prints the id and writes nothing. Do not add -w in this lab; dangling blobs are noise, not a lesson.
Walk HEAD to a blob
# HEAD^{tree} is the root tree of that commit.
git rev-parse HEAD^{tree}
git ls-tree HEAD
git cat-file -t <blob-id-from-ls-tree>
git cat-file -p <blob-id-from-ls-tree>
# Same blob id without looking it up first:
git hash-object note.txtTest what you learned
Type the Git command that lists the root tree of HEAD.
Each line from git ls-tree HEAD is mode, type, object id, and name. A blob line is a file. A tree line is a directory; git ls-tree that id to go one level down. git ls-tree -r HEAD flattens the walk. Learn the one-level listing first so you can see a nested tree as an object, not a path trick.
echo 'hello' > note.txt
git add note.txt
git commit -m "Add note"
echo 'hello' > copy.txt
git add copy.txt
git commit -m "Add a copy of the same bytes"
git ls-tree HEAD
# note.txt and copy.txt should list the same blob id.
git hash-object note.txt
git hash-object copy.txtIdentical bytes share a blob. Different bytes create a new blob. A later commit that only changes a message still points at a tree; if the tree did not change, Git reuses that tree object too. That is why two snapshots can be cheap without copying the project.
Refs are names, not copies
# HEAD is usually a pointer to a branch name, not a commit id.
git symbolic-ref HEAD
# prints refs/heads/main
git rev-parse HEAD
git rev-parse refs/heads/main
# On a typical new repo these two files match that SHA:
# .git/HEAD → ref: refs/heads/main
# .git/refs/heads/main → 9c2e…git rev-parse HEAD and git rev-parse refs/heads/main print the same SHA while you are on main. git symbolic-ref HEAD prints the branch name HEAD currently follows. That is the attached state Git Branch taught. Detached HEAD means .git/HEAD stores a commit SHA directly instead of ref: refs/heads/….
.git/HEAD
Usually one line: ref: refs/heads/main. It is not the commit object.
.git/refs/heads/
One file per local branch, contents a SHA plus newline. Creating a branch adds a name at a commit; it does not duplicate blobs.
.git/refs/tags/
Lightweight tags store a SHA here. Annotated tags store the tag object's id; peel with v1.4.0^{} as Git Tags already showed.
packed-refs
After enough history, Git may pack ref names into .git/packed-refs. The commands still work. Do not invent a second naming scheme by editing that file.
Read those files if you want the disk picture. Change refs with git switch, git branch, and git tag—not with a text editor. A truncated SHA in a ref file is a broken repository, not a shortcut.
Reachable is not the same as exists
An object can sit in the database without any branch, tag, or HEAD naming it. Commands that walk history start from refs. That is why git log can miss a commit you still git cat-file -p if you still know the SHA.
Git Recovery teaches reflog and how to get a durable name back on a commit. This lesson only needs the distinction: deleting a branch does not immediately delete objects. Running git gc may eventually prune unreachable ones. Do not prune as homework. Do not delete files under .git/objects to make the folder look tidy.
What not to invent
Editing objects
No hex editors in .git/objects. No renaming SHA files. Inspection is cat-file.
A GitHub tour
Do not open github.com to “see the blob.” The object is already local. Remotes copy Git objects; they do not replace them.
hash-object -w as a habit
Writing a blob that no tree names leaves junk. Print the id. Let git add write objects that belong to a snapshot.
Treating SHA as a secret
The id is a name. It does not hide content. GitHub Security signs exact object bytes without encrypting them.
Guided practice: open one commit
- 01Pick a local repository you own
Clean status. On a branch.
git rev-parse HEADprints a SHA. - 02Print the commit
git cat-file -t HEADiscommit.git cat-file -p HEADshows tree, parent, message. - 03List the tree
git ls-tree HEAD. Copy one blob id.git cat-file -pthat id. Confirm it matches the working file if that path is unchanged. - 04Prove a ref
git symbolic-ref HEADandgit rev-parse refs/heads/$(git branch --show-current)matchgit rev-parse HEAD. - 05Optional reuse
Commit a second path with the same bytes.
git ls-tree HEADshows one blob id twice.
Independent lab: prove the graph
- In a disposable local repository you own, record two commits. The second commit adds a second path whose content matches an existing file so two tree entries share one blob.
- Show
git cat-file -t HEADascommitandgit cat-file -p HEADwith atreeline and aparentline. Copy the tree id. Showgit ls-tree HEADwith two names and the same blob id. Showgit hash-objecton either file matching that blob. - Show
git symbolic-ref HEADand the SHA fromgit rev-parse HEAD. If you read.git/HEADand the branch file under.git/refs/heads/, they must agree with those commands. Do not edit them. - Write six lines: commit id, tree id, shared blob id, current branch ref (for example
refs/heads/main), one sentence that distinguishes an object from a ref, and one sentence that says why you did not edit.git/objects. - Do not push. Do not open GitHub. Do not run
git gc --prune=now. Keep this object walk separate from the published Git Recovery investigation.
Common internals mistakes
Calling the working file the blob
The blob is committed (or hashed) bytes. The working file can disagree.
Calling a branch a folder of copies
The ref is a SHA. Checkout writes a worktree from the commit's tree.
Expecting git show to be the object
show adds a patch. git cat-file -p is the stored commit.
Cleaning .git by hand
Unknown files under objects are not clutter. Leave them. Recovery and gc come later.
Lesson review
You can inspect Git objects and refs until commit, branch, and tag are locations in a graph rather than product-button names. Git Recovery uses that model to preserve displaced commits and isolate regressions. GitHub Security binds a signing key to exact commit bytes. The team CI project stays planned.
- I can
git cat-file -t HEADandgit cat-file -p HEADand read tree, parent, and message. - I can
git ls-tree HEAD, open a blob, and explain why two names can share one blob id. - I can name the current branch ref and show it points at the same SHA as
HEAD. - I inspect with Git commands and leave
.git/objectsunedited.