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

# Branch Strategies

How long a branch lives determines what gets written to history: the same twelve commits leave 12 with no branch opened, 15 under a pattern that returns every four commits, and 24 under a pattern that returns every commit — and once the divergence window closes, a merge conflict never arises at all.

In the previous lesson, the format choice was made for a single merge: how this branch
should be taken into main. Teams do not make this decision anew each time; they make it
once and turn it into a rule.

The rule does not only cover the moment of merge. It also determines how long branches
will live, how many there will be, and how often they will return to main. This lesson's
question is this: when the same twelve-commit development is run under different branch
patterns, what remains in history — and which question does shortening branch lifetime
remove.

The discussion of continuous integration's build, test, and deployment pipeline is not
this course's subject; it is built in the **Continuous Integration and Delivery** course
and is not repeated here. The only thing measured is what the strategy leaves in
history.

## Branch Lifetime Is a Number

The difference between branch strategies reduces to a single quantity: **how many
commits does a branch accumulate before returning to main?** This number determines
every other property of the pattern.

Under the **long-lived branch** pattern the number is large. The branch stays open until
an entire piece of work is finished; it can live for weeks and carry dozens of commits.
Under the **short-lived branch** pattern the number is small; the branch lives less than
a day, carries a few commits, and closes. There is a further pattern at the extreme: no
branch is opened at all, and everyone commits directly to the main line.

All three patterns get the same work done and produce the same code. Where they differ
is the record they leave in history and how far apart the branches drift from each
other.

The short-lived pattern's daily cycle reduces to a few commands and repeats identically
every round.

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

$ git switch -c metrics/threshold main
$ git commit -am "metrics: print threshold value"
$ git switch main
$ git merge --no-ff metrics/threshold
$ git branch -d metrics/threshold
```

The fifth line is the pattern's distinguishing part: the branch is closed right after
the work enters main. Short branches that go unclosed pile up, and the branch list
turns into a pointer junkyard — since the record already sits in history as a closing
commit, the pointer's presence answers no question.

Long-lived branches are not a single class. A feature branch carrying one piece of work
closes when the work is done; a branch carrying a published release does not close, it
lives for months and takes only fixes. In the second case, divergence is **by design** —
that branch drifting away from main is not a flaw, it is the purpose. Because of this,
fixes have to go separately to that branch and to the development line; the
single-commit copy from the fourth lesson is used most often here, and the duplication
its copy demands piles up here too.

The naming pattern becomes decisive here. Under the short-lived pattern, branch count
grows not with the size of the work but with the frequency of returns; a piece of
work's successive rounds become separate branches. If branch names are not set up to
tie these rounds to the same piece of work, branch records in history multiply but none
of them shows the work as a whole. Naming's contribution to history's first question was
established in this course's third lesson; here that same contribution is multiplied by
branch count.

## Divergence and the Conflict Window

The second lesson's conflict was born on the setup's third step: `metrics` and `report`
touched the same file. But touching the same file is not enough for a conflict to be
born. **The two changes have to have been developed in parallel** — meaning, when the
second one started, the first must not yet have entered main.

If the first has already entered, the second branch sees it **as its base**. There is
then no two-sided change; there is a one-sided change, and the tool makes the decision
on its own. The same line changed twice, but in sequence, not in conflict.

From this comes branch lifetime's second effect: the longer a branch lives, the more
commits it **diverges** from main by, and the greater the chance that two pieces of work
touch the same file in parallel. Divergence is not a duration, it is a commit count; and
it equals the pattern's number.

This is the real reason for the short-lived branch pattern. The reason is not "resolving
a conflict becomes easier"; it is that **the conflict never arises at all**. In
exchange, a short branch requires unfinished work to sit on main; for unfinished work to
sit there unseen, it has to be able to be turned off at run time, and that mechanism
itself is the subject of the Continuous Integration and Delivery course.

The measurement's assumptions:

- **MR36** — A branch pattern is represented by a single number: how many commits a
  branch accumulates before returning to main. **Zero** means the branch is never
  opened.
- **MR37** — Every return writes one closing commit. If the pattern returns every four
  commits, this is exactly the merge commit format measured in the first lesson.
- **MR38** — Under the short-lived pattern, every round is a separate branch and
  carries a separate branch record; there is no field linking one piece of work's
  rounds to each other.
- **MR39** — Divergence is the largest number of commits a branch stays away from main,
  and it equals the pattern's number.
- **MR40** — Two touches count as parallel if, when the second one's branch opened, the
  first had not yet entered main. A merge conflict is born only from parallel touches.
- **MR41** — Time values are the setup's own clock, not an environment-dependent
  timestamp; they are used only for ordering.
- **MR42** — The six questions' definitions are not changed; they are run separately
  for each pattern.
- **MR43** — The set's resolution is **1/12** at twelve commits and **1/6** at six
  questions.

## Measurement

```python
"""Branch strategy: what branch lifetime leaves in history.

Part 1 - four branch patterns: no branch, returning every one, two, and four commits.
Part 2 - whether two commits touching the same shared file stay parallel.
"""
SEED = 20260813
BRANCHES = ("metrics", "report", "identity")
FILES = {"metrics": "metrics.py", "report": "report.py", "identity": "identity.py"}
BUGGY = ("report", 2)
SHARED_FILE = "config.py"
PATTERNS = ((0, "no branch"), (1, "return every commit"),
            (2, "return every two commits"), (4, "return every four commits"))


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 close_out(group, branch, round_num):
    """A round's commits and its closing commit."""
    name = f"{branch}#{round_num}"
    y = [{**x, "branch": name, "work": branch, "branch_record": name, "time_preserved": True}
         for x in group]
    y.append({"branch": name, "work": branch, "step": 0, "file": None, "buggy": False,
              "time": group[-1]["time"], "branch_record": name, "merge_commit": True,
              "time_preserved": True})
    return y


def history(record, k):
    """Branch pattern returning to main every k commits. k=0: branch never opened."""
    ordered = sorted(record, key=lambda y: y["time"])
    if k == 0:
        return [{**x, "work": x["branch"], "branch_record": None, "time_preserved": True}
                for x in ordered]
    t, buffer, round_count = [], {d: [] for d in BRANCHES}, {d: 0 for d in BRANCHES}
    for x in ordered:
        buffer[x["branch"]].append(x)
        if len(buffer[x["branch"]]) == k:
            round_count[x["branch"]] += 1
            t.extend(close_out(buffer[x["branch"]], x["branch"], round_count[x["branch"]]))
            buffer[x["branch"]] = []
    for d in BRANCHES:
        if buffer[d]:
            round_count[d] += 1
            t.extend(close_out(buffer[d], d, round_count[d]))
    return t


def q1_grouping(t):
    return all(x.get("branch_record") for x in t if not x.get("merge_commit"))


def q2_bug(t):
    buggy = [x for x in t if x["buggy"]]
    return len(buggy) == 1 and not buggy[0].get("squashed")


def q3_order(t):
    return all(x.get("time_preserved") for x in t)


def q4_file(t):
    return all(isinstance(x["file"], str) or x.get("merge_commit") for x in t)


def q5_integrity(t):
    positions = {}
    for i, x in enumerate(t):
        if x.get("merge_commit"):
            continue
        positions.setdefault(x["branch"], []).append(i)
    return all(y[-1] - y[0] == len(y) - 1 for y in positions.values())


def q6_conflict(t):
    return any("chosen" in x for x in t)


QUESTIONS = (("feature grouping", q1_grouping), ("bug isolation", q2_bug),
             ("true order", q3_order), ("file trail", q4_file),
             ("branch integrity", q5_integrity), ("conflict decision", q6_conflict))


def entry_time(record, k, branch, step):
    """The moment that commit entered main."""
    group = [x for x in record if x["branch"] == branch]
    if k == 0:
        return group[step - 1]["time"]
    return group[min(((step - 1) // k + 1) * k, len(group)) - 1]["time"]


def round_start(record, k, branch, step):
    """The moment the branch holding that commit's round was opened."""
    group = [x for x in record if x["branch"] == branch]
    if k == 0:
        return group[step - 1]["time"]
    return group[((step - 1) // k) * k]["time"]


record = development()
touches = [(k["branch"], k["step"]) for k in record if k["file"] == SHARED_FILE]
first, second = touches[0], touches[1]

print(f"real development: {len(record)} commits | commits touching {SHARED_FILE}: "
      f"{touches}")
print()
print(f"{'branch pattern':<24s} {'commits':>7s} {'closings':>8s} {'branch records':>14s}"
      f" {'branches/work':>13s} {'answered':>8s}")
for k, name in PATTERNS:
    t = history(record, k)
    closings = sum(1 for x in t if x.get("merge_commit"))
    records = {x["branch_record"] for x in t if x["branch_record"]}
    print(f"{name:<24s} {len(t):7d} {closings:8d} {len(records):14d}"
          f" {(len(records) / len(BRANCHES) if records else 0):13.1f}"
          f" {sum(s(t) for _, s in QUESTIONS):8d}")
print()
print(f"{'branch pattern':<24s} {'divergence':>10s} {'first touch enters':>19s}"
      f" {'second branch opens':>20s} {'parallel':>8s}")
for k, name in PATTERNS:
    enters = entry_time(record, k, first[0], first[1])
    opens = round_start(record, k, second[0], second[1])
    print(f"{name:<24s} {k:10d} {enters:19d} {opens:20d}"
          f" {('yes' if enters > opens else 'no'):>8s}")
```

```
real development: 12 commits | commits touching config.py: [('metrics', 3), ('report', 3)]

branch pattern           commits closings branch records branches/work answered
no branch                     12        0              0           0.0        3
return every commit           24       12             12           4.0        5
return every two commits      18        6              6           2.0        5
return every four commits      15        3              3           1.0        5

branch pattern           divergence  first touch enters  second branch opens parallel
no branch                         0                  17                   19       no
return every commit               1                  17                   19       no
return every two commits          2                  23                   19      yes
return every four commits          4                  23                    4      yes
```

## What They Left in History

The top table's last column is surprising at first glance: three of the four branch
patterns answer **5** of the six questions. The pattern that returns every four commits
is exactly the merge commit format from the first lesson and gives the expected number;
but the pattern that returns every commit and the pattern that returns every two
commits give the same **5**. **Six questions do not tell branch patterns apart.** The
no-branch pattern, meanwhile, stays at **3** — since no branch record is ever written,
feature grouping and branch integrity drop, and the result matches fast-forward's
result.

What tells the patterns apart is the columns on the left. The same twelve-commit
development leaves **15** commits under the pattern returning every four commits,
**18** under every two commits, **24** under every commit. The closing-commit count is
**3**, **6**, and **12** respectively. Under the last pattern, **half** of history is
closing commits: twelve of twenty-four records touch no file at all. This is four times
the **3/12** closing price measured in the previous lesson, and it grows linearly with
branch count — when return frequency doubles, closing count doubles too.

The branch-record column gives the second cost. Under the pattern returning every
commit, there are **12** separate branch records, and split across three pieces of work
that comes to **4.0** branches per piece of work. This number means history does not
show one piece of work as a single record: the feature grouping question says yes, but
the group it shows is not a piece of work, it is a **round**. The question is answered
at the branch level, and a branch has shrunk below a piece of work.

A warning follows from this. The six questions were set up to measure an integration
format, and they do that job; on their own they are not enough to measure a **branch
pattern**. The pattern's cost sits outside the questions, in the closing ratio and the
branches-per-work count. Knowing how far a measure holds matters as much as the measure
itself.

## What Divergence Costs

The bottom table looks the other way. The two commits touching `config.py` are the same
two commits in every pattern; what changes is whether the first one is on main by the
time the second one's branch opens.

Under the pattern returning every four commits, the `metrics` branch's third commit
enters main at moment **23**, while the `report` branch was opened at moment **4**. The
second branch opened so much earlier that it never sees the first one's change: the two
touches are parallel, and a conflict is born. Under the pattern returning every two
commits, divergence is cut in half but the result does not change; entry moment is
**23**, branch opening is **19**, and the touches are still parallel.

Under the pattern returning every commit, the table turns. The `metrics` branch's third
commit enters main at the moment it is written, **17**; the `report` branch's round
opens at **19**. The second branch sees the first one's change **as its base**, and
parallel is **no**. Even though the same file is touched, there is no conflict left to
resolve. Under the no-branch pattern, divergence is **0** and the result is the same.

The pattern is this: **once the divergence window closes, conflict does not arise.**
What the short-lived branch buys is not conflicts that are easy to resolve, it is
conflicts that **never arise**. What it pays is in the top table: twelve closing
commits and four branch records per piece of work.

A third cost sits outside the measurement and needs to be stated. Keeping divergence low
depends on a branch being able to return to main quickly; if the return goes through an
approval step, divergence stretches by the length of that step. So branch lifetime is
not only a writing habit, it is a **review speed**. The review flow's mechanics are set
up in this course's next topic; the review axis and culture are a separate course's
subject.

Read together, the two tables settle the strategy decision. A long-lived branch keeps
history short and readable at the level of a piece of work, and accumulates divergence
and conflict. A short-lived branch removes conflict, inflates history with closing
commits, and splits work into rounds. There is no single correct point between the two;
which cost is affordable is the team's question. The measurement only shows that the
two costs **cannot be reduced at the same time**.

## Summary

- The difference between branch strategies reduces to a single quantity: the number of
  commits a branch accumulates before returning to main. This number determines both
  history's length and its divergence.
- The same twelve-commit development leaves **12** commits under the no-branch pattern,
  **15** under every four commits, **18** under every two commits, **24** under every
  commit; the closing counts are **0**, **3**, **6**, and **12**.
- Three of the four branch patterns answer **5** of the six questions: six questions do
  not tell branch patterns apart. The no-branch pattern stays at **3**.
- Under the pattern returning every commit, **4.0** branch records fall to each piece
  of work; feature grouping says yes, but the group it shows is a round, not a piece of
  work.
- Conflict is born only from parallel touches; when divergence is **1** or **0**, the
  second branch sees the first one's change as its base, and conflict does not arise at
  all.

## Next Step

Everything this topic has built — four integration formats, resolving a conflict,
branch lifetime, and the strategy decision — was measured under a single-repository
assumption. History sat in one place, merges were done there, branch records were
written there.

In a real team, this assumption does not hold: history lives in **multiple copies**.
Each person has their own repository, the copies advance separately from each other, and
every question measured is asked once more — this time asking "in which copy." The next
topic starts from here and first answers the most basic question: how does one
repository recognize another, and how are multiple copies named and kept separate.
