Search Tech Journey

Find topics, journeys and posts

6-month learning plan2 / 130
back to blog
systemsbeginner 50m read

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.

🧰SetupM00 · Setup & Tools· Session 002 of 130 90 min

🎯 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.

You will be able to
  • 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



(a) Intuition · 5 min

What Git actually is
🌍 Real world

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.

💻 Code world

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

If these three don't feel solid, nothing else in Git will
  • 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

  1. 1972
    SCCS at Bell Labs
    First real version control — stored diffs, one file at a time.
  2. 2000
    Subversion (SVN)
    Centralised: one server holds truth, everyone else has a checkout. Better than CVS, still slow.
  3. 2005
    Git · Linus Torvalds
    Written in a weekend after BitKeeper revoked the Linux kernel's free license. Distributed by design.
  4. 2008
    GitHub launches
    Puts a friendly web UI on top of Git, invents the pull request. Becomes the de-facto home of open source.
  5. 2018
    Microsoft 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 reflog still remembers it for 90 days.

The lifecycle of a change

1working tree
Edit files

Change code in your editor. `git status` shows them as ‘modified’ (red).

2stage
git add <files>

Move the changes you want in this commit into the staging area. `git status` shows them as ‘to be committed’ (green).

3commit
git commit -m "…"

Freeze the staging area into a new commit on the current branch. A SHA-1 is minted; history grows.

4publish
git push origin <branch>

Upload local commits to the remote (GitHub). Others can now see them.

5collaborate
Open a Pull Request

Ask maintainers to merge your branch into `main`. Reviewers comment, CI runs, you iterate.

6ship
Merge + delete branch

PR gets approved and merged. Delete the branch locally + on the remote. Repeat.

Branch strategies you'll see in the wild

Trunk-based

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
GitHub Flow

Most open source, small teams

  • main is always deployable
  • Branch → PR → merge → deploy
  • No release branches
  • Simple, forgiving
  • Where you should start
Git Flow

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


Common misconception
✗ What most people think

"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."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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 unchanged
From first principles
Start with the question

Why 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?

  1. 1
    Git 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
  2. 2
    A 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
  3. 3
    A 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
  4. 4
    Therefore 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

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.

Mental modelDAG of snapshots, branches are sticky notes

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.
  • reset moves a sticky note. checkout/switch moves HEAD. merge adds a node with two parents. rebase copies 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.
🔔 Fires when you see

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".

The tradeoff

Your feature branch has fallen behind main. Do you merge main into it, or rebase onto it?

Merge main in
+ you gain never rewrites history, so it is safe on shared branches; conflicts are resolved exactly once, in one commit; the true chronological record of what happened is preserved and git bisect still works over real states
− you pay history becomes a braid rather than a line; git log interleaves unrelated work; on long-lived branches you accumulate merge commits that carry no information about the feature
pick when the branch is shared with anyone else, or it is long-lived, or you genuinely care about the forensic record of when integration happened
Rebase onto main
+ you gain linear, readable history; each commit is a clean self-contained change reviewable in isolation; bisect over a straight line is easy to reason about
− you pay every commit gets a new SHA, so anyone else on the branch is broken; conflicts can recur once per replayed commit; the replayed commits were never tested in the state they now claim to be in
pick when the branch is yours alone and unpushed (or push is --force-with-lease and the team agreed), and you want a reviewable stack of commits
Squash on merge
+ you gain one commit per unit of review, so main's history matches the PR list exactly; no need to police intermediate commit hygiene during development
− you pay the internal steps are lost forever, which hurts when you later bisect inside a large change or need to revert only part of it
pick when the team treats a PR as the atomic unit of change and PRs are kept small — the common default in large orgs for good reason
What a senior engineer actually does

The 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.

#!/usr/bin/env bash# git-workshop.sh a 5-minute tour of the entire Git lifecycle.# Idempotent-ish: deletes the workshop folder and starts fresh each run.set -euo pipefail WORK_DIR="$HOME/projects/learning/s002-git-workshop"GH_USER="${GH_USER:-}" # export GH_USER=your-github-handle to also push log() { printf "\033[1;36m %s\033[0m\n" "$*"; } log "1/9 Fresh workspace at $WORK_DIR"rm -rf "$WORK_DIR" && mkdir

What each block does

Anatomy of the script

Line 15 · git config --global pull.rebase true
Makes `git pull` rebase your local commits on top of upstream instead of creating merge commits. History stays linear and readable — the modern default.
config
Line 24 · git init -q
Creates the `.git/` directory — the entire repo lives there. Delete `.git/` and your project is no longer versioned. Nothing else on disk is Git-owned.
init
Line 33 · git switch -c feature/hello
Modern replacement for `git checkout -b`. Creates AND switches to a new branch. Use `switch` for branches, `restore` for files — `checkout` overloads both and is confusing.
branch
Line 44 · git commit --amend --no-edit
Replaces the previous commit with a new one (new SHA) that includes the staged changes. `--no-edit` keeps the old message. NEVER amend a commit you've already pushed to a shared branch.
history
Line 51 · git log --graph --all --decorate
The single most useful log invocation. `--graph` draws the DAG, `--all` shows every branch, `--decorate` labels branches and tags. Alias it to `git lg` in your dotfiles.
inspect
Line 57 · git merge --no-ff
Forces a merge commit even when a fast-forward is possible. Keeps the ‘this was a feature branch’ information in history. Some teams prefer squash-merge instead — both are valid.
merge
Line 65 · git reflog
A private log of every move HEAD has made in the last 90 days. Even ‘destroyed’ commits are here. If you've panicked, run this before anything else.
safety-net
Try itFeel the difference between merge and rebase in 60 seconds

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 --decorate
💡 Hint · After rebasing, `git log --graph` shows a linear history with no merge commit — as if you'd written those commits on top of main all along.

The commit message rules that reviewers judge you by

Chris Beams' seven rules — every senior engineer follows these
  • 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

War story GitLab· 20176 hours of data loss for 300 GB of production DB
🔥 What broke

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.

🧯 The fix
The one backup that saved them was a 6-hour-old snapshot on a staging server, taken for a demo. They live-streamed the recovery on YouTube for six hours (transparency ftw). Data older than 6 hours was recoverable; 6 hours of comments and issues were permanently lost.
🎓 Lesson to steal
Backups you have never restored are not backups. Git's 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.
Post-mortem
War story A Series-A startup · 2021Two days of team output lost
🔥 What broke

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).

🧯 The fix

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.

🎓 Lesson to steal
Never --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.
War story Common failure mode · every teamuniversal
🔥 What broke
A dev commits .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.
🧯 The fix

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.

🎓 Lesson to steal
Once a secret hits a public repo, treat it as compromised forever. Even in a private repo, rotate — CI logs, forks, and third-party integrations may have already exfiltrated it.

Where this shows up in the rest of the plan

Git is the backbone of everything you'll build in the next six months
S003 · Command line
Every Git command is a shell command. The pipes, aliases, and grep-fu you learn next apply directly.
S004 · Reading docs
Uses `git help <cmd>` and `man git-<cmd>` — the largest official manual you'll grep through.
S055 · CI/CD basics
GitHub Actions triggers on every push and PR — the flow you built here is the input to CI.
S056 · Docker
Dockerfiles live in Git; image tags often use commit SHAs.
S099 · Code review
Pull requests are where 90% of team communication happens. This session was the mechanics; that one is the craft.
S120 · System design interviews
‘Design GitHub’ is a real interview question. Understanding the DAG makes the answer obvious.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three without notes, redo the session:

  1. What is a commit? (one sentence, no analogies)
  2. What does the staging area do? (one concrete example)
  3. 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.