0. Introduction
In 2005, the Linux kernel was the world’s largest open-source collaboration project
The 1000+ contributors worldwide were using a commercial version control tool called BitKeeper
In April 2005, on a weekend, an Australian programmer named Andrew Tridgell (the guy who wrote Samba) reverse-engineered BitKeeper
Larry McVoy, the man behind BitKeeper, found out and announced it would stop providing free licences to the entire Linux community
Linus lost his tool overnight
Linus didn’t panic. He first spent two weeks studying every version control tool on the market — CVS, SVN, Monotone, Darcs…
Then he reached a conclusion:
These things are all garbage.
The exact words in his email were:
No existing system meets my needs, so I decided to write my own
0.1 Why?
Linus’s needs weren’t those of an ordinary developer; what he managed was the most complex collaborative project in the world
His daily work looked like this:
Developers around the world send their patches to the maintainers of each subsystem, the subsystem maintainers organise them and send them to Linus, and Linus finally decides what goes into the kernel
This is a tree-shaped network of trust, not a centralised commit system
So the tool he needed had to satisfy:
1. It has to work offline Developers must be able to commit, view history and branch without a network connection
2. It has to be fast The Linux kernel is millions of lines of code, and operations must complete in seconds or even milliseconds. He mocked CVS at the time: “If an operation takes more than 3 seconds, the design is wrong.”
3. History must be tamper-proof He had to be able to verify that the code a developer sent him, from the moment they wrote the first line to the moment it reached Linus’s hands, hadn’t been touched by anyone along the way. Not by trust, but by mathematics.
4. Branch merging has to be good enough The Linux development model is inherently parallel, with dozens of development lines advancing at once; merging must be reliable and must be fast. SVN’s merges are a nightmare — unacceptable to him
April 3, 2005: Linus starts writing Git
April 7, 2005: Git can host its own source code
April 18, 2005: Linux kernel developers start collaborating with Git
June 2005: Linux 2.6.12 is officially released with Git
From zero to hosting the world’s largest open-source project in under three months
0.2 diff vs snapshot
The essence of version control isn’t recording “how the files changed”, but recording “what the complete state of the project was at each moment”
CVS and SVN record diffs, i.e. which lines changed this time
Linus thought that was wrong
What he wanted to record was a snapshot; every commit is a complete snapshot of the entire project
This one decision led to almost all of Git’s design
1. One decision led to all the design
Linus sat there not thinking “how should I design a version control tool”
He was thinking about a more fundamental question:
What does “the same code” mean?
1000 developers, each with a copy of the Linux kernel code on their machine. How do I know whether yours and mine are in the same state? How do I know which state the patch you sent me was written against?
SVN’s answer at the time was to look at the revision number: r1042 is r1042
Linus thought that was bullshit. Revision numbers are assigned by a central server; you have to trust that server. And a revision number is just a counter; it contains no information about the content itself
1.1 The content itself is the identity
Linus’s idea was:
Don’t identify a piece of code with an externally assigned ID; use the hash of the code content itself
Specifically, hash the entire content of a file with SHA-1 to get a 40-character string
SHA-1("Chongxi") = f5c3bc7e6fcd7b7b8d04b99f5d7e607302d3a911This string has a mathematical property:
If the content changes, the hash necessarily changes. If the hash hasn’t changed, the content necessarily hasn’t changed
This isn’t a protocol, isn’t trust — it’s a mathematical proof
So two people holding the same hash value can confirm the files in their hands are completely identical, without needing any third party
1.2 But a single file isn’t enough; he needs the whole project
A file can be hashed; what about the whole project?
The Linux kernel has tens of thousands of files spread across hundreds of directories. Linus needed a way to represent the state of the entire project with one hash value
His solution was to hash recursively:
the content of main.c → hash Athe content of util.c → hash Bthe src/ directory → hash(A + B + filename) = hash Cthe content of README → hash Dthe root directory → hash(C + D + filename) = hash EThe root directory’s hash E is computed from the hashes of all child nodes
This means:
If any file changes by a single byte, every parent node’s hash above it changes, all the way up, and the root hash changes too
Conversely: if the root hash hasn’t changed, not a single byte of the entire project has changed
This is Git’s tree object, and also the core idea of the Merkle Tree
1.3 Snapshot
With tree objects, Linus could lock the state of the entire project to one hash value at any moment
Then he wrapped another layer around the outside, adding metadata:
commit { tree: e5f8c2... ← the root hash of the entire project at this moment parent: a3d9f1... ← the hash of the previous commit author: Linus time: 2005-04-07 message: "Initial commit"}Then he hashed this entire structure once more to get this commit’s hash value
There’s a very crucial point here:
The commit’s hash contains the parent’s hash
This means you can’t quietly modify the content of some commit in history without changing the hash. Change that commit and its hash changes; the parent hash recorded by the next commit no longer matches, and the one after that doesn’t match either — the entire chain breaks
History is locked with cryptography — not by permission control, not by trust, but by mathematics
1.4 snapshot vs diff: who will win
SVN stores:
r1: the original filer2: line 3 changed to thisr3: line 17 deleted, line 20 added thisTo know the state at r3, you have to stack up all the diffs from the beginning and compute
Git stores:
commit1: hash E1 of the entire projectcommit2: hash E2 of the entire projectcommit3: hash E3 of the entire projectTo know the state at any moment, just take that hash, find the corresponding tree, and expand it. No computation, no replay
The cost is storage space? Git solved that with object deduplication: if a file hasn’t changed between two commits, their blob hashes are the same, and Git stores only one copy
1.5 What can we derive from this
At this point, Linus had not yet invented branches, had not invented merge, had not invented any advanced concept
He had done only one thing:
Use hashes to lock the project state at each moment into a tamper-proof object, and use parent pointers to string these objects into a chain
Everything that remains — branches, merging, rebase — grew naturally out of this foundation
2. The object model
Git isn’t a complex version control system
Linus’s earliest definition was very plain:
Git is a content-addressable filesystem. You put things in, it gives you a hash. Hold the hash, and you can get the thing back out at any time
That’s all
You can open any Git repository and look at the .git/objects/ directory; what’s stored inside is all the objects, split into folders by the first two characters of the hash:
.git/objects/ a3/f8c2d9b1e4... ← this is one object file d9/f3a1b2c4e5... e5/c2f8a3d9b1...The content of each file is compressed object data. A whole Git repository is, in essence, this folder
2.1 blob
The simplest object in Git is called a blob; it stores the raw content of a file, without the filename, without any metadata
blob "CEPATO"↓ SHA-1d1fcd7b25f99b9f9d1a577557f45c10b6c1f9642A blob doesn’t know what it’s called, nor which directory it’s in
Filenames are a matter for the upper layer; blobs only care about content
This design has one direct benefit:
Two files with completely identical content at different paths — Git stores only one blob
2.2 tree
Filenames and directory structure are handed to tree objects to manage
A tree object is just a table:
tree d9f3a1... { 100644 blob 8c7e5a... main.c 100644 blob f7d8e9... util.c 040000 tree b2c4e5... src/}Each line records: file permissions, object type, hash value, name
Trees can nest trees, and that builds the complete directory structure
Then hash this table to get the hash value of this tree
Because a tree’s hash is computed from its children’s hashes:
main.c's content changed→ main.c's blob hash changed→ the tree containing it changed→ the upper tree hash changed→ the root tree hash changedAny change in any leaf is reflected all the way up to the root node
Let me clarify something here. A friend came to me with a question separately; I thought the question was good, so I’m putting it out separately to talk about
Git’s tree is not a hash table; it’s more like an ordered list, storing a mapping of filename → child node hash, but the lookup method is linear traversal, not hash-table O(1) addressing
Compared with the Patricia Trie part of an MPT
Git tree: filename → child node hash // simple mapping, no lookup optimisationMPT: path → leaf value // prefix-compressed, O(log n) lookupThe two look very similar on the surface — both are some kind of key pointing at a hash — but the design goals differ:
- Git’s tree doesn’t need efficient lookup; you checkout a commit and Git just expands the whole tree, with no need to frequently query individual nodes
- Ethereum’s MPT needs efficient lookup, because every transaction has to query the state of a particular account individually, and O(n) is unacceptable
So a simple structure is enough for Git, and only Ethereum needs a Patricia Trie for optimisation
In other words, Git’s tree is for describing state, MPT is for querying state efficiently
2.3 commit
With trees, we can represent the state of the project at a certain moment
But two things are still missing: who created this state and when, and what its previous state was
That’s the commit object:
commit a3f8c2... { tree: d9f3a1... ← root tree, representing all files at this moment parent: prev_hash ← the previous commit author: Linus Torvalds time: 2005-04-07 15:16:22 message: "Initial commit"}Then hash this entire structure to get this commit’s hash
Because the commit hash contains the parent hash, and the parent hash contains its parent, all the way back to the first commit
The entire history chain is locked by a single hash value
Holding the latest commit hash, you can verify that every change to every line of code, from the first day to today, hasn’t been touched by anyone
2.4
Now let’s look at the three layers together:
commit a3f8c2 └── tree d9f3a1 (root directory) ├── blob 8c7e5a main.c ├── blob f7d8e9 util.c └── tree b2c4e5 src/ └── blob 3a9f1c helper.cEach node is an independent object, stored in .git/objects/ and addressed by hash
There’s no containment relationship between them, only reference relationships. Each object records only its child nodes’ hashes, without copying their content
Suppose you make a second commit, changing only main.c:
commit e5c2f8 └── tree 7a1b3d (the new root tree) ├── blob 9f2e4a main.c ← new blob ├── blob f7d8e9 util.c ← same as last time └── tree b2c4e5 src/ ← same as last timeutil.c and the src/ directory didn’t change, so Git reuses their objects directly — no copying, no re-storing
For two commits, Git created only three new objects: the new blob, the new root tree, and the new commit
Git stores snapshots, but because of object reuse, the space actually used is close to storing diffs
You can have your cake and eat it too
2.5 Branches
Having derived this far, Git’s storage layer is already complete: a hash object database containing blobs, trees and commits, connected into trees by references, and strung into a timeline by parent pointers
But there’s one problem:
A commit’s hash is a string like a3f8c2d9b1e4f7..., and humans can’t memorise that
How do you find the latest commit?
3. Pointers
The object database is built, holding thousands of objects, each with a 40-character hash address
Linus faced a very practical problem:
How do I know where the latest commit is?
Write the latest commit hash down on paper every time? That would be direct, sure
git stores this hash in a text file, and gives that file a human-readable name
That’s a branch. Yep, that simple
3.1 refs
Git puts these text files together in the .git/refs/ directory:
.git/refs/ heads/ main ← content: a3f8c2d9... feature ← content: d9f3a1b2... remotes/ origin/ main ← content: e5c2f8a3... tags/ v1.0 ← content: f7d8e9b1...You can open these files directly and look — inside is just one line of hash value, nothing else
cat .git/refs/heads/main# a3f8c2d9b1e4f7a8b2c3d4e5f6a7b8c9d0e1f2a33.2 The essence of a branch
So “creating a new branch” — what actually happens at the filesystem level?
git branch featureWhat happened:
Create a new file called
featureunder.git/refs/heads/, and write the current commit’s hash into it
That’s it. No copying code, no copying history, none of the enterprise-grade operations you might imagine — just this simple
This is why creating a branch in Git is instant, while creating a branch in SVN requires copying the entire directory
3.3 How branches move
You commit new code on the feature branch, producing a new commit b2c4e5...
What happened:
Update the hash in the
.git/refs/heads/featurefile tob2c4e5...
It’s just changing one line of text in one file
Before the commit: feature file content = a3f8c2...After the commit: feature file content = b2c4e5...The branch follows you along; in essence it’s this file updating itself
3.4 HEAD
Now you have two branches, main and feature. How does Git know which branch you’re currently on?
Add one more file, called HEAD, placed in .git/HEAD:
cat .git/HEADIt doesn’t store a commit hash, but a reference pointing to a branch
So:
HEAD → refs/heads/feature → b2c4e5...(commit)When you commit new code:
- Generate a new commit object
- The
featurefile is updated to the new hash - HEAD doesn’t move; it still points to
feature
3.5 What about switching branches
git checkout mainWhat happened:
- Change
.git/HEADto:
ref: refs/heads/main- Based on the commit that
mainpoints to, expand the corresponding file tree into your working directory
You see the files change; in essence Git has rewritten your working directory with content from the object database
3.6 detached HEAD
Now we understand that HEAD is a pointer to a branch
But if you checkout a commit hash directly:
git checkout a3f8c2At this point there’s no branch it can point to, so HEAD can only store the hash directly:
cat .git/HEAD# a3f8c2d9b1e4f7...This is a detached HEAD — the head pointer detached
Why does everyone say the detached state is dangerous?
Because commits you make in this state produce new commits. But no branch file records the new commit’s hash
Once you switch to another branch, HEAD updates, and nothing points to that new commit’s hash anymore
It’s still lying in .git/objects/, but you can never find it again
Git periodically runs garbage collection, cleaning up objects that no reference points to. Your commit just disappears like that
3.7 Remote pointers
Finally there’s another kind of pointer, placed under .git/refs/remotes/:
origin/main ← content: the hash of the remote main at the last fetchIts job is: record the state of the remote repository at your last sync
It’s read-only; you can’t commit on it directly. It only updates when you git fetch
What git push and git pull essentially do is make these local and remote pointers line up
3.8 Summary
HEAD └── refs/heads/main → commit C4 └── refs/heads/feature → commit C5
refs/remotes/origin/main → commit C3 (the state at the last fetch)
refs/tags/v1.0 → commit C1 (doesn't move)The object database is immutable; the pointer system is mutable
Every Git operation is essentially moving these pointers, or adding objects to the object database
4. The essence of the core everyday operations
Let’s first set up a scenario; all the operations below are based on it:
C1 ← C2 ← C3 main ↑ └── C4 ← C5 featuremain points to C3, feature points to C5, and C4 and C5 forked off from C3.
4.1 merge: create a new node
You’re on main and run:
git merge featuregit does the following
- Find the branches’ parent node
main is at C3, feature is at C5; follow the parents upward and the parent node is C3
- diff
C3's file stateWhat main changed between C3 → C3What feature changed between C3 → C5If different places changed, merge automatically. If the same place changed, conflict — you resolve it by hand
- Generate a new commit C6
commit C6 { tree: the merged file state parent: C3 ← the main side parent: C5 ← the feature side}C6 has two parents, which is entirely legal in Git’s object model
Then the main pointer moves to C6:
C1 ← C2 ← C3 ← C6 main ↑ ↑ └── C4 ← C5 featureThe essence of merge is creating a new node in the object database, joining the two lines together, and moving the current branch pointer
History stays real; the fork and the join are both preserved
4.2 rebase: rebuild the nodes, change their dad
C1 ← C2 ← C3 main ↑ └── C4 ← C5 featureSame scenario; you’re on feature and run:
git rebase maingit then replays the two commits C4 and C5 after C3
- Find the common parent node C3
- Extract the change content of C4 and C5
- Starting from main’s latest position C3, reapply these changes one by one, generating new commits
C4 → C4'(parent changed from C2 to C3)C5 → C5'(parent changed from C4 to C4')C4’ and C4 may have the same file content, but completely different hash values, because the parent changed
Result:
C1 ← C2 ← C3 ← C4' ← C5' feature ↑ mainHistory becomes a straight line, as if feature had always been developed sequentially after C3
The essence of rebase is copying a stretch of commit history to another position and rebuilding it; the original objects are still in the database, but no pointers point at them anymore
Note that C4 and C4’ both exist in .git/objects/
After the rebase, the feature pointer has moved to C5’, and no pointer points at the original C4 and C5 anymore
They get cleaned up when Git garbage collects
That’s why after a rebase we say “history has been rewritten”: the original commits are abandoned, and a batch of parallel objects has been forked
4.3 reset: move the pointer, create no new nodes
git reset C3This operation is the simplest and also the easiest to misunderstand
It does only one thing:
forcibly move the current branch’s pointer to the specified commit
Before: main → C5After: main → C3C4 and C5 are still in the object database, but main no longer points at them
reset has three modes; the difference lies in how the working directory and staging area are handled:
--soft move the pointer only; working directory and staging area untouched--mixed move the pointer, clear the staging area, working directory untouched (default)--hard move the pointer, clear the staging area, roll back the working directory tooFrom the pointer’s perspective, all three modes do the same thing. The only difference is how the files in your working directory change
4.4 cherry-pick: copy a single node
git cherry-pick C4You’re on main and want to bring over a single commit from feature
The operation:
Extract C4’s change content relative to its parent, reapply it at the current position, and generate a new commit C4’
C1 ← C2 ← C3 ← C4' main ↑ └── C4 ← C5 featureC4’ and C4 have the same content, different hashes
Just like rebase, both are “copying a commit to a new position and rebuilding it”, except rebase copies a stretch and cherry-pick copies one
A quick table:
| Operation | New object? | Moves pointer? | Rewrites history? |
|---|---|---|---|
| merge | Yes, creates a merge commit | Yes | No |
| rebase | Yes, rebuilds commits | Yes | Yes |
| reset | No | Yes | Depends on the mode |
| cherry-pick | Yes, copies commits | Yes | No |
Every operation is a combination of two things: adding things to the object database, and moving pointers
4.5 Summary
We can now understand any Git operation:
- The object database only takes in, never gives out (except garbage collection); it’s append-only
- Pointers can be moved freely
- The history you see depends on where the pointers point
This is why Git almost never loses data — as long as the object is still in the database, you can always find where a pointer used to point via git reflog and fish the thing back out
Next we’ll talk about collaboration, and why you shouldn’t use rebase when there are multiple people
5. Collaboration
First, set up a realistic scenario
You and Chongxi have each cloned the same remote repository. At the moment of cloning, all three of your states are completely identical:
Remote origin: C1 ← C2 ← C3 mainYour local: C1 ← C2 ← C3 mainChongxi's local: C1 ← C2 ← C3 mainThen we start working separately
5.1 What happens on clone

clone isn’t simply “downloading the code”
More precisely:
Copy the entire remote object database to your machine, then store a read-only copy of all the remote pointers locally
.git/refs/remotes/origin/main = the hash of C3This origin/main is your local snapshot of the remote state; it records where the remote was the last time I synced with it
5.2 fetch
You work for a while, and the remote has new commits
git fetchGit does the following:
Download all new objects from the remote, and update the
origin/mainpointer. Your localmaindoesn’t move
Remote: C1 ← C2 ← C3 ← C4 ← C5 mainYour local main: C1 ← C2 ← C3 main(unmoved)origin/main: C1 ← C2 ← C3 ← C4 ← C5 (updated)fetch does only one thing: sync objects, update remote pointers. Your work is completely unaffected
5.2 push: push local pointers to the remote
You committed C4’ locally and want to push it to the remote:
git pushGit does the following
- Transfer the new local object (C4’) to the remote object database
- Ask the remote to move the
mainpointer to C4’
But the remote checks one thing:
Is the parent of your C4’ the commit the remote’s current main points to?
If it is, that’s called a fast-forward, and the remote moves the pointer forward directly, no problem at all
If it isn’t, the remote refuses and returns an error:
! [rejected] main -> main (non-fast-forward)Why it gets rejected
You and Chongxi both started working from C3:
You: C1 ← C2 ← C3 ← C4' local mainChongxi: C1 ← C2 ← C3 ← C4 pushed to the remoteChongxi pushed before you, and the remote main is now at C4
You want to push C4’, but C4”s parent is C3, not C4
The remote: your history and my history have diverged; I don’t know how to merge them, you figure it out yourself
5.3 pull: fetch + merge
git pullIt’s essentially two steps:
git fetch ← sync remote objects firstgit merge ← then merge origin/main into local mainAfter merging, a merge commit C5 is generated; then you push again, and this time C5’s parent includes C4, so the remote accepts it
C1 ← C2 ← C3 ← C4 ← C5 remote main(C5 is a merge commit) ↑ ↑ Chongxi You5.4 How conflicts happen
You and Chongxi changed the same place in the same file
Git does a three-way comparison:
Common parent C3: line 5 is "meow"Chongxi's C4: line 5 changed to "qwq"Your C4': line 5 changed to "uwu"Git doesn’t know which one to use, and can only stop and let you decide:
<<<<<<< HEADuwu=======qwq>>>>>>> origin/mainThis isn’t a defect in Git; it’s a real conflict. A machine can’t decide business logic on your behalf
5.5 Why rebase is dangerous in a multi-person scenario
Now let’s go back to the question mentioned earlier and explain it with the object model
You and Chongxi both created a feature branch from C3, and Chongxi has already pushed his C4 and C5 to the remote:
Remote feature: C1 ← C2 ← C3 ← C4 ← C5Your feature: C1 ← C2 ← C3 ← C4 ← C5You run a rebase on feature locally, generating C4’ and C5’:
Your feature: C1 ← C2 ← C3 ← C4' ← C5'Then you force-push to the remote:
git push --force // blindly push --force and you'll get beaten to deathThe remote feature becomes:
Remote feature: C1 ← C2 ← C3 ← C4' ← C5'The original C4 and C5 have disappeared from the remote
But Chongxi still has them locally:
Chongxi's feature: C1 ← C2 ← C3 ← C4 ← C5When Chongxi fetches again, he finds the remote history and his local history completely don’t match, because C4 and C4’ have different hashes, and Git considers them two completely unrelated commits
My local repository plunges into chaos
Root cause: rebase rewrote the commit hashes, and a hash is the only ID card in the Git world. You changed an ID card somebody else depends on, and their world collapses
- So there’s this iron rule in collaboration
Never rebase commits that have already been pushed to the remote and that others may be building on
For your own commits that haven’t been pushed out, rebase all you like, no problem at all
5.6 Deconstructing the essence of the entire collaboration model
Everyone has a complete object database and pointer system locally. The essence of collaboration is syncing objects and aligning pointers between different machines. A conflict is the natural result of pointers not lining up; a rejected push is the remote protecting history
6. Conclusion: one idea, four implementations
Back to the question at the very beginning:
Without a central authority, how do you get people distributed around the globe to agree on the same data?
This is a question that has recurred over and over for decades. People in different fields, in different ways, gave variants of the same answer
6.1 The underlying idea: content addressing + hash chains
All these systems are built on two primitives:
- Content addressing
Instead of locating data with “this file is in the 5th directory of the 3rd server”, locate it with the hash of the data content itself
The hash is the address, and the address is the content’s fingerprint
- Hash chains
Each node contains the previous node’s hash, forming a chain
Change any node on the chain and the hashes of all subsequent nodes change. The chain’s integrity can be independently verified by anyone, with no need for an authority to vouch for it
These two primitives combined solve the same problem:
Let data prove its own innocence
Sound familiar? web3? No no no — this idea can be traced back to the 1970s
6.2 1979: Merkle Tree
Ralph Merkle proposed this data structure in his doctoral thesis
The original goal was very plain: how do you efficiently verify that one piece of data in a large batch hasn’t been tampered with?
The solution:
Put all the data in leaf nodes Each parent node stores the combined hash of its left and right child nodes Compute all the way up to the root node
The root hash represents the state of the entire tree. To verify that C hasn’t been tampered with, you only need to provide D, hash AB and the root hash, without exposing the contents of A and B
This is called a Merkle Proof, with a verification efficiency of O(log n)
It was a purely cryptographic concept, with no concrete application at the time
6.3 2005: Git
Linus independently arrived at the same structure to solve the code collaboration problem
Git’s object model is exactly a Merkle Tree:
commit └── tree(root hash) ├── blob ├── blob └── tree └── blobThen parent pointers string multiple trees into a timeline
Git’s contribution was turning the Merkle Tree from a static data structure into a version history with a time dimension
6.4 2008: Bitcoin
Satoshi Nakamoto faced essentially the same problem as Linus:
Without a central authority like a bank, how do you get people around the world to agree on who owns how much money?
His solution borrowed the same idea:
Each block contains:
block { transactions_root: the Merkle Tree root hash of all transactions prev_block_hash: the hash of the previous block nonce: proof of work}prev_block_hash is Git’s parent, stringing blocks into a chain
Change any historical block and the hashes of all subsequent blocks change, and the whole network notices immediately
Bitcoin’s contribution was adding proof of work (i.e. mining) on top of the hash chain, solving the question of who has the right to write new blocks
Git doesn’t need to solve that problem, because Linus himself is the final authority (lol)
6.5 2014: Ethereum’s MPT
The state Ethereum needs to store is far more complex than Bitcoin’s — not just transaction records, but every account’s balance, the state of smart contracts, code…
A plain Merkle Tree isn’t efficient enough for lookups, so Ethereum combined it with a Patricia Trie (compressed prefix tree) and invented the Merkle Patricia Trie
MPT = Merkle Tree(tamper-proof) + Patricia Trie(efficient lookup)The Patricia Trie addresses by path; for example, the account address 0x1a2b3c... is a path on the tree, and following it finds that account’s state
Each Ethereum block has three MPTs:
state_root ← global account statetransactions_root ← all transactions in this blockreceipts_root ← the execution results of all transactionsCompared with Git:
Git's tree object ≈ an MPT's intermediate nodes (path-addressed)Git's blob object ≈ an MPT's leaf nodes (the actual data)Git's commit ≈ an Ethereum block (state snapshot + pointer to the previous)6.6 2015: IPFS
The problem IPFS wants to solve is:
HTTP is location-addressed: if the server dies, the data disappears. Can we make data content-addressed, so that as long as someone stores it, anyone can retrieve it?
Each IPFS file is split into blocks, each block hashed, and the directory structure is also organised as a Merkle DAG (directed acyclic graph):
directory hash ├── hash of file A │ ├── hash of block 1 │ └── hash of block 2 └── hash of file BYou request a file by giving the root hash, and IPFS fetches it back from any node on the network that stores the data, verifying integrity with the hash
Compared with Git it’s almost one-to-one:
Git blob ≈ IPFS data blockGit tree ≈ IPFS directory nodeGit remote ≈ IPFS network nodeIPFS’s author said outright: IPFS is just Git’s object model extended to the entire internet
Their core ideas are exactly the same; the only difference is:
Each had different additional problems to solve, which led to different extensions on top of the hash chain
Finally let’s go back to 2005, when Linus wrote Git in ten days. He didn’t foresee blockchains, didn’t foresee IPFS
He had just thought one question through clearly:
Trust shouldn’t depend on authority; it should depend on mathematics
That one insight, over the following twenty years, was repeatedly rediscovered by people in different fields, and grew into completely different things
