Git, CI/CD & GitOpsII · Git ArchitectureArchitecture
Plumbing versus porcelain — the low-level commands Git is built on
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
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:
- hashes the staged blobs with
git hash-object(already done atgit addtime), - builds a tree object with
git write-tree(already done atgit addtime), - constructs a commit object and writes it with
git commit-tree, - 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 <oid> prints the type (blob, tree, commit,
tag). git cat-file -s <oid> prints the size in bytes. The two
flags together make a complete type-and-size inspection.
git hash-object <file>
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
- 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.
- Never use
git update-refto bypass protected branches. Branch protection is enforced by the server, not by the client. A localgit update-refwill move your local branch but the server will reject the push. The plumbing layer does not negotiate with the server. - Treat
git hash-object -was 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
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?
Q2. Plumbing commands run hooks and update the index the same way porcelain commands do.
Q3. Name the four plumbing commands Git uses internally to implement a single `git commit`, in the order they are called.
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.