Upstream vs Origin

Every Git repository can talk to other repositories through remotes — named URLs stored in your local repository’s configuration. Two words come up constantly once you start collaborating: origin, the default name Git gives to the remote you cloned from, and upstream, a word that actually means two related but distinct things depending on context. Confusing them is one of the most common stumbling blocks for people learning collaborative Git workflows, especially GitHub’s fork-and-pull-request model. This lesson untangles both meanings for good and shows you exactly how to inspect, configure, and use them correctly.

Overview: What “Origin” and “Upstream” Actually Mean

A remote is nothing more than an alias for a URL, recorded in .git/config. When you run git clone, Git automatically creates one remote and names it origin. That name is a convention, not a keyword built into Git — you could rename it or add a remote called banana and Git would not care. origin simply means "the remote I cloned this repository from," by community habit.

upstream, on the other hand, is used in two different senses that beginners often merge into one:

1. Upstream as a tracking relationship

Every local branch can optionally be linked to a specific remote branch it "tracks." This link is called the branch’s upstream branch (Git also calls it the remote-tracking branch). It is stored as two config values per branch: branch.<name>.remote (which remote) and branch.<name>.merge (which branch on that remote). Once set, plain git push, git pull, and git status know which remote branch to compare against without you typing it every time.

2. Upstream as the original repository in a fork

When you fork a project on GitHub, your fork becomes your own repository, and cloning your fork gives it the remote name origin as usual. But you often also want a connection back to the original project you forked from, so you can pull in new changes. The convention is to add that original repository as a second remote named upstream. In this sense, origin = your copy, upstream = the source of truth you forked from.

These two meanings interact: once you add a remote literally named upstream, you can also set a local branch’s tracking (upstream-branch) relationship to point at upstream/main instead of origin/main — which is exactly what keeping a fork in sync requires.

A quick refresher on the underlying object model

To see why tracking branches behave the way they do, remember what a branch actually is: a lightweight, movable pointer to a commit object. A commit object points to a tree (a snapshot of the whole project at that moment), and a tree points to blobs (file contents) and further trees for subdirectories. HEAD normally points to a branch, not directly to a commit. A remote-tracking branch such as origin/main is also just a pointer — stored under refs/remotes/origin/main — but it only moves when you run git fetch, git pull, or git push. It is Git’s local memory of "where the remote’s branch was, as of my last conversation with it." It is not live; if someone else pushes to the remote right now, your origin/main pointer will not move until you fetch again.

Syntax

git remote -v
git remote add <name> <url>
git remote rename <old-name> <new-name>
git remote remove <name>
git push -u <remote> <branch>
git branch --set-upstream-to=<remote>/<branch> <local-branch>
git branch -vv
Command / Flag What it does
git remote -v Lists all configured remotes with their fetch and push URLs.
git remote add <name> <url> Registers a new remote under the given name (commonly upstream for a fork’s source repo).
git push -u <remote> <branch> Pushes the branch and sets it as the upstream (tracking) branch for future plain git push/git pull calls. -u is short for --set-upstream.
git branch --set-upstream-to=<remote>/<branch> Links an existing local branch to a remote-tracking branch without pushing anything.
git branch -vv Shows every local branch alongside the upstream branch it tracks and how far ahead/behind it is.
git status Reports whether the current branch is ahead, behind, or diverged from its upstream branch.

Examples

Example 1: What cloning sets up automatically

git clone https://github.com/octocat/hello-world.git
cd hello-world
git remote -v
Cloning into 'hello-world'...
remote: Enumerating objects: 42, done.
Receiving objects: 100% (42/42), done.
origin  https://github.com/octocat/hello-world.git (fetch)
origin  https://github.com/octocat/hello-world.git (push)

Cloning created exactly one remote, named origin, pointing at the URL you cloned. Your local main branch was also automatically set to track origin/main, which is why a bare git pull works immediately after cloning with no extra arguments.

Example 2: Setting an upstream (tracking) branch with git push -u

git switch -c feature/login-page
git add login.html
git commit -m "feat: add initial login page markup"
git push -u origin feature/login-page
Enumerating objects: 6, done.
Writing objects: 100% (4/4), 512 bytes | 512.00 KiB/s, done.
remote: Create a pull request for 'feature/login-page' on GitHub
To https://github.com/yourname/hello-world.git
 * [new branch]      feature/login-page -> feature/login-page
Branch 'feature/login-page' set up to track remote branch 'feature/login-page' from 'origin'.

The -u flag did two things: it pushed the new branch to origin, and it recorded in .git/config that this local branch’s upstream is origin/feature/login-page. From now on, plain git push and git pull on this branch know exactly where to go, and git status can compare commit counts:

git status
On branch feature/login-page
Your branch is up to date with 'origin/feature/login-page'.
nothing to commit, working tree clean

Example 3: The fork workflow — origin is yours, upstream is theirs

git remote add upstream https://github.com/original-owner/hello-world.git
git remote -v
git fetch upstream
git switch main
git merge upstream/main
git push origin main
origin    https://github.com/yourname/hello-world.git (fetch)
origin    https://github.com/yourname/hello-world.git (push)
upstream  https://github.com/original-owner/hello-world.git (fetch)
upstream  https://github.com/original-owner/hello-world.git (push)
remote: Enumerating objects: 10, done.
From https://github.com/original-owner/hello-world
 * [new branch]      main       -> upstream/main
Updating a1b2c3d..e4f5a6b
Fast-forward
 README.md | 3 +++
 1 file changed, 3 insertions(+)

Here you have two remotes with clearly separated jobs: origin is your personal fork on GitHub (where you push your work and open pull requests from), and upstream is the original project (where new changes from the maintainers land). Fetching upstream downloads its commits into the local ref upstream/main without touching your working files; merging brings those commits into your local main; pushing to origin updates your fork so it reflects the sync.

How It Works Step by Step

When you run git push -u origin feature/login-page, Git performs these steps internally:

  • It walks your local feature/login-page branch’s commit history and sends any commit and tree/blob objects the remote doesn’t already have, over the network protocol (HTTPS or SSH).
  • The remote repository creates or updates its own ref refs/heads/feature/login-page to point at the commit you pushed — this is the actual branch on GitHub’s server.
  • Your local repository updates its remote-tracking ref, refs/remotes/origin/feature/login-page, to match what was just pushed. This is your local "last known state" of the remote branch.
  • Because you passed -u, Git writes to .git/config:
[branch "feature/login-page"]
    remote = origin
    merge = refs/heads/feature/login-page
  • Every subsequent git status, plain git push, or plain git pull on that branch reads these two config lines to know which remote and which remote branch to compare against or fetch from — that pair is the branch’s upstream.

Adding a remote with git remote add upstream <url> is much simpler: it just writes a [remote "upstream"] section with a url and fetch refspec to .git/config. No objects move until you actually run git fetch upstream.

Common Mistakes

Mistake 1: Assuming git pull talks to the fork’s original repo

After adding an upstream remote for their fork, many beginners assume git pull now pulls from the original project. It does not — a bare git pull still uses whatever remote and branch your current branch’s tracking configuration points to, which after cloning your fork is origin, not upstream. You must be explicit:

git pull upstream main

or set the tracking branch permanently with git branch --set-upstream-to=upstream/main main if you want plain git pull to reach the original repo instead.

Mistake 2: Forgetting -u on the first push of a new branch

git switch -c feature/checkout-flow
git commit -am "feat: scaffold checkout flow"
git push

This fails with fatal: The current branch feature/checkout-flow has no upstream branch, because Git has no config telling it where to push a brand-new branch. The fix is either running the suggested command once (git push --set-upstream origin feature/checkout-flow) or always pushing new branches with -u from the start, as shown in Example 2.

Mistake 3: Pushing to upstream instead of origin

In a fork workflow, running git push upstream main attempts to push directly to the original repository, which you almost never have write access to — it fails with a permission error, or worse, succeeds and unexpectedly modifies a project you don’t maintain. Your changes belong on origin (your fork); you open a pull request from there against upstream.

Mistake 4: Merging upstream/main without fetching first

Running git merge upstream/main right after adding the remote — before ever running git fetch upstream — merges nothing useful, because the local ref upstream/main doesn’t exist yet. Always fetch before merging or rebasing onto a remote-tracking branch.

Best Practices

  • Keep the naming convention: origin for the remote you push your own work to, upstream for the original repository in a fork setup. Don’t invent alternate names unless your team has a documented reason to.
  • Always push new branches with -u the first time so plain git push/git pull work afterward without arguments.
  • Run git branch -vv periodically to see at a glance which local branches track which remote branches, and how far ahead or behind each one is.
  • Before merging or rebasing onto any remote-tracking branch, fetch first (git fetch upstream or git fetch origin) so you’re comparing against current data, not a stale snapshot.
  • Never rewrite history on a branch others have already pulled — if you must rebase your fork’s main onto upstream/main, do it only on branches nobody else has based work on, and prefer git push --force-with-lease over bare --force if you ever need to update a pushed branch.
  • Use git remote -v whenever you’re unsure which URL a remote name actually points to — don’t assume.

Practice Exercises

  • Clone any public repository, create a new branch called feature/practice-upstream, make a small commit, and push it without using -u. Read the error Git gives you, then fix it using the suggested command. Confirm with git branch -vv that the tracking branch is now set.
  • Fork a small public repository on GitHub, clone your fork, add the original repository as a remote named upstream, and verify with git remote -v that origin and upstream point to two different URLs. Fetch upstream and merge upstream/main into your local main.
  • On a branch whose upstream is origin/main, use git branch --set-upstream-to to repoint it at upstream/main instead, without pushing or fetching anything. Explain in your own words what changed in .git/config and what did not change in the working tree.

Summary

  • origin is just the conventional name for the remote you cloned from or push your own work to — it is not a Git keyword.
  • "Upstream" has two meanings: the remote-tracking branch a local branch is linked to (set via git push -u or --set-upstream-to), and the conventional name for the original repository in a fork workflow.
  • Remote-tracking refs like origin/main only update when you fetch, pull, or push — they are a cached snapshot, not a live view of the remote.
  • In a fork, origin = your copy (push here, open PRs from here), upstream = the original project (fetch and merge from here).
  • Always fetch before merging or rebasing onto a remote-tracking branch, and never rebase or force-push a branch other people already rely on.