#!/usr/bin/env bash # Port (cherry-pick -x) commits onto a release branch. # # Usage: port.sh [...] # # Already-ported commits are skipped. Conflicts get a port/- # branch and a comment on the "Port status: " tracking issue. # # Requires: full-history checkout, git identity, push access, gh auth. # Env: PORT_SOURCE_PR - source PR number (optional, for reporting) set -euo pipefail TARGET="${1:?usage: port.sh [...]}" shift [ "$#" -ge 1 ] || { echo "error: no commits given" >&2; exit 1; } REPO="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner --jq .nameWithOwner)}" TRACKING_TITLE="Port status: ${TARGET}" log() { echo "[port] $*" >&2; } git fetch origin "refs/heads/${TARGET}:refs/remotes/origin/${TARGET}" --quiet || { echo "error: branch origin/${TARGET} not found" >&2; exit 1; } tracking_issue() { local num num=$(gh issue list --repo "$REPO" --state open --search "in:title \"${TRACKING_TITLE}\"" \ --json number,title --jq "map(select(.title == \"${TRACKING_TITLE}\")) | .[0].number // empty") if [ -z "$num" ]; then num=$(gh issue create --repo "$REPO" --title "${TRACKING_TITLE}" \ --body "Maintainer tracking issue for ports to \`${TARGET}\`. The port audit updates this body; the port engine reports conflicts as comments." \ | grep -oE '[0-9]+$') fi echo "$num" } already_ported() { local sha="$1" if git log "origin/${TARGET}" --grep="cherry picked from commit ${sha}" --format=%H | grep -q .; then return 0 fi # git cherry prints "- " when a patch-equivalent commit exists upstream if git cherry "origin/${TARGET}" "$sha" "${sha}~1" 2>/dev/null | grep -q "^- "; then return 0 fi return 1 } report_conflict() { local sha="$1" short subject branch issue short=$(git rev-parse --short "$sha") subject=$(git log -1 --format=%s "$sha") branch="port/${short}-${TARGET#release/}" git push origin "refs/remotes/origin/${TARGET}:refs/heads/${branch}" 2>/dev/null || log "conflict branch ${branch} already exists" issue=$(tracking_issue) gh issue comment "$issue" --repo "$REPO" --body "$(cat <> "$GITHUB_STEP_SUMMARY" fi } WORK="_port_worktree_$$" git worktree add --detach "$WORK" "origin/${TARGET}" >/dev/null trap 'cd "${OLDPWD:-.}" 2>/dev/null; git worktree remove --force "$WORK" 2>/dev/null || true' EXIT cd "$WORK" picked=0 for ref in "$@"; do sha=$(git rev-parse --verify "${ref}^{commit}") || { log "skip ${ref}: not a commit"; continue; } short=$(git rev-parse --short "$sha") if already_ported "$sha"; then log "skip ${short}: already on ${TARGET}" continue fi pick_args=(-x) # PR merge commits need the mainline parent if [ "$(git rev-list --no-walk --count --min-parents=2 "$sha")" -gt 0 ]; then pick_args+=(-m 1) fi if git cherry-pick "${pick_args[@]}" "$sha"; then log "picked ${short}" picked=$((picked + 1)) else git cherry-pick --abort || true report_conflict "$sha" fi done if [ "$picked" -gt 0 ]; then if ! git push origin "HEAD:refs/heads/${TARGET}"; then # another run may have advanced the branch; rebase our picks and retry once git fetch origin "refs/heads/${TARGET}:refs/remotes/origin/${TARGET}" git rebase "origin/${TARGET}" git push origin "HEAD:refs/heads/${TARGET}" fi log "pushed ${picked} commit(s) to ${TARGET}" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then echo ":white_check_mark: ported ${picked} commit(s) to \`${TARGET}\`" >> "$GITHUB_STEP_SUMMARY" fi fi