]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
ceph-backport.sh: implement interactive setup routine and new options
authorNathan Cutler <ncutler@suse.com>
Thu, 24 Oct 2019 11:26:50 +0000 (13:26 +0200)
committerNathan Cutler <ncutler@suse.com>
Mon, 4 Nov 2019 18:33:40 +0000 (19:33 +0100)
This commit implements several new features:

* a --cherry-pick-only option
* a --force option
* an --existing-pr option
* an interactive setup routine

The --cherry-pick-only option can be used to test whether a backport
cherry-picks cleanly, for example. This is the same as the --prepare
functionality that was provided by an earlier version of the script, and
--prepare is re-introduced as a synonym for --cherry-pick-only.

The --force option can be used to make the script less careful (less
"cowardly"). For example, if the script refuses to do a backport because the
backport tracker issue is assigned to someone else, the script will "cowardly"
refuse to continue. Use --force to override. Be aware that --force will also
blow away an existing wip branch - the script asks for user confirmation in this
case.

The new --existing-pr option can be used to specify the number (ID) of an
existing backport PR that addresses the backport tracker issue given via the
positional argument.

The new "interactive setup routine" should make the setup process much simpler
for the user. If there is a setup issue, the script produces a report and starts
the interactive setup routine, which prompts the user for the needed
information.

Also, the script no longer requires the user to explicitly provide values for
github_user and redmine_user_id. Instead, it divines the correct values from the
GitHub token and the Redmine key, respectively.

Finally, the existing ~/bin/backport_common.sh file is deprecated in favor of
two files:

    ~/.github_token
    ~/.redmine_key

(The latter is already used by Sage's build-integration-branch tool and it
didn't make sense to have two different configuration files for a single
purpose.)

Signed-off-by: Nathan Cutler <ncutler@suse.com>
src/script/ceph-backport.sh

index aab85741b902baf6b036d37d2efb83d9358ef0b0..39cb37c08d81d79015f615389bbf9cb637b87913 100755 (executable)
@@ -8,19 +8,40 @@
 # This script automates the process of staging a backport starting from a
 # Backport tracker issue.
 #
-# Setup, usage and troubleshooting:
+# Setup:
 #
-#     ceph-backport.sh --help
-#     ceph-backport.sh --setup-advice
-#     ceph-backport.sh --usage-advice
-#     ceph-backport.sh --troubleshooting-advice
+#     ceph-backport.sh --setup
+#
+# Usage and troubleshooting:
 #
+#     ceph-backport.sh --help
+#     ceph-backport.sh --usage | less
+#     ceph-backport.sh --troubleshooting | less
 #
 
-SCRIPT_VERSION="15.0.0.6270"
 full_path="$0"
+
+SCRIPT_VERSION="15.0.0.6612"
+active_milestones=""
+backport_pr_number=""
+backport_pr_url=""
+deprecated_backport_common="$HOME/bin/backport_common.sh"
+github_token=""
+github_token_file="$HOME/.github_token"
+github_token_ok=""
+github_user=""
+non_interactive=""
+original_issue=""
+original_issue_url=""
+original_pr=""
+original_pr_url=""
+redmine_key=""
+redmine_key_file="$HOME/.redmine_key"
+redmine_key_ok=""
+redmine_login=""
+redmine_user_id=""
+setup_ok=""
 this_script=$(basename "$full_path")
-how_to_get_setup_advice="For setup advice, run: \"${this_script} --setup-advice | less\""
 
 if [[ $* == *--debug* ]]; then
     set -x
@@ -29,6 +50,7 @@ fi
 # associative array keyed on "component" strings from PR titles, mapping them to
 # GitHub PR labels that make sense in backports
 declare -A comp_hash=(
+["auth"]="core"
 ["bluestore"]="bluestore"
 ["build/ops"]="build/ops"
 ["ceph.spec"]="build/ops"
@@ -68,25 +90,39 @@ declare -A comp_hash=(
 
 declare -A flagged_pr_hash=()
 
+function abort_due_to_setup_problem {
+    error "problem detected in your setup"
+    info "Run \"${this_script} --setup\" to fix"
+    false
+}
+
+function assert_fail {
+    local message="$1"
+    error "(internal error) $message"
+    info "This could be reported as a bug!"
+    false
+}
+
 function bail_out_github_api {
     local api_said="$1"
+    local hint="$2"
     info "GitHub API said:"
     log bare "$api_said"
-    info "For setup report, run:  ${this_script} --setup"
-    info "For setup advice, run:  ${this_script} --setup-advice"
-    info "(hint) Check the value of github_token"
-    info "(hint) Run the script with --debug"
-    false
+    if [ "$hint" ] ; then
+        info "(hint) $hint"
+    fi
+    abort_due_to_setup_problem
 }
 
 function blindly_set_pr_metadata {
     local pr_number="$1"
     local json_blob="$2"
-    curl --silent --data-binary "$json_blob" 'https://api.github.com/repos/ceph/ceph/issues/'$pr_number'?access_token='$github_token >/dev/null 2>&1 || true
+    curl --silent --data-binary "$json_blob" "https://api.github.com/repos/ceph/ceph/issues/${pr_number}?access_token=${github_token}" >/dev/null 2>&1 || true
 }
 
 function check_milestones {
-    local milestones_to_check="$(echo "$1" | tr '\n' ' ' | xargs)"
+    local milestones_to_check
+    milestones_to_check="$(echo "$1" | tr '\n' ' ' | xargs)"
     info "Active milestones: $milestones_to_check"
     for m in $milestones_to_check ; do
         info "Examining all PRs targeting base branch \"$m\""
@@ -98,6 +134,7 @@ function check_milestones {
 function check_tracker_status {
     local -a ok_statuses=("new" "need more info")
     local ts="$1"
+    local error_msg
     local tslc="${ts,,}"
     local tslc_is_ok=
     for oks in "${ok_statuses[@]}"; do
@@ -111,41 +148,48 @@ function check_tracker_status {
         true
     else
         if [ "$tslc" = "in progress" ] ; then
-            error "Backport $redmine_url is already in progress"
-            false
+            error_msg="backport $redmine_url is already in progress"
         else
-            error "Backport $redmine_url is closed (status: ${ts})"
-            false
+            error_msg="backport $redmine_url is closed (status: ${ts})"
+        fi
+        if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
+            warning "$error_msg"
+        else
+            error "$error_msg"
         fi
     fi
     echo "$tslc_is_ok"
 }
 
 function cherry_pick_phase {
-    local base_branch=
-    local merged=
-    local number_of_commits=
-    local offset=0
-    local singular_or_plural_commit=
+    local base_branch
+    local default_val
+    local i
+    local merged
+    local number_of_commits
+    local offset
+    local sha1_to_cherry_pick
+    local singular_or_plural_commit
+    local yes_or_no_answer
     populate_original_issue
     if [ -z "$original_issue" ] ; then
         error "Could not find original issue"
         info "Does ${redmine_url} have a \"Copied from\" relation?"
         false
     fi
-    info "Parent issue: ${redmine_endpoint}/issues/${original_issue}"
+    info "Parent issue: ${original_issue_url}"
 
     populate_original_pr
     if [ -z "$original_pr" ]; then
         error "Could not find original PR"
-        info "Is the \"Pull request ID\" field of ${redmine_endpoint}/issues/${original_issue} populated?"
+        info "Is the \"Pull request ID\" field of ${original_issue_url} populated?"
         false
     fi
     info "Parent issue ostensibly fixed by: ${original_pr_url}"
 
     verbose "Examining ${original_pr_url}"
-    remote_api_output=$(curl --silent https://api.github.com/repos/ceph/ceph/pulls/${original_pr}?access_token=${github_token})
-    base_branch=$(echo ${remote_api_output} | jq -r .base.label)
+    remote_api_output=$(curl --silent "https://api.github.com/repos/ceph/ceph/pulls/${original_pr}?access_token=${github_token}")
+    base_branch=$(echo "${remote_api_output}" | jq -r '.base.label')
     if [ "$base_branch" = "ceph:master" ] ; then
         true
     else
@@ -154,14 +198,15 @@ function cherry_pick_phase {
         info "You can still use the script to stage the backport, though. Just prepare the local branch \"${local_branch}\" manually and re-run the script."
         false
     fi
-    merged=$(echo ${remote_api_output} | jq -r .merged)
+    merged=$(echo "${remote_api_output}" | jq -r '.merged')
     if [ "$merged" = "true" ] ; then
         true
     else
-        error "${original_pr_url} is not merged yet: cowardly refusing to perform automated cherry-pick"
+        error "${original_pr_url} is not merged yet"
+        info "Cowardly refusing to perform automated cherry-pick"
         false
     fi
-    number_of_commits=$(echo ${remote_api_output} | jq .commits)
+    number_of_commits=$(echo "${remote_api_output}" | jq '.commits')
     if [ "$number_of_commits" -eq "$number_of_commits" ] 2>/dev/null ; then
         # \$number_of_commits is set, and is an integer
         if [ "$number_of_commits" -eq "1" ] ; then
@@ -175,34 +220,60 @@ function cherry_pick_phase {
     fi
     info "Found $number_of_commits $singular_or_plural_commit in $original_pr_url"
 
-    debug "Fetching latest commits from $upstream_remote"
-    git fetch $upstream_remote
+    set -x
+    git fetch "$upstream_remote"
 
-    debug "Initializing local branch $local_branch to $milestone"
-    if git show-ref --verify --quiet refs/heads/$local_branch ; then
-        error "Cannot initialize $local_branch - local branch already exists"
-        false
+    if git show-ref --verify --quiet "refs/heads/$local_branch" ; then
+        if [ "$FORCE" ] ; then
+            if [ "$non_interactive" ] ; then
+                git checkout "$local_branch"
+                git reset --hard "${upstream_remote}/${milestone}"
+            else
+                echo
+                echo "A local branch $local_branch already exists and the --force option was given."
+                echo "If you continue, any local changes in $local_branch will be lost!"
+                echo
+                default_val="y"
+                echo -n "Do you really want to overwrite ${local_branch}? (default: ${default_val}) "
+                yes_or_no_answer="$(get_user_input "$default_val")"
+                [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
+                if [ "$yes_or_no_answer" = "y" ] ; then
+                    git checkout "$local_branch"
+                    git reset --hard "${upstream_remote}/${milestone}"
+                else
+                    info "OK, bailing out!"
+                    false
+                fi
+            fi
+        else
+            set +x
+            error "Cannot initialize $local_branch - local branch already exists"
+            false
+        fi
     else
-        git checkout $upstream_remote/$milestone -b $local_branch
+        git checkout "${upstream_remote}/${milestone}" -b "$local_branch"
     fi
 
-    debug "Fetching latest commits from ${original_pr_url}"
-    git fetch $upstream_remote pull/$original_pr/head:pr-$original_pr
+    git fetch "$upstream_remote" "pull/$original_pr/head:pr-$original_pr"
 
+    set +x
     info "Attempting to cherry pick $number_of_commits commits from ${original_pr_url} into local branch $local_branch"
-    let offset=${number_of_commits}-1 || true # don't fail on set -e when result is 0
-    for ((i=$offset; i>=0; i--)) ; do
-        debug "Cherry-picking commit $(git log --oneline --max-count=1 --no-decorate pr-$original_pr~$i)"
-        if git cherry-pick -x "pr-$original_pr~$i" ; then
-            true
+    offset="$((number_of_commits - 1))" || true
+    for ((i=offset; i>=0; i--)) ; do
+        info "Running \"git cherry-pick -x\" on $(git log --oneline --max-count=1 --no-decorate "pr-${original_pr}~${i}")"
+        sha1_to_cherry_pick=$(git rev-parse --verify "pr-${original_pr}~${i}")
+        set -x
+        if git cherry-pick -x "$sha1_to_cherry_pick" ; then
+            set +x
         else
+            set +x
             [ "$VERBOSE" ] && git status
             error "Cherry pick failed"
             info "Next, manually fix conflicts and complete the current cherry-pick"
             if [ "$i" -gt "0" ] >/dev/null 2>&1 ; then
                 info "Then, cherry-pick the remaining commits from ${original_pr_url}, i.e.:"
-                for ((j=$i-1; j>=0; j--)) ; do
-                    info "-> missing commit: $(git log --oneline --max-count=1 --no-decorate pr-$original_pr~$j)"
+                for ((j=i-1; j>=0; j--)) ; do
+                    info "-> missing commit: $(git log --oneline --max-count=1 --no-decorate "pr-${original_pr}~${j}")"
                 done
                 info "Finally, re-run the script"
             else
@@ -222,29 +293,6 @@ function debug {
     log debug "$@"
 }
 
-function deduce_remote {
-    local remote_type="$1"
-    local remote=""
-    local url_component=""
-    if [ "$remote_type" = "upstream" ] ; then
-        url_component="ceph"
-    elif [ "$remote_type" = "fork" ] ; then
-        url_component="$github_user"
-    else
-        error "Internal error in deduce_remote"
-        false
-    fi
-    remote=$(git remote -v | egrep --ignore-case '(://|@)github.com(/|:)'$url_component'/ceph(\s|\.|\/)' | head -n1 | cut -f 1)
-    if [ "$remote" ] ; then
-        true
-    else
-        error "Cannot auto-determine ${remote_type}_remote"
-        info "There is something wrong with your remotes - to start with, check 'git remote -v'"
-        false
-    fi
-    echo "$remote"
-}
-
 function display_version_message_and_exit {
     echo "$this_script: Ceph backporting script, version $SCRIPT_VERSION"
     exit 0 
@@ -259,7 +307,7 @@ function dump_flagged_prs {
         warning "Some backport PRs had problematic milestone settings"
         log bare "==========="
         log bare "Flagged PRs"
-        log bare "==========="
+        log bare "-----------"
         for url in "${!flagged_pr_hash[@]}" ; do
             log bare "$url - ${flagged_pr_hash[$url]}"
         done
@@ -268,7 +316,7 @@ function dump_flagged_prs {
 }
 
 function eol {
-    log mtt=$1
+    local mtt="$1"
     error "$mtt is EOL"
     false
 }
@@ -277,10 +325,83 @@ function error {
     log error "$@"
 }
 
+function existing_pr_routine {
+    local base_branch
+    local clipped_pr_body
+    local new_pr_body
+    local new_pr_title
+    local pr_body
+    local pr_json_tempfile
+    local pr_title
+    local remote_api_output
+    remote_api_output="$(curl --silent "https://api.github.com/repos/ceph/ceph/pulls/${backport_pr_number}?access_token=${github_token}")"
+    pr_title="$(echo "$remote_api_output" | jq -r '.title')"
+    if [ "$pr_title" = "null" ] ; then
+        error "could not get PR title of existing PR ${backport_pr_number}"
+        bail_out_github_api "$remote_api_output"
+    fi
+    pr_body="$(echo "$remote_api_output" | jq -r '.body')"
+    if [ "$pr_body" = "null" ] ; then
+        error "could not get PR body of existing PR ${backport_pr_number}"
+        bail_out_github_api "$remote_api_output"
+    fi
+    base_branch=$(echo "${remote_api_output}" | jq -r '.base.label')
+    base_branch="${base_branch#ceph:}"
+    if [ -z "$(is_active_milestone "$base_branch")" ] ; then
+        error "existing PR $backport_pr_url is targeting $base_branch which is not an active milestone"
+        info "Cowardly refusing to work on a backport to $base_branch"
+        false
+    fi
+    pr_json_tempfile=$(mktemp)
+    echo "$pr_body" | sed -n '/<!--.*/q;p' > "$pr_json_tempfile"
+    # clipped_pr_body=$(cat "$pr_json_tempfile" | xargs)
+    clipped_pr_body="$(xargs < "$pr_json_tempfile")"
+    rm "$pr_json_tempfile"
+    verbose_en "Clipped body of existing PR ${backport_pr_number}:\n${clipped_pr_body}\n"
+    if [[ "$pr_title" =~ ^${milestone}: ]] ; then
+        verbose "Existing backport PR ${backport_pr_number} title has ${milestone} prepended"
+    else
+        warning "Existing backport PR ${backport_pr_number} title does NOT have ${milestone} prepended"
+        new_pr_title="${milestone}: $pr_title"
+        if [[ "$new_pr_title" =~ \" ]] ; then
+            new_pr_title="${new_pr_title//\"/\\\"}"
+        fi
+        verbose "New PR title: ${new_pr_title}"
+    fi
+    redmine_url_without_scheme="${redmine_url//http?:\/\//}"
+    verbose "Redmine URL without scheme: $redmine_url_without_scheme"
+    if [[ "$clipped_pr_body" =~ $redmine_url_without_scheme ]] ; then
+        verbose "Existing backport PR ${backport_pr_number} mentions $redmine_url"
+    else
+        warning "Existing backport PR ${backport_pr_number} does NOT mention $redmine_url - adding it"
+        new_pr_body="backport tracker: ${redmine_url}"
+        if [ "${original_pr_url}" ] ; then
+            new_pr_body="${new_pr_body}
+possibly a backport of ${original_pr_url}"
+        fi
+        if [ "${original_issue_url}" ] ; then
+            new_pr_body="${new_pr_body}
+parent tracker: ${original_issue_url}"
+        fi
+        new_pr_body="${new_pr_body}
+
+---
+
+original PR body:
+
+$clipped_pr_body
+
+---
+
+updated using ceph-backport.sh version ${SCRIPT_VERSION}"
+    fi
+    maybe_update_pr_title_body "${backport_pr_number}" "${new_pr_title}" "${new_pr_body}"
+}
+
 function failed_mandatory_var_check {
     local varname="$1"
     local error="$2"
-    error "$varname $error"
+    verbose "$varname $error"
     setup_ok=""
 }
 
@@ -292,12 +413,31 @@ function flag_pr {
     flagged_pr_hash["${pr_url}"]="$flag_reason"
 }
 
+function from_file {
+    local what="$1"
+    xargs 2>/dev/null < "$HOME/.${what}" || true
+}
+
+function get_user_input {
+    local default_val="$1"
+    local user_input=
+    read -r user_input
+    if [ "$user_input" ] ; then
+        echo "$user_input"
+    else
+        echo "$default_val"
+    fi
+}
+
 # takes a string and a substring - returns position of substring within string,
 # or -1 if not found
 # NOTE: position of first character in string is 0
 function grep_for_substr {
-    munged="${1%%$2*}"
-    if [ "$munged" = "$1" ] ; then
+    local str="$1"
+    local look_for_in_str="$2"
+    str="${str,,}"
+    munged="${str%%${look_for_in_str}*}"
+    if [ "$munged" = "$str" ] ; then
         echo "-1"
     else
         echo "${#munged}"
@@ -333,12 +473,158 @@ function init_endpoints {
     github_endpoint="${github_endpoint:-"https://github.com/ceph/ceph"}"
 }
 
-function init_remotes {
-    # if github_user is not set, we cannot initialize fork_remote
-    vet_github_user
-    verbose "Initializing GitHub repos (\"remotes\")"
-    upstream_remote="${upstream_remote:-$(deduce_remote upstream)}"
-    fork_remote="${fork_remote:-$(deduce_remote fork)}"
+function init_fork_remote {
+    [ "$github_user" ] || assert_fail "github_user not set"
+    fork_remote="${fork_remote:-$(maybe_deduce_remote fork)}"
+}
+
+function init_github_token {
+    github_token="$(from_file github_token)"
+    if [ "$github_token" ] ; then
+        true
+    else
+        warning "$github_token_file not populated: initiating interactive setup routine"
+        INTERACTIVE_SETUP_ROUTINE="yes"
+    fi
+}
+
+function init_redmine_key {
+    redmine_key="$(from_file redmine_key)"
+    if [ "$redmine_key" ] ; then
+        true
+    else
+        warning "$redmine_key_file not populated: initiating interactive setup routine"
+        INTERACTIVE_SETUP_ROUTINE="yes"
+    fi
+}
+
+function init_upstream_remote {
+    upstream_remote="${upstream_remote:-$(maybe_deduce_remote upstream)}"
+}
+
+function interactive_setup_routine {
+    local default_val
+    local original_github_token
+    local original_redmine_key
+    local yes_or_no_answer
+    original_github_token="$github_token"
+    original_redmine_key="$redmine_key"
+    maybe_delete_backport_common
+    echo
+    echo "Welcome to the ${this_script} interactive setup routine!"
+    echo
+    echo "---------------------------------------------------------------------"
+    echo "Setup step 1 of 4 - GitHub token"
+    echo "---------------------------------------------------------------------"
+    echo "For information on how to generate a GitHub personal access token"
+    echo "to use with this script, go to https://github.com/settings/tokens"
+    echo "then click on \"Generate new token\" and make sure the token has"
+    echo "\"Full control of private repositories\" scope."
+    echo
+    echo "For more details, see:"
+    echo "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line"
+    echo
+    echo -n "What is your GitHub token? "
+    default_val="$github_token"
+    [ "$github_token" ] && echo "(default: ${default_val})"
+    github_token="$(get_user_input "$default_val")"
+    if [ "$github_token" ] ; then
+        set_github_user_from_github_token
+        if [ "$setup_ok" ] ; then
+            true
+        else
+            info "You must provide a valid GitHub personal access token"
+            abort_due_to_setup_problem
+        fi
+    else
+        error "You must provide a valid GitHub personal access token"
+        abort_due_to_setup_problem
+    fi
+    [ "$github_token" ] || assert_fail "github_token not set, even after completing Step 1 of interactive setup"
+    [ "$github_user" ] || assert_fail "github_user not set, even after completing Step 1 of interactive setup"
+    echo
+    echo "---------------------------------------------------------------------"
+    echo "Setup step 2 of 4 - GitHub user"
+    echo "---------------------------------------------------------------------"
+    echo "Based on the GitHub token set in the previous step, the script thinks"
+    echo "your GitHub username (login) is:"
+    echo
+    echo "    $github_user"
+    echo
+    echo -n "Is this GitHub username (login) correct? "
+    default_val="y"
+    [ "$github_token" ] && echo "(default: ${default_val})"
+    yes_or_no_answer="$(get_user_input "$default_val")"
+    [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
+    if [ "$yes_or_no_answer" = "y" ] ; then
+        if [ "$github_token" = "$original_github_token" ] ; then
+            true
+        else
+            debug "GitHub personal access token changed"
+            echo "$github_token" > "$github_token_file"
+            chmod 0600 "$github_token_file"
+            info "Wrote GitHub personal access token to $github_token_file"
+        fi
+    else
+        error "GitHub user does not look right"
+        abort_due_to_setup_problem
+    fi
+    [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1 and 2 of interactive setup"
+    [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1 and 2 of interactive setup"
+    echo
+    echo "---------------------------------------------------------------------"
+    echo "Setup step 3 of 4 - remote repos"
+    echo "---------------------------------------------------------------------"
+    echo "Searching \"git remote -v\" for remote repos"
+    echo
+    init_upstream_remote
+    init_fork_remote
+    vet_remotes
+    [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1-3 of interactive setup"
+    [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1-3 of interactive setup"
+    [ "$upstream_remote" ] || assert_fail "upstream_remote not set, even after completing Steps 1-3 of interactive setup"
+    [ "$fork_remote" ] || assert_fail "fork_remote not set, even after completing Steps 1-3 of interactive setup"
+    echo
+    echo "---------------------------------------------------------------------"
+    echo "Setup step 4 of 4 - Redmine key"
+    echo "---------------------------------------------------------------------"
+    echo "To generate a Redmine API access key, go to https://tracker.ceph.com"
+    echo "After signing in, click: \"My account\""
+    echo "Now, find \"API access key\"."
+    echo "Once you know the API access key, enter it below."
+    echo
+    echo -n "What is your Redmine key? "
+    default_val="$redmine_key"
+    [ "$redmine_key" ] && echo "(default: ${default_val})"
+    redmine_key="$(get_user_input "$default_val")"
+    if [ "$redmine_key" ] ; then
+        set_redmine_user_from_redmine_key
+        if [ "$setup_ok" ] ; then
+            true
+        else
+            info "You must provide a valid Redmine API access key"
+            abort_due_to_setup_problem
+        fi
+        if [ "$redmine_key" = "$original_redmine_key" ] ; then
+            true
+        else
+            debug "Redmine API access key changed"
+            echo "$redmine_key" > "$redmine_key_file"
+            chmod 0600 "$redmine_key_file"
+            info "Wrote Redmine API access key to $redmine_key_file"
+        fi
+    else
+        error "You must provide a valid Redmine API access key"
+        abort_due_to_setup_problem
+    fi
+    [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1-4 of interactive setup"
+    [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1-4 of interactive setup"
+    [ "$upstream_remote" ] || assert_fail "upstream_remote not set, even after completing Steps 1-4 of interactive setup"
+    [ "$fork_remote" ] || assert_fail "fork_remote not set, even after completing Steps 1-4 of interactive setup"
+    [ "$redmine_key" ] || assert_fail "redmine_key not set, even after completing Steps 1-4 of interactive setup"
+    [ "$redmine_user_id" ] || assert_fail "redmine_user_id not set, even after completing Steps 1-4 of interactive setup"
+    [ "$redmine_login" ] || assert_fail "redmine_login not set, even after completing Steps 1-4 of interactive setup"
+    vet_setup --interactive
 }
 
 function is_active_milestone {
@@ -357,8 +643,9 @@ function is_active_milestone {
 function log {
     local level="$1"
     local trailing_newline="yes"
+    local in_hex=""
     shift
-    local msg="$@"
+    local msg="$*"
     prefix="${this_script}: "
     verbose_only=
     case $level in
@@ -372,6 +659,9 @@ function log {
         err*)
             prefix="${prefix}ERROR: "
             ;;
+        hex)
+            in_hex="yes"
+            ;;
         info)
             :
             ;;
@@ -390,7 +680,9 @@ function log {
             prefix="${prefix}WARNING: "
             ;;
     esac
-    if [ "$verbose_only" -a -z "$VERBOSE" ] ; then
+    if [ "$in_hex" ] ; then
+        print_in_hex "$msg"
+    elif [ "$verbose_only" ] && [ -z "$VERBOSE" ] ; then
         true
     else
         msg="${prefix}${msg}"
@@ -402,147 +694,254 @@ function log {
     fi
 }
 
+function maybe_deduce_remote {
+    local remote_type="$1"
+    local remote=""
+    local url_component=""
+    if [ "$remote_type" = "upstream" ] ; then
+        url_component="ceph"
+    elif [ "$remote_type" = "fork" ] ; then
+        url_component="$github_user"
+    else
+        assert_fail "bad remote_type ->$remote_type<- in maybe_deduce_remote"
+    fi
+    remote=$(git remote -v | grep --extended-regexp --ignore-case '(://|@)github.com(/|:)'${url_component}'/ceph(\s|\.|\/)' | head -n1 | cut -f 1)
+    echo "$remote"
+}
+
+function maybe_delete_backport_common {
+    local default_val
+    local user_inp
+    if [ -e "$deprecated_backport_common" ] ; then
+        echo
+        echo "---------------------------------------------------------------------"
+        echo "Deprecated configuration file detected"
+        echo "---------------------------------------------------------------------"
+        echo "You still have a $deprecated_backport_common file,"
+        echo "which was used to store configuration parameters in version"
+        echo "15.0.0.6270 and earlier versions of ${this_script}."
+        echo
+
+        github_token=""
+        redmine_key=""
+        # shellcheck disable=SC1090
+        source "$deprecated_backport_common" 2>/dev/null || true
+
+        if [ "$github_token" ] || [ "$redmine_key" ] ; then
+            github_token="${github_token:-(not set)}"
+            redmine_key="${redmine_key:-(not set)}"
+            echo "This file has been read and the following values extracted:"
+            echo
+            echo "github_token  $github_token"
+            echo "redmine_key   $redmine_key"
+            echo
+            echo "If both of these are shown correctly, no other information is"
+            echo "needed. The values read will be offered as defaults."
+        else
+            echo "This file has been read, but the script could not extract"
+            echo "either the GitHub personal access token, nor the Redmine API"
+            echo "access key from it."
+        fi
+        echo
+        echo "Since $deprecated_backport_common has been deprecated in favor"
+        echo "of this interactive setup routine, the file should be deleted now."
+        echo
+        echo -n "Delete it now? (default: y) "
+        default_val="y"
+        user_inp="$(get_user_input "$default_val")"
+        user_inp="$(echo "$user_inp" | tr '[:upper:]' '[:lower:]' | xargs)"
+        if [ "$user_inp" ] ; then
+            user_inp="${user_inp:0:1}"
+            if [ "$user_inp" = "y" ] ; then
+                set -x
+                rm -f "$deprecated_backport_common"
+                set +x
+            fi
+        fi
+        if [ -e "$deprecated_backport_common" ] ; then
+            error "$deprecated_backport_common still exists. Bailing out!"
+            false
+        fi
+    fi
+}
+
+function maybe_update_pr_milestone_labels {
+    local pr_number="$1"
+    local component
+    local data_binary
+    if [ "$EXPLICIT_COMPONENT" ] ; then
+        debug "Component given on command line: using it"
+        component="$EXPLICIT_COMPONENT"
+    else
+        debug "Attempting to guess component"
+        component=$(guess_component "$title")
+    fi
+    if [ "$component" ] ; then
+        debug "Attempting to set ${component} label and ${milestone} milestone in ${pr_url}"
+        data_binary='{"milestone":'$milestone_number',"labels":["'$component'"]}'
+    else
+        debug "Attempting to set ${milestone} milestone in ${pr_url}"
+        data_binary='{"milestone":'$milestone_number'}'
+    fi
+    blindly_set_pr_metadata "$pr_number" "$data_binary"
+}
+
+function maybe_update_pr_title_body {
+    local pr_number="$1"
+    local new_title="$2"
+    local new_body="$3"
+    local data_binary
+    if [ "$new_title" ] && [ "$new_body" ] ; then
+        data_binary="{\"title\":\"${new_title}\", \"body\":\"$(munge_body "${new_body}")\"}"
+    elif [ "$new_title" ] ; then
+        data_binary="{\"title\":\"${new_title}\"}"
+    elif [ "$new_body" ] ; then
+        data_binary="{\"body\":\"$(munge_body "${new_body}")\"}"
+        #log hex "${data_binary}"
+        #echo -n "${data_binary}"
+    fi
+    if [ "$data_binary" ] ; then
+        blindly_set_pr_metadata "${pr_number}" "$data_binary"
+    fi
+}
+
 function milestone_number_from_remote_api {
-    local mtt=$1  # milestone to try
-    local mn=""   # milestone number
+    local mtt="$1"  # milestone to try
+    local mn=""     # milestone number
+    local milestones
     warning "Milestone ->$mtt<- unknown to script - falling back to GitHub API"
-    remote_api_output=$(curl --silent -X GET 'https://api.github.com/repos/ceph/ceph/milestones?access_token='$github_token)
-    mn=$(echo $remote_api_output | jq --arg milestone $mtt '.[] | select(.title==$milestone) | .number')
+    remote_api_output=$(curl --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones?access_token=${github_token}")
+    mn=$(echo "$remote_api_output" | jq --arg milestone "$mtt" '.[] | select(.title==$milestone) | .number')
     if [ "$mn" -gt "0" ] >/dev/null 2>&1 ; then
         echo "$mn"
     else
         error "Could not determine milestone number of ->$milestone<-"
         verbose_en "GitHub API said:\n${remote_api_output}\n"
-        info "Valid values are $(curl --silent -X GET 'https://api.github.com/repos/ceph/ceph/milestones?access_token='$github_token | jq '.[].title')"
+        remote_api_output=$(curl --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones?access_token=${github_token}")
+        milestones=$(echo "$remote_api_output" | jq '.[].title')
+        info "Valid values are ${milestones}"
         info "(This probably means the Release field of ${redmine_url} is populated with"
         info "an unexpected value - i.e. it does not match any of the GitHub milestones.)"
         false
     fi
 }
 
+function munge_body {
+    echo "$new_body" | tr '\r' '\n' | sed 's/$/\\n/' | tr -d '\n'
+}
+
+function number_to_url {
+    local number_type="$1"
+    local number="$2"
+    if [ "$number_type" = "github" ] ; then
+        echo "${github_endpoint}/pull/${number}"
+    elif [ "$number_type" = "redmine" ] ; then
+        echo "${redmine_endpoint}/issues/${number}"
+    else
+        assert_fail "internal error in number_to_url: bad type ->$number_type<-"
+    fi
+}
+
 function populate_original_issue {
     if [ -z "$original_issue" ] ; then
-        original_issue=$(curl --silent ${redmine_url}.json?include=relations |
+        original_issue=$(curl --silent "${redmine_url}.json?include=relations" |
             jq '.issue.relations[] | select(.relation_type | contains("copied_to")) | .issue_id')
+        original_issue_url="$(number_to_url "redmine" "${original_issue}")"
     fi
 }
 
 function populate_original_pr {
     if [ "$original_issue" ] ; then
         if [ -z "$original_pr" ] ; then
-            original_pr=$(curl --silent ${redmine_endpoint}/issues/${original_issue}.json |
+            original_pr=$(curl --silent "${original_issue_url}.json" |
                           jq -r '.issue.custom_fields[] | select(.id | contains(21)) | .value')
-            original_pr_url="${github_endpoint}/pull/${original_pr}"
+            original_pr_url="$(number_to_url "github" "${original_pr}")"
         fi
     fi
 }
 
-function setup_advice {
-    cat <<EOM
-Setup advice
-------------
-
-${this_script} expects to be run inside a local clone of the Ceph git repo.
-Some initial setup is required for the script to become fully functional.
-
-First, obtain the correct values for the following variables:
-
-redmine_key     # "My account" -> "API access key" -> "Show"
-redmine_user_id # "Logged in as foobar", click on foobar link, Redmine User ID
-                # is in the URL, i.e. https://tracker.ceph.com/users/[redmine_user_id]
-github_token    # https://github.com/settings/tokens -> Generate new token ->
-                # ensure it has "Full control of private repositories" scope
-                # see also:
-                # https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line
-github_user     # Your github username
-
-The above variables must be set explicitly, as the script has no way of
-determining reasonable defaults. If you prefer, you can ensure the variables
-are set in the environment before running the script. Alternatively, you can
-create a file, \$HOME/bin/backport_common.sh (this exact path), with the
-variable assignments in it. The script will detect that the file exists and
-"source" it.
-
-In any case, care should be taken to keep the values of redmine_key and
-github_token secret.
-
-The script expects to run in a local clone of a Ceph repo with
-at least two remotes defined - pointing to:
-
-    https://github.com/ceph/ceph.git
-    https://github.com/\$github_user/ceph.git
-
-In other words, the upstream GitHub repo and the user's fork thereof. It makes
-no difference what these remotes are called - the script will determine the
-right remote names automatically.
-
-To find out whether you have any obvious problems with your setup before
-actually using the script to stage a backport, run:
-
-    ${this_script} --setup
+function print_in_hex {
+    local str="$1"
+    local c
+
+    for (( i=0; i < ${#str}; i++ ))
+    do
+       c=${str:$i:1}
+       if [[ $c == ' ' ]]
+       then
+          printf "[%s] 0x%X\n" " " \'\ \' >&2
+       else
+          printf "[%s] 0x%X\n" "$c" \'"$c"\' >&2
+       fi
+    done
+}
 
-EOM
+function set_github_user_from_github_token {
+    [ "$github_token" ] || assert_fail "set_github_user_from_github_token: git_token not set"
+    #
+    # $ curl --silent https://api.github.com/repos/ceph/ceph/pulls/19999?access_token=invalid
+    # {
+    #   "message": "Bad credentials",
+    #   "documentation_url": "https://developer.github.com/v3"
+    # }
+    #
+    remote_api_output=$(curl --silent "https://api.github.com/user?access_token=${github_token}")
+    github_user=$(echo "${remote_api_output}" | jq -r '.login')
+    if [ "$github_user" = "null" ] 2>/dev/null ; then
+        error "GitHub token ${github_token} is invalid"
+        github_user=""
+        setup_ok=""
+    else
+        [ "$github_user" ] || assert_fail "set_github_user_from_github_token: failed to set github_user"
+        info "my GitHub username is $github_user"
+        github_token_ok="yes"
+    fi
 }
 
-function setup_report {
-    local not_set="!!! NOT SET !!!"
-    local set_but_not_valid="!!! SET, BUT NOT VALID !!!"
-    local redmine_endpoint_display="${redmine_endpoint:-$not_set}"
-    local redmine_user_id_display="${redmine_user_id:-$not_set}"
-    local github_endpoint_display="${github_endpoint:-$not_set}"
-    local github_user_display="${github_user:-$not_set}"
-    local upstream_remote_display="${upstream_remote:-$not_set}"
-    local fork_remote_display="${fork_remote:-$not_set}"
-    local redmine_key_display=""
-    local github_token_display=""
-    verbose Checking mandatory variables
-    if [ "$github_token" ] ; then
-        if [ "$(vet_github_token)" ] ; then
-            github_token_display="(OK, not shown)"
-        else
-            github_token_display="$set_but_not_valid"
-            failed_mandatory_var_check github_token "set, but not valid"
-        fi
+function set_redmine_user_from_redmine_key {
+    [ "$redmine_key" ] || assert_fail "set_redmine_user_from_redmine_key was called, but redmine_key not set"
+    local api_key_from_api
+    remote_api_output="$(curl --silent "https://tracker.ceph.com/users/current.json?key=$redmine_key")"
+    redmine_login="$(echo "$remote_api_output" | jq -r '.user.login')"
+    redmine_user_id="$(echo "$remote_api_output" | jq -r '.user.id')"
+    api_key_from_api="$(echo "$remote_api_output" | jq -r '.user.api_key')"
+    if [ "$redmine_login" ] && [ "$redmine_user_id" ] && [ "$api_key_from_api" = "$redmine_key" ] ; then
+        [ "$redmine_user_id" ] || assert_fail "set_redmine_user_from_redmine_key: failed to set redmine_user_id"
+        [ "$redmine_login" ] || assert_fail "set_redmine_user_from_redmine_key: failed to set redmine_login"
+        info "my Redmine username is $redmine_login (ID $redmine_user_id)"
+        redmine_key_ok="yes"
     else
-        github_token_display="$not_set"
-        failed_mandatory_var_check github_token "not set"
+        error "Redmine API access key $redmine_key is invalid"
+        redmine_login=""
+        redmine_user_id=""
+        setup_ok=""
     fi
-    [ "$redmine_key" ] && redmine_key_display="(OK, not shown)" || redmine_key_display="$not_set"
-    test "$redmine_key"      || failed_mandatory_var_check redmine_key "not set"
-    test "$redmine_user_id"  || failed_mandatory_var_check redmine_user_id "not set"
-    test "$github_user"      || failed_mandatory_var_check github_user "not set"
-    test "$upstream_remote"  || failed_mandatory_var_check upstream_remote "not set"
-    test "$fork_remote"      || failed_mandatory_var_check fork_remote "not set"
-    test "$redmine_endpoint" || failed_mandatory_var_check redmine_endpoint "not set"
-    test "$github_endpoint"  || failed_mandatory_var_check github_endpoint "not set"
-    if [ "$SETUP_ONLY" ] ; then
-        read -r -d '' setup_summary <<EOM || true > /dev/null 2>&1
-redmine_endpoint $redmine_endpoint
-redmine_user_id  $redmine_user_id_display
-redmine_key      $redmine_key_display
-github_endpoint  $github_endpoint
-github_user      $github_user_display
-github_token     $github_token_display
-upstream_remote  $upstream_remote_display
-fork_remote      $fork_remote_display
-EOM
-        log bare "================================"
-        log bare "Setup report"
-        log bare "--------------------------------"
-        log bare "variable name    value"
-        log bare "--------------------------------"
-        log bare "$setup_summary"
-        log bare "================================"
+}
+
+function tracker_component_is_in_desired_state {
+    local comp="$1"
+    local val_is="$2"
+    local val_should_be="$3"
+    local in_desired_state
+    if [ "$val_is" = "$val_should_be" ] ; then
+        debug "Tracker $comp is in the desired state"
+        in_desired_state="yes"
+    fi
+    echo "$in_desired_state"
+}
+
+function tracker_component_was_updated {
+    local comp="$1"
+    local val_old="$2"
+    local val_new="$3"
+    local was_updated
+    if [ "$val_old" = "$val_new" ] ; then
+        true
     else
-        verbose "redmine_endpoint $redmine_endpoint_display"
-        verbose "redmine_user_id  $redmine_user_id_display"
-        verbose "redmine_key      $redmine_key_display"
-        verbose "github_endpoint  $github_endpoint_display"
-        verbose "github_user      $github_user_display"
-        verbose "github_token     $github_token_display"
-        verbose "upstream_remote  $upstream_remote_display"
-        verbose "fork_remote      $fork_remote_display"
+        debug "Tracker $comp was updated!"
+        was_updated="yes"
     fi
+    echo "$was_updated"
 }
 
 function troubleshooting_advice {
@@ -586,47 +985,55 @@ function try_known_milestones {
         luminous) mn="10" ;;
         mimic) mn="11" ;;
         nautilus) mn="12" ;;
-        octopus) echo "Octopus milestone number is unknown! Update the script now." ; exit -1 ;;
+        octopus) echo "Octopus milestone number is unknown! Update the script now." ; exit 1 ;;
     esac
     echo "$mn"
 }
 
 function update_version_number_and_exit {
     set -x
-    local raw_version=$(git describe --long --match 'v*' | sed 's/^v//')
-    # raw_version will look like this: 15.0.0-5774-g4c2f2eda969
-    local munge_first_hyphen=${raw_version/-/.}
+    local raw_version
+    local munge_first_hyphen
     # munge_first_hyphen will look like this: 15.0.0.5774-g4c2f2eda969
-    local script_version_number=${munge_first_hyphen%-*}
-    # script_version_number will look like this: 15.0.0.5774
-    sed -i -e 's/^SCRIPT_VERSION=.*/SCRIPT_VERSION="'"$script_version_number"'"/' $full_path
+    local script_version_number
+    raw_version="$(git describe --long --match 'v*' | sed 's/^v//')"  # example: "15.0.0-5774-g4c2f2eda969"
+    munge_first_hyphen="${raw_version/-/.}"  # example: "15.0.0.5774-g4c2f2eda969"
+    script_version_number="${munge_first_hyphen%-*}"  # example: "15.0.0.5774"
+    sed -i -e "s/^SCRIPT_VERSION=.*/SCRIPT_VERSION=\"${script_version_number}\"/" "$full_path"
     exit 0
 }
 
 function usage {
     cat <<EOM >&2
+Setup:
+
+   ${this_script} --setup
+
 Documentation:
 
-   ${this_script} --setup-advice | less
-   ${this_script} --usage-advice | less
-   ${this_script} --troubleshooting-advice | less
+   ${this_script} --help
+   ${this_script} --usage | less
+   ${this_script} --troubleshooting | less
 
 Usage:
-   ${this_script} --setup
    ${this_script} BACKPORT_TRACKER_ISSUE_NUMBER
 
 Options (not needed in normal operation):
-    --cherry-pick-only (stop after cherry-pick phase)
-    -c/--component COMPONENT
-                       (explicitly set the component label; if omitted, the
-                        script will try to guess the component)
-    --debug            (turns on "set -x")
-    --milestones       (vet all backport PRs for correct milestone setting)
-    -s/--setup         (check the setup and report any problems found)
-    --update-version   (this option exists as a convenience for the script
-                        maintainer only: not intended for day-to-day usage)
-    -v/--verbose       (produce more output than normal)
-    --version          (display version number and exit)
+    --cherry-pick-only    (stop after cherry-pick phase)
+    --component/-c COMPONENT
+                          (explicitly set the component label; if omitted, the
+                           script will try to guess the component)
+    --debug               (turns on "set -x")
+    --existing-pr BACKPORT_PR_ID
+                          (use this when the backport PR is already open)
+    --milestones          (vet all backport PRs for correct milestone setting)
+    --setup/-s            (run the interactive setup routine - NOTE: this can 
+                           be done any number of times)
+    --setup-report        (check the setup and print a report)
+    --update-version      (this option exists as a convenience for the script
+                           maintainer only: not intended for day-to-day usage)
+    --verbose/-v          (produce more output than normal)
+    --version             (display version number and exit)
 
 Example:
    ${this_script} 31459
@@ -642,14 +1049,14 @@ function usage_advice {
 Usage advice
 ------------
 
-Once you have completed setup (see --setup-advice), you can run the script
-with the ID of a Backport tracker issue. For example, to stage the backport
+Once you have completed --setup, you can run the script with the ID of
+a Backport tracker issue. For example, to stage the backport
 https://tracker.ceph.com/issues/41502, run:
 
     ${this_script} 41502
 
-If the commits in the corresponding master PR cherry-pick cleanly, the script
-will automatically perform all steps required to stage the backport:
+Provided the commits in the corresponding master PR cherry-pick cleanly, the
+script will automatically perform all steps required to stage the backport:
 
 Cherry-pick phase:
 
@@ -704,48 +1111,16 @@ function verbose_en {
     log verbose_en "$@"
 }
 
-function vet_github_token {
-    # github_token is set, but we don't know, yet, if the remote API will honor
-    # it. Fortunately, with GitHub it's simple:
-    #
-    # $ curl --silent https://api.github.com/repos/ceph/ceph/pulls/19999?access_token=invalid
-    # {
-    #   "message": "Bad credentials",
-    #   "documentation_url": "https://developer.github.com/v3"
-    # }
-    #
-    local number=
-    local test_pr_id='19999'
-    remote_api_output=$(curl --silent https://api.github.com/repos/ceph/ceph/pulls/${test_pr_id}?access_token=${github_token})
-    number=$(echo ${remote_api_output} | jq .number)
-    # in invalid case, $number will be equal to "null"
-    # in valid case, it will be "19999"
-    if [ "$number" -eq "$test_pr_id" ] 2>/dev/null ; then
-        echo "valid"
-    else
-        echo ""
-    fi
-}
-
-function vet_github_user {
-    if [ "$github_user" ] ; then
-        true
-    else
-        failed_mandatory_var_check github_user "not set"
-        info "$how_to_get_setup_advice"
-        false
-    fi
-}
-
 function vet_pr_milestone {
     local pr_number="$1"
     local pr_title="$2"
     local pr_url="$3"
     local milestone_stanza="$4"
     local milestone_title_should_be="$5"
-    local milestone_number_should_be=$(try_known_milestones "$milestone_title_should_be")
+    local milestone_number_should_be
     local milestone_number_is=
     local milestone_title_is=
+    milestone_number_should_be="$(try_known_milestones "$milestone_title_should_be")"
     log overwrite "Vetting milestone of PR#${pr_number}\r"
     if [ "$milestone_stanza" = "null" ] ; then
         blindly_set_pr_metadata "$pr_number" "{\"milestone\": $milestone_number_should_be}"
@@ -771,30 +1146,139 @@ function vet_prs_for_milestone {
     local pr_title=
     local pr_url=
     # determine last page (i.e., total number of pages)
-    remote_api_output=$(curl --silent --head https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}\&access_token=${github_token} | grep -E '^Link' || true)
+    remote_api_output="$(curl --silent --head "https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}&access_token=${github_token}" | grep -E '^Link' || true)"
     if [ "$remote_api_output" ] ; then
          # Link: <https://api.github.com/repositories/2310495/pulls?base=luminous&access_token=f9b0beb6922e418663396f3ff2ab69467a3268f9&page=2>; rel="next", <https://api.github.com/repositories/2310495/pulls?base=luminous&access_token=f9b0beb6922e418663396f3ff2ab69467a3268f9&page=2>; rel="last"
-         pages_of_output=$(echo "$remote_api_output" | sed 's/^.*&page\=\([0-9]\+\)>; rel=\"last\".*$/\1/g')
+         # shellcheck disable=SC2001
+         pages_of_output="$(echo "$remote_api_output" | sed 's/^.*&page\=\([0-9]\+\)>; rel=\"last\".*$/\1/g')"
     else
          pages_of_output="1"
     fi
     verbose "GitHub has $pages_of_output pages of pull request data for \"base:${milestone_title}\""
-    for ((page=1; page<=${pages_of_output}; page++)) ; do
+    for ((page=1; page<=pages_of_output; page++)) ; do
         verbose "Fetching PRs (page $page of ${pages_of_output})"
-        remote_api_output=$(curl --silent -X GET https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}\&access_token=${github_token}\&page=${page})
-        prs_in_page=$(echo "$remote_api_output" | jq -r '. | length')
+        remote_api_output="$(curl --silent -X GET "https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}&access_token=${github_token}&page=${page}")"
+        prs_in_page="$(echo "$remote_api_output" | jq -r '. | length')"
         verbose "Page $page of remote API output contains information on $prs_in_page PRs"
-        for ((i=0; i<${prs_in_page}; i++)) ; do
-            pr_number=$(echo "$remote_api_output" | jq -r '.['$i'].number')
-            pr_title=$(echo "$remote_api_output" | jq -r '.['$i'].title')
-            pr_url="${github_endpoint}/pull/${pr_number}"
-            milestone_stanza=$(echo "$remote_api_output" | jq -r '.['$i'].milestone')
+        for ((i=0; i<prs_in_page; i++)) ; do
+            pr_number="$(echo "$remote_api_output" | jq -r ".[${i}].number")"
+            pr_title="$(echo "$remote_api_output" | jq -r ".[${i}].title")"
+            pr_url="$(number_to_url "github" "${pr_number}")"
+            milestone_stanza="$(echo "$remote_api_output" | jq -r ".[${i}].milestone")"
             vet_pr_milestone "$pr_number" "$pr_title" "$pr_url" "$milestone_stanza" "$milestone_title"
         done
         clear_line
     done
 }
 
+function vet_remotes {
+    if [ "$upstream_remote" ] ; then
+        verbose "Upstream remote is $upstream_remote"
+    else
+        error "Cannot auto-determine upstream remote"
+        "(Could not find any upstream remote in \"git remote -v\")"
+        false
+    fi
+    if [ "$fork_remote" ] ; then
+        verbose "Fork remote is $fork_remote"
+    else
+        error "Cannot auto-determine fork remote"
+        info "(Could not find GitHub user ${github_user}'s fork of ceph/ceph in \"git remote -v\")"
+        setup_ok=""
+    fi
+}
+
+function vet_setup {
+    local argument="$1"
+    local not_set="!!! NOT SET !!!"
+    local invalid="!!! INVALID !!!"
+    local redmine_endpoint_display
+    local redmine_user_id_display
+    local github_endpoint_display
+    local github_user_display
+    local upstream_remote_display
+    local fork_remote_display
+    local redmine_key_display
+    local github_token_display
+    debug "Entering vet_setup with argument $argument"
+    if [ "$argument" = "--report" ] || [ "$argument" = "--normal-operation" ] ; then
+        [ "$github_token" ] && [ "$setup_ok" ] && set_github_user_from_github_token
+        init_upstream_remote
+        [ "$github_token" ] && [ "$setup_ok" ] && init_fork_remote
+        [ "$redmine_key" ] && set_redmine_user_from_redmine_key
+    fi
+    if [ "$github_token" ] ; then
+        if [ "$github_token_ok" ] ; then
+            github_token_display="(OK; value not shown)"
+        else
+            github_token_display="$invalid"
+        fi
+    else
+        github_token_display="$not_set"
+    fi
+    if [ "$redmine_key" ] ; then
+        if [ "$redmine_key_ok" ] ; then
+            redmine_key_display="(OK; value not shown)"
+        else
+            redmine_key_display="$invalid"
+        fi
+    else
+        redmine_key_display="$not_set"
+    fi
+    redmine_endpoint_display="${redmine_endpoint:-$not_set}"
+    redmine_user_id_display="${redmine_user_id:-$not_set}"
+    github_endpoint_display="${github_endpoint:-$not_set}"
+    github_user_display="${github_user:-$not_set}"
+    upstream_remote_display="${upstream_remote:-$not_set}"
+    fork_remote_display="${fork_remote:-$not_set}"
+    test "$redmine_endpoint" || failed_mandatory_var_check redmine_endpoint "not set"
+    test "$redmine_user_id"  || failed_mandatory_var_check redmine_user_id "could not be determined"
+    test "$redmine_key"      || failed_mandatory_var_check redmine_key "not set"
+    test "$github_endpoint"  || failed_mandatory_var_check github_endpoint "not set"
+    test "$github_user"      || failed_mandatory_var_check github_user "could not be determined"
+    test "$github_token"     || failed_mandatory_var_check github_token "not set"
+    test "$upstream_remote"  || failed_mandatory_var_check upstream_remote "could not be determined"
+    test "$fork_remote"      || failed_mandatory_var_check fork_remote "could not be determined"
+    if [ "$argument" = "--report" ] || [ "$argument" == "--interactive" ] ; then
+        read -r -d '' setup_summary <<EOM || true > /dev/null 2>&1
+redmine_endpoint $redmine_endpoint
+redmine_user_id  $redmine_user_id_display
+redmine_key      $redmine_key_display
+github_endpoint  $github_endpoint
+github_user      $github_user_display
+github_token     $github_token_display
+upstream_remote  $upstream_remote_display
+fork_remote      $fork_remote_display
+EOM
+        log bare
+        log bare "============================================="
+        log bare "        ${this_script} setup report"
+        log bare "============================================="
+        log bare "variable name    value"
+        log bare "---------------------------------------------"
+        log bare "$setup_summary"
+        log bare "---------------------------------------------"
+    else
+        verbose "redmine_endpoint $redmine_endpoint_display"
+        verbose "redmine_user_id  $redmine_user_id_display"
+        verbose "redmine_key      $redmine_key_display"
+        verbose "github_endpoint  $github_endpoint_display"
+        verbose "github_user      $github_user_display"
+        verbose "github_token     $github_token_display"
+        verbose "upstream_remote  $upstream_remote_display"
+        verbose "fork_remote      $fork_remote_display"
+    fi
+    if [ "$argument" = "--report" ] || [ "$argument" = "--interactive" ] ; then
+        if [ "$setup_ok" ] ; then
+            info "setup is OK"
+        else
+            info "setup is NOT OK"
+        fi
+        log bare "=============================================="
+        log bare
+    fi
+}
+
 function warning {
     log warning "$@"
 }
@@ -808,8 +1292,7 @@ if git status >/dev/null 2>&1 ; then
     debug "In a local git clone. Good."
 else
     error "This script must be run from inside a local git clone"
-    info "$how_to_get_setup_advice"
-    false
+    abort_due_to_setup_problem
 fi
 
 
@@ -817,33 +1300,40 @@ fi
 # process command-line arguments
 #
 
-munged_options=$(getopt -o c:dhsv --long "cherry-pick-only,component:,debug,help,milestones,setup,setup-advice,troubleshooting-advice,update-version,usage-advice,verbose,version" -n "$this_script" -- "$@")
+munged_options=$(getopt -o c:dhsv --long "cherry-pick-only,component:,debug,existing-pr:,force,help,milestones,prepare,setup,setup-report,troubleshooting,update-version,usage,verbose,version" -n "$this_script" -- "$@")
 eval set -- "$munged_options"
 
 ADVICE=""
 CHECK_MILESTONES=""
 CHERRY_PICK_ONLY=""
+CHERRY_PICK_PHASE="yes"
 DEBUG=""
+EXISTING_PR=""
 EXPLICIT_COMPONENT=""
+FORCE=""
 HELP=""
+INTERACTIVE_SETUP_ROUTINE=""
 ISSUE=""
-SETUP_ADVICE=""
-SETUP_ONLY=""
+PR_PHASE="yes"
+SETUP_OPTION=""
+TRACKER_PHASE="yes"
 TROUBLESHOOTING_ADVICE=""
 USAGE_ADVICE=""
 VERBOSE=""
 while true ; do
     case "$1" in
-        --cherry-pick-only) CHERRY_PICK_ONLY="$1" ; shift ;;
+        --cherry-pick-only) CHERRY_PICK_PHASE="yes" ; PR_PHASE="" ; TRACKER_PHASE="" ; shift ;;
         --component|-c) shift ; EXPLICIT_COMPONENT="$1" ; shift ;;
         --debug|-d) DEBUG="$1" ; shift ;;
+        --existing-pr) shift ; EXISTING_PR="$1" ; CHERRY_PICK_PHASE="" ; PR_PHASE="" ; shift ;;
+        --force) FORCE="$1" ; shift ;;
         --help|-h) ADVICE="1" ; HELP="$1" ; shift ;;
         --milestones) CHECK_MILESTONES="$1" ; shift ;;
-        --setup|-s) SETUP_ONLY="$1" ; shift ;;
-        --setup-advice) ADVICE="1" ; SETUP_ADVICE="$1" ; shift ;;
-        --troubleshooting-advice) ADVICE="$1" ; TROUBLESHOOTING_ADVICE="$1" ; shift ;;
+        --prepare) CHERRY_PICK_PHASE="yes" ; PR_PHASE="" ; TRACKER_PHASE="" ; shift ;;
+        --setup*|-s) SETUP_OPTION="$1" ; shift ;;
+        --troubleshooting) ADVICE="$1" ; TROUBLESHOOTING_ADVICE="$1" ; shift ;;
         --update-version) update_version_number_and_exit ;;
-        --usage-advice) ADVICE="$1" ; USAGE_ADVICE="$1" ; shift ;;
+        --usage) ADVICE="$1" ; USAGE_ADVICE="$1" ; shift ;;
         --verbose|-v) VERBOSE="$1" ; shift ;;
         --version) display_version_message_and_exit ;;
         --) shift ; ISSUE="$1" ; break ;;
@@ -853,13 +1343,15 @@ done
 
 if [ "$ADVICE" ] ; then
     [ "$HELP" ] && usage
-    [ "$SETUP_ADVICE" ] && setup_advice
     [ "$USAGE_ADVICE" ] && usage_advice
     [ "$TROUBLESHOOTING_ADVICE" ] && troubleshooting_advice
     exit 0
 fi
 
-[ "$SETUP_ONLY" -o "$CHECK_MILESTONES" ] && ISSUE="0"
+if [ "$SETUP_OPTION" ] || [ "$CHECK_MILESTONES" ] ; then
+    ISSUE="0"
+fi
+
 if [[ $ISSUE =~ ^[0-9]+$ ]] ; then
     issue=$ISSUE
 else
@@ -880,42 +1372,61 @@ fi
 
 
 #
-# initialize mandatory variables and check values for sanity
+# make sure setup has been completed
 #
 
-BACKPORT_COMMON=$HOME/bin/backport_common.sh
-[ -f "$BACKPORT_COMMON" ] && source "$BACKPORT_COMMON"
-setup_ok="1"
 init_endpoints
-init_remotes
-setup_report
-if [ "$setup_ok" ] ; then
-    if [ "$SETUP_ONLY" ] ; then
-        log bare "Overall setup is OK"
+init_github_token
+init_redmine_key
+setup_ok="OK"
+if [ "$SETUP_OPTION" ] ; then
+    vet_setup --report
+    if [ "$setup_ok" ] ; then
         exit 0
-    elif [ "$VERBOSE" ] ; then
-        debug "Overall setup is OK"
+    else
+        default_val="y"
+        echo "Setup is NOT OK"
+        echo
+        if [ -e "$deprecated_backport_common" ] ; then
+            warning "$deprecated_backport_common file detected"
+            info "Please run the interactive setup routine now."
+            echo
+        fi
+        echo -n "Run the interactive setup routine now? (default: ${default_val}) "
+        yes_or_no_answer="$(get_user_input "$default_val")"
+        [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
+        if [ "$yes_or_no_answer" = "y" ] ; then
+            INTERACTIVE_SETUP_ROUTINE="yes"
+        fi
     fi
+fi
+if [ "$INTERACTIVE_SETUP_ROUTINE" ] ; then
+    interactive_setup_routine
 else
-    if [ "$SETUP_ONLY" ] ; then
-        log bare "Setup is NOT OK"
-        log bare "$how_to_get_setup_advice"
-        false
+    vet_setup --normal-operation
+fi
+if [ "$INTERACTIVE_SETUP_ROUTINE" ] || [ "$SETUP_OPTION" ] ; then
+    echo
+    if [ "$setup_ok" ] ; then
+        if [ "$ISSUE" ] ; then
+            true
+        else
+            exit 0
+        fi
     else
-        error "Problem detected in your setup"
-        info "Run the script with --setup for a full report"
-        info "$how_to_get_setup_advice"
-        false
+        exit 1
     fi
 fi
+vet_remotes
+[ "$setup_ok" ] || abort_due_to_setup_problem
 
 #
 # query remote GitHub API for active milestones
 #
 
 verbose "Querying GitHub API for active milestones"
-remote_api_output="$(curl --silent -X GET 'https://api.github.com/repos/ceph/ceph/milestones?access_token='$github_token)"
-active_milestones="$(echo $remote_api_output | jq -r '.[] | .title')"
+remote_api_output="$(curl --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones?access_token=$github_token")"
+active_milestones="$(echo "$remote_api_output" | jq -r '.[] | .title')"
 if [ "$active_milestones" = "null" ] ; then
     error "Could not determine the active milestones"
     bail_out_github_api "$remote_api_output"
@@ -930,11 +1441,11 @@ fi
 # query remote Redmine API for information about the Backport tracker issue
 #
 
-redmine_url="${redmine_endpoint}/issues/${issue}"
+redmine_url="$(number_to_url "redmine" "${issue}")"
 debug "Considering Redmine issue: $redmine_url - is it in the Backport tracker?"
 
-remote_api_output=$(curl --silent "${redmine_url}.json")
-tracker=$(echo $remote_api_output | jq -r '.issue.tracker.name')
+remote_api_output="$(curl --silent "${redmine_url}.json")"
+tracker="$(echo "$remote_api_output" | jq -r '.issue.tracker.name')"
 if [ "$tracker" = "Backport" ]; then
     debug "Yes, $redmine_url is a Backport issue"
 else
@@ -944,7 +1455,7 @@ else
 fi
 
 debug "Looking up release/milestone of $redmine_url"
-milestone=$(echo $remote_api_output | jq -r '.issue.custom_fields[0].value')
+milestone="$(echo "$remote_api_output" | jq -r '.issue.custom_fields[0].value')"
 if [ "$milestone" ] ; then
     debug "Release/milestone: $milestone"
 else
@@ -953,28 +1464,43 @@ else
 fi
 
 debug "Looking up status of $redmine_url"
-tracker_status=$(echo $remote_api_output | jq -r '.issue.status.name')
-if [ "$tracker_status" ] ; then
-    debug "Tracker status: $tracker_status"
-    test "$(check_tracker_status "$tracker_status")"
+tracker_status_id="$(echo "$remote_api_output" | jq -r '.issue.status.id')"
+tracker_status_name="$(echo "$remote_api_output" | jq -r '.issue.status.name')"
+if [ "$tracker_status_name" ] ; then
+    debug "Tracker status: $tracker_status_name"
+    if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
+        test "$(check_tracker_status "$tracker_status_name")" || true
+    else
+        test "$(check_tracker_status "$tracker_status_name")"
+    fi
 else
     error "could not obtain status from ${redmine_url}"
     false
 fi
 
-tracker_title=$(echo $remote_api_output | jq -r '.issue.subject')
+tracker_title="$(echo "$remote_api_output" | jq -r '.issue.subject')"
 debug "Title of $redmine_url is ->$tracker_title<-"
 
-tracker_assignee_id=$(echo $remote_api_output | jq -r '.issue.assigned_to.id')
-tracker_assignee_name=$(echo $remote_api_output | jq -r '.issue.assigned_to.name')
-debug "$redmine_url is assigned to $tracker_assignee_name (ID $tracker_assignee_id)"
+tracker_description="$(echo "$remote_api_output" | jq -r '.issue.description')"
+debug "Description of $redmine_url is ->$tracker_description<-"
 
-if [ "$tracker_assignee_id" = "null" -o "$tracker_assignee_id" = "$redmine_user_id" ] ; then
+tracker_assignee_id="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.id')"
+tracker_assignee_name="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.name')"
+if [ "$tracker_assignee_id" = "null" ] || [ "$tracker_assignee_id" = "$redmine_user_id" ] ; then
     true
 else
-    error "$redmine_url is assigned to $tracker_assignee_name (ID $tracker_assignee_id)"
-    info "Cowardly refusing to work on an issue that is assigned to someone else"
-    false
+    error_msg_1="$redmine_url is assigned to someone else: $tracker_assignee_name (ID $tracker_assignee_id)"
+    error_msg_2="(my ID is $redmine_user_id)"
+    if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
+        warning "$error_msg_1"
+        info "$error_msg_2"
+        info "--force and/or --existing-pr given: continuing execution"
+    else
+        error "$error_msg_1"
+        info "$error_msg_2"
+        info "Cowardly refusing to continue"
+        false
+    fi
 fi
 
 if [ -z "$(is_active_milestone "$milestone")" ] ; then
@@ -989,116 +1515,163 @@ if [ "$milestone_number" -gt "0" ] >/dev/null 2>&1 ; then
 else
     milestone_number=$(milestone_number_from_remote_api "$milestone")
 fi
-info "Milestone/release is $milestone"
-debug "Milestone number is $milestone_number"
-
-
-#
-# cherry-pick phase
-#
-
-local_branch=wip-${issue}-${target_branch}
-if git show-ref --verify --quiet refs/heads/$local_branch ; then
-    if [ "$CHERRY_PICK_ONLY" ] ; then
-        error "local branch $local_branch already exists -- cannot -prepare"
-        false
+info "milestone/release is $milestone"
+debug "milestone number is $milestone_number"
+
+
+if [ "$CHERRY_PICK_PHASE" ] ; then
+    local_branch=wip-${issue}-${target_branch}
+    if git show-ref --verify --quiet "refs/heads/$local_branch" ; then
+        if [ "$FORCE" ] ; then
+            warning "local branch $local_branch already exists"
+            info "--force was given: will clobber $local_branch and attempt automated cherry-pick"
+            cherry_pick_phase
+        elif [ "$CHERRY_PICK_ONLY" ] ; then
+            error "local branch $local_branch already exists"
+            info "Cowardly refusing to clobber $local_branch as it might contain valuable data"
+            info "(hint) run with --force to clobber it and attempt the cherry-pick"
+            false
+        fi
+        if [ "$FORCE" ] || [ "$CHERRY_PICK_ONLY" ] ; then
+            true
+        else
+            info "local branch $local_branch already exists: skipping cherry-pick phase"
+        fi
+    else
+        info "$local_branch does not exist: will create it and attempt automated cherry-pick"
+        cherry_pick_phase
     fi
-    info "local branch $local_branch already exists: skipping cherry-pick phase"
-else
-    info "$local_branch does not exist: will create it and attempt automated cherry-pick"
-    cherry_pick_phase
-    [ "$CHERRY_PICK_ONLY" ] && exit 0
-fi
-
-
-#
-# PR phase
-#
-
-current_branch=$(git rev-parse --abbrev-ref HEAD)
-[ "$current_branch" = "$local_branch" ] || git checkout $local_branch
-
-debug "Pushing local branch $local_branch to remote $fork_remote"
-git push -u $fork_remote $local_branch
-
-original_issue=""
-original_pr=""
-original_pr_url=""
-
-debug "Generating backport PR description"
-populate_original_issue
-populate_original_pr
-desc="backport tracker: ${redmine_url}"
-if [ "$original_pr" -o "$original_issue" ] ; then
-    desc="${desc}\n\n---\n"
-    [ "$original_pr"    ] && desc="${desc}\nbackport of ${github_endpoint}/pull/${original_pr}"
-    [ "$original_issue" ] && desc="${desc}\nparent tracker: ${redmine_endpoint}/issues/${original_issue}"
 fi
-desc="${desc}\n\nthis backport was staged using ceph-backport.sh version ${SCRIPT_VERSION}\nfind the latest version at ${github_endpoint}/blob/master/src/script/ceph-backport.sh"
 
-debug "Generating backport PR title"
-if [ "$original_pr" ] ; then
-    title="${milestone}: $(curl --silent https://api.github.com/repos/ceph/ceph/pulls/${original_pr} | jq -r '.title')"
-else
-    if [[ $tracker_title =~ ^${milestone}: ]] ; then
-        title="${tracker_title}"
+if [ "$PR_PHASE" ] ; then
+    current_branch=$(git rev-parse --abbrev-ref HEAD)
+    if [ "$current_branch" = "$local_branch" ] ; then
+        true
     else
-        title="${milestone}: ${tracker_title}"
+        set -x
+        git checkout "$local_branch"
+        set +x
     fi
-fi
-if [[ $title =~ \" ]] ; then
-    title=$(echo $title | sed -e 's/"/\\"/g')
+    
+    set -x
+    git push -u "$fork_remote" "$local_branch"
+    set +x
+    
+    original_issue=""
+    original_pr=""
+    original_pr_url=""
+    
+    debug "Generating backport PR description"
+    populate_original_issue
+    populate_original_pr
+    desc="backport tracker: ${redmine_url}"
+    if [ "$original_pr" ] || [ "$original_issue" ] ; then
+        desc="${desc}\n\n---\n"
+        [ "$original_pr"    ] && desc="${desc}\nbackport of $(number_to_url "github" "${original_pr}")"
+        [ "$original_issue" ] && desc="${desc}\nparent tracker: $(number_to_url "redmine" "${original_issue}")"
+    fi
+    desc="${desc}\n\nthis backport was staged using ceph-backport.sh version ${SCRIPT_VERSION}\nfind the latest version at ${github_endpoint}/blob/master/src/script/ceph-backport.sh"
+    
+    debug "Generating backport PR title"
+    if [ "$original_pr" ] ; then
+        title="${milestone}: $(curl --silent https://api.github.com/repos/ceph/ceph/pulls/${original_pr} | jq -r '.title')"
+    else
+        if [[ $tracker_title =~ ^${milestone}: ]] ; then
+            title="${tracker_title}"
+        else
+            title="${milestone}: ${tracker_title}"
+        fi
+    fi
+    if [[ "$title" =~ \" ]] ; then
+        #title="$(echo "$title" | sed -e 's/"/\\"/g')"
+        title="${title//\"/\\\"}"
+    fi
+    
+    debug "Opening backport PR"
+    remote_api_output=$(curl --silent --data-binary "{\"title\":\"${title}\",\"head\":\"${github_user}:${local_branch}\",\"base\":\"${target_branch}\",\"body\":\"${desc}\"}" "https://api.github.com/repos/ceph/ceph/pulls?access_token=${github_token}")
+    backport_pr_number=$(echo "$remote_api_output" | jq -r .number)
+    if [ -z "$backport_pr_number" ] || [ "$backport_pr_number" = "null" ] ; then
+        error "failed to open backport PR"
+        bail_out_github_api "$remote_api_output"
+    fi
+    backport_pr_url="$(number_to_url "github" "$backport_pr_number")"
+    info "Opened backport PR ${backport_pr_url}"
 fi
 
-debug "Opening backport PR"
-remote_api_output=$(curl --silent --data-binary '{"title":"'"$title"'","head":"'$github_user':'$local_branch'","base":"'$target_branch'","body":"'"${desc}"'"}' 'https://api.github.com/repos/ceph/ceph/pulls?access_token='$github_token)
-backport_pr_number=$(echo "$remote_api_output" | jq -r .number)
-if [ -z "$backport_pr_number" -o "$backport_pr_number" = "null" ] ; then
-    bail_out_github_api "$remote_api_output"
+if [ "$EXISTING_PR" ] ; then
+    populate_original_issue
+    populate_original_pr
+    backport_pr_number="$EXISTING_PR"
+    backport_pr_url="$(number_to_url "github" "$backport_pr_number")"
+    existing_pr_routine
 fi
-backport_pr_url="${github_endpoint}/pull/$backport_pr_number"
-info "Opened backport PR ${backport_pr_url}"
 
-if [ "$EXPLICIT_COMPONENT" ] ; then
-    debug "Component given on command line: using it"
-    component="$EXPLICIT_COMPONENT"
-else
-    debug "Attempting to guess component"
-    component=$(guess_component "$title")
+if [ "$PR_PHASE" ] || [ "$EXISTING_PR" ] ; then
+    maybe_update_pr_milestone_labels "${backport_pr_number}"
+    pgrep firefox >/dev/null && firefox "${backport_pr_url}"
 fi
-if [ "$component" ] ; then
-    debug "Attempting to set ${component} label and ${milestone} milestone in ${backport_pr_url}"
-    data_binary='{"milestone":'$milestone_number',"labels":["'$component'"]}'
-else
-    debug "Attempting to set ${milestone} milestone in ${backport_pr_url}"
-    data_binary='{"milestone":'$milestone_number'}'
-fi
-blindly_set_pr_metadata "$backport_pr_number" "$data_binary"
-
-pgrep firefox >/dev/null && firefox ${backport_pr_url}
-
-debug "Updating backport tracker issue ${redmine_url}"
-redmine_status=2 # In Progress
-remote_api_status_code=$(curl --write-out %{http_code} --output /dev/null --silent -X PUT --header 'Content-type: application/json' --data-binary '{"issue":{"description":"https://github.com/ceph/ceph/pull/'$backport_pr_number'","status_id":'$redmine_status',"assigned_to_id":'$redmine_user_id',"notes":"Updated automatically by ceph-backport.sh version '$SCRIPT_VERSION'"}}' ${redmine_url}'.json?key='$redmine_key)
-if [ "${remote_api_status_code:0:1}" = "2" ] ; then
-    info "${redmine_url} updated"
-elif [ "${remote_api_status_code:0:1}" = "4" ] ; then
-    error "Remote API ${redmine_endpoint} returned status ${remote_api_status_code}"
-    info "This indicates an authentication/authorization problem: is your API access key valid?"
-else
-    error "Remote API ${redmine_endpoint} returned unexpected response code ${remote_api_status_code}"
-fi
-# check if anything actually changed on the Redmine issue
-redmine_result_ok=""
-remote_api_output=$(curl --silent "${redmine_url}.json")
-tracker_description=$(echo $remote_api_output | jq -r '.issue.description')
-if [[ "$tracker_description" =~ "$backport_pr_number" ]] ; then
-    debug "Backport tracker description is set to ->${tracker_description}<-"
-    true  # success
-else
-    info "Failed to automatically update ${redmine_url}."
-    info "Please add a comment to ${redmine_url} to let others know that you"
-    info "are working on the backport. In your comment, consider mentioning the"
-    info "${backport_pr_url} (the URL of the backport PR that was just opened)."
+
+if [ "$TRACKER_PHASE" ] ; then
+    debug "Considering Backport tracker issue ${redmine_url}"
+    status_should_be=2 # In Progress
+    desc_should_be="${backport_pr_url}"
+    assignee_should_be="${redmine_user_id}"
+    if [ "$EXISTING_PR" ] ; then
+        data_binary="{\"issue\":{\"description\":\"${desc_should_be}\",\"status_id\":${status_should_be}}}"
+    else
+        data_binary="{\"issue\":{\"description\":\"${desc_should_be}\",\"status_id\":${status_should_be},\"assigned_to_id\":${assignee_should_be}}}"
+    fi
+    remote_api_status_code="$(curl --write-out '%{http_code}' --output /dev/null --silent -X PUT --header "Content-type: application/json" --data-binary "${data_binary}" "${redmine_url}.json?key=$redmine_key")"
+    if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then 
+        true
+    else
+        if [ "${remote_api_status_code:0:1}" = "2" ] ; then
+            true
+        elif [ "${remote_api_status_code:0:1}" = "4" ] ; then
+            warning "remote API ${redmine_endpoint} returned status ${remote_api_status_code}"
+            info "This merely indicates that you cannot modify issue fields at ${redmine_endpoint}"
+            info "and does not limit your ability to do backports."
+        else
+            error "Remote API ${redmine_endpoint} returned unexpected response code ${remote_api_status_code}"
+        fi
+    fi
+    # check if anything actually changed on the Redmine issue
+    remote_api_output=$(curl --silent "${redmine_url}.json?include=journals")
+    status_is="$(echo "$remote_api_output" | jq -r '.issue.status.id')"
+    desc_is="$(echo "$remote_api_output" | jq -r '.issue.description')"
+    assignee_is="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.id')"
+    tracker_was_updated=""
+    tracker_is_in_desired_state="yes"
+    [ "$(tracker_component_was_updated "status" "$tracker_status_id" "$status_is")" ] && tracker_was_updated="yes"
+    [ "$(tracker_component_was_updated "desc" "$tracker_description" "$desc_is")" ] && tracker_was_updated="yes"
+    if [ "$EXISTING_PR" ] ; then
+         true
+    else
+         [ "$(tracker_component_was_updated "assignee" "$tracker_assignee_id" "$assignee_is")" ] && tracker_was_updated="yes"
+    fi
+    [ "$(tracker_component_is_in_desired_state "status" "$status_is" "$status_should_be")" ] || tracker_is_in_desired_state=""
+    [ "$(tracker_component_is_in_desired_state "desc" "$desc_is" "$desc_should_be")" ] || tracker_is_in_desired_state=""
+    if [ "$EXISTING_PR" ] ; then
+        true
+    else
+        [ "$(tracker_component_is_in_desired_state "assignee" "$assignee_is" "$assignee_should_be")" ] || tracker_is_in_desired_state=""
+    fi
+    if [ "$tracker_is_in_desired_state" ] ; then
+        [ "$tracker_was_updated" ] && info "Backport tracker ${redmine_url} was updated"
+        info "Backport tracker ${redmine_url} is in the desired state"
+        pgrep firefox >/dev/null && firefox "${redmine_url}"
+        exit 0
+    fi
+    if [ "$tracker_was_updated" ] ; then
+        warning "backport tracker ${redmine_url} was updated, but is not in the desired state. Please check it."
+        pgrep firefox >/dev/null && firefox "${redmine_url}"
+        exit 1
+    else
+        data_binary="{\"issue\":{\"notes\":\"please link this Backport tracker issue with GitHub PR ${desc_should_be}\nceph-backport.sh version ${SCRIPT_VERSION}\"}}"
+        remote_api_status_code=$(curl --write-out '%{http_code}' --output /dev/null --silent -X PUT --header "Content-type: application/json" --data-binary "${data_binary}" "${redmine_url}.json?key=$redmine_key")
+        if [ "${remote_api_status_code:0:1}" = "2" ] ; then
+            info "Comment added to ${redmine_url}"
+        fi
+        exit 0
+    fi
 fi
-pgrep firefox >/dev/null && firefox ${redmine_url}