1+ #! /usr/bin/env bash
2+
3+ # Copyright 2015 The Kubernetes Authors.
4+ #
5+ # Licensed under the Apache License, Version 2.0 (the "License");
6+ # you may not use this file except in compliance with the License.
7+ # You may obtain a copy of the License at
8+ #
9+ # http://www.apache.org/licenses/LICENSE-2.0
10+ #
11+ # Unless required by applicable law or agreed to in writing, software
12+ # distributed under the License is distributed on an "AS IS" BASIS,
13+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ # See the License for the specific language governing permissions and
15+ # limitations under the License.
16+
17+ # Usage Instructions: https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md
18+
19+ # Checkout a PR from GitHub. (Yes, this is sitting in a Git tree. How
20+ # meta.) Assumes you care about pulls from remote "upstream" and
21+ # checks them out to a branch named:
22+ # automated-cherry-pick-of-<pr>-<target branch>-<timestamp>
23+
24+ set -o errexit
25+ set -o nounset
26+ set -o pipefail
27+
28+ REPO_ROOT=" $( git rev-parse --show-toplevel) "
29+ declare -r REPO_ROOT
30+ cd " ${REPO_ROOT} "
31+
32+ STARTINGBRANCH=$( git symbolic-ref --short HEAD)
33+ declare -r STARTINGBRANCH
34+ declare -r REBASEMAGIC=" ${REPO_ROOT} /.git/rebase-apply"
35+ DRY_RUN=${DRY_RUN:- " " }
36+ REGENERATE_DOCS=${REGENERATE_DOCS:- " " }
37+ UPSTREAM_REMOTE=${UPSTREAM_REMOTE:- upstream}
38+ FORK_REMOTE=${FORK_REMOTE:- origin}
39+ MAIN_REPO_ORG=${MAIN_REPO_ORG:- $(git remote get-url " $UPSTREAM_REMOTE " | awk ' {gsub(/http[s]:\/\/|git@/,"")}1' | awk -F' [@:./]' ' NR==1{print $3}' )}
40+ MAIN_REPO_NAME=${MAIN_REPO_NAME:- $(git remote get-url " $UPSTREAM_REMOTE " | awk ' {gsub(/http[s]:\/\/|git@/,"")}1' | awk -F' [@:./]' ' NR==1{print $4}' )}
41+
42+ if [[ -z ${GITHUB_USER:- } ]]; then
43+ echo " Please export GITHUB_USER=<your-user> (or GH organization, if that's where your fork lives)"
44+ exit 1
45+ fi
46+
47+ if ! command -v gh > /dev/null; then
48+ echo " Can't find 'gh' tool in PATH, please install from https://github.com/cli/cli"
49+ exit 1
50+ fi
51+
52+ if [[ " $# " -lt 2 ]]; then
53+ echo " ${0} <remote branch> <pr-number>...: cherry pick one or more <pr> onto <remote branch> and leave instructions for proposing pull request"
54+ echo
55+ echo " Checks out <remote branch> and handles the cherry-pick of <pr> (possibly multiple) for you."
56+ echo " Examples:"
57+ echo " $0 upstream/release-3.14 12345 # Cherry-picks PR 12345 onto upstream/release-3.14 and proposes that as a PR."
58+ echo " $0 upstream/release-3.14 12345 56789 # Cherry-picks PR 12345, then 56789 and proposes the combination as a single PR."
59+ echo
60+ echo " Set the DRY_RUN environment var to skip git push and creating PR."
61+ echo " This is useful for creating patches to a release branch without making a PR."
62+ echo " When DRY_RUN is set the script will leave you in a branch containing the commits you cherry-picked."
63+ echo
64+ echo " Set the REGENERATE_DOCS environment var to regenerate documentation for the target branch after picking the specified commits."
65+ echo " This is useful when picking commits containing changes to API documentation."
66+ echo
67+ echo " Set UPSTREAM_REMOTE (default: upstream) and FORK_REMOTE (default: origin)"
68+ echo " to override the default remote names to what you have locally."
69+ echo
70+ echo " For merge process info, see https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md"
71+ exit 2
72+ fi
73+
74+ # Checks if you are logged in. Will error/bail if you are not.
75+ gh auth status
76+
77+ if git_status=$( git status --porcelain --untracked=no 2> /dev/null) && [[ -n " ${git_status} " ]]; then
78+ echo " !!! Dirty tree. Clean up and try again."
79+ exit 1
80+ fi
81+
82+ if [[ -e " ${REBASEMAGIC} " ]]; then
83+ echo " !!! 'git rebase' or 'git am' in progress. Clean up and try again."
84+ exit 1
85+ fi
86+
87+ declare -r BRANCH=" $1 "
88+ shift 1
89+ declare -r PULLS=( " $@ " )
90+
91+ function join { local IFS=" $1 " ; shift ; echo " $* " ; }
92+ PULLDASH=$( join - " ${PULLS[@]/#/# } " ) # Generates something like "#12345-#56789"
93+ declare -r PULLDASH
94+ PULLSUBJ=$( join " " " ${PULLS[@]/#/# } " ) # Generates something like "#12345 #56789"
95+ declare -r PULLSUBJ
96+
97+ echo " +++ Updating remotes..."
98+ git remote update " ${UPSTREAM_REMOTE} " " ${FORK_REMOTE} "
99+
100+ if ! git log -n1 --format=%H " ${BRANCH} " > /dev/null 2>&1 ; then
101+ echo " !!! '${BRANCH} ' not found. The second argument should be something like ${UPSTREAM_REMOTE} /release-0.21."
102+ echo " (In particular, it needs to be a valid, existing remote branch that I can 'git checkout'.)"
103+ exit 1
104+ fi
105+
106+ NEWBRANCHREQ=" automated-cherry-pick-of-${PULLDASH} " # "Required" portion for tools.
107+ declare -r NEWBRANCHREQ
108+ NEWBRANCH=" $( echo " ${NEWBRANCHREQ} -${BRANCH} " | sed ' s/\//-/g' ) "
109+ declare -r NEWBRANCH
110+ NEWBRANCHUNIQ=" ${NEWBRANCH} -$( date +%s) "
111+ declare -r NEWBRANCHUNIQ
112+ echo " +++ Creating local branch ${NEWBRANCHUNIQ} "
113+
114+ cleanbranch=" "
115+ gitamcleanup=false
116+ function return_to_kansas {
117+ if [[ " ${gitamcleanup} " == " true" ]]; then
118+ echo
119+ echo " +++ Aborting in-progress git am."
120+ git am --abort > /dev/null 2>&1 || true
121+ fi
122+
123+ # return to the starting branch and delete the PR text file
124+ if [[ -z " ${DRY_RUN} " ]]; then
125+ echo
126+ echo " +++ Returning you to the ${STARTINGBRANCH} branch and cleaning up."
127+ git checkout -f " ${STARTINGBRANCH} " > /dev/null 2>&1 || true
128+ if [[ -n " ${cleanbranch} " ]]; then
129+ git branch -D " ${cleanbranch} " > /dev/null 2>&1 || true
130+ fi
131+ fi
132+ }
133+ trap return_to_kansas EXIT
134+
135+ SUBJECTS=()
136+ RELEASE_NOTES=()
137+ function make-a-pr() {
138+ local rel
139+ rel=" $( basename " ${BRANCH} " ) "
140+ echo
141+ echo " +++ Creating a pull request on GitHub at ${GITHUB_USER} :${NEWBRANCH} "
142+
143+ local numandtitle
144+ numandtitle=$( printf ' %s\n' " ${SUBJECTS[@]} " )
145+ prtext=$( cat << EOF
146+ Cherry pick of ${PULLSUBJ} on ${rel} .
147+
148+ ${numandtitle}
149+
150+ For details on the cherry pick process, see the [cherry pick requests](https://git.k8s.io/community/contributors/devel/sig-release/cherry-picks.md) page.
151+
152+ \`\`\` release-note
153+ $( printf ' %s\n' " ${RELEASE_NOTES[@]} " )
154+ \`\`\`
155+ EOF
156+ )
157+
158+ gh pr create --title=" Automated cherry pick of ${numandtitle} " --body=" ${prtext} " --head " ${GITHUB_USER} :${NEWBRANCH} " --base " ${rel} " --repo=" ${MAIN_REPO_ORG} /${MAIN_REPO_NAME} "
159+ }
160+
161+ git checkout -b " ${NEWBRANCHUNIQ} " " ${BRANCH} "
162+ cleanbranch=" ${NEWBRANCHUNIQ} "
163+
164+ gitamcleanup=true
165+ for pull in " ${PULLS[@]} " ; do
166+ echo " +++ Downloading patch to /tmp/${pull} .patch (in case you need to do this again)"
167+
168+ curl -o " /tmp/${pull} .patch" -sSL " https://github.com/${MAIN_REPO_ORG} /${MAIN_REPO_NAME} /pull/${pull} .patch"
169+ echo
170+ echo " +++ About to attempt cherry pick of PR. To reattempt:"
171+ echo " $ git am -3 /tmp/${pull} .patch"
172+ echo
173+ git am -3 " /tmp/${pull} .patch" || {
174+ conflicts=false
175+ while unmerged=$( git status --porcelain | grep ^U) && [[ -n ${unmerged} ]] \
176+ || [[ -e " ${REBASEMAGIC} " ]]; do
177+ conflicts=true # <-- We should have detected conflicts once
178+ echo
179+ echo " +++ Conflicts detected:"
180+ echo
181+ (git status --porcelain | grep ^U) || echo " !!! None. Did you git am --continue?"
182+ echo
183+ echo " +++ Please resolve the conflicts in another window (and remember to 'git add / git am --continue')"
184+ read -p " +++ Proceed (anything other than 'y' aborts the cherry-pick)? [y/n] " -r
185+ echo
186+ if ! [[ " ${REPLY} " =~ ^[yY]$ ]]; then
187+ echo " Aborting." >&2
188+ exit 1
189+ fi
190+ done
191+
192+ if [[ " ${conflicts} " != " true" ]]; then
193+ echo " !!! git am failed, likely because of an in-progress 'git am' or 'git rebase'"
194+ exit 1
195+ fi
196+ }
197+
198+ # set the subject
199+ subject=$( gh pr view " $pull " --json title --jq ' .["title"]' )
200+ SUBJECTS+=(" #${pull} : ${subject} " )
201+
202+ # set the release note
203+ release_note=$( gh pr view " $pull " --json body --jq ' .["body"]' | awk ' /```release-note/{f=1;next} /```/{f=0} f' )
204+ RELEASE_NOTES+=(" ${release_note} " )
205+
206+ # remove the patch file from /tmp
207+ rm -f " /tmp/${pull} .patch"
208+ done
209+ gitamcleanup=false
210+
211+ # Re-generate docs (if needed)
212+ if [[ -n " ${REGENERATE_DOCS} " ]]; then
213+ echo
214+ echo " Regenerating docs..."
215+ if ! hack/generate-docs.sh; then
216+ echo
217+ echo " hack/generate-docs.sh FAILED to complete."
218+ exit 1
219+ fi
220+ fi
221+
222+ if [[ -n " ${DRY_RUN} " ]]; then
223+ echo " !!! Skipping git push and PR creation because you set DRY_RUN."
224+ echo " To return to the branch you were in when you invoked this script:"
225+ echo
226+ echo " git checkout ${STARTINGBRANCH} "
227+ echo
228+ echo " To delete this branch:"
229+ echo
230+ echo " git branch -D ${NEWBRANCHUNIQ} "
231+ exit 0
232+ fi
233+
234+ if git remote -v | grep ^" ${FORK_REMOTE} " | grep " ${MAIN_REPO_ORG} /${MAIN_REPO_NAME} .git" ; then
235+ echo " !!! You have ${FORK_REMOTE} configured as your ${MAIN_REPO_ORG} /${MAIN_REPO_NAME} .git"
236+ echo " This isn't normal. Leaving you with push instructions:"
237+ echo
238+ echo " +++ First manually push the branch this script created:"
239+ echo
240+ echo " git push REMOTE ${NEWBRANCHUNIQ} :${NEWBRANCH} "
241+ echo
242+ echo " where REMOTE is your personal fork (maybe ${UPSTREAM_REMOTE} ? Consider swapping those.)."
243+ echo " OR consider setting UPSTREAM_REMOTE and FORK_REMOTE to different values."
244+ echo
245+ make-a-pr
246+ cleanbranch=" "
247+ exit 0
248+ fi
249+
250+ echo
251+ echo " +++ I'm about to do the following to push to GitHub (and I'm assuming ${FORK_REMOTE} is your personal fork):"
252+ echo
253+ echo " git push ${FORK_REMOTE} ${NEWBRANCHUNIQ} :${NEWBRANCH} "
254+ echo
255+ read -p " +++ Proceed (anything other than 'y' aborts the cherry-pick)? [y/n] " -r
256+ if ! [[ " ${REPLY} " =~ ^[yY]$ ]]; then
257+ echo " Aborting." >&2
258+ exit 1
259+ fi
260+
261+ git push " ${FORK_REMOTE} " -f " ${NEWBRANCHUNIQ} :${NEWBRANCH} "
262+ make-a-pr
0 commit comments