Git commands are divided into two categories:

  1. Porcelain: User-friendly commands you use every day (add, commit, push, pull).
  2. Plumbing: Low-level commands designed for scripts and automation (hash-object, update-index, write-tree).

Think of it like a car: Porcelain is the steering wheel and pedals. Plumbing is the engine’s fuel injectors and transmission valves. You usually drive with the wheel, but when the engine seizes, you need to understand the valves.

The Real-World Scenario: A Corrupted Repository

Imagine you are a Senior Staff Engineer, and a critical build server just crashed mid-deployment. The repository state is corrupted, git status throws fatal errors, and standard commands refuse to work. You can’t just clone a new copy—there are unpushed, critical hotfixes trapped inside the .git/objects folder.

To rescue the data or build custom Git automation tools, you need to stop relying on the user interface and start manipulating Git’s internal database directly.

Building a Commit Manually

We are going to create a commit without using git add or git commit. We will use plumbing commands to manipulate the object database directly. This is exactly what Git does under the hood when you type git commit.

The Steps (The “Assembly Line”)

  1. hash-object: Create a Blob from file content. (Manufacturing the parts)
  2. update-index: Add the file to the Index (Staging Area). (Placing parts on the assembly line)
  3. write-tree: Create a Tree object from the Index. (Taking a snapshot of the assembly line)
  4. commit-tree: Create a Commit object pointing to the Tree. (Packaging the snapshot with a label)
  5. update-ref: Move the branch pointer (HEAD) to the new Commit. (Updating the delivery manifest)
git-plumbing-lab
Welcome to the Plumbing Lab.
Task: Create a commit manually.
 
Step 1: Create a blob object from text.
Type: echo "hello" | git hash-object -w --stdin
$

Working Dir

Index

Repository (Objects)

Command Breakdown

1. git hash-object -w

Takes content, computes the SHA-1, creates the blob header, and stores the compressed object in .git/objects.

  • -w: Write the object to the database (don’t just calculate hash).
  • --stdin: Read content from standard input.

Hardware Reality: Under the hood, Git prepends a header (blob <size>\0), calculates the SHA-1 hash of this payload, uses zlib to compress the data, and writes it to disk in a fan-out directory structure (the first two characters of the hash become the directory, the remaining 38 become the filename). This heavily optimizes file system lookups.

2. git update-index

Modifies the binary index file directly.

  • --add: Add file if not present.
  • --cacheinfo <mode> <hash> <filename>: Manually register a file in the index using its hash, without needing it to be in the working directory.

Deep Dive: The <mode> here is typically 100644 for a regular file, or 100755 for an executable. The Index (or Staging Area) isn’t a directory—it’s a single, complex binary file (.git/index) that maps file paths to blob hashes and tracks their cache state (like mtime and ctime) for fast comparisons against the working tree.

3. git write-tree

Scans the current index and creates a tree object representing that directory state. Returns the SHA-1 of the new tree.

Edge Case: If you run write-tree and the index perfectly matches an existing tree object already in the database, Git will simply return the existing hash. It aggressively deduplicates identical trees.

4. git commit-tree <tree-hash>

Creates a commit object.

  • Takes the tree hash as an argument.
  • Takes the commit message from stdin.
  • Returns the new commit SHA-1.
  • (Note: To add a parent, you would use -p <parent-hash>).

5. git update-ref

Updates a reference (like a branch pointer) to a new hash. This is safer than echo <hash> > .git/refs/heads/master because it follows symlinks and handles locking.

Technical Depth: update-ref creates a .lock file (e.g., refs/heads/master.lock) before modifying the reference. If two Git processes try to update the same branch concurrently, the lock file prevents a race condition that could corrupt the branch pointer.

Summary

You just performed the exact steps git commit does for you!

  1. Hashes files (Blobs).
  2. Updates the Staging Area (Index).
  3. Creates a snapshot of the directory (Tree).
  4. Creates a history node (Commit).
  5. Moves the branch pointer (Ref).

Understanding this flow allows you to repair broken repos, script custom workflows, and truly master Git.