S002 · Git & GitHub — Commits, Branches, PRs
The 90 minutes that turns Git from a scary black box into a save-point machine you trust with your career. Real commits, real branches, real PRs — the muscle memory every senior engineer runs on.
🎯 Use Git as a save-point machine you trust: initialise a repo, branch, commit, push, and open your first pull request without googling any command.
Why this session exists
Git is the second most important tool a working engineer touches, after the editor. It is also the tool that most beginners fake their way through — copy-pasting git add . && git commit -m "stuff" for a year without ever understanding what a commit is. That gap catches up with you the first time you get a merge conflict, or the first time you have to explain to a reviewer why your PR has 47 unrelated files in it. This session closes that gap: a real mental model of commits, branches, and the remote — plus the muscle memory to ship your first pull request.
- Explain in one sentence what a commit, a branch, and a remote actually are — with no analogies that break under pressure.
- Initialise a repo, stage, commit, branch, push, and open a pull request on GitHub — end to end, without looking anything up.
- Read `git log --oneline --graph --all` and explain what each line means to a coworker.
- Recover from the top-5 Git mistakes (wrong branch, unstaged changes, force-pushed over a coworker) using `git reflog` and `git restore`.
- Write a commit message that a stranger reading it in three years still understands.
Prerequisites
- S001 — Dev Environment — you need a working shell,
gitinstalled, and a folder under~/projects/….
(a) Intuition · 5 min
Imagine writing a novel and hitting Save As after every paragraph — novel_v1.doc, novel_v2.doc, novel_final.doc, novel_final_FINAL.doc. Now imagine you can also branch: novel_final_but_what_if_the_dog_dies.doc. And undo any save from six months ago in one command. And email the whole history to a friend so they can add their own chapters without touching yours.
That is Git. It's a save-point machine with branching and multi-player mode, invented by the guy who wrote Linux.
More precisely: Git is a content-addressable filesystem plus a directed acyclic graph (DAG) of commits. Every file's contents are hashed to a SHA-1, and every commit is a snapshot pointing at (a) its parent commit and (b) a tree of file hashes.
That is the whole trick. Branches are movable labels on the DAG. Merging is walking two paths back to their common ancestor. Nothing about Git is magic once you see the graph.
The three concepts that unlock everything
- A commit is a snapshot of your entire tree at a point in time, not a diff. It has a SHA-1 hash, a parent (or two, for merges), and a message.
- A branch is a movable pointer to a commit. `main` and `feature/login` are just names for two different commits in the same DAG.
- The staging area (aka ‘index’) is a draft of your next commit. `git add` moves changes into it; `git commit` freezes it into history.
A quick history so you know why the world looks like this
- 1972SCCS at Bell LabsFirst real version control — stored diffs, one file at a time.
- 2000Subversion (SVN)Centralised: one server holds truth, everyone else has a checkout. Better than CVS, still slow.
- 2005Git · Linus TorvaldsWritten in a weekend after BitKeeper revoked the Linux kernel's free license. Distributed by design.
- 2008GitHub launchesPuts a friendly web UI on top of Git, invents the pull request. Becomes the de-facto home of open source.
- 2018Microsoft acquires GitHub$7.5B. The tool their engineers once mocked (Team Foundation Server) becomes the standard everywhere.
(b) Visual walkthrough · 15 min
The three areas Git shuffles files between
Read that diagram until you can draw it from memory. Every Git command you'll ever run moves data along one of those arrows.
The commit graph — what git log --graph is actually showing you
- Each dot is a commit (a snapshot).
- Each arrow points from a child commit to its parent(s) — a merge commit has two.
- main and feature/login are branches — just labels attached to a specific dot.
- Nothing is ever deleted; if you "lose" a commit,
git reflogstill remembers it for 90 days.
The lifecycle of a change
Change code in your editor. `git status` shows them as ‘modified’ (red).
Move the changes you want in this commit into the staging area. `git status` shows them as ‘to be committed’ (green).
Freeze the staging area into a new commit on the current branch. A SHA-1 is minted; history grows.
Upload local commits to the remote (GitHub). Others can now see them.
Ask maintainers to merge your branch into `main`. Reviewers comment, CI runs, you iterate.
PR gets approved and merged. Delete the branch locally + on the remote. Repeat.
Branch strategies you'll see in the wild
Google, Facebook, most SaaS
- One long-lived branch: main
- Feature branches live \<1 day
- Feature flags gate incomplete work
- CI must be green on main, always
- Best for teams >5 people
Most open source, small teams
- main is always deployable
- Branch → PR → merge → deploy
- No release branches
- Simple, forgiving
- Where you should start
Legacy enterprise, some Java shops
- main + develop + release/* + hotfix/*
- Heavy ceremony, many long-lived branches
- Made sense in 2010 for shrink-wrapped software
- Mostly considered obsolete for SaaS
- Learn it only if a job requires it
The mental model to hold
"A Git commit is a diff. The repo is a stack of patches, and git log shows me the patches in the order they were applied."
A commit is a full snapshot of the tree, addressed by the SHA-1/SHA-256 of its content, plus pointers to its parent(s). Diffs are computed on demand for display. Nothing in the object database stores a patch.
Because the diff view is the only view you ever see, and because tools that came before Git (SVN, patch queues) really were patch-based. The illusion holds until you do something that only makes sense for snapshots: git checkout <sha> restoring a whole tree instantly, or two branches sharing identical blobs with zero extra storage, or a cherry-pick producing a different SHA for the "same" change. That last one confuses everyone until you accept that the SHA covers the tree plus the parent plus the author and timestamp — change any of those and it is a different commit object, even if the diff is byte-identical.
Look at the object store directly — there is no patch in there:
git cat-file -p HEAD # tree, parent, author, message. No diff.
git cat-file -p HEAD^{tree} # full listing of every file at that commit
# Same content in two commits = same blob hash, stored once:
git hash-object -w file.txt
git rev-parse HEAD:file.txt HEAD~1:file.txt # identical if unchangedWhy can Git guarantee your history has not been tampered with, without a server, a signature, or a trusted authority? And why does rewriting one old commit change every commit after it?
- 1Git names every object by the cryptographic hash of its own content — a blob's name is the hash of the file bytes.forced by · content addressing gives free deduplication and a free integrity check in one mechanism
- 2A tree object lists filenames together with the hashes of their blobs and subtrees. So the tree's own hash depends on every byte of every file beneath it.forced by · the tree's content is those hashes, and its name is the hash of its content
- 3A commit object contains the tree hash, the parent commit hash(es), author, and message. So the commit hash depends on the entire snapshot and on the whole ancestry.forced by · the parent hash is part of the commit's content, recursively covering all history
- 4Therefore a single commit hash transitively commits to every file and every ancestor commit that ever led to it. Changing one character in a 3-year-old file changes that blob's hash, its tree's hash, its commit's hash, and every descendant hash.forced by · hash functions are collision-resistant, so you cannot alter content and keep the name
Therefore Git is a Merkle DAG, and "the history is intact" reduces to "the tip hash is the one I expected". This is the same structure that secures blockchains and content-addressed storage generally — Git just got there first for source code.
And note what this predicts: rebase, amend, and filter-branch cannot possibly preserve SHAs, so they must produce new commits and orphan the old ones — which is exactly why rewriting shared history forces everyone else to --force-recover. It also predicts that the orphaned originals still exist in the object store until GC, which is why git reflog can save you after almost any "I destroyed my work" moment.
Picture an immutable graph of snapshots in a warehouse — each node a complete photo of the project, each with an arrow to the photo it came from. Nothing in that graph can ever be edited; you can only add new nodes.
Now picture branch names as sticky notes placed on nodes. main, feature/x, and HEAD are all just 41-byte files containing a hash. Every command you fear — merge, rebase, reset, checkout — is either "add nodes" or "move a sticky note". That's the whole system.
- Commits are immutable and permanent until garbage-collected. You never lose a commit; you lose the name pointing at it — recover it with
git reflog. resetmoves a sticky note.checkout/switchmovesHEAD.mergeadds a node with two parents.rebasecopies nodes onto a new base — new SHAs, always.- The three areas — working tree, index (staging), HEAD — are three different trees. Almost every confusing Git message is telling you which pair disagrees.
- Local operations are safe and reversible; only
push(especially--force) makes changes other people must deal with.
Fire this model the moment you see: "detached HEAD" · a rebase conflict that reappears commit after commit · git pull creating a surprise merge commit · a force-push argument in code review · "I think I lost my work".
Your feature branch has fallen behind main. Do you merge main into it, or rebase onto it?
git bisect still works over real statesgit log interleaves unrelated work; on long-lived branches you accumulate merge commits that carry no information about the feature--force-with-lease and the team agreed), and you want a reviewable stack of commitsmain's history matches the PR list exactly; no need to police intermediate commit hygiene during developmentThe operational rule that actually matters: rebase private history, merge public history. Rewriting commits nobody has pulled is free; rewriting commits others have pulled transfers your cleanup cost onto every teammate.
Most mature teams land on rebase-locally + squash-on-merge: developers keep a tidy private stack, and main stays a linear sequence of reviewed units. Whichever you pick, pick one and encode it in branch protection rather than in tribal knowledge — mixed conventions are what produce the unreadable histories people blame Git for.
(c) Hands-on · 25 min
Run this end-to-end. It creates a real repo, branches, commits, and (if you set GH_USER) pushes to a fresh GitHub repo you can open in your browser. Save as git-workshop.sh, chmod +x, run.
What each block does
Anatomy of the script
From inside the workshop repo, add a new feature branch and try rebase instead of merge:
git switch main
git switch -c feature/bye
echo "def bye(name): return f'bye, {name}!'" >> hello.py
git add hello.py && git commit -m "feat: add bye()"
# meanwhile main gets a change
git switch main
echo "# updated" >> README.md
git add README.md && git commit -m "docs: update readme"
# rebase feature/bye on top of the new main
git switch feature/bye
git rebase main
git log --oneline --graph --all --decorateThe commit message rules that reviewers judge you by
- Subject line ≤ 50 characters. Imperative mood: ‘add login’, not ‘added login’ or ‘adds login’.
- Capitalise the subject. No trailing period.
- Blank line between subject and body.
- Body wrapped at 72 chars. Explain WHY and WHAT, not HOW (the diff shows how).
- Use conventional prefixes: feat / fix / docs / chore / refactor / test — makes changelogs trivial.
- Reference issues in the body: ‘Fixes #123’ closes the issue automatically on merge.
- A stranger reading your commit in three years must still understand it. Write for them.
(d) Production reality · 15 min
A tired SRE, at 11 pm, ran rm -rf on what he thought was the secondary database directory. It was the primary. He noticed his mistake in about two seconds. By then 300 GB was gone.
The postmortem later found that five different backup mechanisms were all broken or empty — nobody had ever tested a restore.
reflog is a local safety net, not a backup — push often, and treat the remote as your real backup. Also: rm -rf at 11 pm is a Chesterton's fence.A senior engineer ran git push --force from a stale local main. It overwrote the remote main and erased 47 commits from four other engineers who had pushed while he was on holiday.
Nobody noticed for six hours because CI was still green (his old code compiled fine).
Recovery: every dev still had their own local commits, so they cherry-picked them back onto a new main. Took two days of coordination for a 30-second mistake.
Permanent fix: enable GitHub's branch protection on main — no force pushes, no direct pushes, PR + review required.
--force on a shared branch. Use --force-with-lease — it aborts if the remote has commits you haven't seen. And turn on branch protection on main the day you create the repo, not after the incident..env containing an AWS access key. Pushes to a public repo. Within 4 minutes GitHub's secret-scanning webhook alerts AWS; within 12 minutes a bot has spun up 200 EC2 instances mining crypto on the dev's credit card.Rotate the key immediately — deleting the commit is not enough, the key is already scraped. Then rewrite history with git filter-repo (or the newer bfg).
Prevention: add .env, *.pem, *.key to .gitignore on day one. Enable GitHub's ‘push protection’ setting. Use git-secrets or gitleaks as a pre-commit hook.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
If you can't teach these three without notes, redo the session:
- What is a commit? (one sentence, no analogies)
- What does the staging area do? (one concrete example)
- How do you recover from a bad
git reset --hard? (name the command)
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.