Lesson 4 - Keeping a Fork in Sync

Welcome to Keeping a Fork in Sync

The moment you fork a project, the clock starts ticking. The maintainers keep merging pull requests, fixing bugs, and adding features — but none of that lands in your fork automatically. A fork is a snapshot, not a live mirror. Leave it for a week and the original has moved on while your copy sits exactly where you left it. Start a new contribution on top of that stale code and you’ll be building on yesterday’s project, which is how merge conflicts and out-of-date pull requests are born.

The fix is a small, repeatable routine: tell Git where the original lives (a remote called upstream), pull its latest changes down into your local main, and push that refreshed main back up to your fork. This lesson teaches that routine, plus the rebase trick for keeping a feature branch sitting cleanly on top of the newest code.

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

  • Distinguish the two remotes — origin (your fork) and upstream (the original) — and confirm them with git remote -v
  • Fetch the original’s latest changes with git fetch upstream
  • Merge upstream/main into your local main and push the result to your fork
  • Rebase a feature branch onto the updated main for a clean, linear history
  • Decide when to merge and when to rebase

We’ll keep using the practice repo with the accounts you (your fork) and datatweets (the original). Let’s catch up.


The Two Remotes: origin and upstream

When you cloned your fork, Git gave you one remote automatically — origin, pointing at your copy under your account. To stay in sync, you add a second remote pointing at the original repo, by convention named upstream. (You’d add it once with git remote add upstream [email protected]:datatweets/git-collaboration-practice.git; here it’s already set up.)

Check what you have with git remote -v:

$ git remote -v
origin    [email protected]:you/git-collaboration-practice.git (fetch)
origin    [email protected]:you/git-collaboration-practice.git (push)
upstream  [email protected]:datatweets/git-collaboration-practice.git (fetch)
upstream  [email protected]:datatweets/git-collaboration-practice.git (push)

Read it as two destinations. origin is you/git-collaboration-practice — your fork, the one you push to. upstream is datatweets/git-collaboration-practice — the original project, the one you only read from. The (fetch) and (push) lines just say each remote is wired for both directions, but in practice you push to origin and fetch from upstream. Get those two names straight and the rest of the workflow falls into place.

A diagram with two remotes at top — upstream (the original repo, read-only to you) and origin (your fork, you can push) — and 'your local clone' at the bottom. A green arrow 'git fetch upstream, then merge upstream/main' goes from upstream down to the local clone; a blue arrow 'git push origin' goes from the local clone up to origin. A caption explains to add the original repo as a remote called upstream, fetch its updates, merge them into your branch, then push to your fork.
Add the original repo as an upstream remote: fetch its updates, merge them into your branch, then push to your fork.

Fetch from upstream, Merge into main, Push to Your Fork

The sync itself is three commands. First, fetch from upstream to download the original’s latest commits — this updates your remote-tracking branch upstream/main without touching any of your local branches:

$ git fetch upstream
From github.com:datatweets/git-collaboration-practice
 * [new branch]      main       -> upstream/main

That line tells you Git now knows about the original’s main as upstream/main. Nothing in your working files has changed yet — fetch only downloads; it never modifies your branches.

Next, switch to your local main and merge the upstream branch into it:

$ git switch main
$ git merge upstream/main
Updating 592fe4c..bc8e83c
Fast-forward
 task_manager.py | 3 +++
 1 file changed, 3 insertions(+)

The key word is Fast-forward. Because you hadn’t made any commits of your own on main, your branch was simply behind upstream/main — so Git just slides the main pointer forward to match, with no merge commit needed. Your main is now identical to the original’s. (If you had committed directly on main, you’d get a real merge commit instead — one good reason to keep your work on feature branches and leave main clean.)

Finally, push your freshly updated main up to your fork so origin reflects the new state:

$ git push origin main

That’s the full loop: fetch upstreammerge upstream/mainpush origin main. Run it whenever you’re about to start new work, and you’ll always be building on the latest code.


The Rebase Alternative for Feature Branches

Syncing main is one thing. But what if you already have a feature branch in progress and, meanwhile, main has moved ahead? You want your branch to sit on top of the new main, as if you’d started your work today. That’s exactly what git rebase does: it lifts your branch’s commits off and replays them on top of the latest main, producing a clean, linear history.

Here’s a small repo before rebasing. Your feature/docs branch (“Add docs”) split off the same point where main later gained “Add version file” — so history has forked:

$ git log --oneline --graph --all
* 8d9ea55 Add version file
| * a8b6854 Add docs
|/  
* c10e6f6 Add app

Switch to the feature branch and rebase it onto main:

$ git switch feature/docs
$ git rebase main
Successfully rebased and updated refs/heads/feature/docs.

Now look again — the fork is gone, replaced by a single straight line:

$ git log --oneline --graph --all
* 6cf730d Add docs
* 8d9ea55 Add version file
* c10e6f6 Add app

Notice that “Add docs” now sits on top of “Add version file” with no branching, and its hash changed from a8b6854 to 6cf730d. (Your own hashes will differ — that’s expected.) That hash change is the whole point and the whole catch: rebase doesn’t move your original commits, it rewrites them as new ones. The history is tidier, but the commits are no longer the same objects.

That’s the merge-vs-rebase trade-off in a nutshell. Merge preserves exactly what happened, including the fork, by adding a merge commit — safe and honest, but history can get tangled. Rebase gives you a clean linear story, but only because it rewrites commits. Use rebase to keep a private feature branch current; use merge when you need a faithful record or the branch is shared.

Only rebase a branch you haven’t shared

Rebasing rewrites your commits (new hashes), so it’s safe only on a branch that’s still yours alone — one you haven’t pushed for others to build on. The instant a branch is shared, rebasing it forces everyone else into a painful reconciliation. This is the same rule from Module 2: don’t rewrite history that other people have already pulled. For the shared main, always merge — never rebase it.


Practice Exercises

Exercise 1: Read the remotes

You run git remote -v and see origin pointing at you/project and upstream pointing at datatweets/project. Which remote do you fetch the original’s updates from, and which do you push your synced main to?

Hint

Fetch updates from upstream (the original, datatweets/project) and push to origin (your fork, you/project). You only read from upstream; you write to your own fork.

Exercise 2: Why was it a fast-forward?

After git merge upstream/main, Git reported Fast-forward instead of creating a merge commit. Explain why that happened, and what would have produced a merge commit instead.

Hint

Your local main had no commits of its own — it was simply behind upstream/main — so Git just slid the main pointer forward, no merge commit required. If you’d committed directly on main, the two branches would have diverged and Git would have created a real merge commit.

Exercise 3: Merge or rebase?

You have a feature branch you started yesterday that nobody else has pulled, and main has since moved ahead. You also need to update the shared main itself with the latest from upstream. Which gets a rebase and which gets a merge, and why?

Hint

Rebase the private feature branch onto the latest main — it’s yours alone, so rewriting its commits is safe and gives a clean linear history. Merge upstream/main into the shared main, never rebase it: rebasing shared history would force everyone else to reconcile rewritten commits.


Summary

A fork doesn’t update itself, so you keep it current with a short routine built around two remotes: origin (your fork, where you push) and upstream (the original, where you fetch). Confirm both with git remote -v. To sync, git fetch upstream downloads the original’s latest into upstream/main, then git switch main and git merge upstream/main brings those changes into your local main — a clean fast-forward when you haven’t committed on main yourself — and git push origin main updates your fork. For an in-progress feature branch, git rebase main replays your commits on top of the latest main for a tidy linear history. The catch: rebase rewrites commits (their hashes change), so rebase only branches you haven’t shared, and always merge — never rebase — the shared main.

Key Concepts

  • upstream remote — the original repo you forked from; add it once, fetch updates from it.
  • fetch then mergegit fetch upstream downloads; git merge upstream/main integrates it into your main.
  • fast-forward — Git just advances the main pointer when you haven’t diverged; no merge commit.
  • rebase — replays your branch’s commits onto the latest main for a linear history, rewriting them.
  • merge vs rebase — merge preserves history (safe, shareable); rebase rewrites it (tidy, private branches only).

Why This Matters

Out-of-date forks are the silent cause of most contribution headaches: pull requests that won’t merge cleanly, conflicts that pile up, and work built on code that has already changed. Mastering the upstream sync routine means your contributions always start from the project’s latest state, so your pull requests stay easy to review and easy to merge. And knowing when to merge versus rebase — keeping your own branches tidy without ever rewriting history others depend on — is exactly the judgment that separates a confident collaborator from a cautious one.


Next Steps

Continue to Lesson 5 - Guided Project: Contribute a Feature via PR

Put it all together: fork, branch, sync, and contribute a real feature through a pull request from start to finish.

Back to Module Overview

Return to the Collaboration with Pull Requests module overview


Continue Building Your Skills

You can now keep a fork in step with the project it came from — adding an upstream remote, fetching and merging its changes, pushing to your fork, and rebasing feature branches for a clean history. In the next lesson you’ll bring every skill from this module together in a guided project, contributing a complete feature to a real repository through a pull request.