A zero-dependency Python script for keeping private forks in sync with their upstream.
Designed for teams or freelancers who maintain per-client customisations of a shared upstream codebase. Drop two files into each fork, edit the config once, and run the script whenever you need to pull in upstream changes — with a guided review before anything is merged.
── Step 1 — upstream remote
✔ Remote 'upstream' already configured.
── Step 2 — fetch upstream
→ Fetching upstream…
✔ Fetch complete.
── Step 3 — working tree check
✔ Working tree is clean.
── Step 4 — review & merge
→ 3 new commits on upstream/main ahead of my-client-branch:
SHA SUBJECT AUTHOR WHEN
───────────────────────────────────────────────────────────────────
a1b2c3 Fix null pointer in auth middleware Alice Smith 2 days ago
d4e5f6 Add rate limiting to /api/search Bob Jones 4 days ago
9f8e7d Bump lodash to 4.17.21 dependabot[bot] 6 days ago
Show full diff? [y/N]
Merge upstream/main into my-client-branch? [Y/n]
- No external dependencies — pure Python 3 stdlib, nothing to install
- Interactive review — see new commits and optionally the full diff before merging
- Safe by default — refuses to run with uncommitted changes in the working tree
--dry-runmode — walks through all steps and shows the review, but skips the actual merge- Coloured output — ANSI colours throughout, including a syntax-highlighted diff
- Pager support — long diffs are piped through
$PAGER(defaults toless -R) - Remote management — adds the
upstreamremote automatically; warns if it already points to a different URL and offers to fix it
- Python 3.9 or later
- Git (available on
$PATH)
For each private fork, add two files at the repository root:
1. Download or copy fork-sync.py into the repo root.
2. Create a .fork-sync config file:
# fork-sync configuration
upstream_url = https://github.com/ORIGINAL_OWNER/ORIGINAL_REPO.git
upstream_branch = mainCommit both files:
git add fork-sync.py .fork-sync
git commit -m "chore: add fork-sync"Tip: add
.fork-syncto your.gitignoreif you don't want the upstream URL tracked, and keep it as a local-only file per developer instead.
From anywhere inside the repository:
python3 fork-sync.pyTo preview what would happen without actually merging:
python3 fork-sync.py --dry-run| Step | What happens |
|---|---|
| 1 | Checks whether a remote named upstream exists. Adds it from the config URL if not. If it exists but points to a different URL, prompts to update it. |
| 2 | Runs git fetch upstream to pull down the latest upstream history. |
| 3 | Checks for uncommitted changes. Aborts with a clear message if any are found. |
| 4 | Shows a table of commits on upstream/<branch> that are not yet in the current branch. Offers to show the full diff. Asks for confirmation, then runs git merge upstream/<branch> --ff. |
The --ff flag allows a fast-forward merge when possible but does not require it — a merge commit will be created when histories have diverged, which is normal for forks with client-specific commits.
The .fork-sync file uses a simple key = value format. Lines beginning with # are treated as comments.
| Key | Required | Description |
|---|---|---|
upstream_url |
✔ | Clone URL of the upstream repository (HTTPS or SSH) |
upstream_branch |
✔ | Branch on the upstream to merge from (e.g. main, develop) |
upstream repo your private fork
───────────── ─────────────────────────────
main ──────────────→ main (mirror, rarely touched)
my-client ← your customisations live here
- Do your client work on a dedicated branch (
my-client,production, etc.) - Periodically run
fork-sync.pyfrom that branch - Review the incoming commits; merge when ready
- Resolve any conflicts as you normally would with
git
SSH remotes work fine — just use an SSH URL in the config:
upstream_url = [email protected]:ORIGINAL_OWNER/ORIGINAL_REPO.gitStash before running if you have work in progress:
git stash
python3 fork-sync.py
git stash popOverride the pager by setting $PAGER in your shell:
PAGER=bat python3 fork-sync.py # use 'bat' for even nicer diffsBSD 3-Clause — see LICENSE.