Git Version Control Visualizations

Git is easier once "save" stops doing four jobs. An edit changes the working tree. git add writes selected content into the index and object database. git commit records the staged tree and links it to a parent. git push deals in commits and refs, not whatever happens to be open in your editor.

These visualizations begin with the everyday boundaries, open the local repository to show the objects inside it, then put movable branch names on the commit graph. The merge lessons reuse that same graph twice: first to prove that one ref move is enough, then to find a shared base and combine two diverged snapshots. Rebase copies branch-only patches onto a new parent chain without pretending the old commits moved. The remote lesson puts a second object database beside the first. The release lesson gives one old commit a durable version name while main keeps moving, and the recovery lesson separates copying files, moving refs, and recording an inverse commit.

The boundary model

Git's Four Areas follows one file through the working tree, index, local repository, and remote repository. Its useful trap is an edit after git add: the working file says two, the index still says one, and the next commit records one. That single split explains why staging exists.

The lesson also separates commit from push. A local commit can exist while the remote remains behind, and a working edit cannot cross the network until it has become part of a reachable commit.

The object model

Commits as Snapshots opens the local repository. File bytes become blob IDs, the index becomes a tree, and the commit body names both that tree and its parent. Change only app.py and the next tree keeps the old README.md blob. Make an allow-empty commit and the whole tree is reused, yet the parent line still produces a new commit ID.

That is the compact version of Git's storage model: immutable content-addressed objects plus movable names. Objects do not slide forward when HEAD moves. The ref changes; the history stays put.

The reference model

Branches & HEAD turns "movable names" into an observable mechanism. main and feature first point to one shared base commit. A commit on feature moves only feature; switching back to main changes HEAD without moving either ref; the next commit advances only main. The result is a fork made from one shared object, not two copied histories.

Its detached-HEAD scenario removes the branch-name hop. A new commit advances HEAD directly and leaves every branch unchanged. Switch away without creating a rescue branch and the commit becomes unreferenced, though it remains stored and may still be recoverable through a real repository's reflog.

The first merge

Fast-Forward Merge starts with main behind feature on one unbroken parent chain. Git walks backward from the source tip until it finds the current tip. That ancestry proof permits exactly one write: main advances to the commit already named by feature. The source ref stays still and the object database gains no merge commit.

The sandbox also exposes the boundaries hidden by the happy path. Equal tips and a source already contained by the current branch are already up to date. If both tips have different children after a shared base, neither contains the other and --ff-only refuses the merge without moving a ref.

Merging diverged snapshots

Three-Way Merge picks up at that refusal. It marks the commits reachable from the current tip, probes the source ancestry, and selects the unique best common ancestor. That commit supplies the base snapshot; the two branch tips supply ours and theirs.

The default trace gives each branch a different file to change. main changes app.py, feature changes README.md, and the path rules take the changed value from each side. Only after both paths resolve does the lesson write one result tree, store a commit whose ordered parents are the two old tips, and advance main. The source ref stays fixed.

The conflict sandbox changes the same path differently on both branches. It stops before any tree, commit, or ref write, which makes the boundary between planning a merge and recording a merge commit visible.

Resolving a conflict

Merge Conflicts opens that stopped state instead of treating it as an error screen. HEAD and the current branch remain fixed, MERGE_HEAD remembers the source tip, and the index replaces stage 0 with stage 1/base, stage 2/ours, and stage 3/theirs. The working file gets conflict markers, but those marker lines are only editable bytes on disk.

The default trace edits the file, proves that the index is still unmerged, then runs git add app.txt to collapse the three entries into one stage-0 blob. git merge --continue can finally write the tree, create a two-parent commit, and advance only main. The failure scenarios are the useful part: editing without add is insufficient, while adding marker-bearing content is accepted because Git stages bytes rather than intent.

Rewriting a branch with rebase

Rebase finds the commits unique to the current branch, orders them oldest first, and reapplies each patch on the target tip. The patch and message survive; the parent header changes, so every replacement is a new commit object with a new ID. The old chain remains stored but loses its branch ref after success.

The conflict scenarios expose the pointer discipline that makes the operation recoverable. Temporary detached HEAD advances through successful replays while the named branch stays on its original tip. REBASE_HEAD marks the failing original commit, and git rebase --abort restores the attached branch without reversing a series of partial ref writes.

Collaborating through a remote

Remote Collaboration separates your local main, your cached origin/main, and the remote repository's own main. A teammate pushes first, leaving both local refs stale. Fetch copies the missing objects and advances only origin/main; a fast-forward pull then moves local main and the checkout. After a new local commit, push transfers the missing objects, advances the remote branch, and refreshes the tracking ref.

The divergence cases make the safety checks concrete. Pull with --ff-only keeps its fetch results but refuses to move local main when the histories fork. Push checks the server's actual tip, not a stale local tracking ref, and rejects a non-fast-forward proposal without changing durable remote state.

Naming a release

Tags & Releases separates three facts that hosting dashboards often blur together. A lightweight tag is a ref that points directly to an object. An annotated tag points to a tag object, which in turn names a commit. A hosted release is neither one: it is a platform record outside Git's object database, tied here to the tag's peeled source commit at publication.

The default trace publishes v1.0.0, advances main, and then checks out the old version. The tag and release source remain on the release commit while the branch moves ahead, and HEAD becomes detached at the peeled commit. The force-move scenario shows the dangerous exception: tag refs can be rewritten explicitly, leaving today's tag target at odds with the release's recorded source.

Undoing a change

Undoing Changes keeps the Working Tree, Index, HEAD tree, selected commit, main, and ORIG_HEAD visible at once. Restore copies one snapshot into a file layer. Reset moves main, then its mode decides whether the Index and Working Tree follow. Revert derives an inverse snapshot and appends a commit, so the bad commit stays in ancestry.

The default trace ends with the file back at mode=safe, but that matching text is not the proof. The reset route moves a ref and adds no object; the revert route writes one new commit and keeps the old tip reachable. The hard-reset scenario also shows the dangerous boundary: ORIG_HEAD can name an old commit, but it cannot preserve Working Tree bytes that were never stored by Git.

A recommended learning order

Start with Git's Four Areas. You should be able to answer which area each command reads and writes before object serialization enters the picture.

Then take Commits as Snapshots. It replaces instructional commit labels with real Git-format object IDs and explains why a project snapshot and a commit are not the same thing.

Continue with Branches & HEAD. It keeps those immutable commit objects in view while branch refs and HEAD move around them.

Then run Fast-Forward Merge. It turns the branch model into the first integration rule: prove ancestry first, then move only the checked-out branch.

Continue with Three-Way Merge. The ancestry walk now finds a base instead of merely testing containment, and the object model returns when the clean result becomes a tree and a two-parent commit.

Then run Merge Conflicts. It keeps the same base/ours/theirs roles but moves the main stage to the index and working tree, where resolution is a copy boundary rather than a visual judgment.

Next, run Rebase. It reuses the fork point and conflict rules, but replaces the two-parent merge result with an oldest-first sequence of new one-parent commits. Watch the current branch ref stay still until every patch succeeds.

Then run Remote Collaboration. It reuses the object, ref, and ancestry rules across two repositories, where local knowledge can lag behind authoritative remote state.

Then run Tags & Releases. It combines immutable objects, movable refs, detached HEAD, and hosted metadata into one provenance test: which commit did this version actually publish?

Finish with Undoing Changes. It assumes the boundary, object, and ref models are already familiar, then asks the recovery question precisely: did this command copy a snapshot, move a name, or append a correction?

All Git Version Control Visualizations

Run each default trace once, then use the sandboxes to challenge one assumption at a time: commit from an unstaged working edit in Git's Four Areas, compare a normal and allow-empty commit in Commits as Snapshots, detach HEAD and switch away in Branches & HEAD, compare linear and contained histories in Fast-Forward Merge, contrast clean and conflicting inputs in Three-Way Merge, stage a marker-bearing file in Merge Conflicts, stop Rebase after one successful replay and abort it, make local and remote tips diverge in Remote Collaboration, force-move a published tag in Tags & Releases, then compare hard reset with revert in Undoing Changes while the file ends with the same bytes.