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

# Forking and Cloning

In the two contribution models, the question the upstream repository can answer depends on the integration format: 15 commits answer 5, 12 commits answer 3, 3 commits answer 2, and because the fork repository carries 12 commits and answers 5 questions, three questions stand only in a copy the upstream repository does not own.

The previous lesson's three operations worked under a single assumption: push access existed.
The local copy could change the other side's branch, and the only obstacle was the
fast-forwardability condition. In most contribution flows, this assumption does not hold. There
is no write access to the repository receiving the contribution, and the absence of that access
is not a constraint — it is the definition of ownership itself.

If there is no access, how does a contribution get in? There are two answers, and the two are
not names for the same thing. This lesson's measure is this: the same twelve commits are
produced under both models, but the history the **upstream repository** ends up holding is not
the same — and the difference determines which questions can be answered where.

## Cloning

**Cloning (clone)** is taking a copy's entirety to a new place: every commit, every branch, and
every tag is copied. The copy is complete; even if the cloned repository is deleted, the clone
is on its own a complete repository.

```text
# taught commands — example dump, not executed

git clone https://example.test/source/measurement-networks.git
cd measurement-networks
git remote -v

origin  https://example.test/source/measurement-networks.git (fetch)
origin  https://example.test/source/measurement-networks.git (push)
```

Cloning does three things at once: it takes the copy, writes a remote definition to the source
under the name `origin`, and sets up the default branch tied to this definition. It collects
into a single command what the previous two lessons did by hand; this is why where the
definition comes from is often not noticed.

The contribution model built on cloning is direct: the contributor clones the upstream
repository, opens a branch, makes commits, and **pushes the branch directly to the upstream
repository**. This model only works if the contributor has push access to the upstream
repository — that is, it is the usual form of contribution within a team.

## The Scope of a Clone

Cloning being complete is a default, not a requirement. Options narrow the copy's scope, and
the cost of narrowing can be read in the measure's own language.

A clone given a depth limit takes only the most recent commits; what came before never arrives.
A clone limited to a single branch never writes the other branches' references at all. In both
cases, history is **unchanged** — it stands exactly as it does at the source; only the scope is
what is not taken. The first lesson's distinction applies here exactly: this is not format
loss, it is scope loss, and scope loss is recoverable. Missing depth can be completed later, a
missing branch can be fetched later.

There are options in the opposite direction too. A clone taken without a working directory
carries only the repository data, and this is the usual form for copies kept on the server
side; a clone that copies every reference verbatim carries everything, remote-tracking
references included, and is the most direct way to take a backup.

The choice depends on what the copy is taken for. A copy taken for continuous integration has
no need for ten years of history; a copy searching for which commit a bug entered on needs the
full history, and that search cannot be done in a shallow clone. Narrowing the scope saves time,
and the time it saves is paid for with a question that will be asked later.

## Repository Forking

**Repository forking (fork)** is a copy of the upstream repository taken on the server side and
belonging to the contributor. The term shares a word with the process forking defined in the
Operating System Concepts course and is a **separate concept**: there, a process splitting in
two; here, a repository being copied into a second ownership domain. This capability is a class
of capability provided by repository platforms; no product name appears in this lesson.

The problem a fork solves is authority. The contributor has full authority in the fork
repository: opens whatever branch they want, pushes as much as they want, deletes whatever they
want. In the upstream repository, there is no write access at all.

```text
# taught commands — example dump, not executed

git clone https://example.test/contributor/measurement-networks.git
cd measurement-networks
git remote add upstream https://example.test/source/measurement-networks.git
git fetch upstream
git remote -v

origin    https://example.test/contributor/measurement-networks.git (fetch)
origin    https://example.test/contributor/measurement-networks.git (push)
upstream  https://example.test/source/measurement-networks.git (fetch)
upstream  https://example.test/source/measurement-networks.git (push)
```

In the fork model, the local repository carries **two remote** definitions: the fork itself and
the upstream repository. This is the structure that directly produces the namespace problem the
first lesson measured — `origin/main` and `upstream/main` are separate references, and the fork
falls behind as the upstream repository advances. Keeping the fork current amounts to fetching
from the upstream repository and pushing to the fork, and this work belongs to the contributor;
it does not happen on its own.

When the update is neglected, the divergence table the previous lesson measured comes directly
into play. As the fork's main branch and the upstream repository's main branch diverge, the
merge base falls back, and the contribution branch ends up resting on a base that no longer
exists in the upstream repository. This is the most predictable source of a merge conflict: the
conflict is born not from the contribution's content, but from the base going stale.

## The Measurement's Assumptions

- **RR13** — The same twelve-commit development is produced identically in both models; the
  difference between models is not in production, it is in **which copy the commits sit in**.
- **RR14** — In the cloning model, the contributor pushes branches directly to the upstream
  repository; there is no separate copy, and the measurement writes **0** in the fork column.
- **RR15** — The fork repository carries branches as they were developed: branch record in
  place, time preserved, no squash applied. This is the state the contributor sees in their own
  copy.
- **RR16** — The transition to the upstream repository is done with one of four integration
  formats, and the format enters the measurement; the formats' definitions are identical to the
  Merging and Rebasing topic's and are not changed here.
- **RR17** — The "only in fork" column counts, of the questions the fork repository answers,
  the ones the upstream repository cannot; in the cloning row there is no separate copy, so this
  set is empty.
- **RR18** — The third table is structural and is read not from history but from the two
  models' definitions: the number of contribution branches in the upstream repository, the
  number of remote definitions the contributor must make, and which repository holds push
  access.

## Measurement

```python
"""Two contribution models: cloning and repository forking.

Part 1 - the history the upstream repository carries and the questions it answers.
Part 2 - questions answerable only in the fork.
Part 3 - the structural difference between the two models: references and definitions.
"""
SEED = 20260813
BRANCHES = ("metrics", "report", "identity")
FILES = {"metrics": "metrics.py", "report": "report.py", "identity": "identity.py"}
BUGGY = ("report", 2)
SHARED_FILE = "config.py"


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: three branches, four commits each, one carries the bug."""
    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 integrate(record, fmt):
    """Each integration format produces a separate history."""
    t = []
    if fmt == "merge commit":
        for branch in BRANCHES:
            for k in [x for x in record if x["branch"] == branch]:
                t.append({**k, "branch_record": branch, "time_kept": True})
            t.append({"branch": branch, "step": 0, "file": None, "buggy": False,
                      "time": max(x["time"] for x in record if x["branch"] == branch),
                      "branch_record": branch, "merge": True, "time_kept": True})
    elif fmt == "rebase":
        for branch in BRANCHES:
            for k in [x for x in record if x["branch"] == branch]:
                t.append({**k, "branch_record": None, "time_kept": False})
    elif fmt == "squash":
        for branch in BRANCHES:
            group = [x for x in record if x["branch"] == branch]
            t.append({"branch": branch, "step": 0,
                      "file": sorted({x["file"] for x in group}),
                      "buggy": any(x["buggy"] for x in group),
                      "time": max(x["time"] for x in group),
                      "branch_record": branch, "time_kept": False, "squashed": len(group)})
    elif fmt == "fast-forward":
        for k in sorted(record, key=lambda x: x["time"]):
            t.append({**k, "branch_record": None, "time_kept": True})
    return t


def fork(record):
    """The state the fork repository carries: branches stand as developed."""
    return [{**k, "branch_record": k["branch"], "time_kept": True}
            for branch in BRANCHES for k in record if k["branch"] == branch]


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


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


def question3_order(t):
    return all(x.get("time_kept") for x in t)


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


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


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


QUESTIONS = (("feature grouping", question1_grouping), ("bug isolation", question2_bug),
             ("true order", question3_order), ("file trail", question4_file),
             ("branch integrity", question5_integrity), ("conflict decision", question6_conflict))


def answered(t):
    return {name for name, f in QUESTIONS if f(t)}


MODELS = (("cloning", False, "merge commit"),
          ("forking", True, "merge commit"),
          ("forking", True, "rebase"),
          ("forking", True, "squash"),
          ("forking", True, "fast-forward"))

record = development()
f = fork(record)
print(f"real development {len(record)} commits; fork repository carries {len(f)} commits and "
      f"answers {len(answered(f))} questions")
print()
print(f"{'model':<9s}{'integration format':<23s}{'fork':>6s}{'upstream':>9s}"
      f"{'upstream answer':>18s}{'only in fork':>15s}{'lost upstream':>16s}")
for model, separate, fmt in MODELS:
    t = integrate(record, fmt)
    upstream = answered(t)
    only_fork = answered(f) - upstream if separate else set()
    readable = sum(1 for x in t if not x.get("merge") and not x.get("squashed"))
    print(f"  {model:<9s}{fmt:<21s}{len(f) if separate else 0:6d}{len(t):9d}"
          f"{len(upstream):18d}{len(only_fork):15d}{len(record) - readable:16d}")

print()
for model, separate, fmt in MODELS:
    if not separate:
        continue
    only_fork = sorted(answered(f) - answered(integrate(record, fmt)))
    if only_fork:
        print(f"  {fmt:<21s} only in fork: {', '.join(only_fork)}")

print()
print(f"{'model':<9s}{'contribution branches upstream':>32s}{'contributor remote definitions':>32s}"
      f"{'push access':>14s}")
print(f"  {'cloning':<9s}{len(BRANCHES):32d}{1:32d}  upstream repo")
print(f"  {'forking':<9s}{0:32d}{2:32d}  fork")
```

```
real development 12 commits; fork repository carries 12 commits and answers 5 questions

model    integration format       fork upstream   upstream answer   only in fork   lost upstream
  cloning  merge commit              0       15                 5              0               0
  forking  merge commit             12       15                 5              0               0
  forking  rebase                   12       12                 3              2               0
  forking  squash                   12        3                 2              3              12
  forking  fast-forward             12       12                 3              2               0

  rebase                only in fork: feature grouping, true order
  squash                only in fork: bug isolation, file trail, true order
  fast-forward          only in fork: branch integrity, feature grouping

model      contribution branches upstream  contributor remote definitions   push access
  cloning                                 3                               1  upstream repo
  forking                                 0                               2  fork
```

## What the Upstream Repository Can Answer

The first two rows give the same number: whether the model is cloning or forking, when
integration is done with the merge commit, it leaves the upstream repository at **15 commits**
and **5 questions**.

This result is the lesson's first correction. **Forking by itself does not reduce the upstream
repository's information.** A contribution having come from a separate copy leaves no trace in
history; what enters the upstream repository is the commits themselves, and those commits are
the same commits in the fork and upstream alike. What distinguishes the two models is authority,
not the record.

The remaining three rows show the actual variable. The question answered by the upstream
repository falls from **5** to **3** and to **2**, and the cause of the fall is the same every
time: **the chosen integration format**. The numbers measured by the Merging and Rebasing topic
repeat here exactly, because what is measured is the same history; the only thing that changes
is which repository the history sits in.

The squash row stands apart. The upstream repository carries **3** commits, answers **2**
questions, and the **lost upstream** column reads **12**: the entire real development has
stopped being separately readable in the upstream repository. In the other three rows, this
column is **0** — even when a format loses information, it leaves the commit separately
readable.

## The Answer That Stands Only in the Fork

The second table is the lesson's actual finding. The fork repository carries twelve commits as
they were developed and answers **5** questions. The upstream repository answers **5**, **3**,
or **2** depending on the integration format. The difference between them does not sit in a
void: those questions are **in an answerable state**, but the answer is not in the upstream
repository — it is in the fork.

In the squash row, this difference rises to three. The "bug isolation," "true order," and "file
trail" questions are answered when asked of the fork's twelve commits; they are not answered
when asked of the upstream repository's three commits. In the rebase and fast-forward rows, the
difference is two, and they are different questions — the first leaves "feature grouping" and
"true order" in the fork, the second leaves "feature grouping" and "branch integrity" in the
fork.

The conclusion that follows is a warning. A question the upstream repository cannot answer may
be answerable **in a copy it does not own**. The fork repository belongs to the contributor: it
can be deleted, renamed, have its access closed. None of these notify the upstream repository,
and none of them corrupt the upstream repository's record — they only remove the place where
the missing answer could be found.

This is the permanent form the scope loss measured in the first lesson takes. There, missing
branches could be fetched, and the loss was recoverable. Here, what is missing never entered the
upstream repository, and the only place it could enter is one that can close.

## Structural Difference

The third table gives the two models' difference that is not read from history.

In the cloning model, the contributor's three branches sit **in the upstream repository**. This
means the branch names enter the upstream repository's namespace: the naming convention has to
be enforced in the upstream repository, abandoned branches accumulate in the upstream
repository, and cleanup is the upstream repository's job. The contributor carries a single
remote definition, and push access sits directly in the upstream repository.

In the forking model, the upstream repository holds **no contribution branch at all**. Branches
are opened in the fork, accumulate in the fork, and are deleted in the fork; only the
integration's result passes to the upstream repository. The contributor has to carry **two**
remote definitions, and the cost of this was measured in the first lesson: shared short names,
and the bare name always falling to the local side.

The choice between the two models is therefore not a history decision, it is an **ownership
decision**. The upstream repository decides who can write; the integration format decides what
history records. The two are independent, and conflating them leads to a misreading that loads
information loss onto forking itself.

## Summary

- Cloning takes a copy's entirety and writes a remote definition to the source; repository
  forking is a second copy of the upstream repository belonging to the contributor, and a
  concept separate from the process forking defined in the Operating System Concepts course.
- Both models leave the upstream repository at **15 commits** and **5 questions** under the
  merge commit format: forking by itself does not reduce the upstream repository's information.
- What determines the question the upstream repository answers is the integration format:
  **15/5**, **12/3**, **3/2**, **12/3**; under squash, **12** commits of the real development
  stop being separately readable in the upstream repository.
- The fork repository carries **12 commits** and answers **5** questions; under squash, **3**
  questions remain answerable only in the fork, and the fork is a copy the upstream repository
  does not own.
- The structural difference is not read from history: in the cloning model, contribution
  branches sit in the upstream repository and the contributor carries one definition; in the
  forking model, the upstream repository holds no contribution branch and the contributor
  carries two definitions.

## Next Step

In the fork model, the contributor has no push access to the upstream repository; despite this,
the twelve commits made their way upstream somehow. What accomplishes the transition is not a
command, it is a **flow**: a proposal is submitted, discussed, and eventually someone takes it
into the main branch. The next lesson builds this cycle's mechanics and measures how many
questions the flow's chosen integration format leaves in history. The measurement also counts
how many of the decisions made during the flow leave a trace in history; the answer is smaller
than expected.
