Learning Objectives
By the end of this module, you will be able to:
- Explain what a remote is in Git and how it maps to the object model you learned in Module 3
- Use
git remoteto add, inspect, rename, and remove remotes - Distinguish between
git fetch,git pull, andgit pull --rebaseand choose the right one - Push branches to remotes using
git push,git push -u, andgit push --force-with-lease - 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:
- Downloads the full object database (blobs, trees, commits)
- Creates a remote called
originpointing at the URL you cloned from - Creates remote-tracking branches (
origin/main,origin/HEAD) that mirror the remote's pointers - Creates a local
mainbranch that tracksorigin/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 rungit fetchorgit 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.gitAfter adding, fetch to download their objects:
git fetch upstreamRenaming and Removing
git remote rename origin github # Rename origin → github
git remote remove upstream # Remove upstream entirelyWhen you rename a remote, all remote-tracking branches update automatically: origin/main becomes github/main.
Inspecting a Remote
git remote show originThis 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 deletedWhat 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
maindid not move — your working directory is untouched origin/mainadvanced to where the remote'smainis- 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 trueAfter 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 mainThe Three Pull Strategies
| Command | Equivalent | Result |
|---|---|---|
git pull | fetch + merge | Creates a merge commit if histories diverged |
git pull --rebase | fetch + rebase | Replays your local commits on top of remote |
git pull --ff-only | fetch + merge --ff-only | Fails 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 onlyRecommendation: For most workflows,
pull.rebase = truekeeps your history clean. You avoid unnecessary merge commits that cluttergit 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 originFirst 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-upstreamThis does three things:
- Creates the branch
feature-loginon the remote - Pushes your commits to it
- 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 statustells you "Your branch is ahead/behind by N commits"git pullknows where to pull fromgit pushknows 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:
- Fork the project on GitHub (creates your copy under your account)
- Clone your fork (this becomes
origin) - Add the original project as a second remote called
upstream - Fetch from upstream to stay current
- Push to origin (your fork)
- 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 mainThis 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.gitEach 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-bugYou can fetch selectively:
git fetch alice # Only fetch alice's branches
git fetch --all # Fetch from every remote8. 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-upstreamChecking 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 messageThe ahead/behind count is based on the last fetch, not a live check. Always git fetch first to get accurate numbers.
Command Reference
| Command | Description |
|---|---|
git remote | List remote names |
git remote -v | List 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 fetch | Download objects and update remote-tracking branches |
git fetch --all | Fetch from all configured remotes |
git fetch --prune | Fetch and remove stale remote-tracking branches |
git pull | Fetch + merge (default strategy) |
git pull --rebase | Fetch + rebase |
git pull --ff-only | Fetch + merge only if fast-forward possible |
git push | Push current branch to its upstream |
git push -u origin <branch> | Push and set upstream tracking |
git push --force-with-lease | Safe force push (checks remote state first) |
git push --force | Dangerous force push (overwrites remote history) |
git branch -vv | Show branches with tracking info and ahead/behind |
git branch -r | List 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-lab → Do 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 -vCheckpoint: You should see
originlisted twice (fetch and push).
git push -u origin mainStep 4 — Verify tracking
git branch -vvCheckpoint: You should see
[origin/main]next to yourmainbranch.
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-contributingStep 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 --graphCheckpoint: You should see
origin/mainahead of your localmainby one commit. Your feature branch should be on a separate line.
Step 8 — Update local main
git switch main
git statusNote the "behind by 1 commit" message.
git merge origin/main
git log --onelineCheckpoint: Your local
mainnow matchesorigin/main.
Part C: Simulating a Fork Workflow
Step 9 — Create a "fake upstream"
cd ..
git clone --bare remote-lab upstream-bare.gitThis 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 -vCheckpoint: You should see both
originandupstreamlisted.
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 mainNow 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 bareStep 12 — Sync your fork with upstream
cd ../forked-copy
git fetch upstream
git log --oneline --all --graphYou should see upstream/main ahead of your local main.
git switch main
git merge upstream/main
git push origin mainCheckpoint: Your fork is now in sync with upstream.
git log --onelineshows 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 experimentNow 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 pushExpected failure: Git rejects the push because the histories diverged.
Step 15 — Use --force-with-lease
git push --force-with-leaseCheckpoint: The push succeeds.
--force-with-leaseverified no one else pushed toexperimentsince 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 -rThe remote-tracking branch origin/experiment may still appear.
git fetch --prune
git branch -rCheckpoint:
origin/experimentis gone after pruning.
Cleanup
cd ../..
rm -rf remote-lab forked-copy upstream-bare.gitChallenge
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
| Pitfall | Symptom | Fix |
|---|---|---|
Forgetting -u on first push | git push says "no upstream branch" | git push -u origin <branch> |
| Pushing to wrong remote | Commits appear in wrong repo | Check with git remote -v, use explicit git push <remote> <branch> |
Using --force on shared branch | Colleagues lose work, chaos | Use --force-with-lease only on personal branches |
| Not fetching before checking ahead/behind | git status says "up to date" when it's not | Always git fetch first; ahead/behind is based on last fetch |
| Stale remote-tracking branches | git branch -r shows branches that no longer exist | git fetch --prune or set fetch.prune true globally |
| Cloning with HTTPS but pushing with SSH | Authentication mismatch | git remote set-url origin git@github.com:... |
| Pulling on a dirty working directory | Merge/rebase refuses to start | Commit or stash your changes first |
Merge conflicts on git pull | Conflict markers in files | Resolve conflicts, git add, git commit (or git rebase --continue) |
Pro Tips
-
Set
fetch.prune trueglobally — Never manually prune again:git config --global fetch.prune true -
Use
git pull --ff-onlyas default in strict workflows — It forces you to explicitly handle diverged histories rather than silently creating merge commits:git config --global pull.ff only -
Use
git push --force-with-leaseinstead of--force— Always. Create a shell alias:git config --global alias.pushf "push --force-with-lease" -
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 -
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 -
Push all branches at once (use with caution):
git push origin --all -
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
- Downloads the full repository (object database)
- Creates a remote called
originpointing at the clone URL - Creates a local
mainbranch trackingorigin/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 mainQ9: 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.