Skip to content
academia.sh

Lesson 01 / 15

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.

Contents

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.

# 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:

# 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:

# 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

"""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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close