From 96dd225924a87a1ab086805eb52d335bf80c07e2 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Fri, 7 Aug 2026 13:22:20 -0500 Subject: [PATCH] feat: helper scripts for rebasing and cherry-picking OpenStack changes These scripts are here to assist with rebasing the understack branch on top of the stable openstack branch and to cherry-pick changes from Gerrit before they have merged into their respective stable branch. Rebasing gets us caught up while dropping any patches that have since made it into the stable branch. --- containers/ironic/README.md | 11 ++ containers/neutron/README.md | 11 ++ containers/nova/README.md | 11 ++ scripts/git-review-cherry-pick | 176 +++++++++++++++++++++++++ scripts/git-understack-rebase | 231 +++++++++++++++++++++++++++++++++ 5 files changed, 440 insertions(+) create mode 100755 scripts/git-review-cherry-pick create mode 100755 scripts/git-understack-rebase diff --git a/containers/ironic/README.md b/containers/ironic/README.md index 30a459acb..15e5fa8e1 100644 --- a/containers/ironic/README.md +++ b/containers/ironic/README.md @@ -33,6 +33,17 @@ git checkout -b understack/2026.2 ## Rebasing to keep things clean +The `scripts/git-understack-rebase` script in the understack repo automates this: it checks your +remotes and branches, shows you the commits involved, performs the rebase, and force-pushes both +branches to `rackerlabs` after you confirm each step. It expects a remote named `upstream` +(pointing at `openstack/ironic`, not `origin`) and a remote named `rackerlabs`. + +```bash +scripts/git-understack-rebase ~/work/ironic 2026.1 +``` + +To do it manually instead: + ```bash git checkout stable/2026.1 git pull -p diff --git a/containers/neutron/README.md b/containers/neutron/README.md index dbebc598e..a489a19e3 100644 --- a/containers/neutron/README.md +++ b/containers/neutron/README.md @@ -33,6 +33,17 @@ git checkout -b understack/2026.2 ## Rebasing to keep things clean +The `scripts/git-understack-rebase` script in the understack repo automates this: it checks your +remotes and branches, shows you the commits involved, performs the rebase, and force-pushes both +branches to `rackerlabs` after you confirm each step. It expects a remote named `upstream` +(pointing at `openstack/neutron`, not `origin`) and a remote named `rackerlabs`. + +```bash +scripts/git-understack-rebase ~/work/neutron 2026.1 +``` + +To do it manually instead: + ```bash git checkout stable/2026.1 git pull -p diff --git a/containers/nova/README.md b/containers/nova/README.md index 0b0102f3f..4ff7af678 100644 --- a/containers/nova/README.md +++ b/containers/nova/README.md @@ -46,6 +46,17 @@ git checkout -b understack/2026.2 ## Rebasing to keep things clean +The `scripts/git-understack-rebase` script in the understack repo automates this: it checks your +remotes and branches, shows you the commits involved, performs the rebase, and force-pushes both +branches to `rackerlabs` after you confirm each step. It expects a remote named `upstream` +(pointing at `openstack/nova`, not `origin`) and a remote named `rackerlabs`. + +```bash +scripts/git-understack-rebase ~/work/nova 2026.1 +``` + +To do it manually instead: + ```bash git checkout stable/2026.1 git pull -p diff --git a/scripts/git-review-cherry-pick b/scripts/git-review-cherry-pick new file mode 100755 index 000000000..24b560d08 --- /dev/null +++ b/scripts/git-review-cherry-pick @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +set -euo pipefail + +function usage() { + echo "$(basename "$0") " >&2 + echo "" >&2 + echo "Downloads a Gerrit change via 'git review -d' and cherry-picks it onto" >&2 + echo "understack/\$VERSION, then pushes the result to the rackerlabs remote." >&2 + echo "" >&2 + echo "Run this from inside the OpenStack project checkout (e.g. ~/work/ironic)." >&2 + echo "If this script is on your PATH as 'git-review-cherry-pick', invoke it as:" >&2 + echo "" >&2 + echo " git review-cherry-pick " >&2 + echo "" >&2 + echo "Example: git review-cherry-pick 12345 2026.1" >&2 + echo "" >&2 + echo "Requirements:" >&2 + echo " - 'git-review' installed and this repo has a .gitreview file" >&2 + echo " - a remote named 'rackerlabs' to push the result to" >&2 + echo " - local branch understack/\$VERSION" >&2 + + exit 1 +} + +function log() { echo "[INFO] $*"; } +function warn() { echo "[WARN] $*" >&2; } +function err() { echo "[ERROR] $*" >&2; } +function die() { err "$*"; exit 1; } + +function confirm() { + local prompt="$1" + local ans + read -r -p "${prompt} [y/N] " ans + case "$ans" in + [Yy]*) ;; + *) die "Aborted by user." ;; + esac +} + +if [[ $# -ne 2 ]]; then + usage +fi + +CHANGE="$1" +VERSION="$2" +UNDERSTACK_BRANCH="understack/${VERSION}" + +if [[ -z "$CHANGE" || -z "$VERSION" ]]; then + usage +fi + +# --------------------------------------------------------------------------- +# Step 1: pre-flight checks +# --------------------------------------------------------------------------- + +log "Step 1: pre-flight checks" + +if ! git rev-parse --git-dir >/dev/null 2>&1; then + die "Not inside a git repository. Run this from within the OpenStack project checkout." +fi + +REPO_ROOT=$(git rev-parse --show-toplevel) + +if ! command -v git-review >/dev/null 2>&1; then + die "'git-review' is not installed or not on PATH." +fi + +if [[ ! -f "${REPO_ROOT}/.gitreview" ]]; then + die "No .gitreview file found at ${REPO_ROOT}. Is this a Gerrit-managed OpenStack repo?" +fi + +if [[ -n "$(git status --porcelain)" ]]; then + die "Working tree is not clean. Commit, stash, or clean up changes before continuing." +fi + +GIT_DIR=$(git rev-parse --git-dir) +if [[ -d "${GIT_DIR}/rebase-merge" || -d "${GIT_DIR}/rebase-apply" ]]; then + die "A rebase is already in progress in this repo. Resolve it first." +fi +if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + die "A merge is already in progress in this repo. Resolve it first." +fi +if git rev-parse -q --verify CHERRY_PICK_HEAD >/dev/null 2>&1; then + die "A cherry-pick is already in progress in this repo. Resolve it first." +fi + +RACKERLABS_URL=$(git remote get-url rackerlabs 2>/dev/null) || die "Remote 'rackerlabs' not found in $REPO_ROOT." + +if ! git show-ref --verify --quiet "refs/heads/${UNDERSTACK_BRANCH}"; then + die "Local branch '${UNDERSTACK_BRANCH}' does not exist. See containers/*/README.md for setup steps." +fi + +ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +log "Checking rackerlabs/${UNDERSTACK_BRANCH} for drift" +git fetch rackerlabs --prune -q +LOCAL_UNDERSTACK_SHA=$(git rev-parse "$UNDERSTACK_BRANCH") +REMOTE_UNDERSTACK_SHA=$(git rev-parse -q --verify "rackerlabs/${UNDERSTACK_BRANCH}" || echo "") +if [[ -n "$REMOTE_UNDERSTACK_SHA" && "$LOCAL_UNDERSTACK_SHA" != "$REMOTE_UNDERSTACK_SHA" ]]; then + die "Local ${UNDERSTACK_BRANCH} (${LOCAL_UNDERSTACK_SHA}) does not match rackerlabs/${UNDERSTACK_BRANCH} (${REMOTE_UNDERSTACK_SHA}). Sync your local branch before cherry-picking." +fi + +echo "" +echo "Repo: $REPO_ROOT" +echo "Gerrit change: $CHANGE" +echo "Target branch: $UNDERSTACK_BRANCH" +echo "rackerlabs remote: $RACKERLABS_URL" +echo "Current branch: $ORIGINAL_BRANCH" +echo "" + +confirm "Download change ${CHANGE} with 'git review -d' and cherry-pick it onto ${UNDERSTACK_BRANCH}?" + +# --------------------------------------------------------------------------- +# Step 2: download the change +# --------------------------------------------------------------------------- + +log "Step 2: downloading change ${CHANGE}" + +git review -d "$CHANGE" + +REVIEW_BRANCH=$(git rev-parse --abbrev-ref HEAD) +REVIEW_SHA=$(git rev-parse HEAD) + +if [[ "$REVIEW_BRANCH" == "$ORIGINAL_BRANCH" || "$REVIEW_BRANCH" == "$UNDERSTACK_BRANCH" ]]; then + die "'git review -d' did not switch to a new review branch as expected; aborting to avoid cherry-picking the wrong commit." +fi + +log "Downloaded onto branch ${REVIEW_BRANCH} at ${REVIEW_SHA}" +echo "" +git log -5 --oneline "$REVIEW_BRANCH" +echo "" + +CHAIN_COUNT=$(git rev-list --count "${REVIEW_BRANCH}@{u}..${REVIEW_BRANCH}" 2>/dev/null || echo "") +if [[ -n "$CHAIN_COUNT" && "$CHAIN_COUNT" != "1" ]]; then + warn "Review branch has ${CHAIN_COUNT} commits ahead of its tracked upstream ref." + warn "This change may depend on other unmerged Gerrit changes (a dependency chain)." + warn "Only the tip commit (${REVIEW_SHA}) will be cherry-picked here. Check the log above." +fi + +confirm "Cherry-pick ${REVIEW_SHA} onto ${UNDERSTACK_BRANCH}?" + +# --------------------------------------------------------------------------- +# Step 3: cherry-pick +# --------------------------------------------------------------------------- + +log "Step 3: cherry-picking onto ${UNDERSTACK_BRANCH}" + +git checkout "$UNDERSTACK_BRANCH" + +if ! git cherry-pick "$REVIEW_SHA"; then + err "Cherry-pick stopped due to conflicts." + err "Resolve conflicts, then run 'git cherry-pick --continue' (or '--abort')." + err "Once resolved, push manually with: git push rackerlabs ${UNDERSTACK_BRANCH}" + err "The downloaded review branch '${REVIEW_BRANCH}' was left in place; delete it once you're done: git branch -D ${REVIEW_BRANCH}" + exit 1 +fi + +NEW_SHA=$(git rev-parse HEAD) +log "Cherry-pick succeeded: ${UNDERSTACK_BRANCH} is now at ${NEW_SHA}" + +# --------------------------------------------------------------------------- +# Step 4: push + cleanup +# --------------------------------------------------------------------------- + +confirm "Push ${UNDERSTACK_BRANCH} (${LOCAL_UNDERSTACK_SHA} -> ${NEW_SHA}) to rackerlabs?" + +git push rackerlabs "$UNDERSTACK_BRANCH" + +log "Pushed. Cleaning up temporary review branch ${REVIEW_BRANCH}." +git branch -D "$REVIEW_BRANCH" + +echo "" +log "Done." +echo "rackerlabs ${UNDERSTACK_BRANCH}: ${LOCAL_UNDERSTACK_SHA} -> ${NEW_SHA}" +echo "" +log "Next: update containers/$(basename "$REPO_ROOT")/Dockerfile to pin the new ${UNDERSTACK_BRANCH} HEAD commit: ${NEW_SHA}" diff --git a/scripts/git-understack-rebase b/scripts/git-understack-rebase new file mode 100755 index 000000000..ca96a6a65 --- /dev/null +++ b/scripts/git-understack-rebase @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +set -euo pipefail + +function usage() { + echo "$(basename "$0") " >&2 + echo "" >&2 + echo "Rebases an UnderStack fork branch (understack/\$VERSION) on top of" >&2 + echo "upstream's stable/\$VERSION branch, and syncs the local stable/\$VERSION" >&2 + echo "mirror branch, then force-pushes both to the rackerlabs remote." >&2 + echo "" >&2 + echo "Example: $(basename "$0") ~/work/ironic 2026.1" >&2 + echo "" >&2 + echo "Requirements on the checkout:" >&2 + echo " - a remote named 'upstream' pointing at the openstack/* project" >&2 + echo " - a remote named 'rackerlabs' pointing at the rackerlabs fork" >&2 + echo " - local branches stable/\$VERSION and understack/\$VERSION" >&2 + + exit 1 +} + +function log() { echo "[INFO] $*"; } +function warn() { echo "[WARN] $*" >&2; } +function err() { echo "[ERROR] $*" >&2; } +function die() { err "$*"; exit 1; } + +function confirm() { + local prompt="$1" + local ans + read -r -p "${prompt} [y/N] " ans + case "$ans" in + [Yy]*) ;; + *) die "Aborted by user." ;; + esac +} + +function git_c() { + git -C "$REPO_PATH" "$@" +} + +if [[ $# -ne 2 ]]; then + usage +fi + +REPO_PATH="$1" +VERSION="$2" + +if [[ -z "$REPO_PATH" || -z "$VERSION" ]]; then + usage +fi + +STABLE_BRANCH="stable/${VERSION}" +UNDERSTACK_BRANCH="understack/${VERSION}" +TIMESTAMP=$(date +%Y%m%d%H%M%S) +REPO_NAME=$(basename "$REPO_PATH") + +# --------------------------------------------------------------------------- +# Step 1: pre-flight checks +# --------------------------------------------------------------------------- + +log "Step 1: pre-flight checks" + +if [[ ! -d "$REPO_PATH" ]]; then + die "Path does not exist or is not a directory: $REPO_PATH" +fi + +if ! git_c rev-parse --git-dir >/dev/null 2>&1; then + die "Not a git repository: $REPO_PATH" +fi + +if [[ -n "$(git_c status --porcelain)" ]]; then + die "Working tree is not clean. Commit, stash, or clean up changes in $REPO_PATH before rebasing." +fi + +GIT_DIR=$(git_c rev-parse --git-dir) +if [[ -d "${REPO_PATH}/${GIT_DIR}/rebase-merge" || -d "${REPO_PATH}/${GIT_DIR}/rebase-apply" ]]; then + die "A rebase is already in progress in $REPO_PATH. Resolve it (git rebase --continue/--abort) first." +fi +if git_c rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then + die "A merge is already in progress in $REPO_PATH. Resolve it first." +fi +if git_c rev-parse -q --verify CHERRY_PICK_HEAD >/dev/null 2>&1; then + die "A cherry-pick is already in progress in $REPO_PATH. Resolve it first." +fi + +UPSTREAM_URL=$(git_c remote get-url upstream 2>/dev/null) || die "Remote 'upstream' not found in $REPO_PATH." +RACKERLABS_URL=$(git_c remote get-url rackerlabs 2>/dev/null) || die "Remote 'rackerlabs' not found in $REPO_PATH." + +if ! git_c show-ref --verify --quiet "refs/heads/${STABLE_BRANCH}"; then + die "Local branch '${STABLE_BRANCH}' does not exist in $REPO_PATH. See containers/*/README.md for setup steps." +fi +if ! git_c show-ref --verify --quiet "refs/heads/${UNDERSTACK_BRANCH}"; then + die "Local branch '${UNDERSTACK_BRANCH}' does not exist in $REPO_PATH. See containers/*/README.md for setup steps." +fi + +CURRENT_BRANCH=$(git_c rev-parse --abbrev-ref HEAD) +LOCAL_STABLE_SHA=$(git_c rev-parse "$STABLE_BRANCH") +LOCAL_UNDERSTACK_SHA=$(git_c rev-parse "$UNDERSTACK_BRANCH") + +echo "" +echo "Checkout path: $REPO_PATH" +echo "Version: $VERSION" +echo "Current branch: $CURRENT_BRANCH" +echo "upstream remote: $UPSTREAM_URL" +echo "rackerlabs remote: $RACKERLABS_URL" +echo "local ${STABLE_BRANCH}: $LOCAL_STABLE_SHA" +echo "local ${UNDERSTACK_BRANCH}: $LOCAL_UNDERSTACK_SHA" +echo "" + +confirm "Proceed to fetch upstream and rackerlabs?" + +# --------------------------------------------------------------------------- +# Step 2: fetch + second verification +# --------------------------------------------------------------------------- + +log "Step 2: fetching remotes" + +git_c fetch upstream --prune +git_c fetch rackerlabs --prune + +UPSTREAM_STABLE_REF="upstream/${STABLE_BRANCH}" +RACKERLABS_STABLE_REF="rackerlabs/${STABLE_BRANCH}" +RACKERLABS_UNDERSTACK_REF="rackerlabs/${UNDERSTACK_BRANCH}" + +git_c rev-parse -q --verify "$UPSTREAM_STABLE_REF" >/dev/null 2>&1 \ + || die "Remote ref '${UPSTREAM_STABLE_REF}' does not exist. Check the version or that upstream has cut this stable branch." +git_c rev-parse -q --verify "$RACKERLABS_STABLE_REF" >/dev/null 2>&1 \ + || die "Remote ref '${RACKERLABS_STABLE_REF}' does not exist on rackerlabs." +git_c rev-parse -q --verify "$RACKERLABS_UNDERSTACK_REF" >/dev/null 2>&1 \ + || die "Remote ref '${RACKERLABS_UNDERSTACK_REF}' does not exist on rackerlabs." + +UPSTREAM_STABLE_SHA=$(git_c rev-parse "$UPSTREAM_STABLE_REF") +RACKERLABS_STABLE_SHA=$(git_c rev-parse "$RACKERLABS_STABLE_REF") +RACKERLABS_UNDERSTACK_SHA=$(git_c rev-parse "$RACKERLABS_UNDERSTACK_REF") + +echo "" +echo "${UPSTREAM_STABLE_REF}:" +git_c log -1 --oneline "$UPSTREAM_STABLE_REF" +echo "" +echo "${RACKERLABS_STABLE_REF}:" +git_c log -1 --oneline "$RACKERLABS_STABLE_REF" +echo "" +echo "${RACKERLABS_UNDERSTACK_REF}:" +git_c log -1 --oneline "$RACKERLABS_UNDERSTACK_REF" +echo "" + +PRE_REBASE_PATCH_COUNT=$(git_c rev-list --count "${RACKERLABS_STABLE_REF}..${UNDERSTACK_BRANCH}") + +echo "About to run:" +echo " git rebase --onto ${UPSTREAM_STABLE_REF} (${UPSTREAM_STABLE_SHA}) ${RACKERLABS_STABLE_REF} (${RACKERLABS_STABLE_SHA}) ${UNDERSTACK_BRANCH}" +echo "" + +confirm "Proceed with the rebase?" + +# --------------------------------------------------------------------------- +# Step 3: rebase +# --------------------------------------------------------------------------- + +log "Step 3: rebasing" + +BACKUP_UNDERSTACK_TAG="backup/${UNDERSTACK_BRANCH}/${TIMESTAMP}" +BACKUP_STABLE_TAG="backup/${STABLE_BRANCH}/${TIMESTAMP}" +git_c tag "$BACKUP_UNDERSTACK_TAG" "$UNDERSTACK_BRANCH" +git_c tag "$BACKUP_STABLE_TAG" "$STABLE_BRANCH" +log "Created local backup tags: ${BACKUP_UNDERSTACK_TAG}, ${BACKUP_STABLE_TAG}" + +git_c checkout "$UNDERSTACK_BRANCH" + +if ! git_c rebase --onto "$UPSTREAM_STABLE_REF" "$RACKERLABS_STABLE_REF" "$UNDERSTACK_BRANCH"; then + err "Rebase stopped due to conflicts." + err "Resolve conflicts, then run 'git rebase --continue' (or 'git rebase --abort' to bail out) in $REPO_PATH." + err "Re-run this script afterward to continue with the sync/push steps." + exit 1 +fi + +git_c checkout "$STABLE_BRANCH" +if ! git_c merge --ff-only "$UPSTREAM_STABLE_REF"; then + die "Local ${STABLE_BRANCH} could not be fast-forwarded to ${UPSTREAM_STABLE_REF}. It may have diverged unexpectedly." +fi + +git_c checkout "$UNDERSTACK_BRANCH" + +POST_REBASE_PATCH_COUNT=$(git_c rev-list --count "${STABLE_BRANCH}..${UNDERSTACK_BRANCH}") + +echo "" +echo "Patches ahead of ${STABLE_BRANCH} before rebase: ${PRE_REBASE_PATCH_COUNT}" +echo "Patches ahead of ${STABLE_BRANCH} after rebase: ${POST_REBASE_PATCH_COUNT}" +echo "" + +if [[ "$PRE_REBASE_PATCH_COUNT" != "$POST_REBASE_PATCH_COUNT" ]]; then + warn "Patch count changed across the rebase. Review 'git log ${STABLE_BRANCH}..${UNDERSTACK_BRANCH}' before pushing." +fi + +# --------------------------------------------------------------------------- +# Step 4: pre-push safety + third confirmation +# --------------------------------------------------------------------------- + +log "Step 4: pre-push safety check" + +git_c fetch rackerlabs --prune + +CURRENT_RACKERLABS_STABLE_SHA=$(git_c rev-parse "$RACKERLABS_STABLE_REF") +CURRENT_RACKERLABS_UNDERSTACK_SHA=$(git_c rev-parse "$RACKERLABS_UNDERSTACK_REF") + +if [[ "$CURRENT_RACKERLABS_STABLE_SHA" != "$RACKERLABS_STABLE_SHA" ]]; then + die "rackerlabs/${STABLE_BRANCH} moved since it was checked earlier (someone else pushed). Re-run this script." +fi +if [[ "$CURRENT_RACKERLABS_UNDERSTACK_SHA" != "$RACKERLABS_UNDERSTACK_SHA" ]]; then + die "rackerlabs/${UNDERSTACK_BRANCH} moved since it was checked earlier (someone else pushed). Re-run this script." +fi + +NEW_STABLE_SHA=$(git_c rev-parse "$STABLE_BRANCH") +NEW_UNDERSTACK_SHA=$(git_c rev-parse "$UNDERSTACK_BRANCH") + +echo "" +echo "rackerlabs ${STABLE_BRANCH}: ${RACKERLABS_STABLE_SHA} -> ${NEW_STABLE_SHA}" +echo "rackerlabs ${UNDERSTACK_BRANCH}: ${RACKERLABS_UNDERSTACK_SHA} -> ${NEW_UNDERSTACK_SHA}" +echo "" + +confirm "Force-push ${STABLE_BRANCH} and ${UNDERSTACK_BRANCH} to rackerlabs?" + +git_c push --force-with-lease rackerlabs "$STABLE_BRANCH" +git_c push --force-with-lease rackerlabs "$UNDERSTACK_BRANCH" + +echo "" +log "Done." +echo "rackerlabs ${STABLE_BRANCH}: ${RACKERLABS_STABLE_SHA} -> ${NEW_STABLE_SHA}" +echo "rackerlabs ${UNDERSTACK_BRANCH}: ${RACKERLABS_UNDERSTACK_SHA} -> ${NEW_UNDERSTACK_SHA}" +echo "" +log "Local backup tags (delete once you're satisfied): git -C $REPO_PATH tag -d ${BACKUP_UNDERSTACK_TAG} ${BACKUP_STABLE_TAG}" +echo "" +log "Next: update containers/${REPO_NAME}/Dockerfile to pin the new ${UNDERSTACK_BRANCH} HEAD commit: ${NEW_UNDERSTACK_SHA}"