Lesson 13 / 15
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.
Contents
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.
# 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.
# 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
"""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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.