Skip to main content
RunBook Academy

Git, CI/CD & GitOpsII · Git ArchitectureArchitecture

Plumbing versus porcelain — the low-level commands Git is built on

Intermediate⏱ ~21 mingit

What you'll learn

  • Distinguish plumbing commands from porcelain commands and explain why both exist
  • Use git cat-file, git hash-object, and git write-tree to inspect and build objects directly
  • Use git update-ref to move a branch ref safely
  • Construct a minimal commit from plumbing commands as a forensics exercise
  • Recognise when a scripted plumbing command is the right tool instead of a higher-level alias

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Git has two layers of commands. The porcelain layer is the user-facing commands most engineers know: git status, git commit, git merge, git push. The plumbing layer is the set of low-level commands that Git itself uses internally to implement the porcelain: git cat-file, git hash-object, git write-tree, git update-ref, git mktag. Porcelain is convenient for humans; plumbing is the right tool for scripts, for forensics, and for any situation where you need to manipulate the object database without the safety rails (and the assumptions) of the user-facing commands.

Why both exist

flowchart TB
    subgraph Porcelain
        P1["git status"]
        P2["git commit"]
        P3["git merge"]
        P4["git push"]
    end
    subgraph Plumbing
        L1["git hash-object"]
        L2["git write-tree"]
        L3["git cat-file"]
        L4["git update-ref"]
        L5["git mktag"]
    end
    P2 --> L1
    P2 --> L2
    P2 --> L3
    P2 --> L4

The plumbing layer is the substrate. Every porcelain command is a script over plumbing commands. git commit ultimately:

  1. hashes the staged blobs with git hash-object (already done at git add time),
  2. builds a tree object with git write-tree (already done at git add time),
  3. constructs a commit object and writes it with git commit-tree,
  4. advances the branch ref with git update-ref.

When you run git commit, the porcelain command hides this sequence. When you need to do something Git’s porcelain does not support — commit without a working tree, write a tag with a custom header, advance a ref to a specific OID from a script — you reach for the plumbing.

The four most useful plumbing commands

git cat-file -p <oid>

The single most useful plumbing command. It prints the contents of any object by OID:

OID=$(git rev-parse HEAD)
git cat-file -p "$OID"
# tree f3a9c2...
# parent 8a3f9d2...
# author Engineer <eng@example.com> 1700000000 +0000
# committer Engineer <eng@example.com> 1700000000 +0000
#
# feat(terraform): add logging bucket

To inspect a tree:

git cat-file -p HEAD^{tree}
# 040000 tree abc123...    terraform
# 100644 blob def456...    README.md

To inspect a blob:

git cat-file -p HEAD:terraform/main.tf
# resource "aws_s3_bucket" "logs" {
#   bucket = "prod-logs"
# }

git cat-file -t &lt;oid&gt; prints the type (blob, tree, commit, tag). git cat-file -s &lt;oid&gt; prints the size in bytes. The two flags together make a complete type-and-size inspection.

git hash-object &lt;file&gt;

Reads a file, writes it as a blob into the object store, and prints the OID. Used by git add to materialise the staging blob:

echo 'hello' | git hash-object -w --stdin
# writes the blob "hello\n" and prints the OID

The -w flag is critical: without it, git hash-object only prints the OID the file would have — it does not write the object. Most scripts that build object databases use git hash-object -w to materialise the blob.

git write-tree

Reads the index and writes a tree object that represents the index’s contents. Returns the tree OID. This is the step that turns a staged set of blobs into a single tree the commit can reference:

git add terraform/main.tf
TREE=$(git write-tree)
echo "$TREE"
# abc123...  (the OID of the tree object)

git update-ref

Atomically move a ref to a new OID. This is the only safe way to move a branch from a script:

git update-ref refs/heads/main $COMMIT_OID

git update-ref checks the current value of the ref, refuses to move it if it has changed since you read it (a CAS-style check), and writes the new value. It is the operation every git commit and git push ultimately performs on the receiving end.

Building a commit from plumbing

A complete commit, constructed by hand from plumbing — useful for forensics, for migration scripts, and for understanding what git commit actually does:

# 1. Hash the file content into a blob
BLOB=$(git hash-object -w terraform/main.tf)

# 2. Build a tree that contains the blob
git update-index --add --cacheinfo 100644,"$BLOB",terraform/main.tf
TREE=$(git write-tree)

# 3. Construct a commit object pointing at the tree
PARENT=$(git rev-parse HEAD)
COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "manual commit")

# 4. Move the branch ref to the new commit
git update-ref refs/heads/main "$COMMIT"

# 5. Verify
git log --oneline -1

Every step in this sequence is what git commit -m 'manual commit' would have done, but exposed explicitly. A forensics workflow that needs to reconstruct a commit without contaminating the current working tree can do exactly this in a temporary index (using GIT_INDEX_FILE from lesson II-05) and leave the user’s working tree untouched.

Forensics use case

When a production incident requires inspecting the contents of a blob that was overwritten or amended away, plumbing is the only way:

# Find the OID of the file as it existed in a specific commit
git rev-parse $COMMIT:terraform/main.tf
# abc123...

# Print the file contents
git cat-file -p abc123...
# (the exact bytes of the file at that commit)

# Or, equivalently, in one step
git show $COMMIT:terraform/main.tf

If the commit object itself has been garbage collected, the blob is gone. But while the object is still in the repository (every object is, until git gc runs past its grace period), it can be recovered by OID. The OID is the only handle the repository provides.

Production discipline

  1. Reach for plumbing when you need to script Git. The porcelain commands are for humans; their output is not a contract. Plumbing commands’ output and side effects are stable across Git versions.
  2. Never use git update-ref to bypass protected branches. Branch protection is enforced by the server, not by the client. A local git update-ref will move your local branch but the server will reject the push. The plumbing layer does not negotiate with the server.
  3. Treat git hash-object -w as additive. It writes a blob into the object store. The blob is unreferenced (no tree points at it) and will be garbage collected within ~30 days unless something references it. Use it inside a workflow that builds a tree and a commit on top of the blob.

Cross-course references

  • Linux for Production Sysadmins - Part XXXII (Shell) covers the analogous separation in shell: builtins (the plumbing) vs user-facing commands (the porcelain). The reasoning is the same — builtins are stable and scriptable, user-facing commands add ergonomics.
  • Docker for Production Sysadmins - Part XI (Content addressing) covers the content-addressable store of Docker images, which is the same model Git uses: every object is named by the hash of its contents.
  • Observability for Production Sysadmins - Part XII (Logs) describes how log formats follow the same plumbing/porcelain split: the wire format is stable, the pretty-printed output is not.

Quiz

Knowledge check · 4 questions

  1. Q1. Which plumbing command is the correct choice for a script that needs to read the contents of a blob given its OID, but does not need to know the working tree path?

  2. Q2. Plumbing commands run hooks and update the index the same way porcelain commands do.

  3. Q3. Name the four plumbing commands Git uses internally to implement a single `git commit`, in the order they are called.

  4. Q4. Recover a blob from a previous commit without disrupting the current working tree, and prove the recovery is correct.

    A production incident requires inspecting the exact bytes of `terraform/backend.tf` from commit `8a3f9d2` (the last commit before the file was amended). The repository is currently on `main` with uncommitted changes in the working tree, and the engineer must not check out the old commit (which would disturb the working tree).

Passing score: 75%. Answers are checked in this browser.