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