Git, CI/CD & GitOpsII · Git ArchitectureArchitecture
Git as a content-addressed store — why hashes are the identity
What you'll learn
- Define what "content-addressed" means and how it differs from location-addressed storage
- Trace how a blob OID is computed from the file contents and the object header
- Explain why Git objects are immutable and what rewriting them would require
- Reason about the implications of a SHA-1 hash collision for a Git repository
- Apply the content-addressed model to supply-chain trust and artifact pinning
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 is a content-addressed store. Every object — blob, tree, commit, tag — is named by the SHA-1 (or SHA-256, in newer Git versions) of its contents. The OID is the identity. The file that holds the object is at a path derived from the OID. Two repositories that have stored the same bytes will have the same OID, and the objects are interchangeable. This single design decision is what makes Git’s immutability, deduplication, and forensic trust possible.
What “content-addressed” means
A location-addressed store (a typical filesystem) names data
by where it lives: /var/log/app.log is the file at that path.
If you move the file or rewrite its contents, the path still
points at the same identity.
A content-addressed store names data by what it is. The identity is the hash of the contents; the storage location is a function of the identity. Two files with the same bytes have the same identity, regardless of where they are stored.
flowchart LR
A["File contents\nhello world"] --> B["SHA-1 hash\n2aae6c35c..."]
B --> C["Object at\nobjects/2a/ae6c35c..."]
B --> D["Same OID in any clone,\nany pack, any repo"]
For Git:
git hash-object <file>computes the OID a file would have if it were stored as a blob.git hash-object -w <file>writes the blob into the object store and prints the OID.- Every blob with the same bytes has the same OID, regardless of the file path, the repository, or the time it was written.
How an OID is computed
The OID is the SHA-1 (or SHA-256) of the object body, which is:
<object-type> <byte-length>\0<contents>
For a blob containing the bytes hello world\n:
printf 'blob 12\0hello world\n' | sha1sum
# 2aae6c35c94fcfb415dbe95f408b9e91b0a12b0a
echo 'hello world' | git hash-object --stdin
# 2aae6c35c94fcfb415dbe95f408b9e91b0a12b0a
The header (blob 12\0) is part of the hash. The same text
content stored as a different object type would have a different
OID. For a tree, the header is tree <size>\0<entries>; for a
commit, commit <size>\0<commit-body>. This is why the
content-addressed model is type-safe: a blob and a tree cannot
share an OID even if their content overlaps.
Why objects are immutable
Rewriting an object would mean changing its contents, which would
change its OID, which would mean the file at the original path
no longer matches the OID in the filename. Git’s only response
to “rewriting an object” is to write a new object with a new OID.
The old object remains in the store (until git gc removes it as
unreferenced).
flowchart LR
A["Original blob\nOID 2aae6c..."] --> B["Stored at\nobjects/2a/ae6c..."]
C["Edit the file bytes"] --> D["New blob\nOID 9f3c1d..."]
D --> E["Stored at\nobjects/9f/3c1d..."]
B -.->|"unreferenced after\nedit is committed"| F["Eligible for gc"]
Every operation that “changes” a Git object — git commit,
git tag, even an git update-ref — does not change an existing
object. It writes a new object whose contents reference the old
objects. The history is a DAG of immutable objects, each
identified by the hash of its contents.
This is why git commit --amend is safe in a way that editing
historical commits by hand is not: --amend writes a new commit
object that points at the same tree as the old commit (or a new
tree), and only the ref is updated to point at the new commit.
The old commit object is still in the store, still referenced by
the reflog, and still recoverable.
What a hash collision would imply
The OID is a commitment to the contents. If two different blobs have the same OID, that is a hash collision. In practice, this would mean:
- A Git repository could be tricked into believing that two different files are the same file, because the same OID would resolve to the same object.
- A signed tag signing a commit OID would be ambiguous — the commit OID could refer to two different commits.
- The forensic integrity of the entire repository would be compromised: the OID would no longer be a proof of contents.
flowchart LR
A["Legitimate file\nOID 2aae6c..."] --> C["objects/2a/ae6c..."]
B["Forged file\nOID 2aae6c..."] --> D["Same storage location"]
C --> E["Repository now has one\nobject for two files"]
D --> E
SHA-1 has been considered cryptographically broken since the
SHAttered attack in 2017 demonstrated a chosen-prefix collision
attack. Modern Git offers init.hashObject=sha256 to use
SHA-256 instead, which has a 256-bit hash space (versus SHA-1’s
160-bit) and is not yet known to be feasible to attack. SHA-1
remains the default for backward compatibility, but new
infrastructure repositories should consider SHA-256 for the
forensic guarantees.
The content-addressed model enables supply-chain trust
The properties that follow from “OID is a proof of contents” are the same properties that make supply-chain trust possible:
flowchart TB
A["Commit OID"] --> B["Identifies a tree"]
B --> C["Identifies a set of blobs"]
C --> D["Identifies the exact bytes"]
D --> E["Build artifact is reproducible\nfrom the commit OID"]
A --> F["Signed tag signs the OID"]
F --> G["Auditor can verify\nthe signing identity"]
- A commit OID identifies a tree; the tree identifies a set of blobs; the blobs identify the exact bytes. Pinning an artifact by commit OID is pinning by exact contents.
- A signed tag signs a commit OID. The auditor verifies the signature, reads the OID, and is guaranteed to see the exact bytes that the signer signed.
- A supply-chain attestation (SLSA, in-toto, sigstore) signs a digest that is the OID of the artifact. The chain of trust extends from the artifact back to the commit, because the OID commits to the contents.
The OID is the join point between version control, artifact storage, and attestation. Every other layer in the supply chain is a wrapper around it.
Production discipline
- Treat the OID as the unit of trust. A commit reference pinned by OID is a content commitment. A branch reference is a moving target. In an infrastructure repository, the rule is the same as in lesson I-04: production must be addressable by digest, not by name.
- Choose SHA-256 for new repositories. The forensic
properties of the content-addressed model depend on the
difficulty of finding hash collisions. SHA-1 is broken;
SHA-256 is not. For new repositories,
git config init.hashObject sha256(orgit init --object-format=sha256) is the responsible default. - Never trust an OID you did not compute yourself. A supplied OID is a claim that those are the bytes. The only way to verify the claim is to hash the bytes and compare. The OID is a proof, but the proof is only valid if you generated it locally.
Cross-course references
- Docker for Production Sysadmins - Part XI (Content addressing) covers the same content-addressed model for container images: every layer is named by the hash of its contents, and the image digest is the hash of the manifest. The reasoning is identical.
- Terraform for Production Sysadmins - Part IX (State) describes the analogous model for Terraform state: the state file is content-addressed by its SHA-256, and the state lock prevents concurrent writes that would invalidate the identifier.
- Observability for Production Sysadmins - Part XII (Logs) covers content-addressed log storage in systems like Loki and Splunk, where each log line is identified by a hash of its contents.
Quiz
Knowledge check · 4 questions
Q1. Why is rewriting an existing Git object impossible by design?
Q2. A SHA-1 hash collision in Git would mean that two different files could resolve to the same OID and be treated as the same object.
Q3. What is the input to the SHA-1 hash that produces a Git blob's OID, and why does the header matter?
Q4. Diagnose whether a supplier's claim of a specific commit OID is trustworthy, and identify the production-grade verification.
A vendor claims to have built a Terraform module from commit `8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7` of your infrastructure repository. The vendor supplies the OID and a checksum of the built artifact. The team needs to verify that the artifact is the same build that the commit OID would produce.
Passing score: 75%. Answers are checked in this browser.