LEARN · PHASE 02 · LESSON 18

Remotes, Forks, and the Full Mental Model

You can look at any repo and say which commits exist only on your machine,

Type
Learn
Prerequisites
Phase 02, Lessons 12–17
Time
~40 minutes

Learning Objectives

  • Say what origin/main is, and when it becomes stale
  • List what you have that nobody else has, before every push
  • Set up fork, origin, and upstream and know which one you may push to
  • Explain what a pull request is, and what part of it is not git

The Problem

You git push and it rejects: non-fast-forward. Your colleague pushed first. You did not know, because your local main looked identical to what you last remembered. You now have a choice between force-pushing (which would delete your colleague's commit) and something you have to look up.

Then a week later you see origin/main and main in git branch -a and cannot say what either one is. You cannot answer "is my work safe?" — which is the only question that has ever mattered.

The fix is not a command. It is that git has exactly three categories of commit and you have been guessing between them.


The Concept

Remote — a label pointing at a URL. Not a copy, not a sync. It is just a name for somewhere.

git remote -v          # what is it called, where does it point
git remote add upstream https://github.com/someone/their-repo.git

origin is the conventional name for the remote you cloned from. upstream is the conventional name for the original when you cloned a fork. Both are just labels.

Remote-tracking branch — origin/main is your last-known record of what main looked like on the remote at your last fetch. It updates only when you fetch. It is a cached snapshot, and it is what makes the rejection message meaningful.

git fetch origin       # update the record, change nothing else
git fetch --all --prune  # every remote, and delete records of deleted branches

fetch never touches your working tree or your branches. That is the property worth internalising: you can look at what changed without risking anything.

Push — sending commits your remote does not have. Not a sync. If you have pushed and then amended or rebased, the remote still has the old commit, and a normal push is rejected.

The three categories

flowchart TD
    R[Your repo] --> L[Local commits<br/>on main or a branch]
    R --> K[Remote-tracking record<br/>origin/main<br/>what you knew at last fetch]
    E[Remote<br/>the shared repo] --> K
    L -->|git push| E
    E -->|git fetch| K
    K -->|git merge or rebase| L
Mermaid source — renders as a diagram where mermaid.js is available
CategoryWhere it existsHow to see it
Local onlyYour machine. Not on the remote.git log origin/main..main
On the remoteShared with everyone.git log main..origin/main
Nobody hasA local branch nobody pushedgit branch with no matching remote

git log origin/main..main — "commits I have that the remote does not" — is the single most useful command in this lesson. Before every push on a shared repo, run it.

Fork

A fork is your own server-side copy of someone else's repository. You cannot push to a repo you do not have write access to. So: fork it, push to your fork, open a pull request against theirs.

sequenceDiagram
    participant U as Upstream repo
    participant F as Your fork
    participant Y as Your machine
    Note over U: the original
    Note over F: your server-side copy
    Note over Y: your clone of F
    Y->>F: git remote add upstream + fetch
    F->>U: pull request
Mermaid source — renders as a diagram where mermaid.js is available

Two remotes, two roles: origin is yours to push to, upstream is theirs. You fetch from both and push to only one.

Pull request is not a git operation at all. It is a request on a hosting site, pointing at a branch. Git does the code motion; the platform does the review conversation.

Force-push

git push --force-with-lease    # only if the remote is still where you last saw it
git push --force               # overwrites whatever is there

--force-with-lease refuses if someone else pushed since your last fetch. Plain --force does not check. Use the lease variant every time, or the plain one never.

The reframe: a remote is a place, origin/main is your memory of that

place, and git log origin/main..main is the question "what is mine that

nobody else has?" — which is the only question you need answered before a

push.


Build It

1. Make a remote to push to

A local directory works as a remote — nothing about the mechanism requires a hosting site.

cd ~/git-drill
git init --bare ../remote.git
git remote add origin ../remote.git
git remote -v

✅ You pass when git remote -v shows origin pointing at ../remote.git.

2. Push, then look at the two records

git push -u origin main
git branch -a
git log origin/main..main

origin/main now exists. The range is empty — you have nothing the remote lacks.

✅ You pass when git branch -a lists both main and origin/main, and the git log origin/main..main output is empty.

3. Simulate a colleague

In the bare repo, clone it elsewhere and push a change:

cd ~ && git clone remote.git colleague && cd colleague
printf 'colleague change\n' > colleague.txt
git add colleague.txt && git commit -m "colleague work"
git push origin main

Back in ~/git-drill, run git fetch origin and compare.

✅ You pass when git log main..origin/main lists the colleague's commit, and you pushed nothing to make it appear.

4. See the rejection instead of forcing through

cd ~/git-drill
git push origin main

Rejected. Read the message — it is telling you the remote moved.

✅ You pass when the push is rejected, and you can name the two commits that diverge, one on each side.

5. Resolve it properly

git fetch origin
git merge origin/main       # or: git rebase origin/main
git push origin main

✅ You pass when the push succeeds and git log origin/main..main is empty again.

6. Set up a fork relationship

If you have a second remote — upstream:

git remote add upstream https://example.com/original.git
git fetch upstream
git branch -r                 # origin/* and upstream/*

✅ You pass when git branch -r shows both sets of remote-tracking branches, and you can say which one you may push to.


Use It

Command / conceptWhen you reach for it
git remote -v"What is this repo connected to?"
git fetch originUpdate your record of the remote. Changes nothing else. Safe always.
git fetch --all --pruneUpdate every remote and drop records of deleted branches
git log origin/main..mainWhat do I have that nobody else has? Run before every push
git push -u origin <branch>First push of a new branch, tracking it from then on
git push --force-with-leaseAfter an amend or rebase of pushed work. Refuses if the remote moved
git push --forceOverwrites without checking. Almost never what you want
origin / upstreamYour fork / the original. Just labels
forkServer-side copy of someone else's repo, so you can push to it
pull requestA review request on the platform. Not a git operation

Exercises

  1. Clone something public. Add a remote called upstream pointing at the original. Fetch from both. ✅ You pass when git branch -r shows two sets, and you can say which remote your push would go to.
  1. Make a local branch, commit to it, and deliberately do not push. Find out, with a command, exactly what is local-only. ✅ You pass when git log origin/main..<branch> lists your commits, and you can say what would make them vanish.
  1. Force-push something on a repo where someone else has pushed, using plain --force. Then recover their work and note the correct command. ✅ You pass when you have their commit back, and you have written down --force-with-lease as the answer to "how could I have known".

Key Terms

TermWhat people sayWhat it actually means
remote"the copy on GitHub"A label for a URL. Git does not sync to it automatically.
origin"the remote"The conventional name for the one you cloned from. Renameable.
upstream"the original"The conventional name for the original when you cloned a fork.
origin/main"main on the server"Your cached record of it, from your last fetch. Can be stale.
git fetch"getting updates"Updates the record only. Never touches your tree or branches.
git pull"getting updates"fetch + a merge or rebase you did not choose.
fork"a copy I made"A server-side copy of someone's repo, so you have push access.
pull request"a git merge"A review request on the hosting platform. The merge itself is a git operation.
--force-with-lease"safe force push"Force-push that refuses if the remote moved since your last fetch.

What this lesson skipped

GitHub-specific workflow — gh CLI, branch protection, required reviews, CI status checks. Real, and irrelevant to a repo that has no remote.

Tags, releases, and git describe. Publishing a version. Comes with operational material later.

Submodules and subtrees. Embedding another repository's content. A different mental model from remotes entirely, and not on this course's path.

Rebasing your fork's branch onto upstream before a PR. The single most useful habit in real fork workflow, and it is exactly Lesson 14's rebase with an upstream/ prefix. Everything needed is already here.

Check yourself

4 questions. Answer before you reveal — recognising the right option is not the same as being able to produce it.

  1. Your push is rejected as non-fast-forward. A colleague pushed first. What do you do?

    1. git push --force, so your commit wins
    2. git fetch origin, then merge or rebase origin/main, then push again
    3. git push --force-with-lease, so your commit wins when the remote has not moved
    4. git reset --hard origin/main, discarding your local commits, then push
    Show answer

    B. git fetch origin, then merge or rebase origin/main, then push again

    Non-fast-forward means the remote has commits you do not; the fix is to get them and then combine, in either direction you chose in Lesson 14. Forcing (A) deletes your colleague's commit — the exact failure that flag causes. --force-with-lease (C) still overwrites whenever the remote hasn't moved, which is precisely the case that just failed, so it would reject too. Hard-resetting to origin/main (D) throws away your own work to solve a problem you did not have.

  2. What does origin/main actually represent?

    1. The current state of main on the remote, queried live
    2. Your local record of what main looked like on the remote at your last fetch, which can be stale
    3. A branch on your machine that mirrors the remote automatically
    4. A read-only view maintained by the hosting platform
    Show answer

    B. Your local record of what main looked like on the remote at your last fetch, which can be stale

    It updates only when you fetch — which is exactly why the non-fast-forward rejection in the sibling question could surprise you: your main matched a memory, not the server. Live state (A) would make that rejection impossible to encounter. It is a local ref (C), not a mirror kept in step. And it is a plain branch ref in your own repository (D), maintained by your fetch, not by the platform.

  3. You have no write access to a repository and want to propose a change. What is the path?

    1. Request write access from the owner, then push directly to main
    2. Fork it to your own account, push to your fork, and open a pull request against the original
    3. Push to the original's main and open a pull request from your local machine
    4. Email the maintainer a patch file generated from git format-patch
    Show answer

    B. Fork it to your own account, push to your fork, and open a pull request against the original

    Push access is the constraint, so the fork exists to give you somewhere you can push — then the pull request asks the original to take your commits. Pushing to the original (C) is rejected by definition. Waiting for write access (A) works but turns a five-minute contribution into a wait. A patch email (D) is a genuine 2005 workflow that no longer matches how anyone reviews code, and it is not what the lesson teaches.

  4. Before pushing a long-running feature branch to a shared repo, which one command tells you what you are about to publish?

    1. git status
    2. git log origin/main..feature
    3. git branch -a
    4. git diff origin/main
    Show answer

    B. git log origin/main..feature

    origin/main..feature is the set of commits you have that the remote does not — precisely the set a push would publish, and the set you should be able to defend line by line. git status (A) shows uncommitted work, which is a different question. git branch -a (C) lists labels, not commits. And git diff (D) shows the net tree difference, which can look empty or huge while hiding exactly the intermediate commits you did not mean to ship.