1#!/usr/bin/env bash 2 3# Based on github.com/kubernetes/kubernetes/blob/v1.8.2/hack/cherry_pick_pull.sh 4 5# Checkout a PR from GitHub. (Yes, this is sitting in a Git tree. How 6# meta.) Assumes you care about pulls from remote "upstream" and 7# checks thems out to a branch named: 8# automated-cherry-pick-of-<pr>-<target branch>-<timestamp> 9 10set -o errexit 11set -o nounset 12set -o pipefail 13 14declare -r ETCD_ROOT="$(dirname "${BASH_SOURCE}")/../.." 15cd "${ETCD_ROOT}" 16 17declare -r STARTINGBRANCH=$(git symbolic-ref --short HEAD) 18declare -r REBASEMAGIC="${ETCD_ROOT}/.git/rebase-apply" 19DRY_RUN=${DRY_RUN:-""} 20REGENERATE_DOCS=${REGENERATE_DOCS:-""} 21UPSTREAM_REMOTE=${UPSTREAM_REMOTE:-upstream} 22FORK_REMOTE=${FORK_REMOTE:-origin} 23 24if [[ -z ${GITHUB_USER:-} ]]; then 25 echo "Please export GITHUB_USER=<your-user> (or GH organization, if that's where your fork lives)" 26 exit 1 27fi 28 29if ! which hub > /dev/null; then 30 echo "Can't find 'hub' tool in PATH, please install from https://github.com/github/hub" 31 exit 1 32fi 33 34if [[ "$#" -lt 2 ]]; then 35 echo "${0} <remote branch> <pr-number>...: cherry pick one or more <pr> onto <remote branch> and leave instructions for proposing pull request" 36 echo 37 echo " Checks out <remote branch> and handles the cherry-pick of <pr> (possibly multiple) for you." 38 echo " Examples:" 39 echo " $0 upstream/release-3.14 12345 # Cherry-picks PR 12345 onto upstream/release-3.14 and proposes that as a PR." 40 echo " $0 upstream/release-3.14 12345 56789 # Cherry-picks PR 12345, then 56789 and proposes the combination as a single PR." 41 echo 42 echo " Set the DRY_RUN environment var to skip git push and creating PR." 43 echo " This is useful for creating patches to a release branch without making a PR." 44 echo " When DRY_RUN is set the script will leave you in a branch containing the commits you cherry-picked." 45 echo 46 echo " Set the REGENERATE_DOCS environment var to regenerate documentation for the target branch after picking the specified commits." 47 echo " This is useful when picking commits containing changes to API documentation." 48 echo 49 echo " Set UPSTREAM_REMOTE (default: upstream) and FORK_REMOTE (default: origin)" 50 echo " To override the default remote names to what you have locally." 51 exit 2 52fi 53 54if git_status=$(git status --porcelain --untracked=no 2>/dev/null) && [[ -n "${git_status}" ]]; then 55 echo "!!! Dirty tree. Clean up and try again." 56 exit 1 57fi 58 59if [[ -e "${REBASEMAGIC}" ]]; then 60 echo "!!! 'git rebase' or 'git am' in progress. Clean up and try again." 61 exit 1 62fi 63 64declare -r BRANCH="$1" 65shift 1 66declare -r PULLS=( "$@" ) 67 68function join { local IFS="$1"; shift; echo "$*"; } 69declare -r PULLDASH=$(join - "${PULLS[@]/#/#}") # Generates something like "#12345-#56789" 70declare -r PULLSUBJ=$(join " " "${PULLS[@]/#/#}") # Generates something like "#12345 #56789" 71 72echo "+++ Updating remotes..." 73git remote update "${UPSTREAM_REMOTE}" "${FORK_REMOTE}" 74 75if ! git log -n1 --format=%H "${BRANCH}" >/dev/null 2>&1; then 76 echo "!!! '${BRANCH}' not found. The second argument should be something like ${UPSTREAM_REMOTE}/release-0.21." 77 echo " (In particular, it needs to be a valid, existing remote branch that I can 'git checkout'.)" 78 exit 1 79fi 80 81declare -r NEWBRANCHREQ="automated-cherry-pick-of-${PULLDASH}" # "Required" portion for tools. 82declare -r NEWBRANCH="$(echo "${NEWBRANCHREQ}-${BRANCH}" | sed 's/\//-/g')" 83declare -r NEWBRANCHUNIQ="${NEWBRANCH}-$(date +%s)" 84echo "+++ Creating local branch ${NEWBRANCHUNIQ}" 85 86cleanbranch="" 87prtext="" 88gitamcleanup=false 89function return_to_kansas { 90 if [[ "${gitamcleanup}" == "true" ]]; then 91 echo 92 echo "+++ Aborting in-progress git am." 93 git am --abort >/dev/null 2>&1 || true 94 fi 95 96 # return to the starting branch and delete the PR text file 97 if [[ -z "${DRY_RUN}" ]]; then 98 echo 99 echo "+++ Returning you to the ${STARTINGBRANCH} branch and cleaning up." 100 git checkout -f "${STARTINGBRANCH}" >/dev/null 2>&1 || true 101 if [[ -n "${cleanbranch}" ]]; then 102 git branch -D "${cleanbranch}" >/dev/null 2>&1 || true 103 fi 104 if [[ -n "${prtext}" ]]; then 105 rm "${prtext}" 106 fi 107 fi 108} 109trap return_to_kansas EXIT 110 111SUBJECTS=() 112function make-a-pr() { 113 local rel="$(basename "${BRANCH}")" 114 echo 115 echo "+++ Creating a pull request on GitHub at ${GITHUB_USER}:${NEWBRANCH}" 116 117 # This looks like an unnecessary use of a tmpfile, but it avoids 118 # https://github.com/github/hub/issues/976 Otherwise stdin is stolen 119 # when we shove the heredoc at hub directly, tickling the ioctl 120 # crash. 121 prtext="$(mktemp -t prtext.XXXX)" # cleaned in return_to_kansas 122 cat >"${prtext}" <<EOF 123Automated cherry pick of ${PULLSUBJ} 124 125Cherry pick of ${PULLSUBJ} on ${rel}. 126 127$(printf '%s\n' "${SUBJECTS[@]}") 128EOF 129 130 hub pull-request -F "${prtext}" -h "${GITHUB_USER}:${NEWBRANCH}" -b "coreos:${rel}" 131} 132 133git checkout -b "${NEWBRANCHUNIQ}" "${BRANCH}" 134cleanbranch="${NEWBRANCHUNIQ}" 135 136gitamcleanup=true 137for pull in "${PULLS[@]}"; do 138 echo "+++ Downloading patch to /tmp/${pull}.patch (in case you need to do this again)" 139 curl -o "/tmp/${pull}.patch" -sSL "http://github.com/coreos/etcd/pull/${pull}.patch" 140 echo 141 echo "+++ About to attempt cherry pick of PR. To reattempt:" 142 echo " $ git am -3 /tmp/${pull}.patch" 143 echo 144 git am -3 "/tmp/${pull}.patch" || { 145 conflicts=false 146 while unmerged=$(git status --porcelain | grep ^U) && [[ -n ${unmerged} ]] \ 147 || [[ -e "${REBASEMAGIC}" ]]; do 148 conflicts=true # <-- We should have detected conflicts once 149 echo 150 echo "+++ Conflicts detected:" 151 echo 152 (git status --porcelain | grep ^U) || echo "!!! None. Did you git am --continue?" 153 echo 154 echo "+++ Please resolve the conflicts in another window (and remember to 'git add / git am --continue')" 155 read -p "+++ Proceed (anything but 'y' aborts the cherry-pick)? [y/n] " -r 156 echo 157 if ! [[ "${REPLY}" =~ ^[yY]$ ]]; then 158 echo "Aborting." >&2 159 exit 1 160 fi 161 done 162 163 if [[ "${conflicts}" != "true" ]]; then 164 echo "!!! git am failed, likely because of an in-progress 'git am' or 'git rebase'" 165 exit 1 166 fi 167 } 168 169 # set the subject 170 subject=$(grep -m 1 "^Subject" "/tmp/${pull}.patch" | sed -e 's/Subject: \[PATCH//g' | sed 's/.*] //') 171 SUBJECTS+=("#${pull}: ${subject}") 172 173 # remove the patch file from /tmp 174 rm -f "/tmp/${pull}.patch" 175done 176gitamcleanup=false 177 178# Re-generate docs (if needed) 179if [[ -n "${REGENERATE_DOCS}" ]]; then 180 echo 181 echo "Regenerating docs..." 182 if ! hack/generate-docs.sh; then 183 echo 184 echo "hack/generate-docs.sh FAILED to complete." 185 exit 1 186 fi 187fi 188 189if [[ -n "${DRY_RUN}" ]]; then 190 echo "!!! Skipping git push and PR creation because you set DRY_RUN." 191 echo "To return to the branch you were in when you invoked this script:" 192 echo 193 echo " git checkout ${STARTINGBRANCH}" 194 echo 195 echo "To delete this branch:" 196 echo 197 echo " git branch -D ${NEWBRANCHUNIQ}" 198 exit 0 199fi 200 201if git remote -v | grep ^${FORK_REMOTE} | grep etcd/etcd.git; then 202 echo "!!! You have ${FORK_REMOTE} configured as your etcd/etcd.git" 203 echo "This isn't normal. Leaving you with push instructions:" 204 echo 205 echo "+++ First manually push the branch this script created:" 206 echo 207 echo " git push REMOTE ${NEWBRANCHUNIQ}:${NEWBRANCH}" 208 echo 209 echo "where REMOTE is your personal fork (maybe ${UPSTREAM_REMOTE}? Consider swapping those.)." 210 echo "OR consider setting UPSTREAM_REMOTE and FORK_REMOTE to different values." 211 echo 212 make-a-pr 213 cleanbranch="" 214 exit 0 215fi 216 217echo 218echo "+++ I'm about to do the following to push to GitHub (and I'm assuming ${FORK_REMOTE} is your personal fork):" 219echo 220echo " git push ${FORK_REMOTE} ${NEWBRANCHUNIQ}:${NEWBRANCH}" 221echo 222read -p "+++ Proceed (anything but 'y' aborts the cherry-pick)? [y/n] " -r 223if ! [[ "${REPLY}" =~ ^[yY]$ ]]; then 224 echo "Aborting." >&2 225 exit 1 226fi 227 228git push "${FORK_REMOTE}" -f "${NEWBRANCHUNIQ}:${NEWBRANCH}" 229make-a-pr 230