---
title: 'The Concept of a Branch'
source: 'https://academia.sh/en/courses/branching-and-collaboration/concept-of-a-branch'
course: 'Branching and Collaboration'
language: en
updated: '2026-08-17T18:10:43+00:00'
license: 'CC BY-SA 4.0'
---

# The Concept of a Branch

A branch is a pointer: in a twelve-commit setup, opening three branches writes zero new commits and 123 bytes of reference, and while the copy model copies twelve thousand files for the same tree at four thousand files, the pointer model's cost stays constant.

The Introduction to Version Control course built history as a single line. Working
directory, staging area, and commit followed one another; every new commit pointed to
the one before it, and the order stayed the same however the history was read — a
single path from newest to oldest. In that course, history was **linear and
single-author**: one person, one order, one line.

When more than one person works in the same repository, this assumption breaks down.
Two people start from the same commit and move in separate directions, and history is
no longer a line but a **fork**. This course's question is not how a fork opens —
opening one is cheap, and this lesson measures exactly how cheap. The real question is
how a fork **closes**, because the way it closes determines which questions the history
can answer afterward.

## The Course's Measure

The course uses a single setup throughout. Three people start three separate pieces of
work in the same repository: `metrics`, `report`, and `identity`. Each makes four
commits, and each commit touches its own file; on the third step, two of them both touch
the `config.py` file. The `report` work's second commit introduces a bug. The real
development is **twelve commits**, and its order is known, because we wrote the setup
ourselves.

These twelve commits can be brought into the main branch in four separate **integration
formats**: merge commit, rebase, squash, and fast-forward. All four produce the same
**code**. The **history** they produce differs, and the course measures this difference
with a single thing: six questions are asked of the history, and how many it can answer
is counted. These are called the course's **answerable questions**, and their
definitions do not change throughout the course.

1. **Feature grouping** — which commits belong to which piece of work?
2. **Bug isolation** — can the commit that introduced the bug be found on its own?
3. **True order** — in what order was the work developed?
4. **File trail** — can every commit that touched a file be found individually?
5. **Branch integrity** — do a branch's commits stay together?
6. **Conflict decision** — where was the conflict resolved, and which side was chosen?

This lesson answers none of the questions. What it measures is an earlier step: the
cost of opening a fork.

## A Branch Is a Pointer

The **reference (ref)** was introduced in the Creating and Configuring a Repository
lesson: a commit ID bound to a name. A branch is nothing more than this. The content of
a reference under `refs/heads` is a forty-hexadecimal-digit ID and a line ending;
forty-one bytes in total.

```text
# taught command and example dump — not executed

$ git branch metrics
$ git branch
* main
  metrics
$ git rev-parse metrics
3f1a9c4e6b2d8057a41c9e3b7d604f2a8c15be93
$ git symbolic-ref HEAD
refs/heads/main
```

References can be kept either as loose files or in packed form, so it is the output of
`git rev-parse` that is trusted, not the existence of the `refs/heads/metrics` file. The
ID itself is specific to the repository; the value here is from the setup, and you will
see a different value in your own repository. What stays fixed is the reference's
**structure**: one name, one ID.

`HEAD`, meanwhile, points not to a commit but to a **reference**; this is called a
symbolic reference. The chain has three links: `HEAD` points to a branch name, the
branch name points to a commit ID, and the commit points to its own parent. Committing
on a branch changes only the middle link — the ID in the branch reference is replaced
with the new commit's ID, and forty-one bytes are rewritten. `HEAD` stays in place,
because it still points to the same branch.

The lightweight tag from the Tagging lesson also keeps an ID under `refs`; the
difference comes down to one word: **mobility**. A tag binds to a commit once and never
moves on its own; a branch reference is rewritten on every commit and tracks the
branch's tip. They are two uses of the same data structure: one for fixing a point, the
other for dragging one along. This is why work does not happen on top of a tag, it
happens on top of a branch — and as long as `HEAD` points to a branch name, every commit
made **attaches** to something. The case where `HEAD` points directly to a commit ID
instead of a branch name has a separate name, and it is covered in this topic's fourth
lesson.

This has a direct consequence: **a branch does not contain commits.** A branch only
points to one tip commit, and what we call "the branch's commits" is the set reachable
from that tip by following parent edges. The definition of **reachability** from the
Reading History lesson applies here exactly as it was. Two branches' common ancestors
are found in both sets at once, and they have not been copied anywhere.

## The Shape of the Fork

In the Reading History lesson, the output of `git log --graph` gave a single-column
drawing; that history had no branching. The setup's history has three columns. It is
clearer to draw the shape independent of the command's output:

```text
# the shape of the history — a diagram, not command output

                 metrics  ->  4 commits
                /
  base   ------+------->  report   ->  4 commits
                \
                 identity ->  4 commits
```

The first commit reached when all three branches are traced backward is called the
**merge base**. The merge base is a commit, not a branch; it is the same commit
regardless of which branch's tip you are standing on, and it determines where the
difference starts when two branches are compared. The rest of this course works on top
of the merge base: merging, rebasing, and the question "what is in this branch that is
not in the other" are all computed starting from the base.

Branch tips can also be listed on a single line:

```text
# taught command and example dump — not executed

$ git branch -v
* main     0a5f3c9 Add metrics scaffold
  identity 9d4c1a7 Complete identity verification step
  metrics  6e2d70b Move metrics threshold into configuration
  report   7c3f9d2 Format report heading
```

The asterisk marks which branch `HEAD` points to. Every line in the list is nothing more
than a name and an ID; there is no "content" column for a branch, because no such thing
is stored.

## The Copy Model and the Pointer Model

The habit before version control was to copy the entire working tree to separate out a
piece of work. This is the **copy model**, and its cost is obvious: as many copies as
there are files in the tree. The cost grows directly proportional to the tree's size,
because what gets set aside is the tree itself.

The copy model has a heavier cost still, and this cost is not measured in bytes. The
copied tree sits **outside** history: no parent edge is attached to the copy, it has no
commit ID, and it is reachable from no reference. Merging two copies afterward means
comparing them with no knowledge of what the common base was — which change is new and
which already exists in the other can only be worked out by hand. None of this course's
six questions can be asked of the copy model, because there is no history there to
query.

In the **pointer model**, what gets set aside is not the tree but a name given to a
snapshot of the tree. The files already sit in the object database, and because it is
content-addressed, the same content is never stored twice. A new branch is nothing more
than giving a second name to an existing commit. The measurement puts these two models
side by side over the same setup.

The measurement's assumptions:

- **BR1** — The setup is three pieces of work and twelve commits; the oracle is known
  because we produced the setup ourselves, and it tells which piece of work every commit
  belongs to and which file it touches.
- **BR2** — The three branches diverge from a shared base commit. The base is outside
  the twelve commits and is the only commit all three branches can reach.
- **BR3** — A branch reference consists of a forty-hexadecimal-digit ID and a line
  ending: forty-one bytes. The measurement is not a disk measurement, it is a count of
  the content written.
- **BR4** — The copy model counts a branch as a full copy of the working tree, and its
  cost is the number of files copied. The pointer model counts a branch as a single
  reference, and its cost is the bytes written.
- **BR5** — The working tree is tried at four sizes: the number of files the setup
  touches, and three enlargements. The setup's commit count does not change across
  these rows; only the tree's width changes.
- **BR6** — Reachability only follows parent edges. A commit reachable from no
  reference cannot be queried in history; sitting in the object database does not put
  it back into history.

## Measurement

```python
"""A branch is a pointer: branching's cost is not files copied, but the reference written."""
SEED = 20260813
BRANCHES = ("metrics", "report", "identity")
FILES = {"metrics": "metrics.py", "report": "report.py", "identity": "identity.py"}
BUGGY = ("report", 2)
SHARED_FILE = "config.py"
REF_BYTES = 40 + 1        # forty hex digits + line ending


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def development():
    """Real development: (branch, step, file, buggy, time) tuples."""
    draw, record, time = rng(SEED), [], 0
    for step in range(1, 5):
        for branch in BRANCHES:
            time += 1 + draw(3)
            file = SHARED_FILE if step == 3 and branch in ("metrics", "report") else FILES[branch]
            record.append({"branch": branch, "step": step, "file": file,
                          "buggy": (branch, step) == BUGGY, "time": time})
    return record


record = development()
tree = sorted({k["file"] for k in record})
print(f"setup: {len(record)} commits, {len(BRANCHES)} branches, "
      f"{len(record) // len(BRANCHES)} commits per branch, files touched {len(tree)} {tree}")
print(f"opening three branches: new commits 0, references written {len(BRANCHES)}, "
      f"bytes written {len(BRANCHES) * REF_BYTES}")

print()
print("working tree  copy model: files copied  pointer model: bytes written")
for n in (len(tree), 40, 400, 4000):
    print(f"{n:13d} {n * len(BRANCHES):27d} {len(BRANCHES) * REF_BYTES:26d}")

print()
print("branch    tip commit  reachable commits  unreachable if ref deleted")
for branch in BRANCHES:
    group = [k for k in record if k["branch"] == branch]
    print(f"  {branch:8s} {group[-1]['step']:10d} {1 + len(group):18d} {len(group):27d}")
print(f"union reachable from three refs: {1 + len(record)} commits "
      f"(base + {len(record)}), references written {len(BRANCHES)}")
```

```
setup: 12 commits, 3 branches, 4 commits per branch, files touched 4 ['config.py', 'identity.py', 'metrics.py', 'report.py']
opening three branches: new commits 0, references written 3, bytes written 123

working tree  copy model: files copied  pointer model: bytes written
            4                          12                        123
           40                         120                        123
          400                        1200                        123
         4000                       12000                        123

branch    tip commit  reachable commits  unreachable if ref deleted
  metrics           4                  5                           4
  report            4                  5                           4
  identity          4                  5                           4
union reachable from three refs: 13 commits (base + 12), references written 3
```

## Reading the Numbers

The first line gives the setup's size: **12** commits, **3** branches, **4** files
touched. The second line is this lesson's main measure. Opening three branches writes
**0** new commits and **123** bytes. Zero means branching adds nothing to history;
opening a branch is not a record, it is a name.

The middle table shows how the cost behaves. In the setup's four-file tree, the copy
model copies **12** files, the pointer model writes **123** bytes. When the tree grows
to forty files, the copy model climbs to **120**; at four hundred files, to **1200**; at
four thousand, to **12000**. The pointer column stays **123** on all four rows. The
copy model's cost depends on the size of the tree; the pointer model's cost depends
**only on the number of branches**. The gap between them opens a thousandfold on a
thousandfold-larger tree, and this is the single reason branching could become a habit
instead of an exception.

The bottom table gives a second result. **5** commits are reachable from every branch
tip: the branch's own four, plus the shared base. The three tips do not add up to
fifteen but to **13**, because the base is found in all three sets and is counted once.
**Sharing** a branch is subject to the same economy: writing a third name pointing at
the same base is, again, forty-one bytes.

The setup's resolution is also read off from here. In a twelve-commit set, the smallest
measurable difference is **1/12**; in a six-question set, the smallest difference is
**1/6**. A branch's four commits are a third of the set, a single buggy commit is a
twelfth. Differences below this resolution cannot be defended with this setup, and the
numbers written throughout the course are kept above it.

The last column looks ahead in the course. If a branch reference is deleted, the **4**
commits reachable from that branch become reachable from no reference at all. The
commits keep sitting in the object database, but none of the six questions asked of
history can reach them — because the questions are asked of history, and history is the
set reachable from references. The pointer's cheapness and its fragility are two faces
of the same fact: if writing forty-one bytes is easy, losing forty-one bytes is easy
too.

## The Main Branch Has No Privilege

There is a result the measurement does not say directly but that follows from its
numbers. Opening three branches writes **123** bytes, and this byte count is the same
for every branch; the setup's `main` branch, too, is a forty-one-byte reference under
`refs/heads`. As far as the tool is concerned, the main branch has no privilege: it is
not read faster, kept more safely, or protected from deletion. Privilege is the team's
decision, not the data structure's.

This has two practical consequences. First, the name "main branch" is itself a
convention and can differ from repository to repository; commands look not at a default
name but at the reference `HEAD` points to. Second, every protection that makes the
main branch privileged is set up **outside the repository**: who can write to which
branch, which branch cannot be deleted, which branch cannot be committed to
directly — none of this is written into the reference itself. The repository does not
ask who wrote the forty-one bytes.

What matters for this lesson is this: **which** commit a branch tip points to is a data
question; what that branch **means** is a convention question. The answer to the second
is naming, and this topic's third lesson measures it.

## Summary

- A branch is a commit ID bound to a name under `refs/heads`; its content is forty hex
  digits and a line ending, forty-one bytes in total.
- `HEAD` points not to a commit but to a branch reference; committing changes only the
  ID in the branch reference, and `HEAD` stays in place.
- A branch does not contain commits; "the branch's commits" is the set reachable from
  the branch tip by following parent edges, and shared ancestors are found in both sets
  without being copied.
- Opening three branches writes **0** new commits and **123** bytes; the copy model
  does the same job by copying **12** files in a four-file tree and **12000** files in
  a four-thousand-file tree.
- **5** commits are reachable from one branch tip, **13** from the union of three tips;
  if a reference is deleted, the **4** commits reachable from that branch fall outside
  history.

## Next Step

Opening a branch is a name, and its cost has been measured. But the moment the name is
written, the working tree still carries the old branch's content; **switching** to the
branch is a separate operation and requires the working directory and staging area to
be adapted to the new branch. If there is an uncommitted change in hand at that moment,
a decision has to be made: does the change move along with the switch, and if it cannot,
is the switch blocked? The next lesson separates branch creation from switching and
counts, over twelve cases, which change moves, which one blocks, and which one is left
behind.
