Skip to content
AyoKoding

Overview

Prerequisites

  • Prior topics: 4 · Just Enough Python and 5 · Just Enough Bash. This topic assumes comfortable terminal fluency (piping, exit codes, editing files from a shell) and enough scripting background to read a small commit hook.
  • Tools & environment: a macOS/Linux terminal; Git at a recent stable release (current stable is 2.55.0, released 2026-06-29 -- the exact version is deliberately unpinned in this topic, since the core CLI and object model this topic teaches have been stable for years); a GitHub (or equivalent) account for the remote/pull-request flow; Neovim/VSCode with Git integration for diffs and blame.
  • Assumed knowledge: navigating a filesystem and running CLI tools (topic 5); reading a small script well enough to understand a commit hook (topic 4).

Why this exists -- the big idea

Before version control, coordinating change meant zipped folders, final_v2_REALLY_final filenames, and silently overwriting a colleague's work with no way back -- there was no shared, trustworthy record of who changed what and why. The one idea worth keeping if you forget everything else: a commit is an immutable snapshot of the whole tree, identified by the hash of its content, and a branch is just a movable pointer to one commit -- once that clicks, merge, rebase, reset, and reflog stop being magic and become graph operations you can reason about directly.

Cross-cutting big ideas: coupling-vs-cohesion -- a branch-and-pull-request flow keeps changes that belong together in one reviewable commit and unrelated work apart, the same discipline that shapes good module boundaries applied to a change's shape over time. correctness-vs-pragmatism -- rebase buys a clean, linear history but rewrites shared state; the disciplined compromise this topic teaches is rebase local work, merge shared work.

Concretely, this topic covers the object model (blob/tree/commit/tag), everyday staging and committing, branching with fast-forward and three-way merges, conflict resolution, rebase (including interactive rebase), the undo family (reset/revert/restore/stash), the safety net (reflog), remotes and the push/fetch/pull cycle, tagging, .gitignore, commit hooks, cherry-pick, and the pull-request / trunk-based flow that ties it all together. The build-your-own-Git pass that reconstructs the content-addressed object store from scratch lives in a later topic; this one builds the working engineer's fluency with the CLI as it exists today.

How verification works in this topic

Every worked example on the following pages is : driven entirely from the git command line, no GUI. Unlike a config file you can cat and inspect at rest, most of what Git does only becomes observable by actually running a command and reading its real output -- there is no way to "read" a fast-forward merge without performing one. Every example on the Beginner, Intermediate, and Advanced pages is therefore a real, self-contained shell script (setup.sh) that builds a fresh scratch repository, runs the commands under discussion, and captures genuine terminal output into transcript.txt -- nothing in this topic's output was hand-written or simulated. Each page's inline "Output" block is a trimmed excerpt of its example's own code/*/transcript.txt for readability (hint text, diffstat lines, and repeated diff headers are sometimes cut); the full, byte-for-byte real transcript is always available alongside the setup.sh that produced it under learning/code/. Examples that need a second repository (remote/clone/push/pull, Examples 64-74) use a local bare repository (git init --bare) in place of a network host -- deterministic, needs no network, and behaves identically to a real remote for every command this topic teaches.

%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
%% Six concept clusters, in the order this page teaches them (co-01 through co-29)
graph TD
    A["Object model &amp; states<br/>co-01 to co-05"]:::blue
    B["Staging, committing, history<br/>co-06 to co-10"]:::orange
    C["Branching &amp; merging<br/>co-11 to co-14"]:::teal
    D["Rebase &amp; the undo family<br/>co-15 to co-21"]:::purple
    E["Safety net: reflog<br/>co-22"]:::brown
    F["Remotes, tags, hooks, flow<br/>co-23 to co-29"]:::blue
 
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
 
    classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
    classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px

Concepts

Every worked example in this topic's follow-up pages cites the co-NN concept it exercises -- this section is the 1:1 reference those citations point back to. Read it in order: the object model comes first because everything else (staging, branching, rebase, reflog) is ultimately a graph operation over the objects this section introduces.

co-01 · Repository Init and Clone

git init creates the .git/ directory that turns a plain folder into a repository, and git clone copies an existing repository plus its full history into a brand-new working tree.

Why it matters: every repository starts one of exactly two ways -- from nothing (init) or from something that already exists (clone) -- and clone's defining property (the FULL history travels with it, not just the latest snapshot) is what makes every clone a complete, independent backup of the project.

Verify it: ls -a .git confirms the directory genuinely exists after init; git -C <dest> log --oneline after a clone shows the same commits the source repository has.

co-02 · Three-States Model

Every tracked file lives in one of three areas: the working tree (what you edit on disk), the staging area / index (what the next commit will contain), and committed history (permanent, already-recorded snapshots).

Why it matters: nearly every confusing Git moment ("why didn't my edit get committed," "why does git diff show nothing") resolves instantly once you ask "which of the three states am I looking at." git add moves content forward one state; git commit moves it forward one more; git restore moves it backward.

Verify it: git status names, for every changed file, exactly which of the three states it currently occupies (untracked/unstaged working tree, staged index, or clean relative to the last commit).

co-03 · Object Model

Git stores content as four hash-addressed object types -- blob (file content), tree (directory listing), commit (a snapshot plus metadata), and tag -- each identified by the hash of its own content.

Why it matters: content-addressing is what makes Git's storage both self-verifying (a corrupted object's hash simply stops matching) and naturally deduplicating (two identical files anywhere in history share one blob). Once this clicks, a "commit" stops being a mysterious black box and becomes exactly four kinds of plain, inspectable objects.

Verify it: git cat-file -p <hash> prints any object's raw content; git cat-file -t <hash> prints just its type -- both work identically whether <hash> is a full hash, an abbreviation, or a ref like HEAD.

co-04 · Refs, Branches, HEAD

A branch or tag is just a named pointer to a commit, and HEAD is a pointer to the current branch (or, in a detached state, directly to a commit) that decides what "current" means at any moment.

Why it matters: branches being cheap, plain pointers (not copies of a project) is the entire reason Git branching is fast and nearly free -- creating one is a few bytes, not a directory copy.

Verify it: git rev-parse HEAD and git rev-parse <branch> resolve to the identical hash whenever that branch is checked out; git show-ref lists every ref alongside the commit hash it currently points to.

co-05 · Staging and Status

git add moves changes from the working tree into the index, and git status reports which files are untracked, modified-but-unstaged, or staged.

Why it matters: staging is the deliberate pause between "I edited something" and "this edit becomes part of the next commit" -- it is what lets one physical editing session become several small, focused commits instead of one large one.

Verify it: git status after any edit names the file under exactly one of "Untracked files," "Changes not staged for commit," or "Changes to be committed."

co-06 · Hunk Staging

git add -p stages selected hunks (or, with s, split sub-hunks) so that one physical edit to a file can become several separately-committed, focused changes.

Why it matters: real edits rarely align with commit boundaries -- you fix a bug and clean up a typo in the same file at the same time. Hunk staging is what lets those two unrelated edits still land as two separate, reviewable commits.

Verify it: after git add -p answers y to one hunk and n to another, git diff --staged shows only the accepted hunk, and plain git diff still shows the rejected one.

co-07 · Committing and Messages

git commit records the staged snapshot as a new, permanent commit with a subject line and optional body, following the imperative-mood convention (Chris Beams' widely cited rules).

Why it matters: a commit message is the only artifact that survives to explain why a change happened -- the diff itself only shows what changed. A second -m (or a blank line in an editor) separates the subject from an explanatory body.

Verify it: git log --format=%B -1 prints a commit's complete, raw message -- subject, blank separator, and body, exactly as recorded.

co-08 · Amending Commits

git commit --amend rewrites the most recent commit (its message and/or its content), producing a brand new hash that replaces the previous tip entirely.

Why it matters: a typo in the last commit message, or a file forgotten from the last commit, does not need a whole new "fix typo" commit cluttering history -- amend folds the correction directly into the commit it belongs to, as if it had been right the first time.

Verify it: git log --oneline before and after an amend shows the SAME count of commits but a DIFFERENT tip hash -- proof the old commit was replaced, not appended to.

co-09 · Diffing

git diff compares states: working-tree-vs-index (the default), index-vs-HEAD (--staged), or any two arbitrary commits or branches.

Why it matters: "what changed" is only a meaningful question once you know between which two points -- the same git diff command answers three genuinely different questions depending only on its flags and arguments.

Verify it: git diff and git diff --staged show different content for the same working tree the instant a change is staged but not committed; git diff <a>..<b> compares two arbitrary endpoints directly.

co-10 · History Inspection

git log (with --oneline/--graph/--pretty/-N/-- path), git show, and git cat-file reveal the commit graph and the raw objects underneath it, at whatever level of detail a question requires.

Why it matters: history is Git's actual product -- code review, blame, bisecting a regression, and understanding why a line looks the way it does all start with reading history effectively, not just committing to it.

Verify it: git log --oneline -- <path> restricts the log to only the commits that actually touched that path; git log --graph --all renders the shape of every branch's history as ASCII art.

co-11 · Branching

git branch/git switch/git checkout create, list, rename, delete, and move between branches, each one a cheap, instantly-created movable pointer (co-04).

Why it matters: near-free branching is what makes it practical to isolate every piece of work, however small, on its own branch -- the foundation the pull-request and trunk-based flow (co-28) is built on.

Verify it: git branch lists every local branch with * marking the checked-out one; git switch -c <name> creates and checks out a branch in a single step.

co-12 · Fast-Forward Merge

When the target branch has not diverged from the branch being merged, git merge simply advances the target's pointer -- no new commit is created.

Why it matters: a fast-forward is the cheapest possible integration -- recognizing when one is possible (and when --no-ff, co-13, is deliberately forcing a real merge commit instead) explains a large fraction of "why does my history look like this" questions.

Verify it: git merge reports the literal word "Fast-forward" in its own output, and git log --graph afterward shows a single straight line with no fork.

co-13 · Three-Way Merge

When both branches have new commits since their common ancestor, git merge creates a genuine merge commit with two parents that combines both divergent trees.

Why it matters: this is Git's actual reconciliation mechanism -- most real integration work (landing a feature branch after main has also moved) is a three-way merge, whether or not the two sides' edits overlap.

Verify it: git cat-file -p HEAD on a merge commit shows two separate parent lines -- the unambiguous, low-level signature of a real merge, distinct from any ordinary commit.

co-14 · Conflict Resolution

Overlapping edits produce conflict markers that must be resolved by hand, re-staged, and committed -- or the merge/rebase/cherry-pick can be aborted entirely to return to the pre-attempt state.

Why it matters: a conflict is not a failure state to fear -- it is Git correctly refusing to guess which of two genuinely competing edits is "right," leaving that judgment call to a human.

Verify it: git status during an unresolved conflict lists the affected path as "both modified"; git diff shows Git's combined diff --cc view with markers for both candidate sides at once.

co-15 · Rebase

git rebase replays a branch's own commits, one at a time, onto a new base commit -- producing brand-new commit hashes and a linear history with no merge commit.

Why it matters: rebase is the tool for keeping a branch's history clean and linear as it sits on top of a moving base, at the cost that every replayed commit's identity (hash) genuinely changes.

Verify it: git log --oneline --graph --all after a clean rebase shows a single straight line, no fork -- and every replayed commit's hash differs from what it was before the rebase.

co-16 · Interactive Rebase

git rebase -i opens (or, scripted, processes) a todo list that curates local history: reordering, squashing, rewording, editing, or dropping individual commits before they land anywhere permanent.

Why it matters: real work rarely produces a clean commit sequence on the first pass -- interactive rebase is how a messy sequence of "wip" commits becomes the clean, reviewable history a teammate actually wants to read.

Verify it: after a squash, git log --format=%B -1 shows the combined message of every squashed commit; after a drop, that commit's content never appears anywhere in the resulting log or working tree.

co-17 · Rebase-vs-Merge Policy

Because rebase rewrites commit identity, the discipline this topic teaches is: rebase local work, merge shared work -- never rebase history someone else has already pulled.

Why it matters: rebasing a branch others have fetched forces every one of them to reconcile diverging histories by hand; the clean linear history rebase buys is worth it only while the work is still entirely yours.

Verify it: integrating the identical two-branch situation once by merge and once by rebase (comparing git log --graph for each) makes the shape difference -- and therefore the stakes of picking wrong -- directly visible.

co-18 · Reset Modes

git reset moves HEAD and, depending on mode, the index and/or the working tree: --soft moves only HEAD; --mixed (the default) also resets the index; --hard also overwrites the working tree.

Why it matters: the three modes form an escalating scope of "how much do I want to undo" -- soft keeps everything staged, mixed keeps everything on disk but unstaged, hard discards the content entirely. Given one path argument instead of a commit, reset instead just unstages that one file, leaving HEAD alone.

Verify it: git status after each mode shows a different state for the exact same undone commit -- staged (soft), unstaged-but-present (mixed), or gone entirely (hard).

co-19 · Revert

git revert records a brand-new commit whose diff is the exact inverse of an earlier one, undoing its effect without rewriting or removing anything from history.

Why it matters: revert is the safe, additive undo -- unlike reset, it is safe to use even on commits that have already been shared, because it never rewrites a hash anyone else may already have.

Verify it: git log --oneline after a revert shows one MORE commit than before (never fewer), and git cat-file -p HEAD shows a completely ordinary, single-parent commit whose diff is the inverse of the target.

co-20 · Restore Files

git restore (with --staged/--source) recovers file content from the index, or from any commit, back into the working tree, without touching HEAD or any branch pointer at all.

Why it matters: restore is the file-scoped counterpart to reset -- reach for it when the goal is "fix this one file's content," not "move where HEAD points."

Verify it: git restore <path> with no flags discards an uncommitted edit back to HEAD's version; git restore --source=<commit> <path> pulls that file's content from any arbitrary older commit instead.

co-21 · Stash

git stash shelves uncommitted changes onto a stack, restoring a clean working tree, and pop/apply restores them later -- pop also removes the entry, apply leaves a copy behind.

Why it matters: stash is the answer to "I need a clean working tree right now, but I'm not ready to commit this yet" -- switching branches, pulling updates, or just context-switching, all without losing in-progress work.

Verify it: git stash list shows one entry per shelved change, each labeled either with an auto-generated description or a custom one from git stash push -m.

co-22 · Reflog

git reflog records every movement of HEAD in this local repository -- every commit, checkout, reset, and rebase step -- making "lost" commits and deleted branches recoverable long after a destructive operation.

Why it matters: this is Git's actual safety net. Nothing a reset --hard or a botched rebase does is truly permanent locally, as long as the reflog still remembers where things used to be. Git tracks two separate default expiration windows: reflog entries still reachable from a branch tip expire after 90 days (gc.reflogExpire), but entries made unreachable by a reset --hard or rebase -- exactly the recovery scenario here -- expire after only 30 days by default (gc.reflogExpireUnreachable).

Verify it: git reflog lists entries as HEAD@{n}, newest first; git reset --hard 'HEAD@{1}' (or recreating a branch at a hash pulled from reflog) restores exactly what a prior destructive command discarded.

co-23 · Remotes, Fetch, Push, Pull

git remote, git fetch, git push, and git pull connect a local repository to remote copies and synchronize commits between them -- fetch downloads without touching local branches, push uploads, and pull is fetch plus an integration step.

Why it matters: this is the entire mechanism of distributed collaboration -- every teammate has a complete, independent repository, and these four commands are how independent histories stay in sync without a single point of failure.

Verify it: git remote -v lists every configured remote's fetch/push URLs; a push to a remote that has since moved is REJECTED, not silently overwritten (co-23's non-fast-forward safety).

co-24 · Tracking Branches

An upstream (tracking) relationship links a local branch to a remote-tracking ref, so that plain push and pull know their default target without naming it every time.

Why it matters: without a tracking relationship, every push/pull needs an explicit remote and branch name; git push -u sets it up once, and git branch -vv shows exactly which remote branch (and how far ahead/behind) each local branch tracks.

Verify it: git branch -vv shows [origin/<branch>] next to a tracked branch's name; switching to a branch that exists only on the remote auto-creates a local tracking branch from it.

co-25 · Tagging

git tag marks a commit with a lightweight (a bare pointer) or annotated (a genuine tag object, with its own message and tagger) name, typically for a release.

Why it matters: unlike a branch, a tag is meant to never move -- it is the permanent, human-readable name for "this exact commit was release v1.0," and an annotated tag additionally records who tagged it and why.

Verify it: git cat-file -t <tag> prints "tag" for an annotated tag but resolves straight through to "commit" for a lightweight one -- the object-model proof of the difference.

co-26 · Gitignore

.gitignore patterns keep generated or secret files untracked and out of git status's listings, while git add -f can deliberately override an ignore rule for one specific file.

Why it matters: without it, every build artifact, dependency directory, and local secret file would constantly clutter git status and risk accidental commits -- .gitignore is a project's explicit declaration of what was never meant to be tracked.

Verify it: a file matching an ignore pattern never appears in git status, under any section, even though it genuinely exists on disk; git add -f stages it anyway, on purpose, for the rare deliberate exception.

co-27 · Commit Hooks

Executable scripts in .git/hooks/ (for example pre-commit) run automatically at specific lifecycle points, and a nonzero exit code from one of them blocks the operation it guards.

Why it matters: hooks are how local, automated policy (linting, secret-scanning, message format) gets enforced BEFORE a commit exists at all, rather than caught later in review.

Verify it: a pre-commit hook that exit 1s genuinely prevents git commit from creating a commit object; the exact same hook exit 0s and lets a clean commit through unaffected.

co-28 · Pull-Request / Trunk-Based Flow

Short-lived branches are integrated to trunk through review (a pull request) and frequent merges -- the backbone of trunk-based development, where branches exist only briefly rather than accumulating drift.

Why it matters: this is the collaboration pattern nearly every real engineering team runs on top of everything else this topic teaches -- branch, commit, push, review, merge, delete, repeat, on a cadence measured in hours or a couple of days rather than weeks.

Verify it: a short-lived branch, pushed and reviewed, lands on trunk via a --no-ff merge (visible as a merge commit) or a fast-forward for a trivially small change -- either way, git log on trunk afterward shows the change genuinely present and the short-lived branch gone.

co-29 · Cherry-Pick

git cherry-pick applies the change introduced by one specific, individual commit onto the current branch as a brand-new commit -- without pulling in anything else from the source branch.

Why it matters: this is the tool for "I need JUST that one fix, not the rest of that branch's history" -- a hotfix that needs to land on a release branch and on trunk independently is the canonical use case.

Verify it: after a cherry-pick, the target branch's log shows a new commit with the same message and content-change as the source, but a genuinely DIFFERENT hash.

Examples by Level

Beginner (Examples 1–28)

Intermediate (Examples 29–60)

Advanced (Examples 61–82)


← Previous: 5 · Just Enough Bash Drilling · Next: Beginner Examples

Last updated July 13, 2026

Command Palette

Search for a command to run...