Introduction
You've forked a repository to contribute a feature, but weeks pass and the upstream project has moved on without you. Your branch sits on an older commit, and merging suddenly becomes a minefield of conflicts. Tracking upstream changes in a fork: a step-by-step guide will show you how to keep your fork current with the original project, ensuring smooth collaboration and painless integration of your work.
When you fork a repository on GitHub, you create your own copy, but you lose the automatic connection to the original project. As the upstream repository evolves, your fork becomes stale. Without a strategy to track upstream changes, you risk working on outdated code, discovering incompatible changes mid-development, or struggling through merge conflicts when you finally open a pull request. The good news is that Git provides straightforward tools to stay synchronized.
Why tracking upstream matters
In most professional workflows, you'll fork a repository not to own it, but to contribute. Whether you're fixing a bug in an open-source library or proposing a feature for a team project, your fork is a temporary workspace. The upstream repository is the source of truth—the version that matters to users and maintainers. If your fork drifts too far behind, several problems emerge:
- Your pull request becomes harder to review because it includes unrelated changes from intervening commits.
- Dependencies or APIs may have changed, breaking code that worked when you started.
- Merge conflicts multiply, forcing you to resolve conflicts that have nothing to do with your actual changes.
- CI/CD pipelines may fail on stale code that no longer aligns with the project's current standards.
By actively tracking upstream changes, you ensure your work stays relevant and mergeable. This is not a nice-to-have—it's a professional practice that separates smooth collaboration from painful rebases.
Adding the upstream remote
Git tracks repositories through remotes: named references to remote URLs. When you clone your fork, Git automatically sets up an origin remote pointing to your fork. To track the original project, you need to add another remote called upstream.
First, verify your current remotes:
git remote -vYou should see something like:
origin https://github.com/your-username/project.git (fetch)
origin https://github.com/your-username/project.git (push)Now add the upstream remote. Navigate to the original repository on GitHub and copy its HTTPS or SSH URL. Then run:
git remote add upstream https://github.com/original-owner/project.gitVerify the addition:
git remote -vYou should now see:
origin https://github.com/your-username/project.git (fetch)
origin https://github.com/your-username/project.git (push)
upstream https://github.com/original-owner/project.git (fetch)
upstream https://github.com/original-owner/project.git (push)That push URL for upstream won't be used by you—you push to your fork, not the original project—but it doesn't hurt to have it there. Some teams prefer to set git remote set-url --push upstream disabled to prevent accidental pushes, but that's optional.
Add this step to your fork setup checklist, ideally right after cloning. It takes seconds now and saves hours later.
Fetching upstream changes
Once the upstream remote is configured, you can pull down all the commits from the original repository without modifying your local branches. This is what git fetch upstream does.
Run:
git fetch upstreamGit contacts the upstream repository, compares what it has with what you have, and downloads any new commits. You'll see output like:
remote: Enumerating objects: 245, done.
remote: Counting objects: 100% (245/245), done.
remote: Compressing objects: 100% (87/87), done.
Unpacking objects: 100% (124/124), 42 KB | 1.2 MB/s
From https://github.com/original-owner/project
* [new branch] feature/new-api -> upstream/feature/new-api
* [new branch] main -> upstream/main
+ 1a2b3c4...5f6g7h8 develop -> upstream/develop (forced update)
a1b2c3d..1a2b3c4 main -> upstream/mainNotice the remote-tracking branches: upstream/main, upstream/develop, etc. These represent what exists in the upstream repository. They're read-only references on your machine—you never commit to them directly. They update only when you fetch.
Fetching is safe. It brings information into your repository without changing your working directory or local branches. You can fetch multiple times a day with no side effects.
Make fetching a habit. Before starting work on a feature branch, fetch upstream. Before opening a pull request, fetch upstream. Some teams automate this by running git fetch --all in CI/CD pipelines or pre-commit hooks, but the manual approach is fine for most workflows.
Rebasing your branch on upstream/main
Now that you have the latest commits from upstream, you need to rebase your feature branch onto them. Rebasing rewrites your commits on top of a new base, ensuring your changes apply cleanly to the current state of the project.
First, check which branch you're on:
git branchSwitch to your feature branch if needed:
git checkout feature/my-fixNow rebase onto upstream/main:
git rebase upstream/mainGit will replay your commits on top of the latest upstream/main. If there are no conflicts, you're done:
Successfully rebased and updated refs/heads/feature/my-fix.If conflicts arise, Git pauses and marks the conflicted files. Open them, resolve the conflicts manually (keeping the logic you need), then stage the changes:
git add src/api.js
git rebase --continueIf you realize you made a mistake during the rebase, abort it:
git rebase --abortYour branch returns to its pre-rebase state, and you can try again.
Here's a concrete scenario: your team's main branch has moved from commit A to commit C. You've been working on commits B1 and B2 based on A. After rebasing onto upstream/main, your commits are now C-B1-B2. Your changes are the same, but they're applied to the current codebase. This makes review easier and integration smoother.
Some developers worry that rebasing "changes history," but that's exactly what you want here. You're ensuring your branch stays linear and clean, which is a mark of professional Git discipline.
Pushing with --force-with-lease
After rebasing, your local branch history no longer matches the remote version of your branch (which still reflects the old, pre-rebase commits). A normal git push will fail because Git refuses to overwrite remote history by default.
You need to force the push, but do it safely. Never use git push --force in a shared branch. Use git push --force-with-lease instead:
git push origin feature/my-fix --force-with-leaseWhat's the difference? --force-with-lease checks that the remote hasn't changed since your last fetch. If someone else has pushed to this branch (unlikely for a feature branch you own, but critical if you're working with others), the push fails, protecting you from accidentally overwriting their work:
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behindThis gives you a chance to fetch and understand what happened. By contrast, git push --force would just overwrite everything, silently destroying work.
In a typical workflow, your feature branch is yours alone until you open a pull request, so this scenario is rare. But --force-with-lease is the professional choice because it costs nothing and provides crucial safety.
After the push, your remote branch is updated. Your GitHub PR will automatically update, often showing that your branch is now mergeable (if conflicts were resolved during the rebase).
Common mistakes
Mistake 1: Merging upstream instead of rebasing. Some developers run git merge upstream/main instead of git rebase upstream/main. Merging works but creates a merge commit, polluting your branch history. Reviewers see both your changes and a merge commit, making the PR harder to follow. Rebase keeps history clean.
Mistake 2: Forgetting to fetch before rebasing. If you rebase on upstream/main without fetching first, you're rebasing on stale data. Always fetch upstream before rebasing to ensure you're working with current commits.
Mistake 3: Using --force instead of --force-with-lease. --force is a sledgehammer. It bypasses all safety checks and can destroy collaborative work. Develop the habit of always using --force-with-lease for any forced push.
Mistake 4: Rebasing shared branches. Never rebase a branch that others depend on. Rebase only your own feature branches. If you accidentally rebase and push a shared branch, notify your team immediately so they can re-clone or reset their local branches.
Mistake 5: Losing work during a failed rebase. If a rebase goes wrong and you panic, you can always recover using git reflog. But the simpler approach is to use git rebase --abort before things spiral. Your original branch is always safe if you abort.
Key takeaways
- Add upstream as a remote immediately after forking:
git remote add upstream <url>. - Fetch upstream regularly to stay aware of changes:
git fetch upstreamis always safe. - Rebase your feature branch onto upstream/main to keep it current and conflict-free:
git rebase upstream/main. - Push rebased branches safely with
--force-with-lease, never with--force. - Fetching, rebasing, and force-pushing are standard practices in professional Git workflows, not signs of advanced usage.
- Aborting a rebase is always an option if something goes wrong; your original branch is never lost.






Comments
Loading comments…