---
title: "05 — Version Control — An Undo Button for Whole Teams"
uid: version-control-and-git
tags: ["collaboration", "version-control", "git", "roadmap:frontend", "fundamentals"]
excerpt: "Version control is an undo button for whole teams: snapshots make every change reversible, and many people can edit the same project without overwriting each other."
date: 2026-08-12T18:35:12+0000
source: https://www.aveshina.my.id/en/blog/version-control-and-git
---

"Save my files somewhere safe before I change them" was my version-control plan, and it was a backup, not version control. The real model: **version control is an undo button that works for whole teams.** It snapshots history so every change is reversible, and so multiple people can change the same project at the same time without overwriting each other [1].

The thing I had to see clearly is what a Git repository actually *is*. I spent a long time thinking of it as a folder with a memory, when the model that fits is this: **a repo is a graph of commits, where each commit is a snapshot of the whole project, a branch is just a movable label pointing at one commit, and merging is rejoining two paths that diverged in that graph** [2]. Once that picture landed, checkout, branch, merge, and revert stopped being incantations and became obvious operations on the graph.

Here's that graph — one line of commits that forks into two branches and then merges back — which is the picture I keep in my head for everything that follows:

```figure
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A Git commit graph. A horizontal chain of three commit dots on the left labelled main, then the line forks upward into a chain of two dots labelled feature, then both lines rejoin at a merge commit on the right. Each dot is a snapshot of the whole project. Branch labels main and feature are sticky tags pointing at the tip of their respective chains. Arrows point from each commit to its parent.">
  <defs>
    <marker id="garrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>

  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- main chain: C1 -> C2 -> C3 (then later -> merge) -->
    <line x1="90" y1="200" x2="170" y2="200" stroke="#6366f1" stroke-width="2"/>
    <line x1="190" y1="200" x2="270" y2="200" stroke="#6366f1" stroke-width="2"/>
    <line x1="290" y1="200" x2="600" y2="200" stroke="#6366f1" stroke-width="2"/>

    <!-- feature chain forks up from C3: C3 -> C4 -> C5 -> merge -->
    <path d="M290,200 C330,200 350,110 380,110" fill="none" stroke="#db2777" stroke-width="2"/>
    <line x1="400" y1="110" x2="480" y2="110" stroke="#db2777" stroke-width="2"/>
    <path d="M500,110 C540,110 560,200 580,200" fill="none" stroke="#db2777" stroke-width="2"/>

    <!-- commit dots -->
    <g fill="#e0e7ff" stroke="#6366f1" stroke-width="2">
      <circle cx="80" cy="200" r="12"/>
      <circle cx="180" cy="200" r="12"/>
      <circle cx="280" cy="200" r="12"/>
      <circle cx="620" cy="200" r="13" fill="#fef9c3" stroke="#ca8a04" stroke-width="2"/>
    </g>
    <g fill="#fce7f3" stroke="#db2777" stroke-width="2">
      <circle cx="390" cy="110" r="12"/>
      <circle cx="490" cy="110" r="12"/>
    </g>

    <!-- commit labels -->
    <g font-family="ui-monospace, monospace" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">
      <text x="80" y="204">C1</text>
      <text x="180" y="204">C2</text>
      <text x="280" y="204">C3</text>
      <text x="620" y="204" fill="#422006">M</text>
      <text x="390" y="114" fill="#500724">C4</text>
      <text x="490" y="114" fill="#500724">C5</text>
    </g>

    <!-- branch labels (sticky tags pointing at a tip) -->
    <g font-size="11" font-weight="700" text-anchor="middle">
      <rect x="580" y="226" width="80" height="22" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
      <text x="620" y="241" fill="#052e16">main</text>
      <rect x="450" y="74" width="80" height="22" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
      <text x="490" y="89" fill="#500724">feature</text>
    </g>

    <!-- merge caption -->
    <text x="620" y="265" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">merge commit — two parents</text>
    <text x="370" y="40" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">each commit points to its parent · labels move, the graph stays</text>
  </g>
</svg>
```

## Why version control at all

Before I used Git, my "version control" was project_final_v2_REAL_final.zip. It worked until two things happened at once: I wanted to try a risky change without wrecking the working version, and someone else needed to edit the same files. Copying folders solves neither cleanly [1].

A VCS fixes both by storing a **history of snapshots** of the project, not the current files. Every saved state — a *commit* — is a complete record of what the project looked like at one point, with a message explaining why it changed. The payoff is two-fold [3]:

- **Reversibility.** Any change can be undone, because every previous state still exists in the graph. "I broke it" stops being scary when the fix is "move the label back one commit."
- **Parallelism.** Two people can branch off the same point, work independently, and rejoin later without one clobbering the other's work. That's the "for whole teams" part of the undo button.

This is the layer that makes modern collaboration possible — code review, pull requests, releasing a fix while a feature is half-built, all rest on it. Without it you're back to shouting across the room before touching a file.

## The three states — where files actually live

Git confused me for a long time because files seem to be in several places at once. The model that cleared it up is that a file is always in one of **three states**, and the commands are mostly about moving files between them [4]:

```figure
<svg viewBox="0 0 620 200" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Git's three states as three boxes left to right. Working directory where you edit files, Staging area where you stage changes to be saved, and the repository where committed snapshots live. Arrows show: edits land in the working directory, git add moves them to staging, git commit moves them from staging into the repository.">
  <defs>
    <marker id="sarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- working directory -->
    <rect x="20" y="60" width="150" height="80" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="95" y="90" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Working directory</text>
    <text x="95" y="108" font-size="10" fill="#475569" text-anchor="middle">the files I'm editing</text>
    <text x="95" y="124" font-size="10" fill="#475569" text-anchor="middle">on disk, right now</text>

    <!-- staging -->
    <rect x="235" y="60" width="150" height="80" rx="10" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="310" y="90" font-size="13" font-weight="700" fill="#500724" text-anchor="middle">Staging area</text>
    <text x="310" y="108" font-size="10" fill="#475569" text-anchor="middle">changes I've marked</text>
    <text x="310" y="124" font-size="10" fill="#475569" text-anchor="middle">as "ready to save"</text>

    <!-- repository -->
    <rect x="450" y="60" width="150" height="80" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="525" y="90" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Repository (.git)</text>
    <text x="525" y="108" font-size="10" fill="#475569" text-anchor="middle">committed snapshots</text>
    <text x="525" y="124" font-size="10" fill="#475569" text-anchor="middle">the permanent history</text>

    <!-- arrows -->
    <line x1="172" y1="100" x2="233" y2="100" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <text x="202" y="92" font-size="10" font-family="ui-monospace, monospace" fill="#475569" text-anchor="middle">git add</text>

    <line x1="387" y1="100" x2="448" y2="100" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <text x="417" y="92" font-size="10" font-family="ui-monospace, monospace" fill="#475569" text-anchor="middle">git commit</text>

    <text x="310" y="170" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">edit → stage → commit. The commit is what writes a snapshot into history.</text>
  </g>
</svg>
```

The two-step mattered more than I expected. git add doesn't save anything to history — it just says "these edits are the ones I intend to save next." That lets me stage three files, leave a fourth half-finished in the working directory, and commit exactly the three. git commit is the act that actually writes a snapshot into the graph. Confusing the two is why beginners think they "saved" a change that isn't in any commit.

## Commits as snapshots, not diffs

The other thing I had to unlearn: a commit is not a diff (a list of lines that changed). Internally Git stores it as a **full snapshot of the project at that moment** — it just compresses and dedupes unchanged files so storing thousands of snapshots stays cheap [4]. The diff is *derived* afterward when you ask Git to compare two commits.

This is why reverting is clean. Because each commit is a complete state, moving to any past commit puts the project back exactly as it was, no matter how tangled the changes look in diff form. The history is a series of complete photographs, and the "what changed" view is computed by holding two photographs side by side.

## Branching and merging — labels on a graph

This is where the graph model earns its keep. A **branch** in Git is not a copy of the project and not a folder. It's a tiny pointer — a label — that names one commit, the *tip*. When I commit on a branch, Git creates a new commit whose parent is the current tip, and slides the label forward to the new commit [2]. That's it. Branches are cheap because they're just labels; the project data is shared across all of them.

**Merging** is the act of rejoining two paths that diverged. Two branches share a common ancestor; each has moved on since. A merge produces a commit with **two parents** — the tips of both branches — combining both lines of work into one new state (the merge commit M in the figure above). Most of the time Git works out the combination automatically; when the same line was edited on both branches it pauses and asks me to resolve the **conflict** by hand, picking the intended result [3].

The mental shift was treating branches as questions about the graph rather than as separate folders. "Create a branch" is "make a new label here and move it as I commit." "Switch branches" is "move my working directory to the commit this other label points at." The graph never duplicates; only the labels move.

## Local and remote — the distributed split

The last piece is why Git is called *distributed*. A Git repo is fully functional on my laptop with no network connection — every commit, branch, and the entire history live locally in the .git directory [5]. I can branch, commit, and merge all day offline.

A **remote** is just another copy of the same repo, usually on a host like GitHub. git push sends my local commits to the remote; git pull fetches the remote's new commits into my local graph [5]. The remote is not the source of truth in the way a central database is — it's a peer that the team has agreed to sync through. That's why I can commit on a plane and push when I land, and why "the server is down" never blocks me from saving work to history.

This local-vs-remote split is the concrete shape of the "undo button for whole teams." History lives on every machine, so it's durable; the remote is the shared meeting point where parallel histories get synced and merged back together.

## How I use this

The payoff of the graph model is that the commands stopped feeling arbitrary. When something goes wrong — I committed to the wrong branch, or I want to throw away the last hour — I now ask *what shape does the graph have, and what shape do I want?* Most operations are then obvious: undo a commit by moving the label back, combine branches by merging, experiment safely by making a new label and committing there. The history is a graph I can walk in any direction, and branches are just the names I gave to the spots worth coming back to.

## References

[1] Atlassian, "What is version control?," Git Tutorials, 2024. [Online]. Available: [https://www.atlassian.com/git/tutorials/what-is-version-control](https://www.atlassian.com/git/tutorials/what-is-version-control)

[2] S. Chacon and B. Straub, "Getting Started — What is Git?," *Pro Git*, 2nd ed., 2024. [Online]. Available: [https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F](https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F)

[3] Atlassian, "Git branching," Git Tutorials, 2024. [Online]. Available: [https://www.atlassian.com/git/tutorials/using-branches](https://www.atlassian.com/git/tutorials/using-branches)

[4] S. Chacon and B. Straub, "Recording Changes to the Repository," *Pro Git*, 2nd ed., 2024. [Online]. Available: [https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)

[5] Atlassian, "Setting up a repository — What is a version control repository?," Git Tutorials, 2024. [Online]. Available: [https://www.atlassian.com/git/tutorials/setting-up-a-repository](https://www.atlassian.com/git/tutorials/setting-up-a-repository)

[6] "Why version control?," inter-git.com, 2024. [Online]. Available: [https://inter-git.com/lessons/introduction](https://inter-git.com/lessons/introduction)

```quiz
Q: A Git branch is best described as…
- a full copy of the project stored in a separate folder
- a movable label that names one commit (the tip)
correct: 1
explain: In Git, a branch is just a pointer to a commit. Committing on a branch creates a new commit and slides the label forward — the project data is shared, never duplicated.

Q: What does `git add` actually do?
- Saves the changes permanently into history
- Marks selected changes as "ready to be committed" (moves them to staging)
correct: 1
explain: `git add` moves edits from the working directory into the staging area. Nothing enters history until `git commit` writes the snapshot.

Q: Internally, a Git commit stores…
- a diff — only the lines that changed
- a full snapshot of the project (compressed and deduped)
correct: 1
explain: A commit is a complete snapshot of the project at that moment. Git compresses and reuses unchanged data, so diffs are derived by comparing two snapshots, not stored.

Q: Why is Git called a "distributed" version control system?
- The full history lives on every clone, so it works fully offline; a remote is just a peer to sync with
- It requires a central server for every commit
correct: 0
explain: Every clone holds the entire history in .git, so committing, branching, and merging all work offline. The remote is a shared peer you push to and pull from, not a mandatory authority.

Q: A merge commit is special because it has…
- one parent, same as every other commit
- two parents — one from each branch being rejoined
correct: 1
explain: A merge rejoins two diverged paths, so the merge commit records both tips as its parents, combining both lines of work into one new state.
```
