# 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
# 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"
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\""
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
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
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
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
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
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
}
function eol {
- log mtt=$1
+ local mtt="$1"
error "$mtt is EOL"
false
}
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=""
}
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}"
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 {
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
err*)
prefix="${prefix}ERROR: "
;;
+ hex)
+ in_hex="yes"
+ ;;
info)
:
;;
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}"
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 {
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
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:
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}"
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 "$@"
}
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
# 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 ;;
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
#
-# 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"
# 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
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
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
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}