#!/bin/bash # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, you can obtain one at https://mozilla.org/MPL/2.0/. # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. set -e SELF="$(basename $0)" SELF="${SELF/-/ }" STATE_FILE=".git/REPLAY_MERGE" DONT_TAG=${DONT_TAG:=false} DONT_PUSH=${DONT_PUSH:=false} DONT_ACCEPT=${DONT_ACCEPT:=false} die() { for MESSAGE in "$@"; do echo -e "${MESSAGE}" >&2 done exit 1 } die_with_usage() { die "Usage:" \ "" \ "${SELF} [options] " \ "${SELF} --continue" \ "${SELF} --abort" \ "" \ "options:" \ " --no-push" \ " --no-tag" \ "" } die_with_continue_instructions() { die "" \ "Replay interrupted. Conflicts need to be fixed manually." \ "When done, run \"${SELF} --continue\"." \ "Use \"${SELF} --abort\" to abort the replay." } die_before_push() { die "" \ "Replay finished locally. Now check the result in ${REPLAY_BRANCH}." \ "When done, run \"${SELF} --continue\" to push and create MR in gitlab." \ "Use \"${SELF} --abort\" to abort the replay." } die_if_wrong_dir() { if [[ ! -d ".git" ]]; then die "You need to run this command from the toplevel of the working tree." fi } die_if_not_in_progress() { die_if_wrong_dir if [[ ! -f "${STATE_FILE}" ]]; then die "No replay-merge in progress?" fi } die_if_in_progress() { die_if_wrong_dir if [[ -f "${STATE_FILE}" ]]; then die "Another replay-merge in progress. Use --continue or --abort." fi } die_if_local_behind_target() { TARGET_REF_HEAD="$(git rev-list --max-count=1 "${TARGET_REF}")" if [[ "$(git merge-base "${TARGET_REF}" "${TARGET_BRANCH}")" != "${TARGET_REF_HEAD}" ]]; then die "Local branch ${TARGET_BRANCH} is behind ${TARGET_REF}, cannot merge into it." \ "Update or remove the local branch, then run \"${SELF} --continue\"." \ "Use \"${SELF} --abort\" to abort the replay." fi } branch_exists() { ESCAPED_BRANCH_NAME=${1//\//\\\/} BRANCH_REGEX="/^(remotes\/)?${ESCAPED_BRANCH_NAME}$/" if [[ -n "$(git branch -a | awk "\$NF ~ ${BRANCH_REGEX} {print \$NF}")" ]]; then return 0 else return 1 fi } go() { # Process parameters. SOURCE_COMMIT="$1" TARGET_REMOTE="$2" TARGET_BRANCH="$3" TARGET_REF="${TARGET_REMOTE}/${TARGET_BRANCH}" # Establish the range of commits comprising the source branch. REPLAY_COMMIT_RANGE="$( git show --format="%P" "${SOURCE_COMMIT}" 2>&1 \ | sed -n "1s/\([0-9a-f]\{40\}\) \([0-9a-f]\{40\}\)/\1..\2/p;" )" if [[ -z "${REPLAY_COMMIT_RANGE}" ]]; then die "${SOURCE_COMMIT} is not a valid merge commit ID." fi # Extract the name of the source branch. SOURCE_BRANCH="$( git log --max-count=1 --format="%B" "${SOURCE_COMMIT}" \ | sed -n "s/^Merge branch '\([^'][^']*\).*/\1/p;" \ | head -n 1 )" if [[ -z "${SOURCE_BRANCH}" ]]; then die "Unable to extract source branch name from ${SOURCE_COMMIT}." fi # Ensure the target ref is valid. if ! branch_exists "${TARGET_REF}"; then die "${TARGET_REF} is not a valid replay target." fi # Abort if a local branch with the name about to be used for replaying # the merge already exists. REPLAY_BRANCH="${SOURCE_BRANCH}-${TARGET_BRANCH}" if branch_exists "${REPLAY_BRANCH}"; then die "Local branch with name ${REPLAY_BRANCH} already exists." \ "Cannot use it for replaying a merge." fi # Get the name of the currently checked out branch so that it can be # checked out again once the replay is finished. CHECKED_OUT_BRANCH="$(git branch | awk "\$1 == \"*\" {print \$2}")" # Store state in case it needs to be restored later. cat <<-EOF >"${STATE_FILE}" CHECKED_OUT_BRANCH="${CHECKED_OUT_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" SOURCE_BRANCH="${SOURCE_BRANCH}" REPLAY_BRANCH="${REPLAY_BRANCH}" TARGET_REMOTE="${TARGET_REMOTE}" TARGET_BRANCH="${TARGET_BRANCH}" TARGET_REF="${TARGET_REF}" EOF # Announce the plan. echo "Attempting to replay ${REPLAY_COMMIT_RANGE} on top of ${TARGET_REF} in ${REPLAY_BRANCH}..." # Switch to the replay branch. git checkout -t -b "${REPLAY_BRANCH}" "${TARGET_REF}" >/dev/null # Try replaying the branch. If there is any conflict, the command will # fail, which means we need to bail and let the user fix the current # cherry-pick manually, expecting "git replay-merge --continue" to be # used afterwards. If there is no conflict, just proceed with what # --continue would do. if ! git cherry-pick -x "${REPLAY_COMMIT_RANGE}"; then die_with_continue_instructions fi resume } resume() { # If cherry-picking has not yet been completed, resume it. If it # fails, bail. If if succeeds, we can proceed with merging. if [[ -f ".git/sequencer/todo" ]]; then if ! git cherry-pick --continue; then die_with_continue_instructions fi fi if $DONT_PUSH; then die_before_push fi if $DONT_ACCEPT; then AUTO_MERGE="" else AUTO_MERGE="merge_request.merge_when_pipeline_succeeds" fi TITLE="" LABEL_VERSION="" LABEL_BACKPORT="" if ! $DONT_TAG && [[ $TARGET_BRANCH == bind-9.[0-9][0-9] ]]; then version="${TARGET_BRANCH#bind-}" TITLE="$(git show --format=%b ${SOURCE_COMMIT} | head -n 1)" TITLE="merge_request.title=[${version}] ${TITLE}" LABEL_VERSION="merge_request.label=v${version}" LABEL_BACKPORT="merge_request.label=Backport" fi git push -u ${TARGET_REMOTE} \ -o merge_request.create \ -o merge_request.remove_source_branch \ -o "merge_request.target=${TARGET_BRANCH}" \ ${AUTO_MERGE:+-o} "${AUTO_MERGE}" \ ${TITLE:+-o} "${TITLE}" \ ${LABEL_VERSION:+-o} "${LABEL_VERSION}" \ ${LABEL_BACKPORT:+-o} "${LABEL_BACKPORT}" \ "${REPLAY_BRANCH}:${REPLAY_BRANCH}" cleanup exit 0 } cleanup() { # Restore working copy state from before the replay was started, # ignoring any potential errors to prevent "set -e" from interfering. { git merge --abort git cherry-pick --abort git checkout "${CHECKED_OUT_BRANCH}" } &>/dev/null || true rm -f "${STATE_FILE}" } cd $(git rev-parse --show-toplevel) next_action="go" args=3 while [[ $# -ge 1 ]]; do case "$1" in "--no-push") DONT_PUSH=true ;; "--push") DONT_PUSH=false ;; "--no-tag") DONT_TAG=true ;; "--tag") DONT_TAG=false ;; "--abort") die_if_not_in_progress source "${STATE_FILE}" next_action="cleanup" args=0 shift break ;; "--continue") die_if_not_in_progress source "${STATE_FILE}" next_action="resume" args=0 shift break ;; --*) die_with_usage ;; *) break ;; esac shift done if [[ $# -ne $args ]]; then die_with_usage fi $next_action "$@"