Stage 2 · Tools
Git Fundamentals
Objects & Refs
blobs, trees, commits, and tags — what every .git object actually is.
Blobs — File Content
A blob is the raw content of a file. It has no filename, no permissions, no metadata — just bytes. Git compresses the content with zlib before writing it to .git/objects. The SHA-1 hash of that compressed content becomes the blob's name.
echo "Hello, Git internals" > greeting.txt
BLOB_HASH=$(git hash-object greeting.txt)
echo $BLOB_HASH
git cat-file -t $BLOB_HASH
git cat-file -p $BLOB_HASHgit hash-object computes the SHA-1 hash and stores the blob in the object database. cat-file -t prints the type (blob). cat-file -p prints the content. Two files with identical content share the same blob — Git does not duplicate data.
If you create two files with the exact same content, Git stores only one blob. The filename lives in the tree object, not the blob. This is why Git is so storage-efficient for projects with many duplicated files.
Trees — Directory Structure
A tree object represents a directory. It contains a list of entries: each entry has a mode (file permissions), a type (blob or subtree), a filename, and a SHA-1 hash pointing to the object. A commit points to a root tree that represents the entire project's directory structure.
echo "content A" > file-a.txt
mkdir src
echo "content B" > src/file-b.txt
git add .
git commit -m "add two files in nested directories"
git cat-file -p HEAD^{tree}HEAD^{tree} dereferences the commit to its root tree. The output shows entries: 100644 blob <hash> file-a.txt and 040000 tree <hash> src. The src entry is itself a tree object pointing to file-b.txt.
TREE_HASH=$(git rev-parse HEAD^{tree})
git ls-tree $TREE_HASH
git ls-tree $TREE_HASH src/git ls-tree lists the entries of a tree object. You can recursively list with -r to see every blob in the entire project. Each subtree is another tree object — the structure is recursive.
Commits — Snapshot + Metadata
A commit object ties everything together. It contains: a pointer to a root tree (the snapshot), zero or more parent commit hashes, the author identity and timestamp, the committer identity and timestamp, and the commit message.
git cat-file -p HEADThe output shows: tree <hash> (the root snapshot), parent <hash> (the previous commit), author and committer lines with name, email, and timestamp, followed by a blank line and the commit message.
# Create a blob
BLOB=$(echo "raw content" | git hash-object -w --stdin)
# Create a tree with that blob
TREE=$(echo "100644 blob $BLOB hello.txt" | git mktree)
# Create a commit pointing to the tree
COMMIT=$(echo "tree $TREE
author Your Name <you@example.com> $(date +%s) +0000
committer Your Name <you@example.com> $(date +%s) +0000
Add hello.txt" | git commit-tree)
echo $COMMIT
git cat-file -p $COMMITThis is what git add and git commit do under the hood. You are building the object graph manually: blob → tree → commit. Understanding this makes every Git command predictable.
Tags — Pointers to Commits
A tag is a named pointer to a commit. There are two kinds: lightweight tags (a simple ref under refs/tags/) and annotated tags (a full tag object with tagger identity, date, message, and optional GPG signature).
# Lightweight — just a pointer
git tag v1.0.0
# Annotated — a full tag object with metadata
git tag -a v1.1.0 -m "Release 1.1.0: add user profiles"
# Inspect the difference
git cat-file -t v1.0.0 # commit (the tag IS the commit)
git cat-file -t v1.1.0 # tag (dereference with ^{} to see commit)Lightweight tags are fast but have no metadata. Annotated tags are recommended for releases because they store who created the tag, when, and why. Git describe and git push --follow-tags work best with annotated tags.
Refs — Branch Pointers
A ref is a human-readable name that points to a SHA-1 hash. The most important refs are branches. A branch is simply a file in refs/heads/ that contains the SHA-1 of the latest commit on that branch. When you make a new commit, Git updates the branch file to point to the new commit.
cat .git/refs/heads/main
# Output: a4b8c3d... (the SHA-1 of the latest commit)
git branch feature-login
cat .git/refs/heads/feature-login
# Same SHA-1 as main — both branches point to the same commit until you divergeCreating a branch is instantaneous because it is just writing a 40-character hash to a file. This is why branching in Git is fundamentally different from SVN, where branching creates a full copy on the server.
HEAD is usually a symbolic ref pointing to a branch name (e.g., ref: refs/heads/main). When you checkout a commit directly (detached HEAD), HEAD contains the raw SHA-1 instead. This is why git status tells you when your HEAD is detached.
Plumbing in Practice
Git splits commands into plumbing (low-level, machine-friendly) and porcelain (high-level, human-friendly). Most day-to-day work uses porcelain. Plumbing is useful for scripting, debugging, and understanding what Git is actually doing.
# Show the raw object type
git cat-file -t <sha>
# Show the raw object content
git cat-file -p <sha>
# Show the type and size
git cat-file -s <sha>
# Hash a file and store it
git hash-object -w <file>
# List tree entries
git ls-tree <tree-sha>
# Find the SHA-1 of a ref
git rev-parse HEAD
git rev-parse refs/heads/mainThese commands are the building blocks of every Git operation. When something goes wrong, plumbing commands help you inspect the exact state of the object database without any abstraction getting in the way.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.