Plumbing Commands, Index Internals, cat-file, hash-object, and commit-tree: Guided Hands-On Workflow and Core Operations
Build a disposable repository with both porcelain and plumbing: hash/store a blob, update the index, write a tree, create a commit, inspect it, attach a ref, and isolate a second index with GIT_INDEX_FILE.
Learning objectives
- Compare normal add/commit results with the underlying object/index/tree/ref layers.
- Create and inspect blob, tree, and commit objects using documented plumbing.
- Directly stage a known blob with update-index --cacheinfo and verify its tuple.
- Attach a temporary ref only after inspecting the manually created commit.
- Use read-tree and GIT_INDEX_FILE to build a hypothetical index/tree without altering live staging.
1. Create a disposable repository and record the normal porcelain path first
Before using plumbing, create one ordinary commit. That gives you a known-good reference point and demonstrates what the high-level workflow accomplishes.
Git Bash, Bash, or zsh
mkdir git-plumbing-lab
cd git-plumbing-lab
git init -b trunk repo
cd repo
git config user.name "Plumbing Lab"
git config user.email "plumbing-lab@example.invalid"
printf "normal=one\n" > normal.conf
git status --porcelain=v2
git add normal.conf
git ls-files --stage
git commit -m "porcelain: create baseline"
BASE=$(git rev-parse HEAD)
git show --stat --oneline "$BASE"
PowerShell setup alternative
New-Item -ItemType Directory git-plumbing-lab | Out-Null
Set-Location git-plumbing-lab
git init -b trunk repo
Set-Location repo
git config user.name "Plumbing Lab"
git config user.email "plumbing-lab@example.invalid"
Set-Content normal.conf 'normal=one'
git status --porcelain=v2
git add normal.conf
git ls-files --stage
git commit -m "porcelain: create baseline"
$BASE = git rev-parse HEAD
git show --stat --oneline $BASE
git add caused the index to contain a path/mode/object
tuple. git commit produced a tree and commit and
advanced the current branch. Porcelain also handles user-facing
concerns that the manual sequence below does not reproduce
automatically.
2. Inspect the baseline commit as objects
git cat-file -t "$BASE"
git cat-file -p "$BASE"
BASE_TREE=$(git rev-parse "$BASE^{tree}")
git cat-file -t "$BASE_TREE"
git cat-file -p "$BASE_TREE"
git cat-file -p "$BASE:normal.conf"
The commit names a tree; the tree names the staged blob for
normal.conf. commit:path syntax resolves
the blob through the commit's tree without looking at the current
working-tree file.
3. Write a new blob without staging it
printf "manual=two\n" > manual.conf
CALCULATED=$(git hash-object manual.conf)
BLOB=$(git hash-object -w manual.conf)
test "$CALCULATED" = "$BLOB"
git cat-file -t "$BLOB"
git cat-file -s "$BLOB"
git cat-file -p "$BLOB"
git ls-files --stage -- manual.conf
git status --short -- manual.conf
Expected: the object exists, but
ls-files --stage has no entry for the path. Status
reports manual.conf as untracked. Object creation and
staging are independent.
4. Put the known blob into the index with a documented mode/path tuple
git update-index --add --cacheinfo "100644,$BLOB,manual.conf"
git ls-files --stage -- manual.conf
git status --short -- manual.conf
git diff --cached -- manual.conf
--cacheinfo directly registers mode
100644, the blob object ID, and the path in the index.
Because the working-tree file contains the same bytes, the staged
path and working file agree.
5. Serialize the current stage-0 index into a tree
TREE=$(git write-tree)
git cat-file -t "$TREE"
git cat-file -p "$TREE"
The resulting tree includes both normal.conf and
manual.conf because both are in the index.
write-tree does not create a commit and does not move
trunk.
6. Create a child commit object manually
MANUAL_COMMIT=$(printf 'plumbing: add manual configuration\n' |
git commit-tree "$TREE" -p "$BASE")
git cat-file -t "$MANUAL_COMMIT"
git cat-file -p "$MANUAL_COMMIT"
git rev-parse trunk
The new commit points at TREE and has
BASE as its parent. The final
rev-parse trunk still prints BASE. That
observation is crucial: commit-tree created history
data but did not attach it to the branch.
7. Inspect several objects efficiently with
cat-file --batch-check
printf '%s\n' "$BLOB" "$TREE" "$MANUAL_COMMIT" |
git cat-file --batch-check='%(objectname) %(objecttype) %(objectsize)'
Expected rows report a blob, tree, and commit with their full object
IDs and sizes. Batch mode is better for object-heavy tooling than
starting a new git cat-file process for every ID.
8. Inspect first, then attach a temporary branch ref
git cat-file -p "$MANUAL_COMMIT"
git diff "$BASE" "$MANUAL_COMMIT"
git update-ref refs/heads/plumbing-demo "$MANUAL_COMMIT"
git rev-parse plumbing-demo
git log --graph --decorate --oneline --all --max-count=6
The ref is created only after object/tree inspection.
update-ref is preferable to opening files under
.git/refs. Chapter 19 adds expected-old-value checks
and reference transactions.
9. Switch to the manually assembled history using porcelain
git switch plumbing-demo
git status --short --branch
git log -2 --oneline
cat manual.conf
This step proves that ordinary porcelain understands the low-level objects because they use the same object/index/ref model. There is no separate “plumbing repository.”
10. Create an alternate index instead of experimenting on the live one
GIT_INDEX_FILE for each operation. The normal index
remains untouched.
Git Bash, Bash, or zsh
ALT_INDEX="$PWD/alternate.index"
GIT_INDEX_FILE="$ALT_INDEX" git read-tree "$TREE"
GIT_INDEX_FILE="$ALT_INDEX" git ls-files --stage
git ls-files --stage
PowerShell
$ALT_INDEX = Join-Path (Get-Location) 'alternate.index'
$env:GIT_INDEX_FILE = $ALT_INDEX
git read-tree $TREE
git ls-files --stage
Remove-Item Env:GIT_INDEX_FILE
git ls-files --stage
read-tree loaded the chosen tree into the alternate
index. It did not update working-tree files because
-u was not used. The final normal
ls-files reads the live index again.
11. Add an index-only path to the alternate index
Git Bash, Bash, or zsh
printf "alternate=only\n" > alternate.conf
ALT_BLOB=$(git hash-object -w alternate.conf)
GIT_INDEX_FILE="$ALT_INDEX" \
git update-index --add --cacheinfo "100644,$ALT_BLOB,alternate.conf"
GIT_INDEX_FILE="$ALT_INDEX" git ls-files --stage
ALT_TREE=$(GIT_INDEX_FILE="$ALT_INDEX" git write-tree)
git cat-file -p "$ALT_TREE"
git ls-files --stage -- alternate.conf
The alternate tree contains alternate.conf, while the
live index has no entry for that path. The working-tree file exists,
so normal status will still see it as untracked. This isolation is
the preferred way to teach or test index-manipulating plumbing.
12. Understand what read-tree changes before using it
Without -u, git read-tree TREE populates
the selected index from a tree without writing corresponding files
into the working tree. With merge modes it can also populate
unmerged index stages. Because it directly replaces/merges index
state, use an alternate index for experiments.
13. Use rev-parse as a boundary check, not only an
object-name resolver
git rev-parse --verify plumbing-demo^{commit}
git rev-parse --show-toplevel
git rev-parse --absolute-git-dir
git rev-parse --git-path index
git rev-parse --show-object-format
git rev-parse --show-ref-format
These checks let automation prove the target repository and the expected object type before running lower-level mutations.
14. Challenge — choose the layer from the requested outcome
- You have file bytes and need only the would-be object ID, with no repository mutation. Which command and flag combination?
- You have a known blob ID and need a path/mode mapping in a disposable alternate index. Which command?
- You need a tree snapshot of the selected index. Which command?
- You need a commit object but do not yet want any branch to move. Which command?
- After inspecting the commit, you want to name it through a temporary branch. Which documented ref command?
15. Verification checklist
- The normal porcelain commit remains the parent of the manually created commit.
-
hash-object -wcreated a blob beforemanual.confentered the index. -
update-index --cacheinfocreated a stage-0 index tuple with the expected mode/object/path. -
write-treecreated a tree containing both baseline and manual paths. -
commit-treelefttrunkunchanged untilupdate-refcreatedplumbing-demo. -
git switch plumbing-demomade the low-level commit visible through normal porcelain. -
The alternate index contains
alternate.confwhile the live index does not. -
No command manually edited a file inside
.git/refsor replaced the live index file.
16. Cleanup
Git Bash / Bash / zsh
cd ../..
pwd
rm -rf git-plumbing-lab
PowerShell
Set-Location ../..
Get-Location
Remove-Item -Recurse -Force git-plumbing-lab
17. Knowledge check
Question 1. What did git commit do that
git commit-tree did not do in this lab?
Question 2. Why was GIT_INDEX_FILE useful?
Question 3. Did git read-tree TREE update
working-tree files here?
Question 4. Why inspect with cat-file/diff
before update-ref?
Question 5. Why is this sequence a conceptual model rather than a guarantee that porcelain internally invokes the same commands?
18. Summary
You built the object/index/tree/commit/ref chain manually and then returned to ordinary porcelain to prove interoperability. The safest low-level habit introduced here is alternate-index isolation: if the experiment concerns the index, redirect it rather than damaging the developer's real staging state.
Authoritative references
git-update-index
git-write-tree
git-commit-tree
git-read-tree
git-update-ref
git-rev-parse
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.