Module 8 — Remotes (git clone, fetch, pull, push)

Updated 8 September 2026

Module 8 — Remotes (git clone, fetch, pull, push). Everything so far happened on one machine — which, per Module 1, was the point of a distributed VCS. Now we connect copies. The four network commands (clone, fetch, pull, push) move snapshots between repositories, and one idea — the remote-tracking branch — makes their behavior predictable. You will build a real "server" on your own disk, so every exercise works offline.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Module 6 (merging — pulls can merge) and Module 7 (the shared-history rules). Exercises continue the kitchen repository from Module 7's end state. No internet or account is needed — the "remote" lives on your disk, which is a fully legitimate Git remote.
This page contains Mermaid diagram blocks. Notion shows them as code by default — click the block and switch it to Preview to see the diagram. You only need to do this once per block.

Part A — What a remote actually is

A1. Remotes are named addresses; servers are bare repositories

A remote is nothing more than a nickname for another copy of the repository — a name bound to an address (URL or filesystem path), stored in .git/config. There is no special "server software" in Git itself: the thing GitHub hosts, and the thing you are about to create in a directory, is a bare repository — a repository with no working tree: just the contents of .git, standing alone. Why bare? Because nobody edits files on the meeting-point copy; it only receives and serves snapshots — and pushing into a repository where someone has files checked out would yank the working tree out from under them, so Git refuses that by default. Convention names bare repo directories with a .git suffix: project.git.

🧪 Exercise 8.1 — build the "server" on your own disk
bash
cd ~/git-course
git init --bare bakery.git
ls bakery.git
Expected result — click to reveal
plain text
Initialized empty Git repository in /home/aisha/git-course/bakery.git/
HEAD
branches
config
description
hooks
info
objects
refs

What to read out of it: compare this listing with Module 3's Exercise 3.1 — it is the inside of .git, promoted to be the whole directory: objects, refs, HEAD, config, and no project files anywhere (also no index — nothing is ever staged here, because nothing is ever edited here). This directory is now exactly what GitHub stores for each project. Every "Git server" you will ever use is this, plus access control and a web UI on top.

Real-world analogy — the PO box

A remote is a PO box: a named address (origin = "the box on Main Street") where copies of your work are dropped off and picked up. The box itself is bare — nobody lives there, no desk, no editing; it exists purely for exchange. Renting a second box under another name (backup, upstream) is routine, and each correspondent keeps their own private note of what was in the box last time they checked.

Where the analogy stops working. A PO box holds whatever was last deposited; old contents leave when picked up. A bare repository is an accumulating database: every snapshot ever pushed stays (immutable objects, Module 3), and a pickup (fetch) copies rather than removes. And unlike mail, exchanges are verified end-to-end — content-addressing means what you fetch provably is what was pushed, hash for hash.

A2. Connecting an existing repository: remote add, then push -u

Your kitchen was born locally, so it knows no remotes. Two steps connect it. git remote add origin <address> records the nickname — origin is pure convention (the name clone sets automatically, Part B), used here for consistency. Then the first git push -u origin main does two jobs: uploads main's missing snapshots to the remote and creates its main branch there; and -u (--set-upstream) records that your local main "tracks" origin/main — so future git push/git pull with no arguments know their counterpart, and git status can say "ahead/behind".

🧪 Exercise 8.2 — connect the kitchen and publish main
bash
cd ~/git-course/kitchen
git remote add origin ~/git-course/bakery.git
git remote -v
git push -u origin main
git branch -vv | head -1        # -vv adds each branch's upstream in [brackets]
Expected result — click to reveal
plain text
origin	/home/aisha/git-course/bakery.git (fetch)
origin	/home/aisha/git-course/bakery.git (push)
To /home/aisha/git-course/bakery.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
* main      9531dc7 [origin/main] Reapply "Merge branch 'seeds'"

What to read out of it: remote -v lists the nickname twice — fetch and push addresses are configurable separately, though nearly always identical. The push output reads main -> main: your local branch → the remote's branch, [new branch] because the bare repo had none. The set up to track line is -u doing its job, confirmed by branch -vv: local main, its tip, and [origin/main] — its upstream — in brackets. From now on, bare git push and git pull on this branch need no arguments. (Your terminal will usually also show Enumerating objects… Counting objects… Writing objects… progress lines — Git prints that chatter whenever output goes to an interactive terminal; the transcripts on this page omit it.)

🎯 Interview questions — Part A

🎯 "What are the different types of Git repositories?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

Two types. A non-bare (normal) repository has a working tree plus the .git database — where humans edit, stage, and commit. A bare repository (git init --bare, conventionally named name.git) is the database alone: no working tree, no index, nobody edits there. Bare repos are what servers host, because their job is exchange — receiving pushes and serving fetches — and because pushing into a checked-out branch of a non-bare repo would desynchronize someone's working tree, which Git refuses by default (receive.denyCurrentBranch).

The details that separate candidates: an average answer says "bare has no working directory." A strong answer explains why that is the right shape for a server (no working tree to corrupt, no index, pure database) and knows the operational touchpoints: the .git-suffix convention, that a bare repo is literally the contents of .git standing alone, and that "Git hosting" = bare repositories + authentication + UI. Field bonus: bare repos on a shared filesystem or over SSH are a complete zero-vendor Git server — teams ran exactly that for years before hosting platforms.

🎯 "What is the origin in Git?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

origin is a remote nickname — the default name git clone gives to the repository you cloned from, and the conventional name for a repo's primary remote when you add one by hand. It has no special powers: it is a config entry mapping a name to an address, editable (git remote rename/set-url) and deletable like any other. Commands use it as shorthand: git push origin main, git fetch origin; and remote-tracking branches are namespaced under it: origin/main = "where main on origin was, last time we synchronized."

The details that separate candidates: an average answer says "the default remote." A strong answer stresses that it is convention, not mechanism — a repo can have zero remotes, or five with none named origin — and demonstrates the namespace understanding: origin/main is a local ref (refs/remotes/origin/main), not something living on the server. The classic follow-up trap this defuses: "is origin the server?" — no; origin is your name for an address; the server neither knows nor cares what you call it.

Part B — clone, and the remote-tracking idea

B1. git clone — the everything-at-once starter

Official docs: git-clone manual

git clone <address> [directory] bundles what you just did by hand, inverted: create a directory, init it, add the source as a remote named origin, fetch all of its history (Module 1's promise — the full snapshot database, not just the latest state), create remote-tracking branches for every branch it has, then check out its default branch with upstream tracking already configured. One command, ready to work. Cloning is how every repository you did not create arrives on your machine.

🧪 Exercise 8.3 — clone the bakery: your "second laptop"
bash
cd ~/git-course
git clone bakery.git kitchen-laptop2
cd kitchen-laptop2
git remote -v
git branch -a                   # -a: local AND remote-tracking branches
git log --oneline -1
Expected result — click to reveal
plain text
Cloning into 'kitchen-laptop2'...
done.
origin	/home/aisha/git-course/bakery.git (fetch)
origin	/home/aisha/git-course/bakery.git (push)
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
9531dc7 Reapply "Merge branch 'seeds'"

What to read out of it: the clone's tip is exactly what you pushed in 8.2 — the whole history traveled, and git log here replays every module's commits. remote -v: origin was configured for you. branch -a is the interesting output: one local branch (main), plus remotes/origin/main — a remote-tracking branch, B2's subject — and origin/HEAD, a symbolic ref recording the remote's default branch (what you get when you clone). This clone is a complete, independent repository: full history, full object database, working offline from this second on.

B2. Remote-tracking branches: your last photo of the other side

origin/main deserves precision, because every fetch/pull/push behavior follows from it. It is a local ref (a file under .git/refs/remotes/origin/, packed or loose — Module 3 rules apply) that answers one question: where did main point on origin, last time we communicated? You cannot commit to it — Git moves it, only during network commands. It is a bookmark of someone else's bookmark, photographed at last contact.

This one idea splits "syncing" into two honest halves: updating your photo (fetch — safe, touches nothing of yours) and acting on the difference between your branch and the photo (merge/rebase — your decision, on your schedule). Every scary network behavior in Git is one of these halves misunderstood.

Diagram source
flowchart LR
    subgraph L["your clone"]
        M["main<br>(yours to move)"]
        RT["origin/main<br>(photo: moved only by<br>fetch, pull, push)"]
    end
    subgraph R["bakery.git (origin)"]
        SM["main<br>(the shared truth)"]
    end
    RT -.->|"records last-seen<br>position of"| SM
Counter-intuitive: origin/main is not "the branch on the server," live. It is your cached last observation of it — possibly minutes or months stale, updated only when you fetch/pull/push. Consequences everywhere: git status saying "up to date with origin/main" means up to date with your photo, not with reality (fetch first if freshness matters); comparing main..origin/main (Module 4 ranges) compares against the photo; and after someone force-changes the server, your photo is wrong until the next fetch. Engineers who internalize "origin/main = stale local bookmark" stop being surprised by Git's network behavior, permanently.

🎯 Interview questions — Part B

🎯 "What does git clone do?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

git clone <url> creates a complete local copy of a repository: it initializes a new repo, registers the source as remote origin, downloads the entire history — every commit, tree, and blob, not just the latest snapshot — creates remote-tracking branches (origin/*) for the source's branches, checks out the default branch, and wires up its upstream. The clone is fully independent and offline-capable: all Module 1's local operations work immediately, and it is a complete backup of the shared history by construction.

The details that separate candidates: an average answer says "downloads a repository." A strong answer enumerates what gets set up (origin, tracking refs, upstream, checkout) — because that setup is exactly what distinguishes clone from init+fetch done manually — and notes what does not transfer: uncommitted work has no objects to copy, and the source's reflog, hooks and config stay local to it. Scale-fluency closer: --depth 1 (shallow) and --filter=blob:none (partial) clones trade completeness for speed on huge repos — CI's standard tricks (Module 15).

🎯 "What differentiates between the commands git remote and git clone?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

Different jobs at different moments. git clone is a creation command: run once, outside any repo, to manufacture a new local repository from an existing one — directory, history, origin remote, tracking branches, checkout, all in one step. git remote is a management command: run inside an existing repo to administer its address book — git remote -v lists nicknames and URLs, add/remove/rename edit them, set-url repoints one (the fix when a project migrates hosts). Clone writes your address book's first entry; remote is how you read and edit that book forever after.

The details that separate candidates: an average answer says "clone copies, remote manages remotes." A strong answer places them in time (birth vs administration) and shows the overlap point: everything clone configures can be replicated manually (initremote addfetchswitch), which proves clone is convenience, not magic — and explains why a locally-born repo (like this track's kitchen) meets git remote add first and never gets cloned at all.

Part C — Moving snapshots: fetch, pull, push

C1. fetch — update the photo, touch nothing

Official docs: git-fetch manual

git fetch contacts the remote, downloads every object you are missing, and moves your remote-tracking branches to match what it saw. That is all. Your branches do not move; your working tree does not change; nothing can conflict. Fetch is the always-safe command — run it whenever you want fresh information, then look before acting: git status (ahead/behind, computed against the freshened photo), git log main..origin/main (Module 4 ranges: what arrived that you lack), and merge when you choose.

🧪 Exercise 8.4 — simulate a teammate, then fetch and inspect
bash
cd ~/git-course/kitchen-laptop2       # the "teammate's laptop"
echo "let dough rest 20 min before shaping" > resting.txt
git add resting.txt && git commit -m "Add resting step"
git push
cd ../kitchen                         # back to YOUR machine
git fetch
git status | head -3
git log --oneline main..origin/main   # what they have that you don't
git merge origin/main                 # integrate, deliberately
Expected result — click to reveal
plain text
To /home/aisha/git-course/bakery.git
   9531dc7..b008f11  main -> main
From /home/aisha/git-course/bakery
   9531dc7..b008f11  main       -> origin/main
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
b008f11 Add resting step
Updating 9531dc7..b008f11
Fast-forward
 resting.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 resting.txt

What to read out of it: the fetch line mirrors the push line — main -> origin/main: the remote's main moved your photo, nothing else (git log --oneline -1 before the merge would still show your old tip). Status computes "behind by 1... can be fast-forwarded" from the photo. The range command shows exactly the arriving commit — read it before integrating; that habit is what fetch exists for. The merge is an ordinary Module 6 fast-forward, because your main never diverged — origin/main is just a branch name to merge.

C2. pull — fetch plus integrate, in one word

Official docs: git-pull manual

git pull = git fetch + immediately integrate origin/<branch> into your branch. When you have no local commits, the integration is a fast-forward and pull is pure convenience — the everyday "get me up to date." The distinction with fetch is when integration happens: pull decides now, fetch lets you look first. Both are correct tools; knowing which you are holding is the skill.

🧪 Exercise 8.5 — the everyday pull
bash
cd ~/git-course/kitchen-laptop2       # teammate ships another change
echo "score the top before baking" > scoring.txt
git add scoring.txt && git commit -q -m "Add scoring step" && git push -q
cd ../kitchen
git pull
Expected result — click to reveal
plain text
From /home/aisha/git-course/bakery
   b008f11..e2419cd  main       -> origin/main
Updating b008f11..e2419cd
Fast-forward
 scoring.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 scoring.txt

What to read out of it: both halves visible in one output — the fetch half (-> origin/main: photo updated) and the integrate half (Fast-forward: your main caught up, Module 6 mechanics). With no local divergence this is the whole story. The interesting case — you and the teammate both committed — is exercise 8.6, where pull stops being one-word convenience and demands a decision.

C3. push, rejection, and the divergence decision

git push uploads your branch's new commits and asks the remote to advance its branch to your tip. The remote agrees only if that advance is a fast-forward — your history must contain its current tip. If someone else pushed first, your history does not contain their commit, the advance would discard it, and the remote refuses: the famous ! [rejected]. The refusal is the distributed system's safety working: nobody's shared work can be silently overwritten by a slower pusher. The cure is mechanical: integrate their work (pull), then push your now-containing-everything history.

One modern wrinkle you must meet on purpose: when both sides have new commits, plain git pull on an unconfigured machine refuses to choose how to integrate — merge or rebase — and asks you to decide. Until Module 10 teaches rebase, the honest choice is merge: git pull --no-rebase (or set it once: git config pull.rebase false).

🧪 Exercise 8.6 — the race: rejection, decision, resolution
bash
cd ~/git-course/kitchen-laptop2       # teammate pushes first...
echo "dust with flour after baking" > finishing.txt
git add finishing.txt && git commit -q -m "Add finishing touch" && git push -q
cd ../kitchen                         # ...while you commit without fetching
echo "brush with butter after baking" > butter.txt
git add butter.txt && git commit -q -m "Add butter finish"
git push
echo "exit code: $?"
git pull
git pull --no-rebase --no-edit
git push
Expected result — click to reveal (the push and the first pull fail on purpose)
plain text
To /home/aisha/git-course/bakery.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '/home/aisha/git-course/bakery.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
exit code: 1
From /home/aisha/git-course/bakery
   e2419cd..ba35841  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
Merge made by the 'ort' strategy.
 finishing.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 finishing.txt
To /home/aisha/git-course/bakery.git
   ba35841..8a85b3e  main -> main

What to read out of it, hint block by hint block: the rejection names its reason — (fetch first): the remote contains work that you do not have locally — and prescribes the cure. Then the modern surprise: plain git pull also stops, because integrating divergent branches is a merge-or-rebase decision Git now refuses to default silently; its hint lists the three configs (pull.ff only being "never integrate implicitly — I'll do it myself"). --no-rebase chooses merge: an ordinary Module 6 three-way (conflicts possible, resolved the usual way — none here since the files differ), producing a merge commit Merge branch 'main' of …. The final push fast-forwards the remote onto a history that now contains everyone's work. This reject→integrate→push loop is the daily heartbeat of every Git team on earth.

Trap: the rejection hint's other famous escape — git push --force — makes the remote accept your history by discarding theirs. It has legitimate uses (Module 10, on branches that are yours alone), but on a shared branch it is how teammates' pushed work gets destroyed at a distance. The safer spelling to adopt now: git push --force-with-lease refuses if the remote moved since your last fetch — force, with a freshness check on your photo. Protected branches (Module 9) exist largely to make the plain force impossible where it hurts most.
Now imagine this at 500 hosts. Fetch-vs-pull becomes an automation rule: unattended systems fetch and compare; humans pull. A config-drift detector, a deploy watcher, a mirror job — all run git fetch + git rev-parse origin/main and act on the observation; anything that auto-integrates (pull) on a machine nobody is watching turns network hiccups and force-pushes into mysterious state changes. GitOps controllers (Argo CD, Flux) are exactly this pattern productized: observe the remote, diff against reality, reconcile deliberately — Module 8's photo model running your production.

🎯 Interview questions — Part C

🎯 "What is the difference between git fetch and git pull?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025 and GeeksforGeeks, updated Jul 2026

git fetch downloads new objects and updates remote-tracking branches (origin/main) — your own branches and working tree are untouched, so fetch is always safe and cannot conflict. git pull is fetch plus immediate integration of the upstream into your current branch — a fast-forward when you have no local commits, otherwise a real merge or rebase (modern Git makes you choose: pull.rebase config or --no-rebase/--rebase flags). So the difference is not what is downloaded — identical — but whether your branch changes now or when you decide.

The details that separate candidates: an average answer says "fetch downloads, pull downloads and merges." A strong answer routes the explanation through the remote-tracking branch — fetch moves the photo, pull moves the photo and your branch — because that model also explains status's ahead/behind, range comparisons, and why fetch can never break anything. The operational rule that lands: automation fetches and inspects; humans pull — and the divergent-branches refusal (fatal: Need to specify how to reconcile) is worth describing before the interviewer's follow-up gets there first.

🎯 "What is git push?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

git push uploads your branch's commits the remote lacks and asks it to advance its copy of the branch to your tip. The remote accepts only fast-forward updates — your history must contain its current tip — otherwise ! [rejected], the guarantee that a push can never silently discard someone else's shared work; the standard cure is pull-then-push. First push of a new branch: git push -u origin <branch> creates it remotely and records the upstream so later pushes need no arguments. Other verbs ride the same command: git push origin --delete <branch> removes a remote branch; --tags ships tags (Module 12); --force-with-lease overrides the fast-forward rule with a staleness check, for deliberately rewritten branches (Module 10).

The details that separate candidates: an average answer says "uploads local commits." A strong answer states the fast-forward acceptance rule and why it exists (distributed overwrite-protection), distinguishes --force from --force-with-lease unprompted, and mentions that push also transfers exactly the missing objects — content-addressing lets both ends negotiate by hash (Module 3's fleet callout, now load-bearing). Knowing push.default's modern simple behavior (push the current branch to its upstream name) rounds it out.

Part D — Remote branches and multiple remotes

D1. Publishing, tracking, and deleting branches

Branches are born local and private (Module 5); they become shared only when pushed. The lifecycle: git push -u origin <branch> publishes it and sets tracking; teammates see it after their next fetch as origin/<branch> and join in with git switch <branch> (Git auto-creates a local branch tracking the remote one when the name matches exactly one remote). When the work is merged and done, git push origin --delete <branch> removes it from the shared copy — and everyone else's stale origin/<branch> photos linger until they run git fetch --prune (worth making automatic: git config --global fetch.prune true).

🧪 Exercise 8.7 — publish a branch, then retire it
bash
cd ~/git-course/kitchen
git push -u origin sourdough          # publish Module 5's branch
git push origin --delete sourdough    # and retire it from the shared copy
git branch -vv | head -2              # local branch: untouched (but read its bracket)
Expected result — click to reveal
plain text
To /home/aisha/git-course/bakery.git
 * [new branch]      sourdough -> sourdough
branch 'sourdough' set up to track 'origin/sourdough'.
To /home/aisha/git-course/bakery.git
 - [deleted]         sourdough
* main      8a85b3e [origin/main] Merge branch 'main' of /home/aisha/git-course/bakery
  sourdough 8a00eea [origin/sourdough: gone] Add sourdough starter notes

What to read out of it: the publish line (* [new branch]) and the retirement line (- [deleted]) are the two ends of a shared branch's life. Deleting the remote branch left your local sourdough fully intact — but -vv shows its upstream bracket now reads gone: the tracking configuration from push -u survived while the branch it tracked did not. gone is informational, not an error — clear it with git branch --unset-upstream sourdough if it bothers you. Remote and local branches are independent pointers that merely correspond by name. This asymmetry runs both ways: deleting a local branch (Module 5) never touches the remote one. A branch is only truly gone when both copies and everyone's photos are — which is why hosted platforms make "delete branch on merge" a tidy little button (Module 9).

D2. More than one remote

Official docs: git-remote manual

A repository's address book holds any number of remotes: git remote add backup <address> and you can git push backup main for an off-site copy; git remote set-url origin <new> repoints after a host migration; git remote rename/remove do what they say. Each remote gets its own photo namespace (backup/main, upstream/main — all under refs/remotes/). The pattern to file away for Module 9: in fork-based open source, origin is your fork and upstream is the project you forked — you fetch from upstream, push to origin, and the two-remote address book is what makes that dance one-word simple.

🎯 Interview questions — Part D

🎯 "How do you manage multiple remotes in a Git repository?" — asked verbatim in Interview Coder's 90+ Git questions, Sep 2025

With the git remote family: git remote add <name> <url> registers each additional remote (backup, upstream, a mirror); git remote -v lists them; set-url, rename, remove administer them. Every command that talks to the network takes a remote name: git fetch upstream, git push backup main, and each remote keeps its own tracking namespace (upstream/main vs origin/main), so observations never mix. Branch upstreams pick one specific pairing (git branch -u upstream/main) for argument-less pull.

The details that separate candidates: an average answer lists the subcommands. A strong answer gives the two canonical multi-remote patterns — fork workflow (origin = your fork, upstream = the source project: fetch upstream, merge/rebase locally, push origin, PR — Module 9) and migration/mirroring (set-url for host moves; a backup remote pushed by a scheduled job as cheap disaster recovery, since every push target is a full history copy). Nuance that impresses: per-remote fetch vs push URLs exist, enabling read-from-mirror/write-to-primary setups.

Part E — Production practice

E1. Symptom → cause → diagnosis → fix

Click the symptom you're seeing.

⚠️ ! [rejected] main -> main (fetch first) on push

What is really happening: Someone pushed since your last sync; your history lacks their tip, and the remote won't discard it.

Diagnose: git fetch then git log --oneline main..origin/main

The fix: Integrate (git pull --no-rebase, or rebase — Module 10), resolve any conflicts, push again.

⚠️ fatal: Need to specify how to reconcile divergent branches.

What is really happening: Pull found commits on both sides and modern Git refuses to pick merge-vs-rebase for you.

Diagnose: Read the hint block — it lists all three choices

The fix: One-off: git pull --no-rebase (merge). Permanent: git config --global pull.rebase false (revisit after Module 10).

⚠️ git status says "up to date with origin/main" but the host shows newer commits

What is really happening: Status compares against your photo; you haven't fetched since those commits landed.

Diagnose: git fetch then git status again

The fix: Fetch first when freshness matters; status never contacts the network.

⚠️ Deleted branches still listed as origin/<branch> in git branch -a

What is really happening: Stale photos: fetch does not remove tracking refs for branches deleted remotely.

Diagnose: git remote prune origin --dry-run — see what would go

The fix: git fetch --prune now; git config --global fetch.prune true forever.

⚠️ fatal: not a git repository … or push asks "where?": fatal: The current branch X has no upstream branch.

What is really happening: The branch was never published/tracked — Git doesn't know its remote counterpart.

Diagnose: git branch -vv — no [origin/…] bracket on the branch

The fix: git push -u origin X once; thereafter bare git push works.

⚠️ Pushed to the "wrong project" for weeks (e.g., old host after migration)

What is really happening: origin still points at the old address — remotes are config, not truth.

Diagnose: git remote -v

The fix: git remote set-url origin <new-url>; verify with a fetch.

⚠️ Teammate "lost" work after someone ran git push --force on a shared branch

What is really happening: The forced history discarded pushed commits; everyone's photos and branches now disagree.

Diagnose: git reflog on any clone that had the old tip (Module 7)

The fix: Re-push the rescued tip; ban plain --force on shared branches (protection rules — Module 9; --force-with-lease where force is legitimate).

E2. Capstone — four tickets

Ticket 1 — "Set up a Git server with zero budget and zero vendors." A 4-person internal tools team needs a shared repository today. They have a Linux box everyone can SSH into, and no approval for external hosting.

Worked answer: a bare repository over SSH is a complete Git server: on the box, git init --bare /srv/git/tools.git (owned by a shared group, g+ws permissions, --shared=group at init); each engineer runs git remote add origin ssh://buildbox/srv/git/tools.git (or clones it) and pushes/pulls normally — Git speaks SSH natively, no daemon required. What you give up versus a hosting product, stated honestly: no web UI, no pull requests or reviews (Module 9's collaboration layer), no fine-grained permissions beyond Unix groups, and backups are now your job — a cron job pushing to a second bare repo elsewhere (a backup remote, D2) is the minimum. For 4 people iterating on internal tools, that trade is often right; the migration path later is one git remote set-url per clone. SSH itself — keys, agents, ~/.ssh — is covered in depth in the Linux track in this same DevOps Learning library.

Ticket 2 — "The morning race." Two engineers both pushed "first" this morning; one got ! [rejected] (fetch first) and, following a blog, ran git push --force — erasing the other's commit from the shared branch. Reconstruct, repair, prevent.

Worked answer: reconstruct: B's push was rejected (correct behavior — B's history lacked A's tip), and the force override made the server adopt B's history, discarding A's pushed commit from the shared copy — though not from existence. Repair: A's clone still has the commit (and its reflog does even if A reset); from A's machine, git push — wait, that would now be rejected too, so: A fetches, confirms origin/main lost the commit, then merges or re-pushes: git pull --no-rebase && git push restores both lines of work into shared history (worst case, the commit is retrievable by hash from A's reflog and cherry-picked or merged). Prevent, in layers: educate that the rejection is a stop sign, not an obstacle (pull is the cure); replace force habits with --force-with-lease; and make the class of accident impossible where it matters — branch protection forbidding force-push to main (Module 9).

Ticket 3 — "Automate a mirror for disaster recovery." Compliance wants an independent, continuously-updated copy of every repository, on infrastructure separate from the primary host.

Worked answer: every Git copy is a full-history backup by construction (Module 1), so mirroring is small: on the DR box, git clone --mirror <primary-url> repo.git — a bare repo whose refs exactly mirror the source, including all branches and tags; refresh on schedule with git remote update (or git fetch --prune) from cron/CI. --mirror matters over a plain clone: it replicates every ref and prunes deleted ones, so the mirror tracks reality rather than accreting stale branches. Wire monitoring to compare git rev-parse main between primary and mirror, alerting on sustained drift. Two honest caveats for the compliance doc: mirrors copy pushed history only (local-only work is out of scope by definition), and a force-push propagates to the mirror on the next sync — so point-in-time snapshots (or a mirror that fetches but never prunes, plus periodic bundles) are the layer that protects against destructive rewrites, not the live mirror itself.

Ticket 4 — "Why is origin/main lying to me?" An engineer insists Git is broken: git status says their branch is up to date, but a teammate's fix "isn't in it," and git log origin/main doesn't show the fix either.

Worked answer: nothing is broken; both observations read a stale photo. origin/main is a local bookmark updated only by network commands (B2) — if nobody fetched since the teammate pushed, both status (which compares local vs photo, offline) and log origin/main (which reads the photo's history, offline) faithfully report an outdated observation. One git fetch updates the photo; then status says "behind", the fix appears in git log main..origin/main, and git pull integrates it. Turn the incident into the team lesson: no Git command contacts the network except clone/fetch/pull/push (Module 1's rule, now with its full meaning) — everything else answers from local state, so "Git says X" always means "my last observation says X." Engineers who internalize that stop filing this ticket.

E3. Documentation reference

TopicOfficial sourceWhat it covers
Remotes (tutorial)Git Book §2.5 — Working with Remotesadd/rename/remove, fetch vs pull, inspecting remotes
Remote-tracking branchesGit Book §3.5 — Remote BranchesThe photo model, upstreams, deleting remote branches
git clonegit-clone manualAll clone modes incl. --bare, --mirror, --depth
git fetchgit-fetch manualRefspecs, --prune, what fetch updates
git pullgit-pull manualfetch+integrate, --rebase/--no-rebase/--ff-only
git pushgit-push manualFast-forward rule, -u, --delete, --force-with-lease
git remotegit-remote manualThe address book: add/-v/set-url/rename/remove/prune

E4. Self-assessment

Try each question aloud, from memory, before opening its answer — the gap between your answer and the hidden one is what to study.

1. What physically is a remote, and where is it stored?

A remote is nothing more than a nickname for another copy of the repository — a name bound to an address (URL or filesystem path). It is stored in .git/config, editable and deletable like any other config entry (git remote rename/set-url/remove). There is no special server software in Git itself.

2. Why are server-side repositories bare? Two reasons.

First, nobody edits files on the meeting-point copy — it only receives and serves snapshots, so it needs no working tree (and no index; nothing is ever staged there). Second, pushing into a repository where someone has files checked out would yank the working tree out from under them, which Git refuses by default (receive.denyCurrentBranch).

3. List everything git clone sets up beyond copying history.

Clone creates the directory, initializes the repository, registers the source as a remote named origin, fetches the full history, creates remote-tracking branches for every branch the source has, then checks out the default branch with upstream tracking already configured. It also records origin/HEAD, the remote's default branch. One command, ready to work.

4. Define origin/main precisely — where it lives, what moves it, and what it is not.

It is a local ref — a file under .git/refs/remotes/origin/, packed or loose — answering one question: where did main point on origin, last time we communicated. You cannot commit to it; only network commands (fetch, pull, push) move it. It is not "the branch on the server" live — it is your cached last observation, possibly minutes or months stale.

5. What does git fetch change, and what does it never change? Why can it never conflict?

Fetch downloads every object you are missing and moves your remote-tracking branches to match what it saw — that is all. Your branches never move and your working tree never changes. It cannot conflict because it only updates your photo of the other side, touching nothing of yours.

6. Write git pull as an equation. When is its second half trivial, and when does it demand a decision?

git pull = git fetch + immediately integrate origin/<branch> into your branch. When you have no local commits, the integration is a fast-forward — pure convenience. When both sides have new commits, modern Git refuses to choose merge-vs-rebase for you and demands a decision (--no-rebase, --rebase, or the pull.rebase config).

7. What exact condition makes a push acceptable to the remote, and what social guarantee does that rule provide?

The remote advances its branch only if that advance is a fast-forward — your history must contain its current tip. The guarantee: nobody's shared work can be silently overwritten by a slower pusher; if someone else pushed first, you get ! [rejected] instead of destroying their commit.

8. Recite the reject→integrate→push loop, including the modern divergence refusal and its resolution.

Your push is rejected with (fetch first) because the remote contains work you lack. You pull — but plain git pull also stops with fatal: Need to specify how to reconcile divergent branches, because integrating divergence is a merge-or-rebase decision. You choose merge with git pull --no-rebase (or set pull.rebase false), resolve any conflicts, and push again — now a fast-forward containing everyone's work.

9. --force vs --force-with-lease: what does the lease check, against what?

Plain --force makes the remote accept your history by discarding theirs — how teammates' pushed work gets destroyed at a distance. --force-with-lease refuses if the remote moved since your last fetch: it checks the remote's current tip against your remote-tracking photo, so it is force with a freshness check.

10. You delete a branch on the remote. What happens to your local branch, and to teammates' origin/<branch> refs — and what cleans the latter?

Your local branch is left fully intact — remote and local branches are independent pointers linked only by name — though its upstream bracket in git branch -vv now reads gone (informational; clear it with git branch --unset-upstream). Teammates' stale origin/<branch> photos linger until they run git fetch --prune; git config --global fetch.prune true makes that automatic.

11. Describe the two-remote fork pattern: which remote do you fetch from, which do you push to?

In fork-based open source, origin is your fork and upstream is the project you forked. You fetch from upstream to stay current and push to origin, and each remote keeps its own photo namespace (origin/main vs upstream/main) under refs/remotes/.

12. "Git says I'm up to date but I'm not." Diagnose in one sentence using the photo model.

git status compares your branch against your stale local photo (origin/main), which no command updates except clone/fetch/pull/push — so fetch first, and status will tell the truth.

E5. Sources

Interview questions in this module were captured verbatim from: Interview Coder — 90+ Common Git Interview Questions (Sep 20, 2025) and GeeksforGeeks — Top 70+ Git Interview Questions (updated Jul 30, 2026). The corpus is rich on fetch-vs-pull and clone but thin on remote-tracking branches as a standalone question — B2 teaches them anyway because every other answer depends on the concept. Technical claims were verified against the official Git documentation in E3; every command and output on this page was executed on Git 2.43.0 on Linux, using a local bare repository as the remote — outputs over a network add transfer-progress lines but are otherwise identical. Commit hashes will differ on your machine.

🗒️ Cheat sheet — Module 8

CommandWhat it does
git init --bare <name>.gitCreate a server-style repository: the database alone, no working tree
git clone <url> [dir]Full local copy + origin + tracking branches + checkout, in one step
git remote -v · add · set-url · rename · removeRead and edit the repository's address book
git push -u origin <branch>Publish a branch and record its upstream (first push only)
git fetch · git fetch --pruneUpdate remote-tracking photos, touch nothing else · also drop photos of deleted branches
git pull · git pull --no-rebasefetch + integrate now · same, explicitly choosing merge on divergence
git pushUpload commits; accepted only as a fast-forward of the remote branch
git log main..origin/main · git branch -vv · git branch -aWhat arrived that you lack · upstreams per branch · include remote-tracking refs
git push origin --delete <branch>Remove a branch from the shared copy (local branch unaffected)
git push --force-with-leaseForce, but only if the remote still matches your last fetch — never plain --force on shared branches
git clone --mirror <url>git remote updateFull-fidelity mirror for backup/DR, refreshed on schedule

Key concepts: remote = named address in config; server = bare repo (the .git contents standing alone) · clone = init + origin + full history + tracking + checkout · origin/<branch> = your local photo of the remote's branch, moved only by network commands — status and logs read the photo, not the server · fetch updates photos only (always safe); pull = fetch + integrate now; divergence makes modern pull demand merge-vs-rebase · push accepted only as fast-forward — the rejection protects teammates' pushed work; cure is pull-then-push · -u records upstream once; --force-with-lease over --force, and neither on protected shared branches · local and remote branches are independent pointers linked by name; prune stale photos · exactly four commands touch the network — everything else reports your last observation.

Next: Module 9 — Collaboration Workflows — you can now move history between machines. Module 9 adds the human layer: feature branches, pull requests and reviews, forks, and the trunk-based vs Git Flow decision every team argues about.
Spotted a mistake or want something added? Send me a note.