beginner26 min

Remote Repositories, Push, Pull & Collaboration

How a local repository connects to a remote one (GitHub or otherwise): configuring origin, pushing, fetching, pulling, cloning, and recovering from a rejected push.

What you'll learn

  • Explain the difference between a local repository and a remote repository
  • Configure, inspect, and correct a remote with git remote -v / add / rename / set-url
  • Explain what -u (--set-upstream) does on the first push, and why later pushes don't need it
  • Distinguish git fetch from git pull, and explain what git clone does
  • Read a non-fast-forward push rejection and recover from it safely, without force-pushing or resetting

Prerequisites

Explanation

Everything in the first two lessons happened in one place: your own .git folder, on your own machine. A local repository is exactly that -- private to you until you connect it to something else. A remote repository is a copy of that same repository hosted somewhere reachable over a network (GitHub, GitLab, a company server, or -- for this lesson's safe hands-on practice -- another folder on your own machine standing in for one). Git tracks the relationship between your local repository and any remotes it knows about; nothing is shared automatically. Nothing leaves your machine until you explicitly run a command that says so.

Remotes are just named URLs. git remote -v lists every remote your repository currently knows about (nothing, for a brand-new repository). git remote add origin <repository-url> registers one, under a name -- almost always origin by convention for "the main remote this repository was cloned from or primarily pushes to." That name is just a label Git lets you choose; origin isn't special to Git itself, it's a near-universal human convention. If you typo'd a URL or need to point at a different host, git remote set-url origin <corrected-url> fixes it in place, and git remote rename origin upstream renames it without touching anything else.

The first push needs -u (or its long form, --set-upstream). git push -u origin main does two things at once: it uploads your commits, and it records that your local main branch tracks origin/main -- meaning Git now remembers which remote branch main corresponds to. Every push or pull after that first one can just be git push or git pull with no arguments, because Git already knows where "there" is. git status and git branch -vv both show this tracking relationship, including whether you're ahead of, behind, or in sync with the remote.

git fetch and git pull are not the same command, even though they're easy to conflate. git fetch downloads whatever's new on the remote and updates Git's record of it (visible as origin/main), but it never touches your working files or your current branch -- it's always safe to run, purely informational. git pull does a fetch, then immediately merges the fetched changes into your current branch -- it's really git fetch + git merge in one step. When you're not sure what a pull would change, fetching first and looking at the difference is the more cautious move.

git clone <repository-url> creates a brand-new local repository by copying an existing remote one in full -- history and all -- and automatically sets up origin pointing back at it. It's how you'd start working on a project someone else already created, instead of running git init yourself.

A non-fast-forward rejection is Git protecting you, not a sign of a broken repository. It happens when you try to push, but the remote branch has commits your local branch doesn't have yet -- usually because a teammate pushed first. Git refuses to silently overwrite work it can't see. Before doing anything else, understand what this means: your local history and the remote's history have diverged, and Git needs you to reconcile them, not discard one side. The safe fix is almost always git pull (which fetches the missing commits and merges them in, prompting you to resolve any real conflict) and then git push again, now that your branch actually contains everything the remote has. Do not reach for git reset --hard or a force push as a routine fix for this -- git reset --hard discards your own local work, and a force push (git push --force) can discard a teammate's pushed work from the remote entirely, permanently. If a force push is ever genuinely necessary (rewriting your own already-pushed history), git push --force-with-lease is the safer form -- it refuses to overwrite anything if the remote has changed since you last looked, unlike a bare --force. Neither is something a beginner workflow should need.

Authentication happens outside of any command shown here: most setups today use a credential manager your OS or Git already has configured, an SSH key, or a browser-based sign-in prompt the first time you push to a new host -- never a token typed directly into a repository URL, which would leave it sitting in your shell history and Git's own remote configuration in plain text.

Pull requests, once more: a pull request is not a Git command -- Git has no concept of one. It's a review workflow that GitHub (and similar platforms) builds on top of ordinary push, fetch, and branches: you push a branch, the platform lets you open a pull request comparing it to main, and once approved, the platform performs the actual merge (or you do, locally, then push the result) -- exactly as covered in the previous lesson.

Guided local lab

Push, Pull, Fetch, and Recover From a Rejected Push -- With a Real (Local) Remote

Runs on your computer
This lab runs on your own computer, in your own terminal and editor — not in your browser. VisaSparkSchools does not execute, run, or verify these commands for you. Follow the verification steps yourself to confirm your result.

Practice every remote command for real, without needing a GitHub account or network access: a second folder on your own machine, created as a Git "bare" repository, stands in for a real remote exactly the way GitHub would -- push, fetch, pull, and clone all work against it for real. A third folder plays a "teammate" so you can see a genuine non-fast-forward rejection and recover from it safely. Every command below runs in YOUR terminal; this platform does not execute any of them.

Required tools

  • Git (2.x or newer)
  • A terminal (or Git Bash on Windows) (any current version)

Setup

  1. Open a terminal.
  2. Create a dedicated, disposable practice folder: `mkdir git-remotes-practice && cd git-remotes-practice` — everything in this lab stays inside this one folder, so nothing outside it is ever at risk.

Project structure

git-remotes-practice/
  project/            (your working repository)
  fake-remote.git/    (a bare repo standing in for GitHub -- created by you)
  teammate-clone/      (a second clone, standing in for a collaborator)

Starter files

project/README.md

TODO: create this yourself with the setup commands below.

Requirements

  • project/ is a real Git repository with an initial commit, pushed to fake-remote.git via a remote named origin.
  • teammate-clone/ is a real clone of fake-remote.git that has pushed at least one change back.
  • project/ has fetched and pulled the teammate's change.
  • A non-fast-forward push rejection was reproduced and recovered from with git pull, not a force push or reset.

Commands to run

  • Create the working repository and its first commit

    mkdir project && cd project && git init && echo "# Remote Practice" > README.md && git add README.md && git commit -m "Initial commit"
  • Create a bare repository one level up to stand in for a real remote (GitHub, GitLab, etc.) -- entirely on your own machine, no account or network needed

    cd .. && git init --bare fake-remote.git
  • Back in project/, confirm no remote is configured yet

    cd project && git remote -v
  • Add the bare repo as a remote named origin (the conventional name)

    git remote add origin ../fake-remote.git
  • Confirm origin is registered, for both fetch and push

    git remote -v
  • Rename it, then rename it back -- this is exactly what you'd run to fix a mistyped remote name

    git remote rename origin upstream && git remote rename upstream origin
  • Push and set the upstream tracking relationship in one step (only needed this first time)

    git push -u origin main
  • Confirm the tracking relationship git push -u just set up

    git branch -vv
  • Simulate a teammate: clone the same remote into a second folder

    cd .. && git clone fake-remote.git teammate-clone
  • As the teammate, make a change and push it (a normal push -- no -u needed, clone set tracking up automatically)

    cd teammate-clone && echo "Added by teammate" >> README.md && git add README.md && git commit -m "Teammate: update README" && git push
  • Back in your own project/, fetch: this downloads the teammate's commit but does NOT touch your files

    cd ../project && git fetch origin
  • Confirm fetch alone changed nothing in your working files

    cat README.md
  • Now pull, which fetches AND merges -- this is the step that actually updates your files

    git pull
  • Confirm your file now has the teammate's change too

    cat README.md
  • Set up a rejection on purpose: as the teammate, push another change first

    cd ../teammate-clone && echo "Second teammate change" >> README.md && git add README.md && git commit -m "Teammate: second update" && git push
  • Meanwhile, without pulling first, make your own local commit in project/

    cd ../project && echo "Your own local change" >> README.md && git add README.md && git commit -m "Your own change"
  • Try to push: Git REJECTS this (non-fast-forward) because origin/main now has a commit you don't have locally

    git push
  • The safe recovery: pull first (fetches and merges the teammate's commit into yours), THEN push

    git pull && git push

Expected behavior

The first `git push -u origin main` succeeds and prints a line ending in something like "Branch 'main' set up to track 'origin/main'." After the teammate pushes, your `git fetch` prints an update to `origin/main` but `cat README.md` shows no change; `git pull` then updates it for real. The rejected push prints a line containing `[rejected]` and `(non-fast-forward)`, with a hint to "Integrate the remote changes ... before pushing again" -- that hint IS the recovery: `git pull` (which may open an editor for a merge commit message, exactly as in the previous lesson) followed by a normal `git push`, which then succeeds.

Verify it yourself

  • git remote -v

    Expected: origin listed twice (fetch and push), pointing at ../fake-remote.git

  • git log --oneline

    Expected: shows your initial commit plus both the teammate's commits and your own change, all combined

  • git branch -vv

    Expected: shows main tracking origin/main, with no "ahead"/"behind" count remaining after the final push succeeds

Troubleshooting

  • `fatal: 'origin' does not appear to be a git repository` (or similar) when pushingConfirm `git remote -v` shows origin pointing at ../fake-remote.git, and that you're running the command from inside project/, not git-remotes-practice/ itself.
  • `! [rejected] main -> main (non-fast-forward)`This is expected at the step that deliberately reproduces it -- it means the remote has a commit you don't have locally yet. Run `git pull` to merge it in, then `git push` again. Never `git push --force` or `git reset --hard` to make a rejection like this "go away" -- that discards real work instead of integrating it.
  • A real GitHub/GitLab push instead asks for a username/password or hangs waiting for authenticationThis lab's local bare-repo remote never needs authentication, but a real hosted remote will use your OS credential manager, an SSH key, or a browser sign-in prompt -- never type a token directly into the remote URL; if a real push ever prompts unexpectedly, stop and confirm you're pushing to the repository you intend to before entering anything.

Stuck? Get a hint.

Extension challenge

Run `git log --oneline --graph --all` inside project/ after the final push and identify which commits came from you and which came from teammate-clone/ -- then run `git remote show origin` to see the full tracking-branch summary Git keeps for a configured remote.

When you've verified this locally, use the "Mark lesson complete" button below to record your progress.

Guided lab

Guided walkthrough: connecting to a real hosted remote (e.g. GitHub)

Git / BashNot executed
This lab does not run in your browser or on VisaSparkSchools's servers. Read the code, follow each edit step by step and see the expected output after every change.

The guided local lab above uses a local folder as a safe stand-in for a real remote. This walkthrough shows what the exact same commands look like against a REAL hosted remote such as GitHub -- read it here; the repository URL, username, and commit hashes below are EXAMPLE PLACEHOLDERS, not real values.

Step 1 of 6

Adding a real hosted remote works identically -- only the URL differs from the local practice lab. <repository-url> and <your-username> are placeholders here, never real values to copy.

git remote add origin https://github.com/<your-username>/<repository-name>.git

Stuck? Get a hint.

Common mistakes

  • Typing a personal access token or password directly into a remote URL (`https://TOKEN@github.com/...`) -- it ends up saved in plain text in `.git/config` and your shell history.
  • Treating a non-fast-forward rejection as something to force past with `git push --force` -- that can permanently discard a teammate's already-pushed commits.
  • Reaching for `git reset --hard` to 'clean up' after a rejected push -- that discards your OWN uncommitted or unpushed work instead of integrating it.
  • Assuming `git fetch` updates your working files -- it only updates Git's record of the remote; only `git pull` (or a later manual merge) actually changes what you see.
  • Forgetting `-u` on the very first push, then being confused why later `git push` alone fails with no upstream configured.

Knowledge check

Knowledge check

1. What does `git push -u origin main` do that a later plain `git push` doesn't need to repeat?
2. What is the key difference between `git fetch` and `git pull`?
3. What does a non-fast-forward push rejection mean?
4. What is the safest recommended recovery from a non-fast-forward rejection?
5. Where should real authentication credentials for a hosted remote live?

Takeaway

A remote is just a named URL Git can push to and pull from; -u sets up that relationship once, fetch is always safe, pull merges, and a rejected push means 'integrate first' -- never 'force past it.'

Summary

git remote -v/add/rename manage named connections to other repositories (conventionally origin). git push -u origin main pushes and sets up tracking for every later plain git push. git fetch downloads without touching files; git pull fetches and merges. git clone recreates a whole repository from a remote. A non-fast-forward rejection means the remote has commits you don't -- pull, then push again, never force or reset.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.