Git: Plumbing — Objects and Refs

Git's user-facing commands (add, commit, branch, etc.) are called "porcelain." Underneath them is a layer of lower-level "plumbing" commands that operate directly on Git's object database and ref system. Understanding plumbing reveals how Git actually works — every porcelain command is ultimately built from these primitives.

Commands Covered

git cat-file · git hash-object · git ls-tree · git read-tree · git write-tree · git commit-tree · git rev-parse · git rev-list · git for-each-ref · git show-ref · git update-ref · git symbolic-ref · git refs

The Object Model

Git stores four types of objects, each identified by a SHA-1 (or SHA-256) hash of their content:

Object typeWhat it storesCreated by
blobFile content (no filename)git hash-object, git add
treeDirectory: list of blobs and subtrees with names/modesgit write-tree
commitSnapshot: points to a tree, parent commit(s), author, messagegit commit-tree, git commit
tagAnnotated tag: points to a commit with tagger metadatagit tag -a

git hash-object

Computes the SHA hash of content — the same hash Git would use to store it.

# Compute hash of a file (don't store it)
$ git hash-object myfile.txt

# Compute hash and store as a blob object in the database
$ git hash-object -w myfile.txt
# Returns the SHA: e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

# Hash arbitrary content from stdin
$ echo "hello" | git hash-object --stdin
$ echo "hello" | git hash-object -w --stdin   # store it

# Hash of an empty file (the famous empty blob SHA):
# e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

git cat-file

Provides content or details of repository objects. The primary tool for inspecting Git's internal storage.

# Show type of an object
$ git cat-file -t abc1234
# commit

$ git cat-file -t HEAD
# commit

# Show size of an object
$ git cat-file -s abc1234

# Show content of an object
$ git cat-file -p abc1234       # pretty-print (auto-formats by type)
$ git cat-file commit abc1234   # raw content

# Pretty-print a commit:
$ git cat-file -p HEAD
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent def5678abc9012...
# author Jason Smith <jason@dispelled.ca> 1699999999 -0500
# committer Jason Smith <jason@dispelled.ca> 1699999999 -0500
#
# Fix login bug

# Pretty-print a tree:
$ git cat-file -p HEAD^{tree}
# 100644 blob e69de29b README.md
# 100644 blob 3b18e512 src/main.c
# 040000 tree 5f9f2593 tests/

# Pretty-print a blob:
$ git cat-file -p HEAD:src/main.c

# Batch mode — process multiple objects from stdin
$ echo "HEAD\nHEAD~1" | git cat-file --batch
$ echo "HEAD" | git cat-file --batch-check   # just type and size

git ls-tree

Lists the contents of a tree object — like ls for a Git tree.

# List contents of the tree at HEAD
$ git ls-tree HEAD

# Output format:
# mode type SHA name
# 100644 blob e69de29b README.md
# 040000 tree 5f9f2593 src/

# Mode values:
# 100644 = regular file
# 100755 = executable file
# 120000 = symbolic link
# 040000 = directory (tree)
# 160000 = submodule (gitlink)

# Recursive listing (expand subtrees)
$ git ls-tree -r HEAD
$ git ls-tree -r HEAD --name-only

# List contents of a subdirectory at HEAD
$ git ls-tree HEAD src/

# List contents at a specific commit
$ git ls-tree abc1234

# List with long format (includes object sizes)
$ git ls-tree -l HEAD

git write-tree and git read-tree

# Write the current index (staging area) as a tree object
$ git write-tree
# Returns the SHA of the tree object created

# Read a tree into the index (the reverse)
$ git read-tree abc1234         # replace index with this tree
$ git read-tree --reset abc1234 # reset index to this tree
$ git read-tree -m HEAD abc1234 # three-way merge into index

git commit-tree

Creates a commit object from a tree object. The low-level version of git commit.

# Create a commit from a tree, with a parent
$ git commit-tree TREE_SHA -p PARENT_SHA -m "Commit message"
# Returns: new commit SHA

# Create an initial commit (no parent):
$ git commit-tree TREE_SHA -m "Initial commit"

# Example: manually create a commit
$ git write-tree
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904
$ git commit-tree 4b825dc -p HEAD -m "Manual commit"
# abc1234def5678...
$ git reset --hard abc1234def5678   # move HEAD to the new commit

git rev-parse

Translates names, refs, and expressions into raw SHA hashes. The backbone of scripting with Git.

# Get SHA of HEAD
$ git rev-parse HEAD

# Get SHA of a branch
$ git rev-parse main
$ git rev-parse origin/main

# Get SHA of a tag
$ git rev-parse v1.0.0

# Relative references
$ git rev-parse HEAD~3      # 3 commits before HEAD
$ git rev-parse HEAD^       # parent of HEAD (same as HEAD~1)
$ git rev-parse HEAD^^      # grandparent

# Get tree SHA of a commit
$ git rev-parse HEAD^{tree}

# Get blob SHA of a file
$ git rev-parse HEAD:src/main.c

# Abbreviate a SHA
$ git rev-parse --short HEAD
$ git rev-parse --short=8 HEAD

# Show top-level directory of the repo
$ git rev-parse --show-toplevel

# Show .git directory path
$ git rev-parse --git-dir

# Check if we're inside a repo (useful in scripts)
$ git rev-parse --is-inside-work-tree
$ git rev-parse --git-dir >/dev/null 2>&1 && echo "in repo" || echo "not a repo"

# Resolve ~ and symbolic refs
$ git rev-parse --symbolic-full-name HEAD
# refs/heads/main

git rev-list

Lists commit objects in reverse chronological order. The engine behind git log.

# List all commits reachable from HEAD
$ git rev-list HEAD

# List commits in a range
$ git rev-list main..feature   # commits on feature not in main

# Count commits
$ git rev-list --count HEAD
$ git rev-list --count main..feature

# List commits touching a specific file
$ git rev-list HEAD -- src/auth.c

# Combine with cat-file for scripting
$ git rev-list HEAD | while read sha; do
    echo "$(git log -1 --format='%s' $sha)"
done

git for-each-ref

Outputs information on each ref — the low-level version of branch and tag listing. Highly configurable for scripting.

# List all refs
$ git for-each-ref

# List branches only
$ git for-each-ref refs/heads/

# List tags only
$ git for-each-ref refs/tags/

# Custom format
$ git for-each-ref --format='%(refname:short) %(objectname:short)' refs/heads/
# main abc1234
# feature-login def5678

# Sort by committer date (newest last)
$ git for-each-ref --sort=committerdate refs/heads/

# Find branches whose last commit is older than 30 days
$ git for-each-ref --format='%(refname:short) %(committerdate:relative)' \
    --sort=committerdate refs/heads/ | grep "months\|years"

# Show branch, last commit, and author
$ git for-each-ref \
    --format='%(refname:short) %(objectname:short) %(authorname) %(subject)' \
    refs/heads/

git show-ref

# List all refs in the local repo
$ git show-ref

# List only branches
$ git show-ref --heads

# List only tags
$ git show-ref --tags

# Check if a ref exists
$ git show-ref --verify refs/heads/main
$ git show-ref --quiet --verify refs/heads/feature   # exit code 0 = exists

git update-ref and git symbolic-ref

# Move a branch to a specific commit (the low-level version of git reset)
$ git update-ref refs/heads/main abc1234def5678

# Delete a ref
$ git update-ref -d refs/heads/old-branch

# Read HEAD (which branch it points to)
$ git symbolic-ref HEAD
# refs/heads/main

# Set HEAD to point to a branch
$ git symbolic-ref HEAD refs/heads/feature

# Detach HEAD (point directly at a commit)
$ git symbolic-ref -d HEAD   # removes symbolic nature of HEAD

git refs (Git 2.45+)

A newer low-level interface to refs, designed for future ref-database backends (like the reftable format).

# List refs
$ git refs list

# Migrate to reftable format (faster for repos with many refs)
$ git refs migrate --ref-format=reftable

References