---
title: 'Defining a Remote Repository'
source: 'https://academia.sh/en/courses/branching-and-collaboration/defining-a-remote-repository'
course: 'Branching and Collaboration'
language: en
updated: '2026-08-17T18:10:44+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```text
# 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**.

```text
# 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: `local` carries only the
  `metrics` branch, `origin` carries `metrics` and `report`, `upstream` carries 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 `remotes` dictionary
  and does not touch the `history` list; 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

```python
"""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 `origin` and `upstream`, 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.
