Git, in plain words.

For the dev who knows the vocabulary but has never built the mental model. We'll go slow. No walls of text — just diagrams and clear analogies.

Concept 01 — Repository

A repo is just a folder with hidden memory.

That's the whole secret. A folder on your Mac becomes a repo (short for repository) the moment it has a hidden .git/ subfolder inside it. That hidden folder is the project's memory — every save, every branch, every author of every change. Remove .git/ and the folder becomes just files again.

~/lab/projects/duck-hunt/ 📄 README.md 📄 index.html 📁 src/ 📁 .git/ the project's memory every commit ever made every branch, every author
Folder + .git/ = repo. No .git/, no repo.
Try this right now

Look at your own folders. ls -la ~/lab/projects/playground/ shows no .git/ — so playground is not a repo yet. But ls -la ~/lab/projects/migration/ shows .git/ right there — that one IS a repo. Same folders, different status. That hidden directory is the entire difference.

Concept 02 — Commit

A commit is a labeled snapshot.

Every time you save your work with git commit -m "...", git takes a snapshot of every file in the repo at that moment and stamps it with a label. Each snapshot has a message (the words you wrote), an author (you, by email), a parent (the snapshot before it), and a unique hash — a 40-character ID like 8d4d8650abc....

The repo's history is just a chain of these snapshots. Newest one is called HEAD.

8d4d865 first commit jonah · today "start of duck-hunt" a7f2391 add scoreboard jonah · today "track hits + misses" f1c0d2e fix duck flight jonah · today "ducks no longer escape" HEAD
Each commit points back to its parent. HEAD = current position.
Concept 03 — Branch

A branch is a movable sticky note, not a copy.

This is the one that trips everyone. A branch is not a folder. Not a copy of files. It's literally just a sticky note pointing at a commit. The note name is the branch name. When you commit, the note moves forward to the new commit automatically.

Make a new branch (git checkout -b dev) and you've just stuck a second note on the same commit. From here, main and dev can move independently. That's how you get "parallel versions" of your project: each branch is a different lineage of commits with its own sticky note.

c1 c2 c3 d1 c4 d2 main dev started here branch point main HEAD dev HEAD
Two branches, same starting commit, then they diverged.
Why this matters for deploying

Cloudflare Pages and Coolify both watch a specific branch. "When the main sticky note moves, redeploy the live site." Other branches can move freely without affecting production. That's where the whole dev/staging/prod pattern comes from — and we'll dive in fully in Lesson 4.

Concept 04 — Push & pull

Push and pull sync your hidden memory with GitHub's.

GitHub is just a server that holds a copy of your .git/. When you git push, you send your local commits up. When you git pull, you bring GitHub's commits down. That's the whole loop.

A push physically travels: your Mac → over HTTPS or SSH → GitHub's servers → GitHub's storage. GitHub then sends webhooks to anything watching the repo (Cloudflare Pages, Coolify) saying "new commits on main, come get them." Those services pull, build, and deploy — automatically.

Your Mac .git/ (local) ~/lab/projects/... git push GitHub .git/ (origin) lovebuilt/duck-hunt webhook CF Pages deploys duck-hunt.pages.dev git pull
Push goes up. Webhooks fire automatically. Deploy is the side-effect.
# the actual three commands you'll run 95% of the time git add . # stage changes git commit -m "add scoreboard" # label the snapshot git push # send to GitHub → triggers deploy
Concept 05 — .gitignore

What stays out of the repo entirely.

A .gitignore is a plain text file at the root of your repo listing patterns of files git should never track. It's how you keep secrets out, build artifacts out, OS junk out, and giant generated folders out. Without it, your repo bloats and your .env ends up on GitHub.

all your files 📄 README.md 📄 index.html 🔑 .env 📁 node_modules/ 📄 .DS_Store 📁 src/ .gitignore node_modules .env .DS_Store *.log tracked by git 📄 README.md 📄 index.html 📁 src/
.gitignore is the filter. Secrets & junk bounce off. Real files pass through.
A good .gitignore for a Node project

node_modules/
.env
.env.local
.DS_Store
*.log
dist/
.cache/

Common worry — answered

"Should I .gitignore my CLAUDE.md and .claude/ folder?" No. They're useful context, and if your repo is private no one external sees them. If the repo is public, they're still fine — many indie devs ship their .claude/ setup publicly so others can reuse it. The only files you must rigorously keep out are secrets: .env, .mcp.json, anything with API keys or tokens.

Concept 06 — Submodules

Submodules: a footgun you'll want to skip.

A submodule is a way to embed one repo inside another. Sounds clean — you'd think: "I'll put each game in its own repo, and have playground link to them as submodules."

Don't. Submodules are notorious — they don't auto-update, clones need an extra --init flag, push order matters, and collaborators trip over them constantly. The whole modern web ecosystem has migrated away from them. There are three better answers for your situation, depending on what you want:

Option A — Monorepo (recommended for you)

One repo. All sub-experiments live inside. Push once, deploy once. Lowest cost. We'll dig into this in Lesson 3.

Option B — Iframe embed

Each sub-game lives in its own repo + its own deploy URL. Playground's portal page just embeds them via <iframe src="...">. Clean isolation, but harder to share state.

Option C — Build-time copy

Each sub-game in its own repo. A build script downloads their latest release into playground/games/ before deploy. More machinery, more flexibility. Usually overkill until you have collaborators.

Concept 07 — Clone

How Josh sees your CLAUDE.md when he clones.

When Josh runs gh repo clone lovebuilt/playground on his Mac, GitHub sends him a fresh copy of the entire .git/ folder. Git then materializes every file from the latest commit on main into his working directory. Everything not in .gitignore shows up.

That includes CLAUDE.md, the .claude/skills/ folder, your plans/, your AGENTS.md — all of it. Josh opens Claude Code in the cloned folder, and his Claude sees the same context yours does.

GitHub: lovebuilt/playground 📄 README.md 📄 server.js 📄 CLAUDE.md 📁 .claude/skills/ 📁 plans/ 📁 src/ gh repo clone Josh's Mac 📄 README.md 📄 server.js 📄 CLAUDE.md ✓ 📁 .claude/skills/ ✓ 📁 plans/ ✓ 📁 src/
Clone = "give me a copy of everything not in .gitignore."
Concept 08 — The gh CLI

You're already signed in.

gh is GitHub's own command-line tool. We verified you're authed as lovebuilt already. Most repo operations don't need the GitHub website at all once you know these:

# the gh commands you'll actually use gh repo create lovebuilt/duck-hunt --public --source=. --remote=origin --push # creates repo on GitHub AND pushes your local code in one shot gh repo list # your 7 existing repos gh repo view --web # open this repo in browser gh repo clone lovebuilt/playground # clone any of yours gh pr create # open a pull request
✦ Lesson 1 recap

You now have the model for…