Chapter 18Lesson 02~165 minutes

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.

Hands-on plumbingAlternate indexcommit-treeupdate-ref

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

Containment rule: the next commands set 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

  1. You have file bytes and need only the would-be object ID, with no repository mutation. Which command and flag combination?
  2. You have a known blob ID and need a path/mode mapping in a disposable alternate index. Which command?
  3. You need a tree snapshot of the selected index. Which command?
  4. You need a commit object but do not yet want any branch to move. Which command?
  5. 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 -w created a blob before manual.conf entered the index.
  • update-index --cacheinfo created a stage-0 index tuple with the expected mode/object/path.
  • write-tree created a tree containing both baseline and manual paths.
  • commit-tree left trunk unchanged until update-ref created plumbing-demo.
  • git switch plumbing-demo made the low-level commit visible through normal porcelain.
  • The alternate index contains alternate.conf while the live index does not.
  • No command manually edited a file inside .git/refs or replaced the live index file.

16. Cleanup

Confirm the disposable parent path before recursive deletion. The alternate index and all manually created objects are inside this training directory.

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.

Next

Design low-level automation that survives paths, platforms, and repository discovery

Lesson 3 covers environment overrides, index versions, NUL-delimited formats, porcelain v2, object alternates/replacement context, and portability tradeoffs.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.