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.
Prerequisites
Section titled “Prerequisites”- Your local clone is the fork.
originpoints 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).
Recipe: one-shot sync
Section titled “Recipe: one-shot 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 1figit 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 1fi
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 1fi
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 1fi
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"UPSTREAM_WORKSPACE=acme \ UPSTREAM_REPO=widget \ BRANCH=main \ ~/bin/sync-fork.shRecipe: 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.
#!/bin/bashset -euo pipefail
UPSTREAM_WORKSPACE="${UPSTREAM_WORKSPACE:?set UPSTREAM_WORKSPACE}"UPSTREAM_REPO="${UPSTREAM_REPO:?set UPSTREAM_REPO}"FORK_WORKSPACE="${FORK_WORKSPACE:?set FORK_WORKSPACE}" # your workspaceFORK_REPO="${FORK_REPO:?set FORK_REPO}" # your fork's repo slugFEATURE_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 1figit 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 1fi
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 1figit 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.
UPSTREAM_WORKSPACE=acme UPSTREAM_REPO=widget \ FORK_WORKSPACE=myuser FORK_REPO=widget \ FEATURE_BRANCH=feat/login \ TITLE="Add login button" \ ~/bin/sync-and-pr.shCross-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.