Skip to content

Fork synchronization

Forks drift from upstream fast. This recipe uses bb repo view to discover the upstream clone URL and plain git for the fetch, rebase and push.

  • Your local clone is the fork.
  • origin points at your fork.
  • The upstream is reachable as a separate remote.
  • Bash, Git and external jq are installed.
  • Your local branch contains every commit currently on the fork branch.
  • The commits to rebase contain no merges; use a merge-based workflow otherwise.
  • You have permission to push to your fork’s main (or whichever branch you want to keep in sync).

Save as ~/bin/sync-fork.sh, make it executable, and run it from your clone. Keeping the script outside the working tree lets the clean-tree check pass.

#!/bin/bash
# sync-fork.sh - Keep a fork's main branch in sync with its upstream.
set -euo pipefail
UPSTREAM_WORKSPACE="${UPSTREAM_WORKSPACE:?set UPSTREAM_WORKSPACE}"
UPSTREAM_REPO="${UPSTREAM_REPO:?set UPSTREAM_REPO}"
BRANCH="${BRANCH:-main}"
if [ -n "$(git status --porcelain)" ]; then
echo "Commit or remove working-tree changes before syncing." >&2
exit 1
fi
git check-ref-format "refs/heads/$BRANCH"
upstream_url=$(bb repo view -w "$UPSTREAM_WORKSPACE" -r "$UPSTREAM_REPO" --json \
| jq -er '.links.clone[] | select(.name == "https") | .href')
if ! git remote get-url upstream >/dev/null 2>&1; then
git remote add upstream "$upstream_url"
elif [ "$(git remote get-url upstream)" != "$upstream_url" ]; then
echo "Existing upstream remote differs from the selected repository." >&2
exit 1
fi
git fetch upstream "refs/heads/$BRANCH:refs/remotes/upstream/$BRANCH"
git fetch origin "refs/heads/$BRANCH:refs/remotes/origin/$BRANCH"
expected=$(git rev-parse "refs/remotes/origin/$BRANCH")
if ! git merge-base --is-ancestor "$expected" "refs/heads/$BRANCH"; then
echo "Local $BRANCH does not contain the fetched fork tip. Incorporate it first." >&2
exit 1
fi
if [ -n "$(git rev-list --merges "refs/remotes/upstream/$BRANCH..refs/heads/$BRANCH")" ]; then
echo "Fork history contains merges. Merge upstream instead of rebasing." >&2
exit 1
fi
git checkout "$BRANCH"
git rebase "refs/remotes/upstream/$BRANCH"
git push "--force-with-lease=refs/heads/$BRANCH:$expected" \
origin "HEAD:refs/heads/$BRANCH"
echo "Fork '$BRANCH' synced with upstream $UPSTREAM_WORKSPACE/$UPSTREAM_REPO"
Terminal window
UPSTREAM_WORKSPACE=acme \
UPSTREAM_REPO=widget \
BRANCH=main \
~/bin/sync-fork.sh

Recipe: sync, then open a pull request for a feature branch

Section titled “Recipe: sync, then open a pull request for a feature branch”

Save as ~/bin/sync-and-pr.sh alongside the executable sync-fork.sh. Run it from your clone. The feature branch must already exist locally and on the fork.

After syncing the destination branch, rebase your feature branch and open a pull request back to upstream.

bb pr create cannot do this. Its -s/--source and -d/--destination are both plain branch names resolved inside the -w/-r repository, so it has no way to name your fork as the source. Pointing it at upstream either 404s or opens a same-repo PR in upstream. Use bb api to POST the cross-fork body yourself.

sync-and-pr.sh
#!/bin/bash
set -euo pipefail
UPSTREAM_WORKSPACE="${UPSTREAM_WORKSPACE:?set UPSTREAM_WORKSPACE}"
UPSTREAM_REPO="${UPSTREAM_REPO:?set UPSTREAM_REPO}"
FORK_WORKSPACE="${FORK_WORKSPACE:?set FORK_WORKSPACE}" # your workspace
FORK_REPO="${FORK_REPO:?set FORK_REPO}" # your fork's repo slug
FEATURE_BRANCH="${FEATURE_BRANCH:?set FEATURE_BRANCH}"
TITLE="${TITLE:-Update from $FEATURE_BRANCH}"
DESTINATION="${DESTINATION:-main}"
# The feature branch must already exist on the fork.
git check-ref-format "refs/heads/$FEATURE_BRANCH"
if [ -n "$(git status --porcelain)" ]; then
echo "Commit or remove working-tree changes before syncing." >&2
exit 1
fi
git fetch origin "refs/heads/$FEATURE_BRANCH:refs/remotes/origin/$FEATURE_BRANCH"
expected=$(git rev-parse "refs/remotes/origin/$FEATURE_BRANCH")
if ! git merge-base --is-ancestor "$expected" "refs/heads/$FEATURE_BRANCH"; then
echo "Local feature branch does not contain the fetched fork tip." >&2
exit 1
fi
script_dir=$(cd "$(dirname "$0")" && pwd)
BRANCH="$DESTINATION" "$script_dir/sync-fork.sh"
if [ -n "$(git rev-list --merges "$DESTINATION..refs/heads/$FEATURE_BRANCH")" ]; then
echo "Feature history contains merges. Merge the destination instead of rebasing." >&2
exit 1
fi
git checkout "$FEATURE_BRANCH"
git rebase "$DESTINATION"
git push "--force-with-lease=refs/heads/$FEATURE_BRANCH:$expected" \
origin "HEAD:refs/heads/$FEATURE_BRANCH"
# Build the body with jq to quote titles and branch names correctly.
jq -n \
--arg title "$TITLE" \
--arg source "$FEATURE_BRANCH" \
--arg fork "$FORK_WORKSPACE/$FORK_REPO" \
--arg destination "$DESTINATION" \
'{
title: $title,
source: {
branch: { name: $source },
repository: { full_name: $fork }
},
destination: { branch: { name: $destination } }
}' \
| bb api POST "/repositories/$UPSTREAM_WORKSPACE/$UPSTREAM_REPO/pullrequests" --input -

The path is fully interpolated from shell variables, so bb api has no {workspace}/{repo} placeholders left to substitute. Don’t mix the two forms in one path.

Terminal window
UPSTREAM_WORKSPACE=acme UPSTREAM_REPO=widget \
FORK_WORKSPACE=myuser FORK_REPO=widget \
FEATURE_BRANCH=feat/login \
TITLE="Add login button" \
~/bin/sync-and-pr.sh

Cross-fork PRs require permission to read the upstream and create pull requests there. A failed API request exits nonzero; add --json to inspect its structured error.