cherrypick.sh 7.7 KB

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