Git records snapshots and relationships between commits. That model — content-addressed objects, cheap movable references, explicit ancestry — is what makes parallel work, review, and recovery possible. Most Git frustrations come from treating it as a file-sync tool rather than as a database of snapshots with identity rules.
Snapshots, not diffs
Git stores three object types: a blob (file bytes), a tree (a directory: names → blobs and subtrees), and a commit (a tree plus parent(s), author, timestamp, message). Everything is addressed by the SHA of its content:
$ git cat-file -p HEAD
tree 8b7f3c9a…
parent a91c4e0b…
author Priya Nandakumar <priya@example.dev> 1769500000 +0000
feat(checkout): reject stale promo codes at the boundaryCommits do not store diffs — they store complete trees. Identical content is stored once, so a commit that changes one file adds roughly one blob and a tree, and the rest of the tree points at objects that already exist (delta compression on disk is an optimization, not the model). A branch is a 41-byte file containing a commit hash:
$ cat .git/refs/heads/main
3f9c1ab7d2e4b6a0c5f81e2d7b49c0a3e6d5f211That is why branching is free and why history is reproducible: given a commit hash, anyone with the repository can reconstruct exactly those bytes, forever.
Commits are review units
A pull request is only as good as its commit structure. Two commits — one implementation,
one unrelated formatting — reviewed as a single diff force the reviewer to mentally subtract
noise from signal; split, each diff can be read in one pass and either approved or rejected
on its own terms. The same split pays off operationally: git bisect finds a regression by
repeatedly halving the history, so the number of steps is logarithmic.
regression hunt via bisect (each step = checkout + run test):
500 commits since the last release → ceil(log2 500) = 9 checkouts
50 commits → 6 checkouts
8 commits (one per logical change)→ 3 checkouts
at ~90 seconds per automated step: 13.5 min vs 9 min vs 4.5 min —
and with one mixed commit, bisect can land on a state that never
ran on its own, and the search stops being trustworthy.History also carries intent. "Fix bug" loses the why in a week; a message that names the trigger ("reject stale promo codes: validation ran after the redemption call") is the difference between a five-minute and a two-hour incident investigation.
Merge, rebase, and identity
A merge combines two histories and records a new commit with two parents — ancestry is preserved, and both branches' work is visible in the graph. A rebase replays your commits onto a new base, producing new commits with new hashes:
before rebase: A---B---C (feature)
\
D---E---F (main)
after rebase: D---E---F---A'---B'---C' (feature, A/B/C rewritten as A'/B'/C')The practical rules follow directly from what rebase does:
- Never rebase a branch other people have pulled. Their clones reference hashes that no longer exist on your branch; every subsequent push becomes a force-push and a coordination event.
- Rebase your own unreviewed commits freely to keep a linear, readable history.
- Identity matters for traceability. If a commit hash is recorded in an artifact tag, a build log, or an incident timeline, rewriting it breaks the reference. Copy the hash before rewriting if you need to keep the audit trail.
- Rewritten commits are not immediately lost —
git reflogkeeps the previous tips for about 90 days, so a botched rebase is recoverable withgit reflogandgit reset --hard.
Costs and failure cases
Git does not make a change correct or safe; it makes it reviewable and reversible, if you use it that way:
- Mixed-purpose commits obscure intent. A commit that refactors, renames, and fixes a bug cannot be reviewed atomically, cannot be reverted atomically, and cannot be bisected.
- Rewriting shared history disrupts collaborators — force-pushes invalidate local branches and CI builds keyed on hashes.
- Don't rewrite history during an incident. Preserve the original commits and let the post-mortem reference exact hashes; amending a commit that already ran in production destroys the evidence you will need.
- A clean history is a tool, not a virtue. Interactive rebase to polish messages before merge is reasonable; reorganizing a week-old shared branch for aesthetics is not.
Key takeaways
- Content-addressed snapshots + movable refs = free branching, reproducible builds, and hashes that serve as primary keys for artifacts and incidents.
- Small, single-purpose commits pay three dividends: faster review, trustworthy
bisect(log₂ n steps), and a history that explains itself. - Merge preserves identity; rebase replaces it. Rebase your own unpushed work; never rewrite what others have consumed, and keep the hash of anything already referenced.
- Never rewrite evidence during an incident — the original commit is part of the record.
Pro Git Book covers the object model and branching flows in full.
Production cost model and latency numbers
To apply Git History as a Reproducible Change Record effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Git History as a Reproducible Change Record, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.