Practice: Remotes and forks
Self-check questions
Section titled “Self-check questions”Answer each in your own words first, then open the answer to check.
Q1. Explain why git is called “distributed.” What does that mean for offline work and for recovering from a GitHub outage?
Show answer
Git is called “distributed” because every clone is a full repository; it contains the entire history (every commit, every branch, every tag). There’s no architectural “central server”; the team designates one repo as canonical by convention. Practical consequences: most operations work offline (commit, branch, log, diff, status; only push and pull need network). If GitHub has an outage, your local repo still works fully; you just can’t push or pull until GitHub is back. If GitHub data were lost entirely, any developer’s clone could become the new canonical repo with no data loss. The distributed model is more resilient than centralized version control because the team’s history is replicated across every developer’s machine.
Q2. Walk through what git fetch does and how it differs from git pull. Why might a senior engineer prefer fetch over pull for daily work?
Show answer
git fetch downloads new commits from the remote and updates the remote-tracking branch labels (origin/main, etc.) but does NOT change your working files or merge anything into your current branch. It’s “look but don’t touch.” git pull is a shortcut for git fetch + git merge (or git fetch + git rebase with --rebase). The difference: fetch lets you inspect the remote state and decide what to do, while pull automatically integrates. A senior engineer might prefer fetch in two situations: (1) when investigating “what did my teammate push?” without committing to integrate it, and (2) when about to merge and wanting to verify the conflict surface before triggering the merge. The “look first, then decide” workflow is safer than the “integrate automatically” workflow, especially on shared branches.
Q3. Your teammate pushed to main while you were committing. You try git push and it’s rejected. Walk through the resolution in order.
Show answer
Resolution in order:
- Read the push-rejected error message; observe it tells you to “fetch first” and pull before pushing
- Run
git pull --rebase(orgit pullif rebase isn’t your default). This fetches teammate’s commits and integrates them with yours. - If there’s a merge conflict, resolve it (L7 mechanics): edit the file, remove markers,
git add, thengit rebase --continue. - Run
git pushagain. This time it succeeds because your local now has all the commits the remote did, plus yours on top.
Total time: usually under a minute if no conflicts; a few minutes if there are. The push-rejected error is not failure; it’s just git ensuring you don’t accidentally overwrite teammate work.
Q4. In the fork-based model, what’s the difference between origin and upstream? What does each pointer represent?
Show answer
In the fork-based model:
originpoints to YOUR fork (your-username/repo). You have write access to your fork. This is where you push your branches.upstreampoints to the ORIGINAL project (original-owner/repo). You don’t have write access (otherwise you wouldn’t need a fork). You fetch from upstream to stay current, but rarely push to it (typically only maintainers do).
Workflow: branches on your fork (push to origin), PRs target upstream (the original project), and you periodically sync your fork’s main from upstream’s main to stay current. This model scales to almost every open-source contribution scenario on GitHub.
Q5. When is git push --force-with-lease safe to use, and when is it never acceptable?
Show answer
git push --force-with-lease is safe when:
- The branch is your personal feature branch (no one else is committing to it)
- You’ve rebased or amended commits and need to push the rewritten history
- The lease check protects you: it refuses if someone pushed to the branch since your last fetch, preventing silent overwrites
It’s never acceptable on:
- Shared branches like
main,develop,release/*; these are the team’s source of truth; force-push silently breaks everyone’s local work - Production-deployed branches; force-push can leave history in a state inconsistent with what’s deployed
- Anyone else’s branch; your responsibility is your own branches; let others manage theirs
The rule: force-push is for branches whose history is YOUR personal responsibility. Always use --force-with-lease rather than plain --force to get the safety check.
Push-pull drill (uses two clones simulating two developers)
Section titled “Push-pull drill (uses two clones simulating two developers)”This drill simulates two-developer collaboration on a single laptop. Use two folders on your machine.
Setup:
- Create a sandbox folder.
mkdir l8-drill && cd l8-drill - Initialize a “bare” repo (this simulates GitHub):
git init --bare central.git - Clone it as “developer 1”:
git clone central.git dev1 - Clone it again as “developer 2”:
git clone central.git dev2
You now have three folders: central.git (the “remote”), dev1 (clone 1), dev2 (clone 2).
Drill 1: First commits + push:
cd dev1. Create a file:echo "Hello from dev1" > README.mdgit add README.md && git commit -m "Initial commit"git push origin main. (Note: this works even without -u because main already exists locally.)cd ../dev2 && git pull. Observe dev2 now has the README.
Drill 2: Diverging commits + conflict:
cd dev1. Edit README.md to add “Line from dev1” at the end. Commit. Push.cd ../dev2. Edit README.md (WITHOUT pulling first) to add “Line from dev2” at the end. Commit.- Try to push from dev2:
git push. Rejected. - Read the error message carefully; notice it tells you to pull first.
git pull --rebase. Conflict! (L7 territory.)- Resolve the conflict (keep both lines).
git add README.md && git rebase --continue. - Push:
git push. Succeeds. cd ../dev1 && git pull. Observe dev1 has the combined README.
Drill 3: Force-with-lease:
- From dev1, edit README and commit. Push.
- From dev1, IMMEDIATELY rewrite that commit:
git commit --amend -m "Better message". - Try to push:
git push. Rejected (history rewritten). git push --force-with-lease. Succeeds (you’re the last one who pushed, so the lease is intact).- From dev2:
git fetch. Observe the rewritten commit.
Drill 4: Force-with-lease catches concurrent writes:
- From dev1, edit README and commit. Push.
- From dev2,
git pull --rebaseto get the latest. - From dev2, edit README and commit. Push.
- From dev1, BEFORE fetching, try to amend a commit and force-with-lease:
git commit --amend -m "Trying to rewrite"git push --force-with-lease. Refused (dev2 pushed since dev1’s last fetch).
- From dev1,
git fetchto update your view. Now decide: was the rewrite even valuable? If so, rebase your local on top of dev2’s commits, then force-with-lease succeeds.
This drill shows force-with-lease catching the dangerous case. Plain --force would have silently destroyed dev2’s commit.
Fork workflow drill (uses a real GitHub fork)
Section titled “Fork workflow drill (uses a real GitHub fork)”This drill takes you through the open-source contribution workflow end-to-end.
Drill 5: Set up a fork (real GitHub, about 10 minutes):
- Pick a small public repo on GitHub (your own, or a “good first issue” hunt).
- Click “Fork” in the upper-right of the GitHub UI. GitHub creates
<your-username>/<repo>. git clone https://github.com/<your-username>/<repo>.gitcd <repo>git remote add upstream https://github.com/<original-owner>/<repo>.gitgit remote -v: verify you see bothoriginandupstream.git fetch upstream: pull down upstream’s branches without merging.git branch -a: observeremotes/origin/mainANDremotes/upstream/main.
Drill 6: Sync your fork’s main from upstream’s main:
git switch maingit fetch upstreamgit rebase upstream/main: update your local main to match upstreamgit push origin main: push the updated main to your fork
Repeat this whenever upstream has moved and you want your fork’s main current.
Drill 7: Make a contribution (simulated):
git switch -c improvement/example-change- Make a small, low-risk change (a comment, README typo, etc.)
- Commit with a clear message.
git push -u origin improvement/example-change- Open a PR on GitHub from your fork’s branch to upstream’s main.
- If this is a real contribution, address review feedback. If it’s just a practice drill, you can close the PR without merging.
You have now performed the full open-source contribution workflow. The same six steps work for almost every public repo on GitHub.
Scenario reflections
Section titled “Scenario reflections”Scenario A. A teammate says “I always just do git pull; never use git fetch. Why would I bother with fetch?” Walk through two situations where fetch is the better choice.
Show answer
Two situations where fetch is the better choice:
- Investigating before integrating. Your teammate just pushed a huge change. You want to see what they did before merging it into your branch.
git fetchdownloads their commits and lets yougit log origin/main,git diff main origin/main,git show <their-commit>without affecting your working tree. Once you understand what they did, then decide whether to merge, rebase, or wait. - Pre-merge conflict verification. Before merging a long-lived feature branch into main, you want to know how many conflicts to expect.
git fetch && git log origin/main..feature/my-branchshows you what’s diverged. You can scan for files both branches touched. Then you can plan the merge with realistic expectations instead of being surprised mid-conflict.
The general principle: pull is a commitment; fetch is reconnaissance. Senior engineers default to reconnaissance.
Scenario B. Your team’s repo has accumulated dozens of “Merge branch ‘main’ of github.com:…” commits in history. Without writing code, identify the upstream cause and the single config change that prevents the pattern going forward.
Show answer
Upstream cause: every time someone runs git pull with the default behavior (merge), git creates a “Merge branch ‘main’ of …” commit. Over months of pulls, these accumulate. The clutter is visual noise; nobody benefits from those merge commits; they just record “I pulled at 3:47pm.” The single config change that prevents this going forward:
git config --global pull.rebase trueAfter this, every git pull uses rebase instead of merge. Pulled commits go on the bottom of local commits; no merge commit is created. History stays linear.
For the existing merge commits in history: leave them. They’re harmless. The fix prevents future ones; trying to rewrite history to remove the old ones would create a much bigger problem than the visual clutter.
Scenario C. A new contributor on an open-source project you maintain force-pushed to a branch that another contributor was working off of. The other contributor lost work. Walk through the conversation you’d have with the new contributor (without blame) and the policy you’d suggest the project adopt.
Show answer
Conversation with the new contributor (without blame):
- “Hey, I noticed you force-pushed to
<branch>earlier; I think someone else was working off that branch and lost their commits when you pushed. Let me show you--force-with-leasewhich would have caught that case automatically.” - Walk them through
--force-with-leaseand explain when it’s safe vs unsafe. - “For your own branches, force-with-lease is fine after a rebase. For shared branches like main, we don’t force-push at all.”
Policy to suggest:
- Branch protection rules on
main: no force-pushes allowed. (GitHub setting, takes 2 minutes to configure.) - Project CONTRIBUTING.md adds a “force-push policy” section: force-with-lease only, personal branches only.
- If you’re feeling generous, a pre-push git hook that warns when about to force-push (advanced, optional).
The structural fix (branch protection) is more reliable than the cultural fix (telling contributors). Both belong.
Flashcards
Section titled “Flashcards”Q. What is a remote?
A named pointer to another git repository’s URL. origin is the default name and usually points to your team’s GitHub repo.
Q. Why is git called distributed?
Because every clone is a full repository. Every developer’s machine has the entire history. There’s no architectural “central server”; that’s a team convention.
Q. What does git push -u origin (branch name) do?
Pushes the branch to origin AND sets the upstream tracking relationship so future git push calls work without arguments.
Q. What's the difference between git fetch and git pull?
Fetch downloads remote changes but doesn’t merge. Pull = fetch + merge (or fetch + rebase, with --rebase). Fetch lets you inspect before committing to integrate.
Q. What is a remote-tracking branch like origin/main?
A local label pointing to what the remote’s branch looked like at your last fetch. You don’t commit to these directly; they’re read-only snapshots of “what GitHub said.”
Q. What does git status tell you about your branch's relationship to the remote?
Ahead by N commits (you have commits to push), behind by N commits (remote has commits to pull), or diverged (both have new commits that need merging or rebasing).
Q. What's the fork-based contribution model?
origin = your fork. upstream = the original project. You push to origin, open PR from origin to upstream. This is how almost all open-source contributions work.
Q. When is git push --force acceptable?
Almost never. Prefer --force-with-lease, which refuses to overwrite if the remote has moved since your last fetch. Even then, only on personal branches; never on shared main, develop, or release branches.
Q. What does git config --global pull.rebase true do?
Sets all future git pull calls to use --rebase by default instead of merge. Keeps history linear; eliminates the “Merge branch ‘main’ of …” commit clutter.
Q. How does git survive a GitHub outage?
Mostly fine. Local operations (commit, branch, log, diff, status) work offline. Only push and pull require the network. The team’s history is replicated across every developer’s laptop; recovery is always possible from any clone.