$ yuktics v0.1

T1 — Programming Foundations module 01.3 ~4–6 hrs

Git and GitHub, properly

Stop fearing your VCS. Branches, rebase vs merge, PRs, conflict resolution, history rewrites — the operations every serious dev uses weekly, demystified.

Prerequisites

  • 01.2

Stack

  • git
  • gh CLI
  • a GitHub account

By the end of this module

  • Hold the right mental model of git — commits, branches, refs, the staging area — instead of memorizing commands.
  • Resolve a real merge conflict without panicking or running `git push --force` at random.
  • Use interactive rebase to clean up a messy branch into reviewable history.
  • Open and merge a PR from the command line, on someone else's repo, fluently.

Most CS students fear git. They run a small set of commands they half-understand, and the first time something goes wrong they paste an XKCD comic and rebuild the repo from a backup. This works exactly until it doesn’t, which is around the moment they get their first real job and discover that everyone else on the team is doing surgery on history without flinching.

The fix is not “memorize more commands.” It is to internalize the model underneath the commands. Git is, structurally, very simple — a graph of immutable snapshots, with named pointers into the graph. Once you see it that way, every command becomes obvious. Every “oh god I broke it” feeling goes away because the worst case is “I made a pointer point at the wrong place and now I’ll point it back.”

This module hands you the model and then forces you through the operations every working dev uses weekly: branch, rebase, merge, conflict, PR, history rewrite, bisect. By the end you should not look up git rebase --interactive syntax. It should be a verb you have.

Set up

# git is already installed if you did 00.2. Configure it once.
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.editor "code --wait"   # or "vim" or "nano"

# Authenticate the gh CLI (you also did this in 00.2)
gh auth status
# If not logged in: gh auth login

# Useful aliases — short, mnemonic, save your wrists
git config --global alias.s "status -sb"
git config --global alias.l "log --oneline --graph --decorate --all"
git config --global alias.co "checkout"
git config --global alias.br "branch"
git config --global alias.cm "commit -m"

Now make a sandbox repo to break things in:

mkdir git-sandbox && cd git-sandbox
git init
echo "# sandbox" > README.md
git add README.md && git commit -m "initial"
gh repo create git-sandbox --private --source=. --push

Everything below assumes you’re inside this repo unless noted. Break it on purpose. That is the point.

Read these first

Four sources, in this order, then stop.

  1. Pro Git — Chapters 1–3. book · 90 min · the canonical resource, free. Read these three chapters once and almost everything in this module clicks.
  2. Julia Evans — Oh Shit, Git!. post · 15 min · the recovery cookbook for when you panic.
  3. Atlassian — Merging vs. Rebasing. link · 20 min · the cleanest single page on the one debate that matters.
  4. GitHub Docs — Working with the gh CLI. link · 15 min · for PRs from the terminal.

Skip the “Git in 100 seconds” YouTube videos. They will not teach you the model.

The mental model

Git is built on three concepts. If you only remember three things, remember these.

ConceptWhat it isWhat it isn’t
CommitAn immutable snapshot of every tracked file, plus a pointer to its parent commit(s).A diff. (Diffs are computed on the fly between commits.)
BranchA named pointer to a commit. That’s it.A separate copy of the code.
RefAny name that resolves to a commit. Branches, tags, HEAD, remotes are all refs.Magic.

When you git commit, you create a new snapshot and move the current branch’s pointer to it. When you git checkout other-branch, you change which pointer is “current” (HEAD). When you git merge, you create a new commit with two parents, joining two histories.

Internalize that and roughly 90% of git commands stop feeling like incantations.

Visualize a branch as a chain of commits, where each arrow points
back to the parent. main and feature are two pointers into this graph.

   A --- B --- C --- D            <- main
                \
                 E --- F          <- feature

A merge from feature into main creates a new commit M whose parents are D and F. A rebase of feature onto main rewrites E and F as new commits E' and F' whose parent chain is D. Same logical result, different shape of history. That’s the whole rebase-vs-merge thing.

The operations you’ll use weekly

These are not “advanced.” They are the daily working set. Practice them in the sandbox.

Add, commit, push

echo "hello" > a.txt
git status                    # see what changed
git add a.txt                 # stage
git diff --staged             # what am I about to commit?
git commit -m "add a.txt"
git push                      # to origin/main, the first time it'll suggest -u origin main

Branch and switch

git switch -c feat/login      # create + switch (modern verb; replaces checkout -b)
# do work
git add . && git commit -m "wip: login"
git switch main               # back to main
git switch feat/login         # back to your branch

Read history

git log --oneline --graph --decorate --all   # or your alias: git l
git log -p path/to/file       # full diff history of a file
git blame path/to/file        # who last changed each line
git show <sha>                # full commit

Diff

git diff                      # working tree vs staged
git diff --staged             # staged vs HEAD
git diff main..feat/login     # branch vs branch
git diff HEAD~3               # vs three commits ago

Undo, the right way

git restore file.txt          # discard unstaged changes to file
git restore --staged file.txt # un-stage
git commit --amend            # fix the most recent commit (only if not pushed)
git revert <sha>              # create a new commit that undoes <sha>
git reset --soft HEAD~1       # move branch back one, keep changes staged
git reset --hard HEAD~1       # move branch back one, discard changes  ⚠ destructive

The single rule: git reset --hard and git push --force are the two operations that can destroy work. Use them deliberately, never as a panic move.

Rebase vs merge — the one debate worth having

You will hear long arguments about this. The honest version is short.

Merge preserves history exactly. Two streams of work meet at a merge commit. You see the topology, including the branching. Pros: nothing rewrites history; nothing surprises anyone. Cons: history gets noisy fast in active projects.

Rebase rewrites your branch’s commits onto the new tip of the target. The result is a linear history. Pros: clean, readable history; PRs read like one continuous story. Cons: rewrites commit hashes, so never rebase commits other people are also working on.

The default rule that holds 90% of the time:

Rebase your local feature branch onto main before opening or updating a PR.
Use a merge commit when integrating a long-running branch back into main.
Never rebase a branch that anyone else has based work on.

How to do each:

# Rebase (your local feature branch on top of latest main)
git switch feat/login
git fetch origin
git rebase origin/main
# resolve any conflicts — see next section
git push --force-with-lease    # safer than --force; refuses if remote changed unexpectedly

# Merge (combine the histories with a merge commit)
git switch main
git merge --no-ff feat/login   # --no-ff keeps the merge commit explicit
git push

--force-with-lease is the version of --force you should learn first. It refuses the push if someone else pushed to that branch in the meantime, which is exactly the safety you want.

Resolving a real conflict

The first time you hit a merge conflict it feels like the repo is broken. It isn’t. Git just paused and is waiting for you to decide.

Force one in your sandbox:

echo "line 1" > conflict.txt
git add . && git commit -m "base"
git switch -c side
echo "side change" > conflict.txt
git commit -am "side change"
git switch main
echo "main change" > conflict.txt
git commit -am "main change"
git merge side
# CONFLICT (content): Merge conflict in conflict.txt

Look at the file. You’ll see this:

<<<<<<< HEAD
main change
=======
side change
>>>>>>> side

Edit it to whatever the resolution should be (keep one, keep the other, write a new line that combines both). Then:

git add conflict.txt
git commit                     # finishes the merge
# Or if you panic and want out:
git merge --abort

That’s it. Conflicts are not magic. They are git saying “two branches changed the same lines, you decide.” If a conflict feels overwhelming, your branch has drifted too far from main — rebase frequently to keep conflicts small.

Interactive rebase: cleaning up history

The most underrated git skill. Before you push a PR, you can squash six “wip” commits into one clean commit, reorder them, edit messages, drop bad ones.

# Edit the last 5 commits on your branch
git rebase -i HEAD~5

# Editor opens with something like:
pick a1b2c3d wip
pick e4f5g6h fix typo
pick i7j8k9l add login form
pick m0n1o2p wip
pick q3r4s5t actually working

# Change `pick` to:
#   r (reword)  — change the message
#   s (squash)  — fold this commit into the previous one
#   f (fixup)   — like squash but discard this commit's message
#   d (drop)    — delete the commit entirely
# Save and close.

A clean PR usually looks like 1–3 commits, each with a meaningful message. Interactive rebase is how you get there from a working branch.

PR workflow with gh

# On a feature branch:
git push -u origin feat/login

# Open a PR from the CLI
gh pr create --base main --head feat/login --title "Add login form" \
  --body "Closes #42. Adds a basic /login route."

# Or, after pushing:
gh pr create --fill           # uses your last commit message

# Review what you're about to ship
gh pr diff
gh pr view --web              # open in browser

# Merge (squash is the most common in modern teams)
gh pr merge --squash --delete-branch

The --squash move turns your branch’s commits into a single commit on main. Combined with descriptive PR titles, it gives you a main history that reads like a changelog.

Reading history when something goes wrong

Two commands that save your weekend.

# When did this line break? Walk back through the commits that touched it
git log -p --follow path/to/file

# Binary search through history for the commit that broke a test
git bisect start
git bisect bad                 # current commit is broken
git bisect good v1.2.0         # this old tag was fine
# git checks out a commit halfway between. Run your test.
git bisect good                # if it works at this commit
git bisect bad                 # if it breaks
# repeat — log2(N) iterations later you have the exact commit
git bisect reset

git bisect is the move that distinguishes a junior dev who guesses from one who finds the bug.

.gitignore, the patterns to know

A .gitignore you don’t have to think about. Drop this in any new repo.

# Editor / OS
.DS_Store
.vscode/
.idea/

# Python
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Node
node_modules/
.next/
dist/
*.log

# Secrets
.env
.env.local
*.pem

Use gh repo create --gitignore Python (or Node, etc.) to bootstrap with the official one for that ecosystem.

The build

Contribute a small PR to a real public repo. Anything counts: a typo fix in a docs site, a one-line bug in a small library, a new test case for an existing function. Pick a repo with a good first issue label.

# Fork in the GitHub UI, then:
gh repo clone YOUR_USER/their-repo
cd their-repo
git remote add upstream https://github.com/THEIR_USER/their-repo.git
git switch -c fix/typo-in-readme
# make your change
git commit -am "docs: fix typo in install instructions"
git push -u origin fix/typo-in-readme
gh pr create --base main

The first time you do this, it’ll feel ceremonial. The tenth time, it’ll feel like nothing. That nothingness is the goal.

Going deeper

When you have specific questions, in this order:

  1. Pro Git — full book. link · once you have the basics, the later chapters (internals, plumbing) are gold.
  2. Git from the inside out — Mary Rose Cook. post · the model, drawn out.
  3. Think Like (a) Git. link · the model again, slower, with diagrams.
  4. Tig. link · a TUI for git that makes log/diff exploration much faster than the CLI.

Checkpoints

If any one wobbles, the corresponding section above is what to reread.

  1. State, in one sentence each, what a commit is, what a branch is, and what a ref is.
  2. When do you rebase, and when do you merge? Give the rule, and one situation that violates it.
  3. Walk through resolving a merge conflict from memory: what do you see, what do you do, and how do you finish?
  4. What does interactive rebase let you do? Name three operations you can perform on commits with it.
  5. Show a PR you opened on a public repo with gh pr create. (The build for this module.)

If all five hold and your PR is open, you’ve earned 01.3. Next: 01.4 — TypeScript, a typed language that pays. Time to add the most-employable language of 2026 to your toolkit.