Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXX · RemotesRemotes

Multiple remotes — origin, upstream, and forks in one clone

Intermediate⏱ ~20 mingit

What you'll learn

  • Add a second (or third) remote to a clone and explain how the remote namespace isolates the remote-tracking refs
  • Run git fetch --all to refresh every configured remote in a single invocation
  • Push to one remote (a fork) while tracking a different remote (an upstream) and explain the asymmetric relationship
  • Read git remote -v to confirm which remote serves which role and recover the role from the URL pattern
  • Distinguish the origin/upstream/fork convention from the read-mirror/write-primary pattern used by deployments

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A single Git clone can talk to as many remotes as the workflow needs. The default git clone adds one remote (origin), but nothing about the data model or the on-disk layout requires the count to stop there. The two practical patterns that drive multi-remote workflows are: contributing to an upstream open-source project from a personal fork (the forking workflow), and reading from a read-mirror while pushing to a write-primary (the deployment mirror workflow). Both patterns use the same machinery: a remote is a name and a URL, and the remote-tracking refs are partitioned by the remote name.

The two-remote fork pattern

The forking workflow is the most common multi-remote setup. An engineer who wants to contribute to a project they do not own (for example, contributing a Terraform module back to an open-source provider) follows this pattern:

  1. Fork the upstream repository on the forge (GitHub, GitLab). This creates a personal copy under the engineer’s account.
  2. Clone the personal fork. The clone’s origin points at the fork.
  3. Add a second remote named upstream pointing at the canonical project.
  4. Fetch from upstream periodically to stay in sync with the project; pull-merge from upstream/<branch> to refresh local branches; push to origin to publish personal branches; open a pull request from the fork to the upstream.
# After the fork and clone, add the upstream
git remote add upstream https://github.com/kubernetes-sigs/example.git

# Verify the two remotes
git remote -v
# origin    https://github.com/alice/example.git (fetch)
# origin    https://github.com/alice/example.git (push)
# upstream  https://github.com/kubernetes-sigs/example.git (fetch)
# upstream  https://github.com/kubernetes-sigs/example.git (push)

# Fetch both
git fetch --all
# Fetching origin
# Fetching upstream
# remote: Counting objects: 12, done.
# From github.com:kubernetes-sigs/example
#    4d2c8e0..8a3f9d2  main           -> upstream/main

The remote-tracking refs are kept in separate namespaces:

  • refs/remotes/origin/main is the local mirror of the engineer’s fork.
  • refs/remotes/upstream/main is the local mirror of the canonical project.

Both can be checked out, both can be diffed against, and both can be the merge source for a feature branch. The name is what disambiguates them; the underlying refs are just plain commits.

flowchart LR
    A["engineer's clone"] --> B["origin\nrefs/remotes/origin/*\npersonal fork"]
    A --> C["upstream\nrefs/remotes/upstream/*\ncanonical project"]
    D["engineer pushes"] --> B
    E["engineer fetches"] --> C
    C --> F["PR opened on the forge\nfrom fork to canonical"]

The read-mirror / write-primary pattern

The deployment-side pattern is the inverse: read from one remote, write to another. A common setup is a read-only mirror that the deployment tooling fetches from (so the tooling cannot push by accident) and a writable primary that only CI scripts can push to (so the deployment does not bypass review).

# Read-only mirror for the deployment to fetch
git remote add origin https://git-mirror.internal/acme/infra.git

# Writable primary for CI to push
git remote set-url --push origin https://git-primary.internal/acme/infra.git

# Verify the asymmetry
git remote -v
# origin  https://git-mirror.internal/acme/infra.git (fetch)
# origin  https://git-primary.internal/acme/infra.git (push)

The fetch URL points at the mirror; the push URL points at the primary. git fetch origin reads from the mirror; git push origin writes to the primary. The mirror and the primary are usually kept in sync by a separate workflow (often another Git repo with a hook-driven push mirror); from the local clone’s perspective, they are two endpoints with the same remote name.

An equivalent pattern uses two named remotes instead of one with separate URLs:

git remote add mirror  https://git-mirror.internal/acme/infra.git
git remote add primary git@git-primary.internal:acme/infra.git

git remote -v
# mirror   https://git-mirror.internal/acme/infra.git (fetch)
# mirror   https://git-mirror.internal/acme/infra.git (push)
# primary  git@git-primary.internal:acme/infra.git (fetch)
# primary  git@git-primary.internal:acme/infra.git (push)

The named-remote form is more explicit and makes the asymmetry visible in every command; the set-url form is more concise and keeps the single-name convention intact. In production, the named-remote form is usually preferred for CI scripts because the mirror vs primary distinction shows up in every log line.

git fetch —all

git fetch --all is the multi-remote equivalent of git fetch. It iterates over every configured remote and runs the equivalent of git fetch <remote> for each, one after another.

git fetch --all
# Fetching origin
# From github.com:acme/infra
#    8a3f9d2..4d2c8e0  main           -> origin/main
# Fetching upstream
# From github.com:kubernetes-sigs/example
#    4d2c8e0..8a3f9d2  main           -> upstream/main
# Fetching prod
# From git.internal:deploy/infra
#  * [new branch]      feature/oidc    -> prod/feature/oidc

The --all flag is the only thing that distinguishes this command from git fetch <remote>. There is no --parallel option for parallel fetches; remotes are fetched serially. This is rarely a bottleneck, but it is worth knowing.

git fetch --all does not accept additional refspecs; it fetches every configured remote with its configured refspec. To fetch only some remotes, list them by name:

# Fetch only origin and upstream (skip the deployment mirror)
git fetch origin upstream

To fetch only specific branches from every remote, combine --all with a refspec:

# Fetch every remote's main branch
git fetch --all main

This is rarely what you want — --all with no refspec is the common case for “refresh all my caches”.

Cross-repo workflows with per-branch tracking

A subtle but useful feature: each local branch can track a different remote. The upstream configuration is per-branch, not per-remote, so a clone with two remotes can have one branch tracking origin and another tracking upstream.

# main tracks origin (the fork's main)
git branch --set-upstream-to=origin/main main

# A local branch tracking the upstream's release branch
git switch -c backport/release-v1.4 --track upstream/release/v1.4

The result is a clone where some branches push to and pull from the fork and others push to and pull from the upstream. This is exactly what the forking workflow requires: the engineer’s feature branches push to origin (the fork), while their local main pulls from upstream/main (the canonical project).

# Push feature branch to the fork
git push origin feature/iam-rotation
# Pull main from upstream
git pull upstream main

A pull request opened on the forge from the fork’s feature/iam-rotation to the upstream’s main is what the rest of the workflow looks like. The local clone’s role is to keep the fork up to date with the upstream and the engineer’s working branch up to date with the fork.

Common failure modes in multi-remote setups

Five things that go wrong and how to recognise each:

  • Push to the wrong remote. git push with no arguments uses the current branch’s upstream, not the global origin. On a branch tracking upstream, an unguarded git push will try to push to upstream, which the engineer usually does not have write access to. Fix: use explicit git push origin <branch> for the fork side.
  • Fetch missed a remote. git fetch with no arguments fetches only the current branch’s upstream remote. To refresh every remote, use git fetch --all or list the remotes explicitly.
  • Stale remote-tracking refs on a renamed fork. A fork that is renamed on the forge (or moved to a different org) keeps the old URL in .git/config until git remote set-url origin <new-url> runs. git fetch origin will fail with DNS or auth errors until the URL is updated.
  • PR opened against the wrong base branch. A PR from feature/iam-rotation on origin to main on upstream is the normal case; a PR to main on origin is a no-op (the fork already has the commit). Reading the PR target before opening catches this case.
  • Two remotes with overlapping namespaces. A branch that exists on both origin and upstream will produce two remote-tracking refs (origin/main and upstream/main); a script that does git checkout main after a fetch will fail with “path ‘main’ already exists” because Git does not know which one to check out. Use git checkout origin/main or git checkout upstream/main explicitly.

Production discipline

  1. Identify remotes by URL pattern, not by name. A script that hard-codes origin or upstream breaks the first time someone uses a non-default name. Recover the role by matching the URL’s hostname, path, or owner against an expected pattern.
  2. Use git fetch --all as the refresh step. A CI job that depends on remote state should fetch every configured remote, not just the one the current branch tracks. A status check that reads a stale remote-tracking ref is a status check that does not reflect the remote.
  3. Configure read-mirror / write-primary with separate remotes, not set-url --push, for CI scripts. The named-remote form makes the asymmetry visible in every log line, which is what you want when a script silently picks the wrong endpoint.
  4. Push to the fork, pull from the upstream. In the forking workflow, git push origin <branch> is the publish step and git pull upstream <branch> is the refresh step. Scripts that automate the workflow should use the explicit form, not git push and git pull with no arguments.
  5. Document the remote topology. A clone with three remotes (origin, upstream, vendor) is not self-documenting; the team needs to know which remote is canonical, which is read-only, and which is the deployment target. The documentation belongs alongside the bootstrap script that creates the clone.

Cross-course references

  • GitOps with Argo CD - Part V (MultiSource) uses git remote add to register an additional repository as a source for a single Argo CD Application; the URL is supplied by the GitOps control plane, not by the engineer.
  • Ansible for Production Sysadmins - Part XL (RepoMirror) uses git remote set-url --push to push a mirror from a read-only primary to a writable secondary; the fetch URL stays pointed at the primary so the mirror stays read-only on pull.
  • Terraform for Production Sysadmins - Part XV (PrivateMod) shows the same pattern with git remote add to bring a private Terraform module source under the same version control discipline as the main repository.

Quiz

Knowledge check · 4 questions

  1. Q1. In the standard forking workflow, which remote serves which role?

  2. Q2. `git fetch --all` runs every configured remote's fetch in parallel, which makes it the fastest way to refresh the remote-tracking namespace.

  3. Q3. Explain why the fetch refspec includes the remote name in the destination namespace, and what would happen if two remotes shared the same branch name without the namespace prefix.

  4. Q4. An engineer pushes a feature branch with `git push` and the push fails because the branch is tracking `upstream`, which the engineer does not have write access to. Diagnose and recommend a fix.

    An engineer is contributing a feature to an open-source project. They have cloned their fork (so `origin` is the fork) and added `upstream` pointing at the canonical project. They branch from `upstream/main` to work on a feature. When they finish, they run `git push` to publish the branch; the push fails with `remote: Permission to kubernetes-sigs/example.git denied to alice.`

Passing score: 75%. Answers are checked in this browser.