Keeping a Long-Running Fork in Sync When Upstream Rebases Shared History

May 24, 2026 5 min read 28 views
Abstract diagram of two Git branch trees diverging and reconnecting, representing fork synchronization after a rebase

You wake up to find upstream has force-pushed to main. Commits you based your entire feature branch on have been rewritten, squashed, or renumbered. Your fork's history now diverges in ways git merge won't cleanly resolve.

This is a specific, painful situation that trips up even experienced contributors. The good news: there's a systematic way out, and a few habits that prevent it from happening again.

What you'll learn

  • How to detect when upstream has rewritten history your fork depends on
  • The difference between rebasing onto new upstream commits versus merging them in
  • How to use git rebase --onto to transplant your commits onto a clean base
  • Strategies for keeping a long-running fork maintainable over time
  • Common mistakes people make during recovery and how to avoid them

Prerequisites

You should be comfortable with basic Git concepts: branches, commits, remotes, and what a rebase does at a high level. You'll need Git 2.x installed. The examples use a Unix-style shell, but the commands translate directly to Windows PowerShell or Git Bash.

Understanding What Actually Happened

When upstream rebases, every commit that was touched gets a new SHA. Even if the content of a commit is identical, its hash changes because the parent pointer changed. From Git's perspective, those are entirely different commits.

Your fork has commits whose parents are the old SHAs. The new upstream commits have no ancestry relationship with your work, so git merge upstream/main won't do what you expect. It'll try to reconcile two completely independent lines of history, often producing a tangled mess of duplicate changes.

Run this to see exactly where your branch and upstream diverged:

git fetch upstream
git log --oneline --graph upstream/main HEAD

You're looking for the last commit your work shares with the new upstream history. That's your graft point.

Finding the Last Common Ancestor

Before you do anything destructive, identify three things: the old base, the new base, and the tip of your own work.

# The commit on upstream/main just before the rebase happened
# (you may need to check the upstream repo's reflog or release notes)
OLD_BASE=<sha-of-old-upstream-tip>

# The current tip of the rewritten upstream branch
NEW_BASE=$(git rev-parse upstream/main)

# The tip of your fork's branch
FORK_TIP=$(git rev-parse my-feature)

If you have access to the upstream reflog (e.g., on GitHub, the maintainer may have published a note), you can find OLD_BASE precisely. If not, look for the commit message that matches the last upstream commit you merged or rebased onto. Use git log --oneline my-feature to find it in your own history.

The Core Fix: git rebase --onto

git rebase --onto is designed for exactly this situation. It says: take all the commits between <old-base> and <fork-tip>, and replay them on top of <new-base>.

git checkout my-feature
git rebase --onto upstream/main <OLD_BASE> my-feature

Git will walk the commit range, apply each of your patches in order on top of the new upstream base, and stop when it hits a conflict. You resolve conflicts one commit at a time, then run git rebase --continue.

If a commit becomes empty because upstream already incorporated that change, use:

git rebase --skip

Don't force-apply an empty commit β€” it adds noise and confusion to your history.

Resolving Conflicts Without Losing Context

Conflicts during a rebase onto rewritten history are common. The upstream maintainer may have reformatted code, renamed variables, or restructured files while rewriting. Your patch applies to a context that no longer matches.

Use a three-way merge tool to see all three versions: base, upstream, and yours:

git mergetool

Pay attention to the intent of your change, not just the lines. If upstream renamed a function and your commit adds a parameter to that function, your goal is to add the parameter to the renamed version, not restore the old name.

After resolving each file:

git add <resolved-file>
git rebase --continue

If things go badly wrong at any point, you can abort entirely and return to your pre-rebase state:

git rebase --abort

Testing Before You Push

After the rebase completes, your branch looks clean β€” but clean history doesn't mean working code. Run your full test suite before touching the remote.

# Example: a Python project with pytest
python -m pytest tests/

Also do a quick sanity check on the diff between your branch and upstream to confirm you haven't accidentally introduced or lost anything:

git diff upstream/main..my-feature

The output should contain only your intentional changes. If you see upstream changes appearing in this diff, something went wrong during conflict resolution and you need to re-examine those commits.

Force-Pushing Your Fork Safely

Your fork's remote branch still has the old history. You'll need to force-push, but do it carefully.

git push origin my-feature --force-with-lease

--force-with-lease is safer than --force. It checks that the remote branch hasn't been updated by someone else since you last fetched. If another contributor pushed to your branch in the meantime, the push will be rejected and you won't accidentally overwrite their work.

If you're the sole contributor to your fork, the risk is lower, but the habit is still worth keeping.

Common Pitfalls

Merging instead of rebasing

After an upstream history rewrite, merging pulls in the new history as a parallel branch. You end up with duplicate commits β€” one from the old history and one from the rewritten version β€” because Git sees them as different changes even if the content is the same. Always use rebase --onto in this scenario, not merge.

Using the wrong old-base SHA

If you pick an incorrect OLD_BASE, you'll either replay too many commits (including ones already in upstream) or too few (dropping some of your own work). Double-check with git log --oneline OLD_BASE..my-feature to see exactly which commits will be replayed. That list should contain only your work.

Assuming the rebase is a one-time event

Some upstream projects rebase frequently β€” particularly during active development or before a major release. If you're maintaining a long-running fork, build the assumption of occasional history rewrites into your workflow rather than treating it as an emergency each time.

Not keeping a backup branch

Before any destructive operation, tag or branch your current state:

git branch my-feature-backup

It costs nothing and gives you a safe recovery point if the rebase goes sideways.

Keeping Your Fork Maintainable Long-Term

The best time to reduce the pain of upstream rebases is before they happen. A few structural habits make a big difference.

Keep your fork's changes minimal and isolated. The more your commits touch the same files as upstream, the more conflicts you'll face during a rebase. If you can isolate your changes into separate files or modules that upstream rarely touches, your rebase becomes trivial.

Rebase onto upstream regularly, not just after major events. If you sync your fork with upstream every week or two, each rebase involves a small number of new upstream commits. Waiting months means resolving a much larger backlog of conflicts all at once.

Write clean, atomic commits with descriptive messages. When you're replaying commits one by one during a rebase, a commit message like

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.