---
title: 'Branch Creation and Switching'
source: 'https://academia.sh/en/courses/branching-and-collaboration/branch-creation-and-switching'
course: 'Branching and Collaboration'
language: en
updated: '2026-08-17T18:10:43+00:00'
license: 'CC BY-SA 4.0'
---

# Branch Creation and Switching

Opening a branch and switching to it are separate operations: the switch only rewrites files that differ between the two branches, and of the twelve remaining-change cases, 3 move along, 5 block the switch, 4 are left behind; none is silently lost.

The previous lesson measured branching's cost: opening three branches writes **0** new
commits and **123** bytes. The measurement deliberately left one thing out. Once a
branch name is written, there is a new reference under `refs/heads`, but not a single
file in the working directory has changed. `HEAD` still points to the old branch. The
name has been written; it has not been switched to.

This lesson's question is the switch itself. Switching is not just writing a reference;
it has to bring the working directory and staging area to what the target branch sees.
Two questions arise from this: how many files does the switch touch, and if there is an
uncommitted change in hand at that moment, what happens to it? The second question has
no single answer: the outcome depends on the change's type and whether the file differs
between the two branches.

## Two Separate Commands

Creating a branch and switching to it are two separate invocations of the tool. The
first only writes the reference; the second updates `HEAD`, the staging area, and the
working directory together.

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

$ git branch metrics
$ git status --short --branch
## main

$ git switch metrics
Switched to branch 'metrics'
$ git status --short --branch
## metrics
```

After the first call, `git status` still says `main`: the branch exists, but has not
been switched to. After the second call, the same line says `metrics`. No file name
appears in between, because there is no difference between the two branches yet; the
measurement will shortly count the case where there is a difference.

There is a shortcut that does both jobs in one call: `git switch -c metrics` first
writes the reference, then switches to it; the older form is `git checkout -b metrics`.
`git checkout` is an overloaded name — the same command both switches branches and
restores a single file, and the distinction can only be read from the shape of the
call. `git switch` and `git restore` split these two jobs into two names. This course
uses the separate names throughout.

## The Switch's Three Steps

Switching to a branch is three separate write operations, and the three behave as a
single operation: if one fails, none of them are applied.

The first is rewriting `HEAD`. The previous lesson established that `HEAD` points not
to a commit but to a branch name; switching changes this symbolic reference's content,
and its cost is a single line. The second is rebuilding the staging area to match the
target branch's tree. The third is the working directory: files that are **different**
on the target branch are replaced with the target's version.

The scope of the third step is critical. The switch does not rewrite all of the
branch's files, only the ones that are **different** between the two branches. The
difference is of two kinds: content difference (the file exists on both branches, but
their versions differ) and existence difference (the file exists on one branch, not on
the other). In the second case, the switch either creates a file or deletes one. This
is why the switch's cost depends not on the branch's size but on the difference — and
the top table counts exactly this.

## The Change Left in Hand: Three Rules

If there is an uncommitted change in the working tree at the moment of switching, the
tool's behavior is determined by three rules.

**First rule.** If the file is **identical** between the two branches, the switch does
not touch it. Every uncommitted change on it — whether in the working directory or
already staged — appears unchanged on the new branch. This is usually the wanted
behavior: a small fix started on one branch comes along with you if that file has not
changed on the other branch.

**Second rule.** If the file is **different** between the two branches, the switch has
to rewrite it. If there is an uncommitted change on it, the tool **stops** the switch
instead of overwriting that change. Nothing is written, `HEAD` stays in place, and the
error message says which file is blocking it.

**Third rule.** An untracked file sits outside history and is in no branch's tree. If
the target branch has no file by the same name, it stays put through the switch. If a
tracked file by the same name exists, the switch would write over it, and it is
**stopped** again. The subtle point here is that the file blocks the switch even though
it sits outside history, because the block's cause is not the file's record but the
clash of its name.

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

$ git switch report
error: Your local changes to the following files would be overwritten by checkout:
	report.py
Please commit your changes or stash them before you switch branches.
Aborting

$ git switch report
error: The following untracked working tree files would be overwritten by checkout:
	config.py
Please move or remove them before you switch branches.
Aborting
```

The two messages come from two separate rules, and both report the same outcome: the
switch did not happen. The second message's suggested fix is also different — an
untracked file cannot be committed or stashed, so it asks for it to be moved or
removed.

## Three Ways Out

A blocked switch has four options in front of it, and three of them preserve the
change.

The first is **committing** the change. The change is written to the old branch's tip,
becomes reachable from a reference, and the switch is freed. The change does not come
along with the switch; it stays on the old branch and is found there when you return to
it.

The second is taking it to the **stash**. `git stash push` takes the uncommitted change
out of the working tree and writes it to a separate reference under `refs/stash`; the
tree is cleaned, and the switch is freed. `git stash list` lists the stack, `git stash
pop` reapplies the top one. A stash is not tied to a branch — opening it on a different
one can produce a conflict if the file differs there. This behavior belongs to the
Merging topic and is covered there.

The third is **forcing the change to move**: the `git switch -m` option tries to merge
the uncommitted change with the target branch's version. When it succeeds, the change
appears on the new branch; when it does not, it leaves a conflict in the working tree
that has to be resolved. This lesson does not resolve conflicts and mentions the option
only by name.

The fourth is **discarding** the change. The `git switch` command has an option for
this, and it deletes the uncommitted change irrecoverably; the change was never written
to a commit or a reference, so there is nowhere to recover it from. This lesson does
not write that option in executable form. The safe habit: stash before discarding — a
stash can be undone, a deletion cannot.

The measurement's assumptions:

- **BR7** — The setup is the shared definition's twelve commits and is not changed.
  The base tree has three files: `metrics.py`, `report.py`, `identity.py`. `config.py`
  is **not** in the base; on the third step, `metrics` and `report` add it, so it is not
  found in the `identity` branch's tree.
- **BR8** — A branch's working tree is the file versions seen from that branch's tip:
  the version left by the branch's last commit, or the base version if untouched.
- **BR9** — The switch only rewrites the file that differs between the two branches.
  The difference covers both content difference and existence difference.
- **BR10** — Three rules apply: a change on an identical file moves along; an
  uncommitted change on a different file blocks the switch; an untracked file moves
  along if the target has no file by the same name, and blocks the switch if it does.
- **BR11** — Twelve cases are tried in a single switch, from the `identity` branch to
  the `report` branch. Five kinds of change are counted: modified in the working
  directory, staged, untracked, committed, stashed.
- **BR12** — A committed or stashed change is of no concern to the switch: both are
  already written to a reference. The switch does not move them, it leaves them where
  they are; the measurement counts this outcome as **left behind**.
- **BR13** — A silent loss is a change disappearing without the user being told, and
  it is counted separately. A blocked switch is not a loss; nothing was written.
- **BR14** — The measurement is not a filesystem measurement. Two things are counted:
  the file the switch has to rewrite, and where the state left in hand at the moment of
  switching ends up.

## Measurement

```python
"""Creating a branch and switching to it are separate operations.

Part 1 - the file difference between two branches: which files the switch rewrites.
Part 2 - twelve cases: where the change left in hand ends up after the switch.
"""
SEED = 20260813
BRANCHES = ("metrics", "report", "identity")
FILES = {"metrics": "metrics.py", "report": "report.py", "identity": "identity.py"}
BUGGY = ("report", 2)
SHARED_FILE = "config.py"
BASE = ("metrics.py", "report.py", "identity.py")   # config.py is not in the base


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

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


def development():
    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


def tree(record, branch):
    """Working tree at a branch's tip: file -> version."""
    s = {d: "base" for d in BASE}
    for k in record:
        if k["branch"] == branch:
            s[k["file"]] = f"{branch}{k['step']}"
    return s


def diff(record, source, target):
    """Files the switch has to rewrite."""
    a, b = tree(record, source), tree(record, target)
    return sorted(d for d in set(a) | set(b) if a.get(d) != b.get(d))


SOURCE, TARGET = "identity", "report"
CASES = (("metrics.py", "working directory"), ("report.py", "working directory"),
         ("identity.py", "working directory"), ("metrics.py", "staging area"),
         ("report.py", "staging area"), ("identity.py", "staging area"),
         ("note.txt", "untracked"), ("config.py", "untracked"),
         ("report.py", "committed"), ("identity.py", "committed"),
         ("report.py", "stashed"), ("identity.py", "stashed"))
LOCATION = {"moves": "in the working tree", "blocked": "on the old branch, switch did not happen",
            "committed": "at the old branch's tip", "stashed": "on the stash stack"}


def outcome(file, kind, differing, target_tree):
    if kind in ("committed", "stashed"):
        return "left behind"
    if kind == "untracked":
        return "blocked" if file in target_tree else "moves"
    return "blocked" if file in differing else "moves"


record = development()
print(f"setup: {len(record)} commits, {len(BRANCHES)} branches, base tree "
      f"{len(BASE)} files, files branches touch "
      f"{len({k['file'] for k in record})}")
print()
print(f"{'switch':<20s}{'files rewritten':>19s}   which ones")
for a, b in [("base", d) for d in BRANCHES] + [("identity", "report"),
                                               ("metrics", "report")]:
    f = diff(record, a, b)
    print(f"  {a + ' -> ' + b:<18s}{len(f):19d}   {', '.join(f)}")

differing, target_tree = set(diff(record, SOURCE, TARGET)), tree(record, TARGET)
print()
print(f"switch {SOURCE} -> {TARGET}: twelve remaining cases")
print(f"{'  what is in hand':<34s}{'on target':<11s}{'outcome':<14s}where the change goes")
counts = {}
for file, kind in CASES:
    if kind in ("committed", "stashed"):
        on_target = "-"
    elif file not in target_tree:
        on_target = "absent"
    elif kind == "untracked":
        on_target = "present"
    else:
        on_target = "different" if file in differing else "identical"
    s = outcome(file, kind, differing, target_tree)
    counts[s] = counts.get(s, 0) + 1
    print(f"  {file + ' (' + kind + ')':<32s}{on_target:<11s}{s:<14s}"
          f"{LOCATION[kind] if s == 'left behind' else LOCATION[s]}")
print()
for s in ("moves", "blocked", "left behind"):
    print(f"{s:<14s}{counts.get(s, 0):3d} / {len(CASES)}")
print(f"{'silent loss':<14s}{0:3d} / {len(CASES)}")
```

```
setup: 12 commits, 3 branches, base tree 3 files, files branches touch 4

switch                  files rewritten   which ones
  base -> metrics                     2   config.py, metrics.py
  base -> report                      2   config.py, report.py
  base -> identity                    1   identity.py
  identity -> report                  3   config.py, identity.py, report.py
  metrics -> report                   3   config.py, metrics.py, report.py

switch identity -> report: twelve remaining cases
  what is in hand                 on target  outcome       where the change goes
  metrics.py (working directory)  identical  moves         in the working tree
  report.py (working directory)   different  blocked       on the old branch, switch did not happen
  identity.py (working directory) different  blocked       on the old branch, switch did not happen
  metrics.py (staging area)       identical  moves         in the working tree
  report.py (staging area)        different  blocked       on the old branch, switch did not happen
  identity.py (staging area)      different  blocked       on the old branch, switch did not happen
  note.txt (untracked)            absent     moves         in the working tree
  config.py (untracked)           present    blocked       on the old branch, switch did not happen
  report.py (committed)           -          left behind   at the old branch's tip
  identity.py (committed)         -          left behind   at the old branch's tip
  report.py (stashed)             -          left behind   on the stash stack
  identity.py (stashed)           -          left behind   on the stash stack

moves           3 / 12
blocked         5 / 12
left behind     4 / 12
silent loss     0 / 12
```

## Reading the Numbers

The top table gives the switch's cost. Switching from the base to the `identity` branch
writes **1** file; switching to the `metrics` and `report` branches writes **2**.
Switches between two branches write **3** files, because each branch's own file and the
shared `config.py` file both differ. The tree's fourth file stays put on every row.

This number completes the previous lesson's measure. Opening a branch wrote **41**
bytes and was independent of the tree's size. Switching to a branch touches the tree,
but not the whole tree: in a four-thousand-file tree, if two branches diverge on three
files, the switch writes **3** files. In the copy model, the same job would require
copying all four thousand files. This is the pointer model's counterpart at switch
time — the cost is not the size of the tree, but the size of the **difference**.

The bottom table is the lesson's main measure. **3** of the twelve cases move along,
**5** block the switch, **4** are left behind. Silent loss is **0**.

What the three that move along have in common is that the switch never touches that
file at all. `metrics.py` is at the base version on both branches, so it brings along
the change whether it is in the working directory or the staging area. `note.txt` is in
no branch's tree and the switch does not care about it. In both cases, moving along is
not a capability, it is the result of **not colliding**.

The blocked five all have a single cause: the file is different between the two
branches, and the switch is going to rewrite it. The working directory and staging area
make no difference here — all four come from the same two files, `report.py` and
`identity.py`. The fifth, `config.py`, reaches the same outcome by a different path: it
is untracked and appears in no commit, but the target branch has a tracked file by the
same name. Sitting outside history does not mean not blocking the switch.

Zero silent loss is a design decision. The tool never deletes an uncommitted change
without reporting it; it blocks instead. The **5/12** stopping rate is not a defect, it
is the cost of that decision: when a switch gets blocked in your workflow, nothing has
been lost, there is only something not yet recorded.

The four left behind are the user's decision, not the tool's, and there is a difference
between the two. A committed change sits at the old branch's tip; it is reachable from
a branch reference and shows up when the branch name is listed. A stashed change sits
under `refs/stash`; it is reachable but shows up on no branch, and once forgotten it is
harder to find than searching branches. The case where a commit is reachable from no
branch reference at all is this topic's fourth lesson's subject.

In a twelve-case set, the smallest measurable difference is **1/12**. The **2/12** gap
between moved and blocked is twice this band and defensible; finer distinctions cannot
be defended with this setup.

## Summary

- Creating a branch only writes a reference; switching to it updates `HEAD`, the
  staging area, and the working directory together. `git switch -c` does both jobs in
  one call.
- The switch does not rewrite all of a branch's files, only the ones that are
  **different** between the two branches; in the setup, this count is **1** to **2**
  files from the base to a branch, and **3** between branches.
- An uncommitted change on an identical file moves along; a change on a different file
  stops the switch; an untracked file only stops the switch if the target has a file by
  the same name.
- **3** of the twelve cases move along, **5** are blocked, **4** are left behind;
  silent loss is **0** — the tool stops the switch instead of deleting an uncommitted
  change.
- A blocked switch has three safe ways out: committing, stashing, and forcing the move
  with a merge; discarding is irreversible, and the stash is its safe counterpart.

## Next Step

Which file a switch touches is read off the difference between two branches, computed
without looking at their names at all. The name, though, serves a different question.
In the measurement, the names `identity` and `report` never changed the switch's
outcome; but with thirty branches in the same repository, which one belongs to which
piece of work, which one can be closed, which one is long-lived — all of this is read
from names alone. The next lesson measures a naming convention: feature grouping, the
first of history's questions, answers yes once a branch record is kept, but if the
names are disorderly, the group itself still cannot be read — the measurement shows the
two conditions do not substitute for each other.
