Learning Objectives

By the end of this module, you will be able to:

  1. Explain what a remote is in Git and how it maps to the object model you learned in Module 3
  2. Use git remote to add, inspect, rename, and remove remotes
  3. Distinguish between git fetch, git pull, and git pull --rebase and choose the right one
  4. Push branches to remotes using git push, git push -u, and git push --force-with-lease
  5. Set up a fork-based workflow with multiple remotes (origin + upstream)

1. What Is a Remote?

A remote is a named bookmark to another copy of your repository — usually hosted on GitHub, GitLab, or Bitbucket, but it can be any Git repository, even one on another folder on the same machine.

When you git clone, Git automatically:

  1. Downloads the full object database (blobs, trees, commits)
  2. Creates a remote called origin pointing at the URL you cloned from
  3. Creates remote-tracking branches (origin/main, origin/HEAD) that mirror the remote's pointers
  4. Creates a local main branch that tracks origin/main
┌──────────────────────────────────────────────────────┐
│                   GitHub (remote)                     │
│                                                      │
│   A ← B ← C ← D                                     │
│                 ↑                                     │
│               main                                   │
│               HEAD                                   │
└──────────────────────────────────────────────────────┘
               │
               │  git clone
               ▼
┌──────────────────────────────────────────────────────┐
│                   Local machine                       │
│                                                      │
│   A ← B ← C ← D                                     │
│                 ↑                                     │
│               main  (local branch)                   │
│               HEAD                                   │
│               origin/main  (remote-tracking branch)  │
│               origin/HEAD                            │
│                                                      │
│   Remote "origin" → https://github.com/you/repo.git │
└──────────────────────────────────────────────────────┘

Key insight: Remote-tracking branches (like origin/main) are read-only bookmarks. You cannot commit directly to them. They move only when you run git fetch or git push.


2. Managing Remotes with git remote

Listing Remotes

git remote              # Names only: origin
git remote -v           # Names + URLs (fetch and push separately)

The -v (verbose) output shows two lines per remote because Git allows different URLs for fetch and push — a rare but valid configuration:

origin  https://github.com/you/repo.git (fetch)
origin  https://github.com/you/repo.git (push)

Adding a Remote

git remote add <name> <url>

Examples:

# Add a second remote pointing to the original project you forked from
git remote add upstream https://github.com/original-author/repo.git
 
# Add a colleague's fork for cross-review
git remote add alice git@github.com:alice/repo.git

After adding, fetch to download their objects:

git fetch upstream

Renaming and Removing

git remote rename origin github    # Rename origin → github
git remote remove upstream         # Remove upstream entirely

When you rename a remote, all remote-tracking branches update automatically: origin/main becomes github/main.

Inspecting a Remote

git remote show origin

This displays detailed information:

* remote origin
  Fetch URL: git@github.com:you/repo.git
  Push  URL: git@github.com:you/repo.git
  HEAD branch: main
  Remote branches:
    main    tracked
    develop tracked
  Local branches configured for 'git pull':
    main    merges with remote main
    develop merges with remote develop
  Local refs configured for 'git push':
    main    pushes to main    (up to date)
    develop pushes to develop (fast-forwardable)

3. Fetching: git fetch

git fetch is the safe network operation. It downloads new objects (commits, trees, blobs) from the remote and updates your remote-tracking branches — but it never touches your working directory or your local branches.

git fetch origin          # Fetch from origin
git fetch --all           # Fetch from all remotes
git fetch --prune         # Fetch and remove remote-tracking branches
                          # whose upstream branch was deleted

What Fetch Actually Does

Before fetch:
  Remote:   A ← B ← C ← D ← E      ← main
  Local:    A ← B ← C               ← main, origin/main

After git fetch:
  Local:    A ← B ← C               ← main         (unchanged!)
                      ↖
                       D ← E         ← origin/main  (updated)

Notice:

  • Your local main did not move — your working directory is untouched
  • origin/main advanced to where the remote's main is
  • The new commits (D, E) are now in your local object database

Pruning Stale Branches

When someone deletes a branch on the remote (e.g., after merging a PR), your remote-tracking branch sticks around as a ghost. Use --prune to clean up:

git fetch --prune
# or set it as default behavior:
git config --global fetch.prune true

After pruning, git branch -r will no longer show deleted remote branches.


4. Pulling: git pull

git pull is shorthand for two operations:

git pull  =  git fetch  +  git merge

Specifically, git pull on main runs:

git fetch origin
git merge origin/main    # Merge remote changes into your local main

The Three Pull Strategies

CommandEquivalentResult
git pullfetch + mergeCreates a merge commit if histories diverged
git pull --rebasefetch + rebaseReplays your local commits on top of remote
git pull --ff-onlyfetch + merge --ff-onlyFails if a fast-forward isn't possible
Scenario: You have local commit X, remote has commit Y

git pull (merge):                git pull --rebase:
  A ← B ← Y ← M  ← main         A ← B ← Y ← X'  ← main
       ↖      ↗                            ↑
        X ──┘                          origin/main
                                  (X was replayed as X')

Choosing a Default Pull Strategy

# Always rebase on pull (recommended for clean linear history)
git config --global pull.rebase true
 
# Always require fast-forward only (safest, fails loudly)
git config --global pull.ff only

Recommendation: For most workflows, pull.rebase = true keeps your history clean. You avoid unnecessary merge commits that clutter git log. We'll cover rebase in depth in Module 9.

The git pull Warning

Modern Git warns when you haven't set a pull strategy:

warning: Pulling without specifying how to reconcile divergent branches is
discouraged. You can squelch this message by running one of the following
commands sometime before your next pull:

  git config pull.rebase false  # merge (the default strategy)
  git config pull.rebase true   # rebase
  git config pull.ff only       # fast-forward only

Pick one and set it globally. Your future self will thank you.


5. Pushing: git push

git push uploads your local commits to a remote and moves the remote's branch pointer forward.

Basic Push

git push                 # Push current branch to its tracking remote
git push origin main     # Explicitly push main to origin

First Push with -u (Set Upstream)

When you create a new branch locally, the remote doesn't know about it yet. The first push must establish the tracking relationship:

git push -u origin feature-login
# -u is short for --set-upstream

This does three things:

  1. Creates the branch feature-login on the remote
  2. Pushes your commits to it
  3. Configures your local branch to track origin/feature-login

After this, plain git push and git pull work without extra arguments.

What "Tracking" Means

A local branch that tracks a remote branch gains:

  • git status tells you "Your branch is ahead/behind by N commits"
  • git pull knows where to pull from
  • git push knows where to push to

Check tracking configuration:

git branch -vv
  develop   a1b2c3d [origin/develop] Add user dashboard
* main      e4f5g6h [origin/main] Merge PR #42
  feature-x i7j8k9l Fix API timeout           ← no tracking!

Branches without a [remote/branch] marker have no upstream set.

Force Pushing (And Why You Usually Shouldn't)

A normal git push will be rejected if the remote branch has commits you don't have locally (i.e., someone else pushed). Git refuses because a regular push would lose their work.

! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes before pushing again.

Never use git push --force on shared branches — it overwrites the remote history and destroys other people's commits.

Instead, use the safe alternative:

git push --force-with-lease

--force-with-lease only succeeds if the remote branch is exactly where you think it is (i.e., where your remote-tracking branch says it is). If someone else pushed in the meantime, it fails safely.

┌─────────────────────────────────────────────────────────┐
│              Force Push Decision Tree                    │
│                                                         │
│  Need to push after rebase/amend?                       │
│  ├─ Shared branch (main, develop) → NEVER force push    │
│  │   Instead: merge or create a new commit              │
│  └─ Personal feature branch?                            │
│      └─ Use: git push --force-with-lease                │
└─────────────────────────────────────────────────────────┘

6. The Complete Push/Pull Cycle

Here is the lifecycle of a typical feature branch, with every pointer movement shown:

Step 1: Start on main, up to date
  Remote:   A ← B ← C           ← main
  Local:    A ← B ← C           ← main, origin/main, HEAD

Step 2: git switch -c feature
  Remote:   A ← B ← C           ← main
  Local:    A ← B ← C           ← main, origin/main
                     ↑
                   feature, HEAD

Step 3: Make commits on feature
  Remote:   A ← B ← C           ← main
  Local:    A ← B ← C           ← main, origin/main
                      ↖
                       D ← E    ← feature, HEAD

Step 4: git push -u origin feature
  Remote:   A ← B ← C           ← main
                      ↖
                       D ← E    ← feature
  Local:    A ← B ← C           ← main, origin/main
                      ↖
                       D ← E    ← feature, HEAD, origin/feature

Step 5: PR merged on GitHub (merge commit created)
  Remote:   A ← B ← C ─────── M ← main
                      ↖       ↗
                       D ← E     (feature branch deleted)

Step 6: git fetch --prune
  Local:    A ← B ← C ─────── M ← origin/main
                      ↖       ↗
                       D ← E    ← feature, HEAD
                                  (origin/feature removed by prune)

Step 7: git switch main && git merge origin/main
  Local:    A ← B ← C ─────── M ← main, origin/main, HEAD
                      ↖       ↗
                       D ← E    ← feature

Step 8: git branch -d feature
  Local:    A ← B ← C ─────── M ← main, origin/main, HEAD
                      ↖       ↗
                       D ← E     (commits preserved via merge)

7. Working with Multiple Remotes

The Fork Workflow

When contributing to open-source projects, you typically:

  1. Fork the project on GitHub (creates your copy under your account)
  2. Clone your fork (this becomes origin)
  3. Add the original project as a second remote called upstream
  4. Fetch from upstream to stay current
  5. Push to origin (your fork)
  6. Create a PR from your fork to the original project
# Clone your fork
git clone git@github.com:you/project.git
cd project
 
# Add the original repo as upstream
git remote add upstream https://github.com/original/project.git
 
# Verify
git remote -v
# origin    git@github.com:you/project.git (fetch)
# origin    git@github.com:you/project.git (push)
# upstream  https://github.com/original/project.git (fetch)
# upstream  https://github.com/original/project.git (push)
┌──────────────────────────────────┐
│   original/project (upstream)    │
│   A ← B ← C ← D   ← main       │
└──────────────┬───────────────────┘
               │ fork (GitHub)
               ▼
┌──────────────────────────────────┐
│      you/project (origin)        │
│   A ← B ← C        ← main       │
└──────────────┬───────────────────┘
               │ git clone
               ▼
┌──────────────────────────────────┐
│        Local machine             │
│   A ← B ← C        ← main       │
│   remote "origin"   → you/project│
│   remote "upstream" → original/  │
└──────────────────────────────────┘

Keeping Your Fork in Sync

# 1. Fetch the latest from upstream
git fetch upstream
 
# 2. Switch to your local main
git switch main
 
# 3. Merge upstream's main into yours
git merge upstream/main
# Or rebase:
git rebase upstream/main
 
# 4. Push the updated main to your fork
git push origin main

This pattern keeps your fork's main in sync with the original project. Always create feature branches from the freshly synced main.

Multiple Remotes in Practice

You can have as many remotes as you want:

git remote add alice   git@github.com:alice/project.git
git remote add staging ssh://deploy@staging.server/repo.git

Each remote has its own set of remote-tracking branches:

git branch -r
# origin/main
# origin/develop
# upstream/main
# upstream/release-2.0
# alice/fix-auth-bug

You can fetch selectively:

git fetch alice             # Only fetch alice's branches
git fetch --all             # Fetch from every remote

8. Tracking Branches and Upstream Configuration

Automatic Tracking

When you git clone, the default branch (usually main) automatically tracks origin/main. When you git checkout a branch that exists only on a remote, Git creates a local branch and sets up tracking:

git checkout develop
# If origin/develop exists but local develop doesn't:
# Branch 'develop' set up to track remote branch 'develop' from 'origin'.

Manual Tracking Setup

# Set upstream for current branch
git branch --set-upstream-to=origin/feature feature
 
# Or during push
git push -u origin feature
 
# Remove upstream tracking
git branch --unset-upstream

Checking Ahead/Behind Status

Once tracking is configured, git status and git branch -vv tell you how your local branch compares to the remote:

git status
# On branch main
# Your branch is ahead of 'origin/main' by 2 commits.
 
git branch -vv
# * main   a1b2c3d [origin/main: ahead 2] Latest commit message

The ahead/behind count is based on the last fetch, not a live check. Always git fetch first to get accurate numbers.


Command Reference

CommandDescription
git remoteList remote names
git remote -vList remotes with fetch/push URLs
git remote add <name> <url>Add a new remote
git remote remove <name>Remove a remote
git remote rename <old> <new>Rename a remote
git remote show <name>Show detailed info about a remote
git fetchDownload objects and update remote-tracking branches
git fetch --allFetch from all configured remotes
git fetch --pruneFetch and remove stale remote-tracking branches
git pullFetch + merge (default strategy)
git pull --rebaseFetch + rebase
git pull --ff-onlyFetch + merge only if fast-forward possible
git pushPush current branch to its upstream
git push -u origin <branch>Push and set upstream tracking
git push --force-with-leaseSafe force push (checks remote state first)
git push --forceDangerous force push (overwrites remote history)
git branch -vvShow branches with tracking info and ahead/behind
git branch -rList remote-tracking branches
git branch --set-upstream-to=<remote>/<branch>Configure tracking manually

Hands-On Lab: The Complete Remote Workflow

Prerequisites

  • Git installed and configured (Module 2)
  • A GitHub account
  • SSH key added to GitHub (Module 2)

Part A: Basic Remote Workflow

Step 1 — Create a repository on GitHub

Go to GitHub → New Repository → Name it remote-labDo NOT initialize with a README (we want an empty repo).

Step 2 — Initialize locally and connect

mkdir remote-lab && cd remote-lab
git init
echo "# Remote Lab" > README.md
git add README.md
git commit -m "Initial commit"

Step 3 — Add the remote and push

git remote add origin git@github.com:<your-username>/remote-lab.git
git remote -v

Checkpoint: You should see origin listed twice (fetch and push).

git push -u origin main

Step 4 — Verify tracking

git branch -vv

Checkpoint: You should see [origin/main] next to your main branch.

Part B: The Feature Branch Cycle

Step 5 — Create and push a feature branch

git switch -c add-contributing
echo "# Contributing\n\nPRs welcome!" > CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "Add contributing guide"
git push -u origin add-contributing

Step 6 — Simulate a remote change

Go to GitHub → edit README.md directly in the browser → add a line "Updated from GitHub" → commit on main.

Step 7 — Fetch and observe

git fetch origin
git log --oneline --all --graph

Checkpoint: You should see origin/main ahead of your local main by one commit. Your feature branch should be on a separate line.

Step 8 — Update local main

git switch main
git status

Note the "behind by 1 commit" message.

git merge origin/main
git log --oneline

Checkpoint: Your local main now matches origin/main.

Part C: Simulating a Fork Workflow

Step 9 — Create a "fake upstream"

cd ..
git clone --bare remote-lab upstream-bare.git

This creates a bare repository simulating the "original project."

Step 10 — Clone fresh and set up two remotes

git clone remote-lab forked-copy
cd forked-copy
git remote rename origin origin
git remote add upstream ../upstream-bare.git
git remote -v

Checkpoint: You should see both origin and upstream listed.

Step 11 — Simulate upstream changes

cd ../remote-lab
echo "Upstream change" >> README.md
git add README.md
git commit -m "Upstream: update README"
git push origin main

Now push to the bare repo too:

cd ../upstream-bare.git
cd ../remote-lab
git remote add bare ../upstream-bare.git
git push bare main
git remote remove bare

Step 12 — Sync your fork with upstream

cd ../forked-copy
git fetch upstream
git log --oneline --all --graph

You should see upstream/main ahead of your local main.

git switch main
git merge upstream/main
git push origin main

Checkpoint: Your fork is now in sync with upstream. git log --oneline shows the upstream commit.

Part D: Force Push (Safe Practice)

Step 13 — Create a situation that requires force push

git switch -c experiment
echo "Version 1" > notes.txt
git add notes.txt
git commit -m "Add notes v1"
git push -u origin experiment

Now amend the commit (changing history):

echo "Version 2 (improved)" > notes.txt
git add notes.txt
git commit --amend -m "Add notes v2 (improved)"

Step 14 — Try a normal push

git push

Expected failure: Git rejects the push because the histories diverged.

Step 15 — Use --force-with-lease

git push --force-with-lease

Checkpoint: The push succeeds. --force-with-lease verified no one else pushed to experiment since your last fetch.

Part E: Pruning

Step 16 — Delete a remote branch and prune

# Delete experiment on the remote
git push origin --delete experiment
 
# Check remote-tracking branches
git branch -r

The remote-tracking branch origin/experiment may still appear.

git fetch --prune
git branch -r

Checkpoint: origin/experiment is gone after pruning.

Cleanup

cd ../..
rm -rf remote-lab forked-copy upstream-bare.git

Challenge

Set fetch.prune true globally. Then set pull.rebase true globally. Create a new repo, push it, make diverging changes on GitHub and locally, and observe how git pull automatically rebases instead of creating a merge commit.


Common Pitfalls & Troubleshooting

PitfallSymptomFix
Forgetting -u on first pushgit push says "no upstream branch"git push -u origin <branch>
Pushing to wrong remoteCommits appear in wrong repoCheck with git remote -v, use explicit git push <remote> <branch>
Using --force on shared branchColleagues lose work, chaosUse --force-with-lease only on personal branches
Not fetching before checking ahead/behindgit status says "up to date" when it's notAlways git fetch first; ahead/behind is based on last fetch
Stale remote-tracking branchesgit branch -r shows branches that no longer existgit fetch --prune or set fetch.prune true globally
Cloning with HTTPS but pushing with SSHAuthentication mismatchgit remote set-url origin git@github.com:...
Pulling on a dirty working directoryMerge/rebase refuses to startCommit or stash your changes first
Merge conflicts on git pullConflict markers in filesResolve conflicts, git add, git commit (or git rebase --continue)

Pro Tips

  1. Set fetch.prune true globally — Never manually prune again:

    git config --global fetch.prune true
  2. Use git pull --ff-only as default in strict workflows — It forces you to explicitly handle diverged histories rather than silently creating merge commits:

    git config --global pull.ff only
  3. Use git push --force-with-lease instead of --force — Always. Create a shell alias:

    git config --global alias.pushf "push --force-with-lease"
  4. Check what you're about to push before pushing:

    git log origin/main..HEAD --oneline    # Commits that will be pushed
    git diff origin/main..HEAD             # All changes that will be pushed
  5. Different push/fetch URLs — If your company uses a read mirror for cloning but a write endpoint for pushing:

    git remote set-url --push origin git@write-host:repo.git
  6. Push all branches at once (use with caution):

    git push origin --all
  7. Delete a remote branch from the command line:

    git push origin --delete feature-old
    # or the shorthand:
    git push origin :feature-old

Quiz / Self-Assessment

Q1: What three things does git clone set up automatically?

Answer
  1. Downloads the full repository (object database)
  2. Creates a remote called origin pointing at the clone URL
  3. Creates a local main branch tracking origin/main

Q2: What does git fetch do that git pull does not — or rather, what does git pull do that git fetch does not?

Answer

git fetch only downloads objects and updates remote-tracking branches. It never modifies your local branches or working directory. git pull does everything git fetch does plus merges (or rebases) the remote changes into your current branch. git fetch is the safe operation; git pull modifies your local state.

Q3: You just created a branch feature-x locally. What happens if you run git push without any arguments?

Answer

Git will error with "no upstream branch" because the branch has no tracking relationship. You need to run git push -u origin feature-x to create the remote branch and set up tracking. After that, plain git push works.

Q4: Explain the difference between git push --force and git push --force-with-lease.

Answer

--force overwrites the remote branch unconditionally — if someone else pushed commits, they are destroyed. --force-with-lease checks that the remote branch is at the commit your remote-tracking branch says it should be. If someone else pushed since your last fetch, the push is rejected. --force-with-lease is the safe alternative for force pushing.

Q5: In a fork-based workflow, what is the conventional name for the remote pointing at the original project?

Answer

upstream. The convention is: origin = your fork, upstream = the original project you forked from.

Q6: After merging a PR on GitHub, you run git branch -r locally and still see origin/feature-done. Why? How do you fix it?

Answer

Remote-tracking branches are only updated during git fetch. The branch was deleted on the remote, but your local Git doesn't know yet. Run git fetch --prune (or git fetch -p) to remove stale remote-tracking branches. To make this automatic, set git config --global fetch.prune true.

Q7: What does the -u flag in git push -u origin main stand for, and what effect does it have?

Answer

-u is short for --set-upstream. It configures the local branch to track the remote branch, so future git push and git pull commands on that branch don't need arguments. It also enables the ahead/behind status in git status.

Q8: You have two remotes: origin (your fork) and upstream (original project). Write the commands to sync your fork's main with the latest from upstream.

Answer
git fetch upstream
git switch main
git merge upstream/main    # or: git rebase upstream/main
git push origin main

Q9: True or false: git pull --rebase can still cause merge conflicts.

Answer

True. git pull --rebase replays your local commits on top of the remote branch. If any of your changes conflict with the remote changes, you'll get conflicts during the rebase, just as you would during a merge. You resolve them with git add and git rebase --continue.

Q10: Your git status says "Your branch is up to date with 'origin/main'" but you know a colleague pushed 10 minutes ago. Why is git status misleading?

Answer

git status compares your local branch against the local remote-tracking branch (origin/main), which is only updated during git fetch. If you haven't fetched recently, origin/main is stale. Run git fetch first, then git status will show the accurate ahead/behind count.