GitHub
From "what's a repo" to shipping websites, automating your checks, and scripting GitHub like it's your own API — version control without the fear.
Git in 15 Minutes
Every project eventually hits the same wall: you've got config.txt, config.txt.bak, config-FINAL.txt, and config-FINAL-v2-really.txt, and no idea which one actually worked. Git is the tool that kills that workflow forever. It records the full history of your files so you can rewind, compare, and experiment without ever losing work.
The mental model that makes everything click: Git stores snapshots, not diffs. Every time you commit, Git takes a complete picture of your project at that moment and files it away with a message, an author, and a timestamp. Think of commits as save-game slots — each one captures the entire game state, and you can always reload an earlier slot. The "diffs" you see in tools are computed on demand by comparing two snapshots; they're a view, not the storage.
To make that work, Git keeps three areas. Your working directory is the files you're editing right now. The staging area (also called the index) is the loading dock: the exact set of changes you want in your next snapshot. And the repository — the hidden .git folder — is the vault where snapshots live permanently. The whole game is moving changes deliberately from working directory, to staging, to repo. That deliberate two-step is what lets you craft clean snapshots instead of committing whatever happened to be lying around.
Here it is, end to end:
$ git init my-notes
$ cd my-notes
$ echo "hello" > notes.txt
$ git add notes.txt # working dir -> staging
$ git commit -m "First note" # staging -> repo (one snapshot)
Two commands tell you what's going on at any moment. git status shows which files changed and which are staged — run it constantly, it's free. git log --oneline shows your snapshot history:
$ git log --oneline
9f2c1a4 First note
4b8d2e1 Add the todo list
That weird string is the commit's hash — its unique ID. You'll use the first few characters whenever you need to point at a specific snapshot, like checking out an old version to see what changed.
git commit -a -m "stuff" skips the staging area and snapshots every tracked file at once. Convenient, dangerous — it bundles unrelated changes into one mystery snapshot. Use git add deliberately until the habit is automatic.- Commits are full snapshots of your project, each with a unique hash — save-game slots, not diffs.
- Changes flow working directory → staging area (
git add) → repository (git commit). git statusandgit log --onelineare your two always-on instruments.
The full command reference lives at git-scm.com/docs — bookmark it; you'll outgrow tutorials but never this page.
Branches and the Collaboration Loop
Once you can snapshot, the next problem is obvious: how do you try something risky without endangering the version that works? Branches are the answer, and they're cheaper than you think. A branch is just a movable pointer to a commit — creating one doesn't copy any files, it just sticks a label on your current snapshot. The analogy that earns its keep: a branch is an alternate timeline that costs nothing to open and nothing to delete.
The loop looks like this. You're on main, the timeline everyone trusts. You open a branch for one unit of work:
$ git switch -c add-search
$ # ... edit files, commit a few times ...
$ git switch main
$ git merge add-search # fold the timeline back in
If the two timelines touched different files, the merge is automatic and boring — which is exactly what you want. If both changed the same lines, Git stops and asks you to decide. This is the merge conflict, and it's the part where beginners panic. Don't. Git marks the disputed region right in the file:
<<<<<<< HEAD
total = price * 1.0
=======
total = price * 1.08
>>>>>>> add-tax
That's not an error message — it's a question with both answers printed. Pick the right one (or write a third), delete the markers, then git add and git commit to finish the merge. Conflicts are Git refusing to guess on your behalf. You'd rather it ask than silently pick wrong.
Pull requests are a conversation, not a merge button
On GitHub, the collaboration loop runs through pull requests. You push your branch to GitHub and open a PR against main. Yes, it ends with a merge — but treating it as just a merge button wastes the whole point. A PR is where the change gets discussed: reviewers leave line-by-line comments, automated checks run against it, and the author pushes follow-up commits until everyone is satisfied. The merge is the period at the end of the sentence, not the sentence.
This is also where the loop closes for teams: git pull brings everyone else's merged work into your local repo, and short-lived branches (open it, finish the work, merge it, delete it) keep the timeline history readable. Long-lived branches that drift for weeks are where merge conflicts go to become merge disasters.
main on a shared repo. It bypasses review, breaks the conversation, and trains everyone else to do the same. Branches are free — act like it.- A branch is a free, disposable pointer to a commit — open one per unit of work, merge, delete.
- Merge conflicts are Git asking you a question, not an error — read both sides, pick, commit.
- Pull requests are where changes get reviewed and discussed; the merge is just the ending.
GitHub Pages: Free Hosting for Your Stuff
Here's the problem GitHub Pages solves: you built a static site — some HTML, CSS, maybe JavaScript — and now you need it on the internet without renting a server. Pages takes a branch of your repo and serves it as a website, free, with TLS. Push to the branch, the site updates. That's the whole pitch, and it's genuinely that simple.
Setup takes about a minute. In your repo: Settings → Pages → "Deploy from a branch," pick main and /(root). Push an index.html to that branch and your site goes live at https://<your-org>.github.io/<repo>/. Every push rebuilds and redeploys automatically — your deploy pipeline is literally git push.
Custom domains and the certificate wait
Want your own domain instead of the github.io address? Two steps. First, add a file named CNAME (no extension) to the root of your site branch, containing just your domain:
www.<your-domain>
Second, at your DNS provider, point a CNAME record for that hostname at <your-org>.github.io. GitHub sees the CNAME file, verifies the DNS, and starts provisioning a TLS certificate for your domain automatically.
Now the part everyone rushes: do not flip "Enforce HTTPS" before the certificate exists. Certificate issuance takes anywhere from a few minutes to roughly a day. The "Enforce HTTPS" checkbox only becomes available once the cert is actually issued — trying to enable it early fails (via the API you'll get a blunt "certificate does not exist yet"). The correct sequence is: set the domain, wait for the cert, then enforce HTTPS. Patience here is a feature, not a delay.
- Pages serves a branch of your repo as a free static site —
git pushis your deploy. - Custom domains need a
CNAMEfile in the repo plus a DNS CNAME pointing at GitHub. - Wait for the TLS certificate to be issued before enforcing HTTPS — the option only appears once the cert exists.
The official walkthrough is at docs.github.com/en/pages.
Actions: CI That Does Your Chores
Every project accumulates chores: run the tests, lint the code, build the site, check that nothing broke. Humans are terrible at doing these consistently — we forget, we skip "just this once," and that's how broken code lands on main. GitHub Actions is the part of GitHub that does the chores for you, automatically, on every event you care about.
The core idea: a workflow is a YAML file in .github/workflows/ that says "when X happens, do Y on a fresh virtual machine." The "when" is the trigger — push, pull_request, a cron schedule, or a manual workflow_dispatch button. The "do" is a list of steps that run in order. One sharp analogy: Actions is a robot intern watching your repo — you write it a checklist once, and it runs that checklist perfectly every time the doorbell rings.
Here's a real one. This runs your test suite on every push to main and every pull request, then saves the test report as an artifact (a downloadable file attached to the run):
# .github/workflows/checks.yml
name: checks
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npm test
- uses: actions/upload-artifact@v4
with:
name: test-report
path: test-results/
Read it top to bottom: name the workflow, declare the triggers, define a job on a fresh Ubuntu runner, check out the code, set up Node, install dependencies, run the tests, stash the results. If any step fails, the run goes red and — this is the payoff — the pull request shows a big red X before anyone merges it. Broken code has to get past the robot first.
@v4, not @main). An unpinned action can change underneath you overnight and break every workflow in the repo — or worse, run code you didn't review.- A workflow is a YAML checklist that runs on a fresh VM whenever its trigger fires.
- Start with one job: check out, set up, install, test — on every push and PR.
- Artifacts preserve build outputs; pinned action versions keep your pipelines stable.
The workflow syntax reference is at docs.github.com/en/actions — dense but complete.
Scripting GitHub: the API and gh CLI
Clicking through a web UI is fine once. Doing it fifty times is a job for a script. GitHub exposes nearly everything — repos, issues, pull requests, releases, Actions runs — through a REST API, and ships a command-line tool, gh, that wraps it. The rule of thumb: if you're about to do something repetitive in the browser, there's a command for it.
Start with gh auth login — it walks you through signing in once, then every later command just works. From there, the daily drivers:
$ gh issue list --limit 5
$ gh issue create --title "Login page crashes on Safari" \
--body "Steps to reproduce: ..."
$ gh pr create --title "Add search" --body "Closes #12"
$ gh pr merge 14 --squash
That last one is the whole PR loop from the terminal: create the branch, push it, open the PR with a body, merge it with a squash — no browser tabs involved. For anything gh doesn't cover directly, gh api drops you straight onto the REST API with your auth handled:
$ gh api repos/<your-org>/hello-world --jq '.full_name, .default_branch'
The API's shape
The REST API itself is beautifully predictable: nouns are URLs, verbs are HTTP methods. GET /repos/<owner>/<repo> reads a repo, POST /repos/<owner>/<repo>/issues creates an issue, PATCH edits. You can see the shape with plain curl against GitHub's public demo repo — no auth needed:
$ curl -s https://api.github.com/repos/octocat/Hello-World | head -8
{
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"private": false,
...
Everything returns JSON, everything is paginated, and the docs list every endpoint. Once you internalize "noun = URL, verb = method," you can script bulk operations — label a hundred issues, open PRs across ten repos, pull Actions logs nightly — that would take an afternoon of clicking.
gh over raw curl with tokens whenever you can. gh stores your auth securely and handles pagination and JSON for you — one less secret to leak, one less script to debug.ghwraps the common stuff: issues, PRs, and merges without leaving the terminal.gh apigives you the full REST API with auth already handled.- The API's shape is nouns-as-URLs plus HTTP verbs — learn it once, script anything.
Full endpoint reference at docs.github.com/en/rest.
Keeping Repos Clean
A repo is a public artifact — even a private one gets cloned onto laptops, CI runners, and eventually someone's new job. Everything you commit should be something you'd be comfortable explaining. This chapter is the hygiene that separates a repo you're proud to link from one you hope nobody opens.
Start with .gitignore: a file in the repo root listing patterns Git should never track. Dependencies, build output, OS noise, and above all secrets:
# dependencies and build output
node_modules/
dist/
# secrets — never commit these
.env
*.pem
*.key
# OS noise
.DS_Store
Thumbs.db
Check with git status before every commit — if something shows up that shouldn't be there, fix the ignore file first. And if a file is already tracked, ignoring it later isn't enough; untrack it without deleting your local copy via git rm --cached .env.
Sanitize like it's public
Assume every repo becomes public eventually and write accordingly. No real usernames, no real IP schemas, no emails, no org names in committed files. Use placeholders: <your-org> for the org, labuser for usernames, 192.0.2.0/24 (TEST-NET-1, reserved for documentation) for IPs. Your real values live in local, gitignored config files — never in the repo.
CHANGELOG discipline
A CHANGELOG.md is the human-readable story of what changed and when. The format that works: dated sections, newest first, written in the same commit as the change itself — never as an afterthought three commits later.
## 2026-09-29
- Added search to the launcher page
- Fixed crash on empty playlists
## 2026-09-28
- Shipped the podcast episode grid
Pair that with the one-commit deploy habit from Chapter 3 — batch related changes, one commit, one push — and your history reads like a table of contents instead of a crime scene.
The dangerous checklist
- Initialize a repo, craft deliberate commits with real messages, and read the history with
git log. - Open a branch, merge it back, and resolve a merge conflict without breaking a sweat.
- Ship a static site on GitHub Pages with a custom domain — and enforce HTTPS only after the certificate exists.
- Write an Actions workflow that runs your tests on every push and pull request.
- Audit a repo for secrets and personal data: a real
.gitignore, placeholders instead of real values, and a dated CHANGELOG with newest first.