Git, CI/CD & GitOpsXX · RemotesRemotes
Multiple remotes — origin, upstream, and forks in one clone
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
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:
- Fork the upstream repository on the forge (GitHub, GitLab). This creates a personal copy under the engineer’s account.
- Clone the personal fork. The clone’s
originpoints at the fork. - Add a second remote named
upstreampointing at the canonical project. - Fetch from
upstreamperiodically to stay in sync with the project; pull-merge fromupstream/<branch>to refresh local branches; push tooriginto 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/mainis the local mirror of the engineer’s fork.refs/remotes/upstream/mainis 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 pushwith no arguments uses the current branch’s upstream, not the globalorigin. On a branch trackingupstream, an unguardedgit pushwill try to push toupstream, which the engineer usually does not have write access to. Fix: use explicitgit push origin <branch>for the fork side. - Fetch missed a remote.
git fetchwith no arguments fetches only the current branch’s upstream remote. To refresh every remote, usegit fetch --allor 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/configuntilgit remote set-url origin <new-url>runs.git fetch originwill fail with DNS or auth errors until the URL is updated. - PR opened against the wrong base branch. A PR from
feature/iam-rotationonorigintomainonupstreamis the normal case; a PR tomainonoriginis 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
originandupstreamwill produce two remote-tracking refs (origin/mainandupstream/main); a script that doesgit checkout mainafter a fetch will fail with “path ‘main’ already exists” because Git does not know which one to check out. Usegit checkout origin/mainorgit checkout upstream/mainexplicitly.
Production discipline
- Identify remotes by URL pattern, not by name. A script
that hard-codes
originorupstreambreaks 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. - Use
git fetch --allas 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. - 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. - Push to the fork, pull from the upstream. In the
forking workflow,
git push origin <branch>is the publish step andgit pull upstream <branch>is the refresh step. Scripts that automate the workflow should use the explicit form, notgit pushandgit pullwith no arguments. - 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 addto register an additional repository as a source for a single Argo CDApplication; 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 --pushto 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 addto bring a private Terraform module source under the same version control discipline as the main repository.
Quiz
Knowledge check · 4 questions
Q1. In the standard forking workflow, which remote serves which role?
Q2. `git fetch --all` runs every configured remote's fetch in parallel, which makes it the fastest way to refresh the remote-tracking namespace.
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.
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.