Module 8 — Remotes (git clone, fetch, pull, push)
Updated 8 September 2026
🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)
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
cd ~/git-course
git init --bare bakery.git
ls bakery.git✅ Expected result — click to reveal
Initialized empty Git repository in /home/aisha/git-course/bakery.git/
HEAD
branches
config
description
hooks
info
objects
refsWhat 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.
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
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
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
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"
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
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🎯 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 (init → remote add → fetch → switch), 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
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
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
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.txtWhat 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
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
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
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.txtWhat 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
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)
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 -> mainWhat 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.
🎯 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
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
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 notesWhat 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
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
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.
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).
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.
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
| Topic | Official source | What it covers |
|---|---|---|
| Remotes (tutorial) | Git Book §2.5 — Working with Remotes | add/rename/remove, fetch vs pull, inspecting remotes |
| Remote-tracking branches | Git Book §3.5 — Remote Branches | The photo model, upstreams, deleting remote branches |
| git clone | git-clone manual | All clone modes incl. --bare, --mirror, --depth |
| git fetch | git-fetch manual | Refspecs, --prune, what fetch updates |
| git pull | git-pull manual | fetch+integrate, --rebase/--no-rebase/--ff-only |
| git push | git-push manual | Fast-forward rule, -u, --delete, --force-with-lease |
| git remote | git-remote manual | The 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
🗒️ Cheat sheet — Module 8
| Command | What it does |
|---|---|
| git init --bare <name>.git | Create 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 · remove | Read 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 --prune | Update remote-tracking photos, touch nothing else · also drop photos of deleted branches |
| git pull · git pull --no-rebase | fetch + integrate now · same, explicitly choosing merge on divergence |
| git push | Upload commits; accepted only as a fast-forward of the remote branch |
| git log main..origin/main · git branch -vv · git branch -a | What 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-lease | Force, but only if the remote still matches your last fetch — never plain --force on shared branches |
| git clone --mirror <url> • git remote update | Full-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.