Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | # |
| 3 | # Copyright 2018 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | # |
| 7 | # Script to merge upstream tags into chromeos. |
| 8 | # The command creates a new branch with merge results. |
| 9 | # If necessary, it also pushes the tag into the remote |
| 10 | # repository and creates a branch pointing to it. |
| 11 | |
| 12 | readonly notify_to="chromeos-kernel@google.com" |
| 13 | readonly notify_cc="chromium-os-reviews@chromium.org" |
| 14 | |
| 15 | # Valid tag pattern. |
| 16 | PATTERN="^v[2-9](\.[0-9]+)+$" |
| 17 | PATTERN_RC="^v[2-9](\.[0-9]+)+-rc$" |
| 18 | GLOB="v[2-9].*[0-9]" |
| 19 | NUMBER="^[1-9][0-9]*$" |
| 20 | |
| 21 | if git help push | grep -q push-option; then |
| 22 | git_skip_validation="-o skip-validation" |
| 23 | fi |
| 24 | |
| 25 | # Initial parameter values. |
| 26 | changeid="" # Get Change-Id from CL or generate new Change-Id. |
| 27 | bug="" # Get Bug-Id from CL or provide on on command line. |
| 28 | tag="" # Tag to merge; must be provided on command line. |
| 29 | force=0 # Do not override Change-Id / Bug-Id. |
| 30 | prepare=0 # Do not prepare for upload. |
| 31 | upload=0 # Do not upload into Gerrit. |
| 32 | do_dryrun=0 # If 1, don't push anything upstream, don't send email. |
| 33 | notify=0 # Do not send notification e-mail. |
| 34 | deadline=3 # Feedback deadline (in days, default 3). |
| 35 | changes=() # List of uncommitted CLs to be applied prior to merge. |
| 36 | patches=() # List of patches to apply before committing merge. |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 37 | reverts=() # List of patches to revert as part of merge, after merge |
| 38 | prereverts=() # List of patches to revert as part of merge, prior to merge |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 39 | dependency="" # No dependency |
| 40 | Subject="" # default subject |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 41 | namespace="" # default namespace |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 42 | |
| 43 | # derived parameters |
| 44 | skip_merge=0 # Skip actual merge and upload. |
| 45 | # Will be set if tag has already been merged and force is true. |
| 46 | |
| 47 | readonly tmpfile=$(mktemp) |
| 48 | |
| 49 | trap 'rm -f "${tmpfile}"' EXIT |
| 50 | trap 'exit 2' SIGHUP SIGINT SIGQUIT SIGTERM |
| 51 | |
| 52 | error() { |
| 53 | printf '%b: error: %b\n' "${0##*/}" "$*" >&2 |
| 54 | } |
| 55 | |
| 56 | die() { |
| 57 | error "$@" |
| 58 | exit 1 |
| 59 | } |
| 60 | |
| 61 | usage() { |
| 62 | cat <<-EOF |
| 63 | Usage: ${0##*/} [options] tag |
| 64 | |
| 65 | Parameters: |
| 66 | tag Tag, branch, or SHA to merge. Must be either a valid stable |
| 67 | branch release tag, a valid branch name, or a valid SHA. |
| 68 | |
| 69 | Options: |
| 70 | -b bug-id[,bug-id] ... |
William K Lin | 6168372 | 2021-03-22 06:07:53 +0000 | [diff] [blame] | 71 | Bug-id or list of bug IDs. Must be valid buganizer |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 72 | bug ID. Mandatory unless the merge branch already exists |
| 73 | locally or in Gerrit. |
| 74 | -c change-id Change-Id as used by Gerrit. Optional. |
| 75 | -d deadline Feedback deadline in days (default: ${deadline}) |
| 76 | -f Force. Override existing Change-Id and bug number. |
| 77 | -h Display help text and exit. |
| 78 | -l change-id Apply patch extracted from CL:change-id prior to merge. |
| 79 | May be repeated multiple times. |
| 80 | -n Send notification e-mail to ${notify_to}. |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 81 | -N namespace Namespace (branch prefix) to use |
| 82 | Default 'stable-merge/linux' or 'merge', depending on context |
Guenter Roeck | 33f263a | 2019-06-28 08:51:23 -0700 | [diff] [blame] | 83 | -q dependency Add dependency (Cq-Depend: <dependency>) |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 84 | -p Prepare for upload into Gerrit. Implied if -u is specified. |
| 85 | -r Name of branch to base merge on. Determined from stable |
| 86 | release tag or from target branch name if not provided. |
| 87 | Must be existing local branch. Will be pushed into gerrit |
| 88 | as part of the merge process if not already available in |
| 89 | gerrit, and has to follow gerrit commit rules. |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 90 | -P sha Revert patch <sha> as part of merge, pre-merge |
| 91 | May be repeated multiple times. |
| 92 | -R sha Revert patch <sha> as part of merge, post-merge |
| 93 | May be repeated multiple times. |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 94 | -s Simulate, or dry-run. Don't actually push anything into |
| 95 | gerrit, and don't send e-mails. |
| 96 | -S subject Replace default subject line with provided string |
| 97 | -t Target branch name. The branch must exist in the Chrome OS |
| 98 | repository. |
| 99 | -u Upload merge into Gerrit. |
Guenter Roeck | 7acc7c1 | 2019-08-02 12:28:37 -0700 | [diff] [blame] | 100 | -x patchfile Patch to apply before committing merge. Patch will be applied |
| 101 | with "patch -p 1 < patchfile". May be repeated several times. |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 102 | EOF |
| 103 | |
| 104 | if [[ $# -gt 0 ]]; then |
| 105 | echo |
| 106 | die "$@" |
| 107 | fi |
| 108 | exit 0 |
| 109 | } |
| 110 | |
| 111 | # Find and report remote. |
| 112 | find_remote() { |
| 113 | local url="$1" |
| 114 | local remote |
| 115 | |
| 116 | for remote in $(git remote 2>/dev/null); do |
| 117 | rurl=$(git remote get-url "${remote}") |
| 118 | # ignore trailing '/' when comparing repositories |
| 119 | if [[ "${rurl%/}" == "${url%/}" ]]; then |
| 120 | echo "${remote}" |
| 121 | break |
| 122 | fi |
| 123 | done |
| 124 | } |
| 125 | |
| 126 | # Find remote. If there is no remote pointing to the referenced |
| 127 | # kernel repository, create one. |
| 128 | find_create_remote() { |
| 129 | local url="$1" |
| 130 | local default="$2" |
| 131 | local result |
| 132 | |
| 133 | result="$(find_remote "${url}")" |
| 134 | if [[ -z "${result}" ]]; then |
| 135 | git remote add "${default}" "${url}" |
| 136 | result="${default}" |
| 137 | fi |
| 138 | |
| 139 | echo "${result}" |
| 140 | } |
| 141 | |
| 142 | # Find and report CrOS remote. |
| 143 | # This is useful if the command runs on a checked out |
| 144 | # tree with several remotes. |
| 145 | find_chromeos() { |
| 146 | local url="https://chromium.googlesource.com/chromiumos/third_party/kernel" |
| 147 | |
| 148 | find_remote "${url}" |
| 149 | } |
| 150 | |
| 151 | # Find stable remote. If there is no remote pointing to the stable |
| 152 | # kernel repository, create one. |
| 153 | find_stable() { |
| 154 | local url="git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git" |
| 155 | |
| 156 | find_create_remote "${url}" "stable" |
| 157 | } |
| 158 | |
| 159 | # Find stable remote. If there is no remote pointing to the stable |
| 160 | # kernel repository, create one. |
| 161 | find_stable_rc() { |
| 162 | local url="git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable-rc.git" |
| 163 | |
| 164 | find_create_remote "${url}" "stable-rc" |
| 165 | } |
| 166 | |
| 167 | do_getparams() { |
| 168 | local bugs="" # List of bugs |
| 169 | local nbug="" # Numerical part of bug #, for validation. |
| 170 | local _bug |
| 171 | local option |
| 172 | local vtag |
| 173 | |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 174 | while getopts "b:c:d:fhl:N:nP:pq:r:R:st:uS:x:" option; do |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 175 | case ${option} in |
| 176 | b) bugs="${OPTARG}" ;; |
| 177 | c) changeid="Change-Id: ${OPTARG}" ;; |
| 178 | d) deadline="${OPTARG}" |
| 179 | if ! [[ "${deadline}" =~ ${NUMBER} ]]; then |
| 180 | die "Deadline must be numeric value > 0 (${deadline})" |
| 181 | fi |
| 182 | ;; |
| 183 | f) force=1 ;; |
| 184 | l) changes+=("${OPTARG}") ;; |
| 185 | n) notify=1 ;; |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 186 | N) namespace="${OPTARG}" ;; |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 187 | p) prepare=1 ;; |
| 188 | q) dependency="${OPTARG}" ;; |
| 189 | r) rbranch="${OPTARG}" ;; |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 190 | P) prereverts+=("${OPTARG}") ;; |
| 191 | R) reverts+=("${OPTARG}") ;; |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 192 | t) tbranch="${OPTARG}" ;; |
| 193 | s) do_dryrun=1 ;; |
| 194 | S) Subject="${OPTARG}" ;; |
| 195 | u) upload=1 prepare=1 ;; |
| 196 | x) patches+=("${OPTARG}") ;; |
| 197 | h|?|*) usage ;; |
| 198 | esac |
| 199 | done |
| 200 | shift $((OPTIND - 1)) |
| 201 | tag=$1 |
| 202 | if [[ -z "${tag}" ]]; then |
| 203 | usage "tag parameter is mandatory" |
| 204 | fi |
| 205 | vtag=$(echo "${tag}" | grep -E "${PATTERN}") |
| 206 | if [[ "${tag}" != "${vtag}" ]]; then |
| 207 | # Not a stable release tag, meaning we can not get it from a stable release. |
| 208 | # Maybe it is a stable release candidate. |
| 209 | vtag=$(echo "${tag}" | grep -E "${PATTERN_RC}") |
| 210 | if [[ "${tag}" != "${vtag}" ]]; then |
| 211 | # Make sure that the reference exists and bail out if not. |
| 212 | if ! git rev-parse --verify "${tag}" >/dev/null 2>&1; then |
| 213 | die "Unknown reference '${tag}'." |
| 214 | fi |
| 215 | else |
| 216 | die "${tag} references a stable release candidate. Not supported yet." |
| 217 | fi |
| 218 | fi |
| 219 | if [[ -n "${rbranch}" ]]; then |
| 220 | if ! git rev-parse --verify "${rbranch}" >/dev/null 2>&1; then |
| 221 | die "No such branch: ${rbranch}." |
| 222 | fi |
| 223 | fi |
William K Lin | 6168372 | 2021-03-22 06:07:53 +0000 | [diff] [blame] | 224 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 225 | if [[ -n "${bugs}" ]]; then |
| 226 | for _bug in ${bugs//,/ }; do |
| 227 | if [[ "${_bug}" == b:* ]]; then # buganizer |
| 228 | nbug="${_bug##b:}" |
| 229 | elif [[ "${_bug}" == b/* ]]; then # buganizer, alternative |
| 230 | nbug="${_bug##b/}" |
William K Lin | 6168372 | 2021-03-22 06:07:53 +0000 | [diff] [blame] | 231 | else # crbug etc are not allowed. |
| 232 | die "Invalid bug ID '${_bug}'." |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 233 | fi |
| 234 | if [[ ! "${nbug}" =~ ${NUMBER} ]]; then |
| 235 | die "Invalid bug ID '${_bug}'." |
| 236 | fi |
| 237 | done |
| 238 | bug="BUG=${bugs}" |
| 239 | fi |
Guenter Roeck | 33f263a | 2019-06-28 08:51:23 -0700 | [diff] [blame] | 240 | dependency="${dependency:+Cq-Depend: ${dependency}}" |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 241 | } |
| 242 | |
| 243 | # Validate environment and repository. |
| 244 | # We need a couple of commands, the repository must be |
| 245 | # a CrOS kernel repository, and it must be clean. |
| 246 | do_validate() { |
| 247 | local gerrit |
| 248 | local chromeos |
| 249 | local jq |
| 250 | |
| 251 | gerrit=$(which gerrit) |
| 252 | if [[ -z "${gerrit}" ]]; then |
| 253 | die "gerrit is required. Get from chromite or run from chroot." |
| 254 | fi |
| 255 | jq=$(which jq) |
| 256 | if [[ -z ${jq} ]]; then |
| 257 | die "jq is required. Install (apt-get install jq) or run from chroot." |
| 258 | fi |
| 259 | chromeos=$(find_chromeos) |
| 260 | if [[ -z ${chromeos} ]]; then |
| 261 | die "$(pwd) is not a Chromium OS kernel repository." |
| 262 | fi |
| 263 | if [[ -n "$(git status -s)" ]]; then |
| 264 | die "Requires clean repository." |
| 265 | fi |
| 266 | if [[ -n "${tbranch}" ]]; then |
| 267 | if ! git rev-parse --verify "${chromeos}/${tbranch}" >/dev/null 2>&1; then |
| 268 | die "No such branch: ${chromeos}/${tbranch}." |
| 269 | fi |
| 270 | fi |
| 271 | } |
| 272 | |
| 273 | # Validate provided Change-IDs. |
| 274 | do_validate_changeids() { |
| 275 | local cl |
| 276 | local ref |
| 277 | |
| 278 | for cl in "${changes[@]}"; do |
| 279 | ref=$(gerrit --json search "change:${cl}" \ |
| 280 | | jq ".[].currentPatchSet.ref") |
| 281 | if [[ -z "${ref}" ]]; then |
| 282 | die "No such Change-Id: ${cl}." |
| 283 | fi |
| 284 | done |
| 285 | } |
| 286 | |
| 287 | # Initialize global variables, plus some more validation. |
| 288 | do_setup() { |
| 289 | readonly stable=$(find_stable) |
| 290 | readonly stable_rc=$(find_stable_rc) |
| 291 | local vtag |
| 292 | local dvtag |
| 293 | |
| 294 | # If a stable release tag is provided, we need to update stable |
| 295 | # at this point to get the tag if it is not already available. |
| 296 | vtag=$(echo "${tag}" | grep -E "${PATTERN}") |
| 297 | if [[ "${tag}" == "${vtag}" ]]; then |
| 298 | if ! git rev-parse --verify "${tag}" >/dev/null 2>&1; then |
| 299 | if ! git fetch "${stable}" > /dev/null 2>&1; then |
| 300 | die "Failed to update stable release." |
| 301 | fi |
| 302 | if ! git rev-parse --verify "${tag}" >/dev/null 2>&1; then |
| 303 | die "Reference ${tag} not available." |
| 304 | fi |
| 305 | fi |
| 306 | else |
| 307 | # This might be a stable release candidate. |
| 308 | vtag=$(echo "${tag}" | grep -E "${PATTERN_RC}") |
| 309 | if [[ "${tag}" == "${vtag}" ]]; then |
| 310 | git fetch "${stable_rc}" > /dev/null 2>&1 |
| 311 | # The stable release tag is "vX.Y.Z-rc". Stable release candidate |
| 312 | # branches are named "remote/linux-X.Y.y". |
| 313 | # Extract 'X' and 'Y', create the remote branch name, |
| 314 | # clone/update the remote branch, and set a matching tag |
| 315 | # on top of it. |
| 316 | |
| 317 | die "Stable release candidates are not yet supported." |
| 318 | fi |
| 319 | fi |
| 320 | |
| 321 | readonly ctag=$(git describe --match "${GLOB}" --abbrev=0 "${tag}" \ |
| 322 | 2>/dev/null | cut -f1,2 -d. | sed -e 's/v//') |
| 323 | readonly dtag=$(git describe --tags "${tag}") |
| 324 | |
| 325 | # While we accept any valid reference as <tag>, we want it to be based |
| 326 | # on an existing release tag. |
| 327 | dbtag=${dtag%%-*} |
| 328 | dvtag=$(git describe --tags --abbrev=0 "${dtag}") |
| 329 | if [[ "${dbtag}" != "${dvtag}" ]]; then |
| 330 | die "${tag} (${dtag}) is not based on an existing release tag." |
| 331 | fi |
| 332 | |
| 333 | readonly chromeos=$(find_chromeos) |
| 334 | if [[ -z "${chromeos}" ]]; then |
| 335 | die "Chromium OS kernel repository not found." |
| 336 | fi |
| 337 | |
| 338 | # cbranch: Chromeos branch name |
| 339 | # mcbranch: local copy (baseline) |
| 340 | # ocbranch: remote (target) branch |
| 341 | # |
| 342 | # Note: This assumes that the target repository is ${chromeos}, |
| 343 | # even if a remote branch has been specified. It might make sense |
| 344 | # to make this configurable. |
| 345 | if [[ -n "${tbranch}" ]]; then |
| 346 | readonly cbranch="${tbranch}" |
| 347 | else |
| 348 | readonly cbranch="chromeos-${ctag}" |
| 349 | fi |
| 350 | if [[ -n "${rbranch}" ]]; then |
| 351 | readonly ocbranch="${rbranch}" |
| 352 | else |
| 353 | readonly ocbranch="${chromeos}/${cbranch}" |
| 354 | fi |
| 355 | |
| 356 | readonly mcbranch="merge/${cbranch}" |
| 357 | |
| 358 | # Topic to use. |
| 359 | readonly topic="merge-${dtag}" |
| 360 | |
| 361 | if ! git rev-parse --verify "${ocbranch}" >/dev/null 2>&1; then |
| 362 | usage "Invalid tag '${tag}': No such branch: '${ocbranch}'" |
| 363 | fi |
| 364 | |
| 365 | # mbranch: Local branch used to execute the merge. |
| 366 | readonly mbranch="${mcbranch}-${dtag}" |
| 367 | |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 368 | # Determine namespace to use if not provided |
| 369 | if [[ -z "${namespace}" ]]; then |
| 370 | if [[ "${tag}" == "${vtag}" ]]; then |
| 371 | namespace="stable-merge/linux" |
| 372 | else |
| 373 | namespace="merge" |
| 374 | fi |
| 375 | fi |
| 376 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 377 | # obranch: chromeos branch used as reference. |
| 378 | # May include local reverts from merge if necessary. |
| 379 | # If necessary, a branch with this name will be created locally and |
| 380 | # in the chromeos repository. It is necessary to perform the merge. |
Guenter Roeck | fa586d5 | 2020-08-27 11:53:21 -0700 | [diff] [blame^] | 381 | readonly obranch="${namespace}/${dtag}" |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 382 | |
| 383 | if [[ ${do_dryrun} -ne 0 ]]; then |
| 384 | readonly dryrun="--dry-run" |
| 385 | fi |
| 386 | |
Guenter Roeck | 7acc7c1 | 2019-08-02 12:28:37 -0700 | [diff] [blame] | 387 | Subject="CHROMIUM: ${Subject:-Merge '${tag}' into ${cbranch}}" |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 388 | } |
| 389 | |
| 390 | have_version() { |
| 391 | local tag |
| 392 | local tot_tag |
| 393 | local index |
| 394 | local v1 |
| 395 | local v2 |
| 396 | local vtag |
| 397 | |
| 398 | tag=$1 |
| 399 | vtag=$(echo "${tag}" | grep -E "${PATTERN}") |
| 400 | if [[ "${tag}" != "${vtag}" ]]; then |
| 401 | # Not a release tag, can not evaluate. |
| 402 | return 0 |
| 403 | fi |
| 404 | |
| 405 | tot_tag=$(git describe --match "v[2-9].*[0-9]" --abbrev=0 "${ocbranch}") |
| 406 | |
| 407 | index=1 |
| 408 | while true; do |
| 409 | v1=$(echo "${tag}" | cut -f${index} -d. | sed -e 's/[^0-9]//g') |
| 410 | v2=$(echo "${tot_tag}" | cut -f${index} -d. | sed -e 's/[^0-9]//g') |
| 411 | # If both version numbers are empty, we reached the end of the |
| 412 | # version number string, and the versions are equal. |
| 413 | # Return true. |
| 414 | if [[ -z "${v1}" && -z "${v2}" ]]; then |
| 415 | return 1 |
| 416 | fi |
| 417 | # Interpret empty minor version numbers as version 0. |
| 418 | if [[ -z "${v1}" ]]; then |
| 419 | v1=0 |
| 420 | fi |
| 421 | if [[ -z "${v2}" ]]; then |
| 422 | v2=0 |
| 423 | fi |
| 424 | # If ToT version is larger than tag, return true. |
| 425 | if [[ ${v2} -gt ${v1} ]]; then |
| 426 | return 1 |
| 427 | fi |
| 428 | # If tag version is targer than ToT, return false. |
| 429 | if [[ ${v2} -lt ${v1} ]]; then |
| 430 | return 0 |
| 431 | fi |
| 432 | index=$((index + 1)) |
| 433 | done |
| 434 | } |
| 435 | |
| 436 | # Remove double quotes from beginning and end of a string, and |
| 437 | # remove the escape character from double quotes within the string. |
| 438 | dequote() { |
| 439 | local tmp="${1#\"}" # beginning |
| 440 | tmp="${tmp%\"}" # end |
| 441 | echo "${tmp//\\\"/\"}" # remove embedded escape characters |
| 442 | } |
| 443 | |
| 444 | # Try to find the merge CL. |
| 445 | # Walk through all CLs tagged with the merge topic |
| 446 | # and try to find one with the expected subject line. |
| 447 | # If found, set merge_cl to the respective value for later use. |
| 448 | find_merge_cl() { |
| 449 | local cls |
| 450 | local cl |
| 451 | local subject |
| 452 | |
| 453 | cls=($(gerrit --json search "hashtag:${topic}" \ |
| 454 | | jq ".[].number" | sed -e 's/"//g')) |
| 455 | |
| 456 | for cl in "${cls[@]}"; do |
| 457 | subject=$(dequote "$(gerrit --json search "change:${cl}" \ |
| 458 | | jq ".[].subject")") |
| 459 | if [[ "${subject}" == "${Subject}" ]]; then |
| 460 | merge_cl="${cl}" |
| 461 | break |
| 462 | fi |
| 463 | done |
| 464 | } |
| 465 | |
| 466 | # Prepare for merge. |
| 467 | # - Update remotes. |
| 468 | # - Verify that tag exists. |
| 469 | # - Search for merge in gerrit. If it exists, validate bug ID and Change-Id. |
| 470 | # - Push tag and reference branch into CrOS repository if necessary. |
| 471 | do_prepare() { |
| 472 | local vtag |
| 473 | local obug |
| 474 | local ochangeid |
| 475 | local odependency |
| 476 | local ref |
| 477 | |
| 478 | find_merge_cl |
| 479 | |
| 480 | printf "Updating ${chromeos}..." |
| 481 | git fetch "${chromeos}" > /dev/null |
| 482 | printf "\nUpdating ${mcbranch} ..." |
| 483 | if git rev-parse --verify "${mcbranch}" >/dev/null 2>&1; then |
| 484 | if ! git checkout "${mcbranch}" >/dev/null 2>&1; then |
| 485 | die "Failed to check out '${mcbranch}'." |
| 486 | fi |
| 487 | git pull >/dev/null |
| 488 | else |
| 489 | if ! git checkout -b "${mcbranch}" "${ocbranch}"; then |
| 490 | die "Failed to create '${mcbranch}' from '${ocbranch}'." |
| 491 | fi |
| 492 | fi |
| 493 | echo |
| 494 | |
| 495 | # Abort if chromeos already includes the tag unless 'force' is set. |
| 496 | if ! have_version "${dtag}"; then |
| 497 | if [[ ${force} -eq 0 ]]; then |
| 498 | die "Tag or reference '${tag}' already in '${ocbranch}'." |
| 499 | fi |
| 500 | echo "Warning: Tag '${tag}' already in '${ocbranch}'." |
| 501 | echo "Will not merge/notify/prepare/upload." |
| 502 | skip_merge=1 |
| 503 | prepare=0 |
| 504 | notify=0 |
| 505 | upload=0 |
| 506 | fi |
| 507 | |
| 508 | if [[ -n "${merge_cl}" ]]; then |
| 509 | ref=$(dequote "$(gerrit --json search "change:${merge_cl}" \ |
| 510 | | jq ".[].currentPatchSet.ref")") |
| 511 | fi |
| 512 | if [[ -n "${ref}" ]]; then |
| 513 | if ! git fetch "${chromeos}" "${ref}" >/dev/null 2>&1; then |
| 514 | die "Failed to fetch '${ref}' from '${chromeos}'." |
| 515 | fi |
| 516 | git show -s --format=%B FETCH_HEAD > "${tmpfile}" |
| 517 | else |
| 518 | # We may have a local merge branch. |
| 519 | if git rev-parse --verify "${mbranch}" >/dev/null 2>&1; then |
| 520 | local subject |
| 521 | |
| 522 | # Make sure the branch actually includes the merge we are looking for. |
| 523 | git show -s --format=%B "${mbranch}" > "${tmpfile}" |
| 524 | subject="$(head -n 1 "${tmpfile}")" |
| 525 | if [[ "${subject}" != "${Subject}" ]]; then |
| 526 | rm -f "${tmpfile}" |
| 527 | touch "${tmpfile}" |
| 528 | fi |
| 529 | else |
| 530 | rm -f "${tmpfile}" |
| 531 | touch "${tmpfile}" |
| 532 | fi |
| 533 | fi |
| 534 | obug=$(grep "^BUG=" "${tmpfile}") |
| 535 | if [[ -n "${bug}" && -n "${obug}" && "${bug}" != "${obug}" \ |
| 536 | && ${force} -eq 0 ]]; then |
| 537 | die "Bug mismatch: '${bug}' <-> '${obug}'. Use -f to override." |
| 538 | fi |
| 539 | if [[ -z "${bug}" ]]; then |
| 540 | bug="${obug}" |
| 541 | fi |
| 542 | if [[ -z "${bug}" ]]; then |
| 543 | die "New merge: must specify bug ID." |
| 544 | fi |
| 545 | ochangeid=$(grep "^Change-Id:" "${tmpfile}") |
| 546 | if [[ -n "${changeid}" && -n "${ochangeid}" \ |
| 547 | && "${changeid}" != "${ochangeid}" && ${force} -eq 0 ]]; then |
| 548 | die "Change-Id mismatch: '${changeid}' <-> '${ochangeid}'. Use -f to override." |
| 549 | fi |
| 550 | if [[ -z "${changeid}" ]]; then |
| 551 | changeid="${ochangeid}" |
| 552 | fi |
| 553 | |
Guenter Roeck | 33f263a | 2019-06-28 08:51:23 -0700 | [diff] [blame] | 554 | odependency=$(grep "^Cq-Depend:" "${tmpfile}") |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 555 | if [[ -n "${dependency}" && -n "${odependency}" && \ |
| 556 | "${dependency}" != "${odependency}" && ${force} -eq 0 ]]; then |
| 557 | die "Dependency mismatch: '${dependency}' <-> '${odependency}'. Use -f to override." |
| 558 | fi |
| 559 | if [[ -z "${dependency}" ]]; then |
| 560 | dependency="${odependency}" |
| 561 | fi |
| 562 | |
| 563 | # Check out local reference branch; create it if needed. |
| 564 | # It will be retained since it may be needed to apply reverts |
| 565 | # prior to executing the merge. |
| 566 | # It is the responsibility of the user to remove it after it is |
| 567 | # no longer needed. |
| 568 | # Note: git rev-parse returns success if ${obranch} includes an |
| 569 | # abbreviated SHA. It also returns success if a _remote_ branch |
| 570 | # with the same name exists. So let's use show-ref instead. |
| 571 | # if ! git rev-parse --verify --quiet "${obranch}"; then |
| 572 | if ! git show-ref --verify --quiet "refs/heads/${obranch}"; then |
| 573 | if ! git checkout -b "${obranch}" "${tag}"; then |
| 574 | die "Failed to create '${obranch}' from '${tag}'." |
| 575 | fi |
| 576 | else |
| 577 | if ! git checkout "${obranch}"; then |
| 578 | die "Failed to check out '${obranch}'." |
| 579 | fi |
| 580 | fi |
| 581 | |
| 582 | if [[ ${prepare} -ne 0 ]]; then |
| 583 | # Push reference branch as well as the tag into the CrOS repository. |
| 584 | # Assume linear changes only; if the reference branch is reparented, |
| 585 | # the user has to explicitly update or remove the remote branch. |
| 586 | # Only push tag if it is a release tag; otherwise we neither want nor |
| 587 | # need it in the CrOS repository. |
| 588 | vtag=$(echo "${tag}" | grep -E "${PATTERN}") |
| 589 | if [[ -n "${vtag}" ]]; then |
| 590 | git push ${git_skip_validation} --no-verify ${dryrun} "${chromeos}" "refs/tags/${tag}" |
| 591 | else |
| 592 | echo "${tag} is not a release tag, not pushed" |
| 593 | fi |
| 594 | if ! git push ${git_skip_validation} --no-verify ${dryrun} "${chromeos}" "${obranch}"; then |
| 595 | die "Failed to upload '${obranch}' into '${chromeos}'." |
| 596 | fi |
| 597 | fi |
| 598 | } |
| 599 | |
| 600 | gitismerge() |
| 601 | { |
| 602 | local sha="$1" |
| 603 | local msha |
| 604 | |
| 605 | msha=$(git rev-list -1 --merges "${sha}"~1.."${sha}") |
| 606 | [[ -n "$msha" ]] |
| 607 | } |
| 608 | |
| 609 | # Apply patches from gerrit CLs into merge branch. |
| 610 | do_apply_changes() { |
| 611 | local cl |
| 612 | local ref |
| 613 | |
| 614 | for cl in "${changes[@]}"; do |
| 615 | echo "Applying CL:${cl}" |
| 616 | ref=$(dequote "$(gerrit --json search "change:${cl}" \ |
| 617 | | jq ".[].currentPatchSet.ref" | head -n1)") |
| 618 | if [[ -z "${ref}" ]]; then |
| 619 | die "Patch set for CL:${cl} not found." |
| 620 | fi |
| 621 | if ! git fetch "${chromeos}" "${ref}" >/dev/null 2>&1; then |
| 622 | die "Failed to fetch CL:${cl}." |
| 623 | fi |
| 624 | if gitismerge FETCH_HEAD; then |
| 625 | # git cherry-pick -m <parent> does not work since it pulls in |
| 626 | # the merge as single commit. This messes up history and was |
| 627 | # seen to result in obscure and avoidable conflicts. |
| 628 | if ! git merge --no-edit FETCH_HEAD; then |
| 629 | die "Failed to merge CL:${cl} into merge branch." |
| 630 | fi |
| 631 | else |
| 632 | if ! git cherry-pick FETCH_HEAD; then |
| 633 | die "Failed to cherry-pick CL:${cl} into merge branch." |
| 634 | fi |
| 635 | fi |
| 636 | done |
| 637 | } |
| 638 | |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 639 | # Apply reverts from list of SHAs from merge branch prior to |
| 640 | # the actual merge |
| 641 | do_apply_reverts() { |
| 642 | local revert |
| 643 | |
| 644 | for revert in $*; do |
| 645 | echo "Reverting commit ${revert}" |
| 646 | if ! git revert --no-commit "${revert}"; then |
| 647 | die "Failed to revert commit ${revert} in merge branch." |
| 648 | fi |
| 649 | done |
| 650 | } |
| 651 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 652 | # Do the merge. |
| 653 | # - Create merge branch. |
| 654 | # - Merge. |
| 655 | # - Handle conflicts [abort if there are unhandled conflicts]. |
| 656 | # - Create detailed merge commit log. |
| 657 | do_merge() { |
| 658 | # xbranch: Name of branch to merge. |
| 659 | # ref: Baseline reference for request-pull. |
| 660 | local xbranch |
| 661 | local ref |
| 662 | local patch |
Guenter Roeck | 7acc7c1 | 2019-08-02 12:28:37 -0700 | [diff] [blame] | 663 | local file |
| 664 | local files |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 665 | local revert |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 666 | |
| 667 | git branch -D "${mbranch}" >/dev/null 2>&1 |
| 668 | if ! git checkout -b "${mbranch}" "${ocbranch}"; then |
| 669 | die "Failed to create merge branch '${mbranch}'." |
| 670 | fi |
| 671 | |
| 672 | if [[ ${prepare} -eq 0 ]]; then |
| 673 | xbranch="${obranch}" |
| 674 | else |
| 675 | xbranch="${chromeos}/${obranch}" |
| 676 | fi |
| 677 | |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 678 | if [[ -n "${prereverts[@]}" ]]; then |
| 679 | do_apply_reverts ${prereverts[@]} |
| 680 | fi |
| 681 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 682 | do_apply_changes |
| 683 | ref=$(git rev-parse HEAD) |
| 684 | |
| 685 | # Do the merge. |
| 686 | # Use --no-ff to ensure this is always handled as merge, even for linear |
| 687 | # merges. Otherwise linear merges would succeed and move the branch HEAD |
| 688 | # forward even though --no-commit is specified. This lets us add an |
| 689 | # explicit merge commit log. |
| 690 | conflicts=() |
| 691 | if ! git merge --no-commit --no-ff "${xbranch}" > "${tmpfile}"; then |
| 692 | files=$(git rerere status) |
| 693 | if [[ -n "${files}" ]]; then |
Guenter Roeck | 7acc7c1 | 2019-08-02 12:28:37 -0700 | [diff] [blame] | 694 | error "Unresolved conflicts:" |
| 695 | for file in ${files}; do |
| 696 | echo " ${file}" |
| 697 | done |
| 698 | die "Please resolve conflicts, commit changes, and then rerun the merge script.\nMake sure you have 'git rerere' enabled." |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 699 | fi |
| 700 | echo "All conflicts resolved, continuing" |
| 701 | conflicts=($(grep CONFLICT "${tmpfile}" \ |
| 702 | | sed -e 's/.*Merge conflict in //')) |
| 703 | fi |
| 704 | |
| 705 | # Note: The following is no longer needed in recent versions of git. |
| 706 | # Keep it around since it does not hurt. |
| 707 | if [[ ${#conflicts[@]} -gt 0 ]]; then |
| 708 | git add ${conflicts[*]} |
| 709 | fi |
| 710 | |
| 711 | for patch in "${patches[@]}"; do |
| 712 | if ! patch -p 1 < "${patch}" >"${tmpfile}"; then |
| 713 | die "Failed to apply patch ${patch}" |
| 714 | fi |
| 715 | if ! git add $(sed -e 's/.* //' "${tmpfile}"); then |
| 716 | die "Failed to add patched files to git commit list" |
| 717 | fi |
| 718 | done |
| 719 | |
| 720 | if ! git commit -s --no-edit; then |
| 721 | die "Failed to commit merge." |
| 722 | fi |
| 723 | |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 724 | if [[ -n "${reverts[@]}" ]]; then |
| 725 | do_apply_reverts ${reverts[@]} |
| 726 | if ! git commit --amend --no-edit; then |
| 727 | die "Failed to commit merge after post-commit reverts." |
| 728 | fi |
| 729 | fi |
| 730 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 731 | # Update commit message. |
| 732 | |
| 733 | ( echo "${Subject}" |
| 734 | echo |
| 735 | echo "Merge of ${tag} into ${cbranch}" |
| 736 | echo |
| 737 | ) > "${tmpfile}" |
| 738 | |
| 739 | # Add conflicts to description. |
| 740 | if [[ ${#conflicts[@]} -gt 0 ]]; then |
| 741 | ( |
| 742 | echo "Conflicts:" |
| 743 | for conflict in "${conflicts[@]}"; do |
| 744 | echo " ${conflict}" |
| 745 | done |
| 746 | echo |
| 747 | ) >> "${tmpfile}" |
| 748 | fi |
| 749 | |
Guenter Roeck | c98bb9a | 2019-08-02 12:57:46 -0700 | [diff] [blame] | 750 | # Add reverts to description. |
| 751 | if [[ -n "${prereverts[@]}${reverts[@]}" ]]; then |
| 752 | ( |
| 753 | echo "The following patches have been reverted as part of the merge" |
| 754 | echo "to remove code which is obsolete or no longer applicable." |
| 755 | echo |
| 756 | for revert in ${prereverts[@]} ${reverts[@]}; do |
| 757 | echo " $(git show --oneline --no-decorate -s ${revert})" |
| 758 | done |
| 759 | echo |
| 760 | ) >> "${tmpfile}" |
| 761 | fi |
| 762 | |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 763 | if [[ -n "$(git log --oneline "${tag}..${obranch}")" ]]; then |
| 764 | ( echo "Changes applied on top of '${tag}' prior to merge:" |
| 765 | git log --oneline --no-decorate "${tag}..${obranch}" | \ |
| 766 | sed -e 's/^/ /' |
| 767 | echo |
| 768 | ) >> "${tmpfile}" |
| 769 | fi |
| 770 | |
| 771 | ( echo "Changelog:" |
| 772 | git request-pull "${ref}" . | \ |
| 773 | sed -n '/^--------------/,$p' |
| 774 | |
| 775 | echo |
| 776 | |
Guenter Roeck | 33f263a | 2019-06-28 08:51:23 -0700 | [diff] [blame] | 777 | echo "${bug}" |
| 778 | echo "TEST=Build and test on various affected systems" |
| 779 | echo |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 780 | if [[ -n "${dependency}" ]]; then |
| 781 | echo "${dependency}" |
| 782 | fi |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 783 | if [[ -n "${changeid}" ]]; then |
Guenter Roeck | 9aa6957 | 2017-12-12 13:36:30 -0800 | [diff] [blame] | 784 | echo "${changeid}" |
| 785 | fi |
| 786 | ) >> "${tmpfile}" |
| 787 | |
| 788 | # Amend commit with the updated description. |
| 789 | if ! git commit -s --amend -F "${tmpfile}"; then |
| 790 | die "Failed to amend merge with commit log." |
| 791 | fi |
| 792 | } |
| 793 | |
| 794 | do_notify() { |
| 795 | local cl |
| 796 | local email_cc |
| 797 | local cc_notify_cc |
| 798 | local subject |
| 799 | local message |
| 800 | local lbug |
| 801 | local tdeadline |
| 802 | |
| 803 | if [[ -z "${merge_cl}" ]]; then |
| 804 | die "No merge CL, can not send notifications." |
| 805 | fi |
| 806 | |
| 807 | gerrit --json search "change:${merge_cl}" > "${tmpfile}" |
| 808 | |
| 809 | cl=$(dequote "$(jq ".[].number" "${tmpfile}")") |
| 810 | if [[ -z "${cl}" ]]; then |
| 811 | die "Missing CL for topic '${topic}' (upload into gerrit first)." |
| 812 | fi |
| 813 | |
| 814 | subject=$(dequote "$(jq ".[].subject" "${tmpfile}")") |
| 815 | message=$(dequote "$(jq ".[].commitMessage" "${tmpfile}" \ |
| 816 | | sed -e 's/\\n/\n/g')") |
| 817 | email_cc=$(dequote "$(jq ".[].owner.email" "${tmpfile}")") |
| 818 | if [[ -n "${email_cc}" ]]; then |
| 819 | email_cc="-cc=${email_cc}" |
| 820 | fi |
| 821 | |
| 822 | if [[ -n "${notify_cc}" ]]; then |
| 823 | cc_notify_cc="--cc=${notify_cc}" |
| 824 | fi |
| 825 | |
| 826 | if [[ "${bug##BUG=b:}" != "${bug}" ]]; then # buganizer |
| 827 | lbug="https://b.corp.google.com/${bug##BUG=b:}" |
| 828 | elif [[ "${bug##BUG=chromium:}" != "${bug}" ]]; then # crbug |
| 829 | lbug="https://crbug.com/${bug##BUG=chromium:}" |
| 830 | else # unknown/invalid |
| 831 | lbug="${bug##BUG=}" |
| 832 | fi |
| 833 | |
| 834 | tdeadline=$(($(date +%s) + deadline * 86400)) |
| 835 | |
| 836 | cat <<-EOF > "${tmpfile}" |
| 837 | Subject: Review request: "${subject}" |
| 838 | |
| 839 | This is the start of the review cycle for the merge of stable release |
| 840 | ${tag} into ${cbranch}. If anyone has issues or concerns |
| 841 | with this stable release being applied, please let me know. |
| 842 | |
| 843 | Bug: ${lbug} |
| 844 | Code review: https://chromium-review.googlesource.com/#/q/${cl} |
| 845 | |
| 846 | Responses should be made by $(date --date="@${tdeadline}"). |
| 847 | Anything received after that time might be too late. |
| 848 | |
| 849 | Commit message and changelog are as follows. |
| 850 | |
| 851 | ${message} |
| 852 | EOF |
| 853 | |
| 854 | if ! git send-email ${dryrun} --to="${notify_to}" "${cc_notify_cc}" "${email_cc}" \ |
| 855 | --8bit-encoding="UTF-8" \ |
| 856 | --suppress-cc=all "${tmpfile}"; then |
| 857 | die "Failed to send notification e-mail to '${notify_to}'." |
| 858 | fi |
| 859 | } |
| 860 | |
| 861 | do_upload() { |
| 862 | if [[ ${upload} -ne 0 ]]; then |
| 863 | if ! git push --no-verify ${dryrun} "${chromeos}" "${mbranch}:refs/for/${cbranch}%t=${topic}"; then |
| 864 | die "Failed to upload changes into '${chromeos}'." |
| 865 | fi |
| 866 | elif [[ ${prepare} -ne 0 ]]; then |
| 867 | echo "Push into ${chromeos} using the following command:" |
| 868 | echo " git push --no-verify ${chromeos} ${mbranch}:refs/for/${cbranch}%t=${topic}" |
| 869 | fi |
| 870 | } |
| 871 | |
| 872 | main() { |
| 873 | do_getparams "$@" |
| 874 | do_validate |
| 875 | do_validate_changeids |
| 876 | do_setup |
| 877 | do_prepare |
| 878 | if [[ ${skip_merge} -eq 0 ]]; then |
| 879 | do_merge |
| 880 | do_upload |
| 881 | fi |
| 882 | if [[ ${notify} -ne 0 ]]; then |
| 883 | find_merge_cl |
| 884 | do_notify |
| 885 | fi |
| 886 | |
| 887 | exit 0 |
| 888 | } |
| 889 | |
| 890 | main "$@" |