Chapter 9-7 — Plumbing Commands☕ ☕ 22 min read

Plumbing Commands — cat-file, hash-object

Plumbing seekho, toh Git ki koi bhi problem solve kar sakte ho.

01Porcelain vs Plumbing: Two Levels of Git

Git commands are divided into two categories: PORCELAIN (user-facing) and PLUMBING (low-level).

Porcelain commands are what you use daily: git add, git commit, git log, git merge, git rebase. They are designed for humans — helpful output, safety checks, sensible defaults.

Plumbing commands are what Git uses internally: git hash-object, git cat-file, git ls-tree, git update-index, git write-tree, git commit-tree. They are designed for scripts and direct object manipulation — minimal output, no safety nets, precise control.

Porcelain commands are built ON TOP of plumbing commands. Every git commit internally calls write-tree and commit-tree. Every git add internally calls hash-object and update-index. The porcelain layer is a user-friendly wrapper around the plumbing engine.

Learning plumbing helps you: debug repository issues (inspect objects directly when something breaks), understand how Git works (demystifies the magic), impress in interviews (shows genuine depth), and write custom Git tools (automation and scripting).

# Porcelain: what you normally use
git add file.txt
git commit -m "add file"
git log --oneline

# Plumbing: what Git does internally
git hash-object -w file.txt           # creates blob
git update-index --add file.txt       # updates index
git write-tree                        # creates tree from index
git commit-tree <tree-hash> -m "msg"  # creates commit

# The porcelain 'git commit' is a wrapper around:
# 1. git write-tree (snapshot the index)
# 2. git commit-tree (create commit object)
# 3. git update-ref (move branch pointer)
02git hash-object: Creating Objects from Content

git hash-object computes the SHA-1 hash of content and optionally stores it as a Git object. This is the first plumbing command you should learn — it is where every Git object begins.

Without -w: just computes the hash. Does NOT store anything. Useful for checking what hash content would get before committing to storing it.

With -w: computes the hash AND writes the object to .git/objects/. This is what git add does internally — it calls hash-object -w to create blob objects.

--stdin: reads content from stdin instead of a file. Useful for piping content directly.

The hash includes a header: "blob <size>\0<content>". This is why the same content always gets the same hash, but different types (blob vs commit) get different hashes even with the same payload — the type is part of the header.

# Compute hash without storing
echo "hello" | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

# Same content = same hash
echo "hello" | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

# Different content = different hash
echo "Hello" | git hash-object --stdin
# 1d229271928d3f9e2bb0375bd6ce5db6c6d348d9

# Compute AND store the object
echo "hello" | git hash-object -w --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

# Now it exists in the object store!
find .git/objects -type f
# .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a

# Hash a file
echo "my code" > app.js
git hash-object -w app.js
# 49415dd21ab28c1ea8f0ee3f6b8e1a8e5f8f0e3d
# (blob object created in .git/objects/)
03git cat-file: The Object Inspector

git cat-file is THE tool for inspecting Git objects. It is the Swiss Army knife of plumbing — if you need to look inside any Git object, this is the command.

-p (pretty-print): shows the object content in human-readable form. This is the most commonly used flag.

-t (type): shows the object type — blob, tree, commit, or tag.

-s (size): shows the object size in bytes.

For blob objects: -p shows the raw file content. For tree objects: -p shows entries (mode, type, hash, name). For commit objects: -p shows the full commit — tree, parent, author, message.

You can use abbreviated hashes (first 7+ characters) as long as they are unique in the repository.

# Inspect a blob
git cat-file -t ce0136
# blob

git cat-file -p ce0136
# hello

git cat-file -s ce0136
# 5  (bytes)

# Inspect a tree
git cat-file -p HEAD^{tree}
# 100644 blob abc123...    README.md
# 100644 blob def456...    app.js
# 040000 tree 789abc...    src

# Inspect a commit
git cat-file -p HEAD
# tree 9f8321...
# parent 2b4c67...
# author Sai <sai@devinhyd.com> 1700000000 +0530
# committer Sai <sai@devinhyd.com> 1700000000 +0530
#
# add feature

# Follow references: HEAD^{tree} = tree of HEAD's commit
git cat-file -p HEAD^{tree}:app.js
# (shows app.js content at HEAD)

# Bulk inspect all objects
git rev-list --objects --all | \
  while read hash rest; do
    echo "$hash $rest $(git cat-file -t $hash)"
  done
04git ls-tree: Inspecting Directory Trees

git ls-tree lists the contents of a tree object — like ls for Git trees. It shows you exactly what files and directories a tree contains, with their modes and hashes.

git ls-tree HEAD — lists the root tree of the current commit. This shows the top-level files and directories.

git ls-tree HEAD src/ — lists the src/ subtree. You can inspect any subdirectory.

-r (recursive): lists ALL files in ALL subdirectories, not just the top level. This is how you see every file tracked in a commit.

-l (long): shows file sizes alongside entries. Useful for understanding storage.

Each entry shows: mode (100644 for files, 040000 for directories), object type, hash, and filename.

# List root tree of current commit
git ls-tree HEAD
# 100644 blob abc123...    README.md
# 100644 blob def456...    app.js
# 040000 tree 789abc...    src

# List a specific subtree
git ls-tree HEAD src/
# 100644 blob ghi789...    main.js
# 040000 tree jkl012...    utils

# Recursive listing (all files in all directories)
git ls-tree -r HEAD
# 100644 blob abc123...    README.md
# 100644 blob def456...    app.js
# 100644 blob ghi789...    src/main.js
# 100644 blob mno345...    src/utils/helper.js

# With file sizes
git ls-tree -r -l HEAD
# 100644 blob abc123...      42    README.md
# 100644 blob def456...     128    app.js
# 100644 blob ghi789...      85    src/main.js

# Only show specific paths
git ls-tree HEAD -- src/utils/
# 100644 blob mno345...    helper.js

# Compare two trees
diff <(git ls-tree -r HEAD~1) <(git ls-tree -r HEAD)
# Shows which files changed between commits
05Building a Commit from Scratch with Plumbing

You can create a complete commit using ONLY plumbing commands — no porcelain needed. This demonstrates how Git actually works internally, step by step.

The 5-step process: 1) Create blob objects with hash-object, 2) Build the index with update-index, 3) Create tree with write-tree, 4) Create commit with commit-tree, 5) Update branch with update-ref.

This is exactly what git add and git commit do behind the scenes. Steps 1-2 equal git add. Steps 3-5 equal git commit.

Understanding this flow is the ultimate proof that you understand Git's object model. No magic — just objects, trees, and references.

# === Build a commit from scratch using ONLY plumbing ===

# Step 1: Create blob objects (equivalent to git add)
echo "# My Project" > README.md
echo "console.log('hello')" > app.js

BLOB1=$(git hash-object -w README.md)
BLOB2=$(git hash-object -w app.js)

echo "Created blobs: $BLOB1, $BLOB2"

# Step 2: Add blobs to the index (equivalent to git add)
git update-index --add --cacheinfo 100644,$BLOB1,README.md
git update-index --add --cacheinfo 100644,$BLOB2,app.js

# Step 3: Create tree from index (equivalent to git commit's tree creation)
TREE=$(git write-tree)
echo "Created tree: $TREE"

# Step 4: Create commit object (equivalent to git commit)
COMMIT=$(git commit-tree $TREE -m "Initial commit")
echo "Created commit: $COMMIT"

# Step 5: Update branch pointer (equivalent to git commit updating the branch)
git update-ref refs/heads/main $COMMIT

# DONE! Verify with porcelain commands
git log --oneline
# <COMMIT_HASH> Initial commit

git show --stat HEAD
# README.md | 1 +
# app.js | 1 +
# 2 files changed, 2 insertions(+)

# You just recreated 'git add + git commit' using ONLY plumbing!
# This is exactly what Git does internally:
# hash-object -> update-index -> write-tree -> commit-tree -> update-ref
💡 Daily Work vs Deep Knowledge: You'll rarely need to build commits from scratch in daily work. But knowing how to do it is powerful for debugging, writing Git automation scripts, and understanding error messages. When something goes wrong with Git, plumbing commands let you inspect and fix the problem at the object level.
Building a commit from scratch with plumbing commands is the ultimate test of Git internals knowledge. If you can do this, you truly understand how Git's object model works. In interviews, being able to explain this 5-step process (hash-object → update-index → write-tree → commit-tree → update-ref) demonstrates a depth of understanding that sets you apart from developers who only know porcelain commands.

Lo kar liya — Key Points:

  • ✅ Git commands are divided into porcelain (user-facing: add, commit, log) and plumbing (low-level: hash-object, cat-file, ls-tree)
  • ✅ git hash-object computes SHA-1 hashes; with -w it stores the content as a blob object in .git/objects/
  • ✅ git cat-file is the object inspector: -t shows type, -p shows content, -s shows size
  • ✅ git ls-tree lists the contents of a tree object, showing filenames, modes, and blob hashes
  • ✅ A commit can be built from scratch using: hash-object → update-index → write-tree → commit-tree → update-ref
  • ✅ Plumbing commands are for debugging, scripting, and understanding Git internals — not daily use
  • ✅ Every porcelain command is a wrapper around plumbing commands; understanding plumbing gives you deep debugging ability
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase