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