git-sync-upstream

Sync the current branch with upstream, rebasing local commits on top.

#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
usage: git sync-upstream [--dry-run] [--remote <remote>] [--branch <branch>]

Sync local main with upstream/main, then return to the original branch.

  git checkout main
  git fetch upstream
  git merge --ff-only upstream/main
  git checkout <original-branch>

Options:
  --remote <remote>   upstream remote name (default: upstream)
  --branch <branch>   main branch name (default: main)
EOF
}

die() {
  printf 'git sync-upstream: %s\n' "$*" >&2
  exit 1
}

run() {
  printf '+'
  printf ' %q' "$@"
  printf '\n'
  if [[ "${dry_run}" != true ]]; then
    "$@"
  fi
}

dry_run=false
remote=upstream
main_branch=main

while (($#)); do
  case "$1" in
    --dry-run|-n)
      dry_run=true
      shift
      ;;
    --remote)
      (($# >= 2)) || die "--remote requires a value"
      remote=$2
      shift 2
      ;;
    --branch)
      (($# >= 2)) || die "--branch requires a value"
      main_branch=$2
      shift 2
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      die "unknown option: $1"
      ;;
    *)
      die "unexpected argument: $1"
      ;;
  esac
done

(($# == 0)) || { usage >&2; exit 2; }

git rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
  die "not inside a git worktree"

git remote get-url "$remote" >/dev/null 2>&1 ||
  die "remote '$remote' does not exist"

original_branch=$(git branch --show-current)
[[ -n "$original_branch" ]] || die "not on a named branch (detached HEAD?)"

run git checkout "$main_branch"
run git fetch "$remote"
run git merge --ff-only "${remote}/${main_branch}"

if [[ "$original_branch" != "$main_branch" ]]; then
  run git checkout "$original_branch"
fi
download git-sync-upstream