Lesson 11 / 15
Defining a Remote Repository
Defining a remote binds a name to an address and does not touch history; three copies carry 5, 10, and 15 commits respectively and answer 4, 5, 5 questions, and after two definitions the local copy still stays at 5 commits and 4 questions.
Contents
The previous lesson established branch strategies: the long-lived branch, the short-lived branch, and what the chosen strategy leaves in history. That entire discussion took place inside a single copy. Branches opened in the same repository and merged in the same repository; the history we measured lived in one place, and what lived there was the whole of the work.
This assumption does not hold in a distributed version control system. History does not live in one place; it lives in multiple copies, and the copies advance without knowledge of each other — neither one automatically knows the other’s commits. This lesson’s question is not how the copies get synchronized — that is the next lesson’s topic. This lesson’s question comes earlier: how does one copy name the other, and what does naming add to history?
Copy and Remote
The Centralized and Distributed Models lesson in the Introduction to Version Control course established the distinction: in a distributed model, every copy is a complete repository. A copy has its own working directory, its own staging area, its own branches, and its own history. No copy is structurally superior to another; “the center” is a convention, not a tool feature.
A remote is a name, defined in one repository, bound to another copy’s address. This distinction has to be set up front: the copy on the other side is not the remote; the remote is the local name this repository gives to that copy. The same copy can be defined under two different names in two different repositories, and this is not an inconsistency, because the name is local.
Where the definition sits matters for exactly this reason. A remote definition is written to the repository’s configuration file, not to history. The direct consequence: someone else who takes a copy does not get your remote names — they make their own definitions. A definition is not shared data, it is a personal routing table.
What Defining Does
The definition is managed through subcommands. The block below is an example dump; it has not been executed, and the addresses are fictional.
# taught commands — example dump, not executed git remote add origin example.test:team/measurement-networks.git git remote add upstream https://example.test/source/measurement-networks.git git remote -v origin example.test:team/measurement-networks.git (fetch) origin example.test:team/measurement-networks.git (push) upstream https://example.test/source/measurement-networks.git (fetch) upstream https://example.test/source/measurement-networks.git (push)
The -v option gives two lines per name, because fetch and push addresses can be set
separately. A read-only copy’s push address can be deliberately left invalid; an attempted
push to that name then stops before it ever reaches the network.
The remaining subcommands edit the same table: git remote rename changes the name,
git remote set-url changes the address bound to the name, git remote remove deletes
the definition and the remote-tracking references bound to it, git remote show lists the
branches under a name. git remote remove does not delete local branches or commits; what
it deletes is names and the references written under those names.
When a copy is taken for the first time, a remote name is not written by hand: cloning
writes a default name to the source (origin). This lesson’s measurement picks names that
spell out each copy’s role: origin for the copy where write access exists, upstream
for the read-only source it tracks.
Remote-Tracking References
The definition by itself is a name; what sits under that name is kept in a separate set of
references. Local branches sit under refs/heads, and a remote’s branches sit under
refs/remotes/<name>. References in the second set are called a remote-tracking
reference.
The rule for a remote-tracking reference is one sentence: it is not written from the local side. No commit is made on top of it, it is not advanced by hand; it is updated only when data is fetched from the other side. This is why a remote-tracking reference is not a branch, but a record of the other side’s branch as it was last seen.
# taught command — example dump, not executed git branch -a * metrics remotes/origin/metrics remotes/origin/report remotes/upstream/identity remotes/upstream/metrics remotes/upstream/report
The asterisk marks the local branch; the remaining five lines are references under the two remotes. The same short name appears more than once, and this is not a defect — it is proof that the namespace works.
The Measurement’s Assumptions
- RR1 — All three copies are produced from the same setup:
localcarries only themetricsbranch,origincarriesmetricsandreport,upstreamcarries all three. All three use the same integration format — merge commit; the measured difference is not the format, it is the set of branches carried. - RR2 — The copies’ shared branches do not diverge: if a branch exists in two copies, it carries the same commits in both. A diverged copy is the next lesson’s topic.
- RR3 — The defining operation writes a name–address pair to the
remotesdictionary and does not touch thehistorylist; the measurement counts these two fields separately. - RR4 — Addresses are fictional and do not enter the measurement; the measurement looks not at the address’s form but at the number of definitions.
- RR5 — The six questions are the course’s constant and do not change here. The only thing that changes is the copy the questions are asked of.
- RR6 — The reference count counts local branches and remote-tracking references together; the short name is the reference with its remote prefix dropped.
Measurement
"""Defining a remote: a name binds to an address, history is not carried. Part 1 - the history three copies carry and the questions they can answer. Part 2 - where defining touches local history. Part 3 - the namespace a remote name opens. """ 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 history(record, branches): """A copy's history from having taken the given branches with a merge commit.""" t = [] 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}) return t 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)) COPIES = {"local": ("metrics",), "origin": ("metrics", "report"), "upstream": ("metrics", "report", "identity")} def define(repo, name, address): """Defining a remote: an address binds to a name, history is untouched.""" repo["remotes"][name] = address return repo def measure(d): return len(d["history"]), sum(f(d["history"]) for _, f in QUESTIONS) record = development() print(f"real development {len(record)} commits, {len(BRANCHES)} branches; " f"copies {len(COPIES)}") print() print(f"{'copy':<10s}{'branches':>9s}{'commits':>9s}{'answered':>10s} unanswered") for name, branches in COPIES.items(): t = history(record, branches) missing = [question for question, f in QUESTIONS if not f(t)] print(f" {name:<8s}{len(branches):9d}{len(t):9d}{len(QUESTIONS) - len(missing):10d}" f" {', '.join(missing)}") print() repo = {"history": history(record, COPIES["local"]), "remotes": {}} before = measure(repo) define(repo, "origin", "example.test/origin") define(repo, "upstream", "example.test/upstream") after = measure(repo) print(f"local before defining: {before[0]} commits / {before[1]} questions") print(f"local after {len(repo['remotes'])} definitions: {after[0]} commits / {after[1]} questions") print() refs = [("local", branch) for branch in COPIES["local"]] refs += [(remote, branch) for remote in repo["remotes"] for branch in COPIES[remote]] short_names = [branch for _, branch in refs] shared = sorted(a for a in set(short_names) if short_names.count(a) > 1) print(f"refs {len(refs)} | distinct short names {len(set(short_names))} | " f"short names shared by multiple refs {len(shared)}") for a in shared: print(f" {a}: " + ", ".join(f"{u}/{d}" for u, d in refs if d == a))
real development 12 commits, 3 branches; copies 3 copy branches commits answered unanswered local 1 5 4 bug isolation, conflict decision origin 2 10 5 conflict decision upstream 3 15 5 conflict decision local before defining: 5 commits / 4 questions local after 2 definitions: 5 commits / 4 questions refs 6 | distinct short names 3 | short names shared by multiple refs 2 metrics: local/metrics, origin/metrics, upstream/metrics report: origin/report, upstream/report
What the Copy Can Answer
The top table asks the same question of three separate copies. upstream carries fifteen
commits and answers five questions; origin answers the same five with ten commits;
local answers four with five commits.
The missing question is bug isolation, and the reason for the gap is something new
here. In earlier lessons, a question going unanswered came from the chosen integration
format erasing information: squashing collapses twelve commits into three and makes the
buggy commit unreadable on its own. Nothing has been erased here. The local copy cannot
isolate the bug because the buggy commit is not in it at all — the bug comes from the
report branch’s second commit, and the report branch is not present in this copy.
This adds a second source of loss to the course’s measure. History can lose information to its format; a copy can lose information to its scope. The second kind of loss is recoverable: once the missing branches are fetched, the question becomes answerable again. The same cannot be said of format loss.
The comparison between origin and upstream says a second thing. There is a five-commit
difference, and the number of answered questions is the same. Carrying more commits
does not by itself raise the number of answered questions; what raises it is the arrival
of the commit an unanswered question needs. The identity branch’s four commits
complete no question’s missing answer, because the bug is not in that branch.
The sixth question is unanswered in all three copies. Where and how the conflict was resolved is written in none of them; increasing the number of copies does not change this.
What Defining Costs
The middle two lines measure the operation that gives this lesson its name. Before defining, the local copy is at 5 commits and 4 questions; after two remotes are defined, it is still at 5 commits and 4 questions.
This is not a rounding artifact, it is the definition itself. The define function only
touches the remotes dictionary; it never looks at the history list. This is also how
the tool behaves: adding a remote writes two lines to the configuration, does not reach
the network, fetches not a single object, creates not a single reference. If the address
is written wrong, you do not find out at the moment of definition — you find out on the
first fetch attempt.
A practical habit follows from this: a remote definition’s correctness can only be tested
by an operation that actually talks to the other side. git remote -v only shows what was
written; git remote show actually connects to the other side and lists the branches
under the name. The two are separate questions.
What the Namespace Costs
The bottom table counts six references, and they have only three distinct short
names. Three references share the short name metrics, two share the short name
report.
Without a namespace, these six references would have to fit into three slots, and the
copies would overwrite each other’s record. The remote name exists exactly for this
reason: origin/metrics and upstream/metrics are separate references, they can point
to separate commits, and both are independent of the local metrics branch.
The cost is ambiguity. When a command is given the bare name metrics, the tool looks at
local branches first; because the reference is found there, the other side’s metrics is
never searched for. If the two copies’ metrics branches have diverged and you wrote the
bare name, what you see is your branch — not the other side’s. This is the most frequent
reason divergence goes unnoticed for a while. Writing a qualified name (origin/metrics)
removes this ambiguity and lets the two references be queried separately.
The place the namespace proves itself once more is renaming. git remote rename does not
just change the line in the configuration; it also rewrites every remote-tracking
reference under that name with the new prefix. In a definition carrying five references,
the name of five references changes and none of the commits are copied — because a
reference is a name, not the commit it carries.
When Multiple Remotes Are Needed
The measurement worked with two remotes, and that number is not arbitrary. Multiple definitions arise whenever copies carry separate roles.
The first role distinction is authority. If the copy with write access and the read-only
copy are different, two definitions are needed: changes are pushed to one, updates are
fetched from the other. The measurement’s origin and upstream carry exactly this
distinction, and the next two lessons’ topic is the contribution model this distinction
opens up.
The second role distinction is peer collaboration. In a distributed model, a copy’s remote does not have to be a server; another team member’s copy can be defined too. When an unfinished branch needs to be examined without pushing it to a shared copy, this opens a direct path, and the middle copy is never involved.
The third role distinction is backup. Defining a mirror under a separate name keeps the same branches held in two separate sets of references; the two sets’ divergence can only be seen once both are defined.
In all three cases, an increase in the number of definitions does not grow local history. The only thing that grows is the reference namespace, and the shared-short-name problem the bottom table shows repeats once more with every new definition. This is why name choice matters more as the number of definitions grows: when names state the role, which copy a command reaches can be read from the command itself.
Summary
- A remote is not the copy on the other side, it is the name this repository gives to that copy; the definition sits in the configuration, not in history, and does not pass to someone else who takes the copy.
- Remote-tracking references sit under
refs/remotes/<name>, are not written from the local side, and record the other side’s branch as it was last seen. - The three copies carry 5, 10, and 15 commits and answer 4, 5, 5
questions;
local’s missing answer comes not from format loss but from scope loss, because the buggy commit is not in that copy at all. - Carrying more commits does not by itself raise the number of answered questions: there
is a five-commit difference between
originandupstream, and both answer the same five questions. - Two remote definitions leave the local copy at 5 commits and 4 questions; defining brings zero commits and does not test the address’s correctness.
- Six references carry three distinct short names; the namespace resolves this collision, and in exchange, the bare name always falls to the local branch.
Next Step
The definition is a name, and a name by itself brings zero commits. The local copy still carries five commits, the other side carries fifteen, and the gap between them is not closed. The next lesson separates out the three operations that close this gap: which one never touches local history, which one adds a merge commit to it, and which one changes the other side’s history. All three use the same network connection, and the three do not cost history the same price.
To keep your progress and take notes, Log in
My notes
Log in to take notes.