Cloud Explainers
Home › Git

How Git Works

Git10 min videoPublished Nov 3, 2026

How Git works, for beginners: we open the hidden .git folder of a real repository and read every file Git writes. git add saves each file's content as a blob named by its SHA-1 hash, and lists it in the index (the staging area); git commit saves a tree (a snapshot of the folder) and a commit that points to it and to its parent; a branch is a 41-byte file holding a commit's ID, and HEAD names the branch you're on; a push sends only the objects the server is missing, then moves the server's branch.

How Git Works, the full picture: diagram of Working folder, Staging area, Objects, HEAD, Branches, git-server, The.git folder
The whole picture, from the video's recap.

Opening

Git commands like commit, branch and push can feel like magic, until you see the files Git really writes. Here's Ana's tea shop website: two pages, and no Git yet. She runs git init, and Git makes one hidden folder, called.git.

Ana's laptop
ls -a.  ..  about.html  menu.html
Ana's laptop
git initInitialized empty Git repository in /home/ana/shop/.git/
Ana's laptop
ls .gitHEAD  branches  config  description  hooks  info  objects  refs

By the end, you'll open that folder and read exactly what a commit, a branch and a push are. Three parts of it do the real work. The objects hold every version of every file Ana adds. The branches are tiny files that give names to her commits. And the staging area is where the next commit is put together.

Blobs: content gets a name

Right now, the objects folder is empty: zero files. Ana runs git add on both pages, and two files appear in it. Each one has a strange name: forty letters and digits.

Ana's laptop
find .git/objects -type f | wc -l0
Ana's laptop
git add menu.html about.html
Ana's laptop
find .git/objects -type f.git/objects/20/d4a29beb97cea29da9fc26087f865061fe21fc
.git/objects/18/7dce47b49de57a157d495a10fb9536f0a3ed18

Each of those files is a blob: Git's copy of one file's content. Git names it by running the content through a hash function called SHA-1, which turns any data into forty characters. The same content always gives the same name. Change a single character, and the name is completely different.

A blob is named after its content: diagram of menu.html, SHA-1, blob 20d4a29…, blob 2dc444b…

Here's one of those names. Git uses the first two characters as a folder name, and the other thirty-eight as the file name inside it, so no single folder ever holds too many files. Git calls this name the object's ID. The file itself is compressed, so we read objects with Git's own tools.

The name of the menu's blob: diagram of 20d4a29beb97cea29da9fc26087f865061fe21fc, 20 = folder, d4a29beb97cea29da9fc26087f865061fe21fc = file name, The object's ID: 40 characters from SHA-1

We can check that. git hash-object computes the name for any file: the same ID. cat-file dash t asks Git for an object's type: blob. And dash p prints it: the menu, exactly as Ana saved it.

Ana's laptop
git hash-object menu.html20d4a29beb97cea29da9fc26087f865061fe21fc
Ana's laptop
git cat-file -t 20d4a29beb97cea29da9fc26087f865061fe21fcblob
Ana's laptop
git cat-file -p 20d4a29beb97cea29da9fc26087f865061fe21fc<h1>Tea menu</h1>
<p>Sencha: $4</p>
<p>Matcha: $5</p>
<p>Chai: $4</p>

And there's no secret in the name. Git writes the word blob, a space, the content's size in bytes, and a zero byte, then the content itself. The SHA-1 of all that is the ID.

What Git hashes for a blob: diagram of blob 70\0<h1>Tea menu</h1>…, blob = type, 70 = size, \0 = zero byte, <h1>Tea menu</h1>… = the content, SHA-1 of this = the blob's ID

The menu is 70 bytes. Build exactly that, and give it to sha1sum, a tool that knows nothing about Git: the same forty characters.

Ana's laptop
wc -c menu.html70 menu.html
Ana's laptop
(printf 'blob %d\0' $(wc -c < menu.html); cat menu.html) | sha1sum20d4a29beb97cea29da9fc26087f865061fe21fc  -

And the staging area? It's one file too:.git/index. git ls-files dash dash stage reads it: each file's name, next to the ID of the blob that holds its content.

Ana's laptop
git ls-files --stage100644 187dce47b49de57a157d495a10fb9536f0a3ed18 0	about.html
100644 20d4a29beb97cea29da9fc26087f865061fe21fc 0	menu.html

So git add does two things. It saves each file's content as a blob in the objects, and it writes each blob's ID into the index, next to the file's name. The index is the list of what the next commit will contain.

Trees and commits

Ana commits. Now there are four objects: the two blobs, and two new ones. cat-file dash p on HEAD, her newest commit, prints it as plain text: the ID of a tree, her name and the time, and her message.

Ana's laptop
git commit -m 'Add the menu and about pages'[main (root-commit) fdad73f] Add the menu and about pages
 2 files changed, 6 insertions(+)
 create mode 100644 about.html
 create mode 100644 menu.html
Ana's laptop
find .git/objects -type f.git/objects/20/d4a29beb97cea29da9fc26087f865061fe21fc
.git/objects/8b/42a788aaff669bb08c15d3f01add3db9e5009d
.git/objects/fd/ad73f30574dcafcfe4d7558090cdf4f0572616
.git/objects/18/7dce47b49de57a157d495a10fb9536f0a3ed18
Ana's laptop
git cat-file -p HEADtree 8b42a788aaff669bb08c15d3f01add3db9e5009d
author Ana <ana@example.com> 1793724350 +0000
committer Ana <ana@example.com> 1793724350 +0000

Add the menu and about pages

And the tree? HEAD, a caret, and tree in braces asks for that commit's tree. It's a list: each file's name, and the blob that holds it. It's the index, saved as an object.

Ana's laptop
git cat-file -p HEAD^{tree}100644 blob 187dce47b49de57a157d495a10fb9536f0a3ed18	about.html
100644 blob 20d4a29beb97cea29da9fc26087f865061fe21fc	menu.html

That's the whole model. A blob holds one file's content. A tree holds a folder: names that point to blobs, and to other trees for folders inside it. A commit points to one tree, a snapshot of the whole project, plus who made it, when, and why.

Ana's first commit, as objects: diagram of commit fdad73f, tree 8b42a78, blob 20d4a29, blob 187dce4, Objects in.git/objects

Next, Ana raises matcha to six dollars, and commits again: dash a adds changed files for her, so she can skip git add. This commit has one new line: parent, the ID of her first commit. Its tree still lists both files, but only the menu has a new blob.

Ana's laptop
git commit -am 'Raise matcha to $6'[main 026551b] Raise matcha to $6
 1 file changed, 1 insertion(+), 1 deletion(-)
Ana's laptop
git cat-file -p HEADtree aed56ce7ed56077beef83b782f15ce4f48a7daac
parent fdad73f30574dcafcfe4d7558090cdf4f0572616
author Ana <ana@example.com> 1793724350 +0000
committer Ana <ana@example.com> 1793724350 +0000

Raise matcha to $6
Ana's laptop
git cat-file -p HEAD^{tree}100644 blob 187dce47b49de57a157d495a10fb9536f0a3ed18	about.html
100644 blob 2dc444b61f069205d4c365a0cfbd15920aa7b831	menu.html

Every commit is a full snapshot, not a list of changes. But a file that didn't change keeps its blob, so both trees point to the same about page, stored once. The second commit added just three objects: a blob, a tree and a commit. Seven in all. And git log just follows the parent links, from the newest commit back.

Two commits, seven objects: diagram of commit 026551b, commit fdad73f, tree aed56ce, tree 8b42a78, blob 2dc444b, blob 187dce4, blob 20d4a29

There's one more thing hidden in those IDs. A commit's ID is the SHA-1 of its text, and that text includes its tree and its parent. So if anyone changed one old file, its blob, its tree and its commit would all get new IDs, and so would every commit after it. History can't be quietly edited: a changed past shows up as different IDs.

Each ID depends on everything before it: diagram of commit fdad73f, commit 026551b, commit 25854cb, Ana's history

Branches

So what's a branch? Look at.git/HEAD: ref, refs/heads/main. That file holds one thing: the full ID of Ana's newest commit. Now she makes a branch, spring-menu: a new file, with the same ID, 41 bytes long.

Ana's laptop
cat .git/HEADref: refs/heads/main
Ana's laptop
cat .git/refs/heads/main026551ba8b2dcb87891f41100acffcd41b5f80af
Ana's laptop
git branch spring-menu
Ana's laptop
cat .git/refs/heads/spring-menu026551ba8b2dcb87891f41100acffcd41b5f80af
Ana's laptop
wc -c .git/refs/heads/spring-menu41 .git/refs/heads/spring-menu

That's all a branch is: a name, in a small file that holds a commit's ID: forty characters and a line break. Creating one writes those forty characters, however big the project is. And HEAD is how Git knows which branch you're on: it names a branch, and the branch names a commit.

A branch is a name for a commit: diagram of HEAD, main, spring-menu, commit 026551b, .git/refs/heads

git switch rewrites HEAD to point at the new branch. Ana adds cherry blossom tea, and commits. Now the branch files differ: main holds the old ID, spring-menu the new commit's.

Ana's laptop
git switch spring-menuSwitched to branch 'spring-menu'
Ana's laptop
cat .git/HEADref: refs/heads/spring-menu
Ana's laptop
git commit -am 'Add cherry blossom tea'[spring-menu 25854cb] Add cherry blossom tea
 1 file changed, 1 insertion(+)
Ana's laptop
cat .git/refs/heads/main .git/refs/heads/spring-menu026551ba8b2dcb87891f41100acffcd41b5f80af
25854cbbbf74cc15c402948e1f268fe8e904a9a8

So a commit does two things: it writes new objects, then moves the branch that HEAD names to the new commit. Nothing else changes. main stays where it was, on the matcha commit. And when you switch to a branch that holds a different commit, Git also rewrites your working files from that commit's tree, so the folder matches it.

A commit moves the current branch: diagram of main, commit 026551b, commit 25854cb, HEAD, spring-menu

Back on main, Ana merges spring-menu. Fast-forward: main had nothing new of its own, so Git didn't build anything. It just wrote spring-menu's commit ID into main's file.

Ana's laptop
git switch mainSwitched to branch 'main'
Ana's laptop
git merge spring-menuUpdating 026551b..25854cb
Fast-forward
 menu.html | 1 +
 1 file changed, 1 insertion(+)
Ana's laptop
cat .git/refs/heads/main25854cbbbf74cc15c402948e1f268fe8e904a9a8

Here's that merge as a picture. main pointed at the matcha commit, and the cherry blossom commit already has it as its parent. So Git only had to move main forward. If both branches had new commits of their own, Git would write a merge commit instead: a commit with two parents.

A fast-forward only moves a name: diagram of commit 026551b, commit 25854cb, main, spring-menu

Push

So far, all of this lives in Ana's.git folder. If her laptop dies, it's gone. Our lab has a Git server, standing in for GitHub. A push copies two things to it: the objects it doesn't have yet, and the new value of a branch.

She adds the server under the usual name, origin, and pushes main. The server has nothing yet, so Git writes all 10 objects: four blobs, three trees and three commits.

Ana's laptop
git remote add origin git@git-server:shop.git
Ana's laptop
git push -u origin mainEnumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 10 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 908 bytes | 908.00 KiB/s, done.
Total 10 (delta 1), reused 0 (delta 0), pack-reused 0
To git-server:shop.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Ana adds oolong tea, commits, and pushes again. This time: 3 objects. One blob, one tree, one commit: only what's new. Delta one: Git sent one of them as just its difference from an object the server already had.

Ana's laptop
git commit -am 'Add oolong tea'[main 0d4b325] Add oolong tea
 1 file changed, 1 insertion(+)
Ana's laptop
git pushEnumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 10 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 310 bytes | 310.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To git-server:shop.git
   25854cb..0d4b325  main -> main

On the server, the repository is a bare one: just the inside of a.git folder, with no working files. Its main branch file now holds Ana's newest commit ID, and cat-file says that object is a commit.

git-server
lsHEAD  branches  config  description  hooks  info  objects  refs
git-server
cat refs/heads/main0d4b325b80d6ce898e9f0331d3c10fe6462a72fd
git-server
git cat-file -t 0d4b325b80d6ce898e9f0331d3c10fe6462a72fdcommit

Here's that second push, step by step. The server tells Ana's Git which commit its main branch holds. Then Ana's Git asks it to move main to the new commit, and sends only the objects the server is missing, packed together. Once it has them, the server moves main, and confirms.

The second push: diagram of Ana's Git, git-server, my main is 25854cb, please move main to 0d4b325, 3 missing objects, packed, ok: main moved, Git sends only the objects the other side is missing

That's why a push is fast, even on a big project. The first push sent 10 objects, the second just 3. On a big project, Git also packs objects into one file, storing similar ones as differences. But every commit still reads back as a full snapshot.

Objects sent per push: chart of First push: everything: 10 objects, Second push: only what's new: 3 objects, Each push sends only the missing objects

And when a teammate, Ben, clones the project, the server sends him every object and every branch. His laptop gets the whole history, not just today's files. That's why every Git copy is a full backup, and why most commands work with no network at all.

A clone copies the whole repository: diagram of git-server, Ana's laptop, Ben's laptop

Recap

The everyday commands you type, add, commit, switch, merge and push, are built from the same few pieces. Under them sit basic commands that read or write one piece, like hash-object and cat-file. And at the bottom, files in.git: objects, small branch files, HEAD and the index.

Git, from the bottom up: diagram of Everyday commands, Basic commands, Files in.git

So here's how Git works. git add saves each file's content as a blob, named by its SHA-1, and lists it in the index. git commit saves a tree and a commit, and moves the current branch. A branch is a file holding one commit's ID, and HEAD names the branch you're on. And a push sends the missing objects, then moves the server's branch.

Next time a Git command surprises you, ask what it wrote into.git: which objects, and which branch file moved.

What each command writes into.git: cards of git add, git commit, git branch, git push

If opening the.git folder made Git feel less like magic, subscribing helps this small channel reach more beginners.

Every command and ID in this video came from a real run of Git 2.43.0, on our own lab machine. Thanks for watching Cloud Explainers.

Sources

Demos: real output from a small Git lab on our own machine: Ana's laptop and a Git server standing in for GitHub, in Docker containers. File edits were made in an editor off-screen; every Git command is shown as typed. Watch the full video on YouTube.

More videos

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index