1#!/usr/bin/env python 2# Copyright 2019 The ANGLE project authors. All Rights Reserved. 3# 4# Use of this source code is governed by a BSD-style license 5# that can be found in the LICENSE file in the root of the source 6# tree. An additional intellectual property rights grant can be found 7# in the file PATENTS. All contributing project authors may 8# be found in the AUTHORS file in the root of the source tree. 9 10# This is a modified copy of the script in 11# https://webrtc.googlesource.com/src/+/main/tools_webrtc/autoroller/roll_deps.py 12# customized for ANGLE. 13"""Script to automatically roll Chromium dependencies in the ANGLE DEPS file.""" 14 15import argparse 16import base64 17import collections 18import logging 19import os 20import platform 21import re 22import subprocess 23import sys 24import urllib2 25 26 27def FindSrcDirPath(): 28 """Returns the abs path to the root dir of the project.""" 29 # Special cased for ANGLE. 30 return os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))) 31 32 33ANGLE_CHROMIUM_DEPS = [ 34 'build', 35 'buildtools', 36 'buildtools/clang_format/script', 37 'buildtools/linux64', 38 'buildtools/mac', 39 'buildtools/third_party/libc++/trunk', 40 'buildtools/third_party/libc++abi/trunk', 41 'buildtools/third_party/libunwind/trunk', 42 'buildtools/win', 43 'testing', 44 'third_party/abseil-cpp', 45 'third_party/android_build_tools', 46 'third_party/android_build_tools/aapt2', 47 'third_party/android_build_tools/art', 48 'third_party/android_build_tools/bundletool', 49 'third_party/android_deps', 50 'third_party/android_ndk', 51 'third_party/android_platform', 52 'third_party/android_sdk', 53 'third_party/android_sdk/androidx_browser/src', 54 'third_party/android_sdk/public', 55 'third_party/android_system_sdk', 56 'third_party/bazel', 57 'third_party/catapult', 58 'third_party/colorama/src', 59 'third_party/depot_tools', 60 'third_party/ijar', 61 'third_party/jdk', 62 'third_party/jdk/extras', 63 'third_party/jinja2', 64 'third_party/libjpeg_turbo', 65 'third_party/markupsafe', 66 'third_party/nasm', 67 'third_party/proguard', 68 'third_party/protobuf', 69 'third_party/Python-Markdown', 70 'third_party/qemu-linux-x64', 71 'third_party/qemu-mac-x64', 72 'third_party/r8', 73 'third_party/requests/src', 74 'third_party/six', 75 'third_party/turbine', 76 'third_party/zlib', 77 'tools/android/errorprone_plugin', 78 'tools/clang', 79 'tools/clang/dsymutil', 80 'tools/luci-go', 81 'tools/mb', 82 'tools/md_browser', 83 'tools/memory', 84 'tools/perf', 85 'tools/protoc_wrapper', 86 'tools/python', 87 'tools/skia_goldctl/linux', 88 'tools/skia_goldctl/mac', 89 'tools/skia_goldctl/win', 90 'tools/valgrind', 91] 92 93ANGLE_URL = 'https://chromium.googlesource.com/angle/angle' 94CHROMIUM_SRC_URL = 'https://chromium.googlesource.com/chromium/src' 95CHROMIUM_COMMIT_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s' 96CHROMIUM_LOG_TEMPLATE = CHROMIUM_SRC_URL + '/+log/%s' 97CHROMIUM_FILE_TEMPLATE = CHROMIUM_SRC_URL + '/+/%s/%s' 98 99COMMIT_POSITION_RE = re.compile('^Cr-Commit-Position: .*#([0-9]+).*$') 100CLANG_REVISION_RE = re.compile(r'^CLANG_REVISION = \'([-0-9a-z]+)\'') 101ROLL_BRANCH_NAME = 'roll_chromium_revision' 102 103SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 104CHECKOUT_SRC_DIR = FindSrcDirPath() 105CHECKOUT_ROOT_DIR = CHECKOUT_SRC_DIR 106 107# Copied from tools/android/roll/android_deps/.../BuildConfigGenerator.groovy. 108ANDROID_DEPS_START = r'=== ANDROID_DEPS Generated Code Start ===' 109ANDROID_DEPS_END = r'=== ANDROID_DEPS Generated Code End ===' 110# Location of automically gathered android deps. 111ANDROID_DEPS_PATH = 'src/third_party/android_deps/' 112 113NOTIFY_EMAIL = 'angle-wrangler@grotations.appspotmail.com' 114 115CLANG_TOOLS_URL = 'https://chromium.googlesource.com/chromium/src/tools/clang' 116CLANG_FILE_TEMPLATE = CLANG_TOOLS_URL + '/+/%s/%s' 117 118CLANG_TOOLS_PATH = 'tools/clang' 119CLANG_UPDATE_SCRIPT_URL_PATH = 'scripts/update.py' 120CLANG_UPDATE_SCRIPT_LOCAL_PATH = os.path.join(CHECKOUT_SRC_DIR, 'tools', 'clang', 'scripts', 121 'update.py') 122 123DepsEntry = collections.namedtuple('DepsEntry', 'path url revision') 124ChangedDep = collections.namedtuple('ChangedDep', 'path url current_rev new_rev') 125ClangChange = collections.namedtuple('ClangChange', 'mirror_change clang_change') 126CipdDepsEntry = collections.namedtuple('CipdDepsEntry', 'path packages') 127ChangedCipdPackage = collections.namedtuple('ChangedCipdPackage', 128 'path package current_version new_version') 129 130ChromiumRevisionUpdate = collections.namedtuple('ChromiumRevisionUpdate', ('current_chromium_rev ' 131 'new_chromium_rev ')) 132 133 134def AddDepotToolsToPath(): 135 sys.path.append(os.path.join(CHECKOUT_SRC_DIR, 'build')) 136 import find_depot_tools 137 find_depot_tools.add_depot_tools_to_path() 138 139 140class RollError(Exception): 141 pass 142 143 144def StrExpansion(): 145 return lambda str_value: str_value 146 147 148def VarLookup(local_scope): 149 return lambda var_name: local_scope['vars'][var_name] 150 151 152def ParseDepsDict(deps_content): 153 local_scope = {} 154 global_scope = { 155 'Str': StrExpansion(), 156 'Var': VarLookup(local_scope), 157 'deps_os': {}, 158 } 159 exec (deps_content, global_scope, local_scope) 160 return local_scope 161 162 163def ParseLocalDepsFile(filename): 164 with open(filename, 'rb') as f: 165 deps_content = f.read() 166 return ParseDepsDict(deps_content) 167 168 169def ParseCommitPosition(commit_message): 170 for line in reversed(commit_message.splitlines()): 171 m = COMMIT_POSITION_RE.match(line.strip()) 172 if m: 173 return int(m.group(1)) 174 logging.error('Failed to parse commit position id from:\n%s\n', commit_message) 175 sys.exit(-1) 176 177 178def _RunCommand(command, working_dir=None, ignore_exit_code=False, extra_env=None, 179 input_data=None): 180 """Runs a command and returns the output from that command. 181 182 If the command fails (exit code != 0), the function will exit the process. 183 184 Returns: 185 A tuple containing the stdout and stderr outputs as strings. 186 """ 187 working_dir = working_dir or CHECKOUT_SRC_DIR 188 logging.debug('CMD: %s CWD: %s', ' '.join(command), working_dir) 189 env = os.environ.copy() 190 if extra_env: 191 assert all(isinstance(value, str) for value in extra_env.values()) 192 logging.debug('extra env: %s', extra_env) 193 env.update(extra_env) 194 p = subprocess.Popen( 195 command, 196 stdin=subprocess.PIPE, 197 stdout=subprocess.PIPE, 198 stderr=subprocess.PIPE, 199 env=env, 200 cwd=working_dir, 201 universal_newlines=True) 202 std_output, err_output = p.communicate(input_data) 203 p.stdout.close() 204 p.stderr.close() 205 if not ignore_exit_code and p.returncode != 0: 206 logging.error('Command failed: %s\n' 207 'stdout:\n%s\n' 208 'stderr:\n%s\n', ' '.join(command), std_output, err_output) 209 sys.exit(p.returncode) 210 return std_output, err_output 211 212 213def _GetBranches(): 214 """Returns a tuple of active,branches. 215 216 The 'active' is the name of the currently active branch and 'branches' is a 217 list of all branches. 218 """ 219 lines = _RunCommand(['git', 'branch'])[0].split('\n') 220 branches = [] 221 active = '' 222 for line in lines: 223 if '*' in line: 224 # The assumption is that the first char will always be the '*'. 225 active = line[1:].strip() 226 branches.append(active) 227 else: 228 branch = line.strip() 229 if branch: 230 branches.append(branch) 231 return active, branches 232 233 234def _ReadGitilesContent(url): 235 # Download and decode BASE64 content until 236 # https://code.google.com/p/gitiles/issues/detail?id=7 is fixed. 237 logging.debug('Reading gitiles URL %s' % url) 238 base64_content = ReadUrlContent(url + '?format=TEXT') 239 return base64.b64decode(base64_content[0]) 240 241 242def ReadRemoteCrFile(path_below_src, revision): 243 """Reads a remote Chromium file of a specific revision. Returns a string.""" 244 return _ReadGitilesContent(CHROMIUM_FILE_TEMPLATE % (revision, path_below_src)) 245 246 247def ReadRemoteCrCommit(revision): 248 """Reads a remote Chromium commit message. Returns a string.""" 249 return _ReadGitilesContent(CHROMIUM_COMMIT_TEMPLATE % revision) 250 251 252def ReadRemoteClangFile(path_below_src, revision): 253 """Reads a remote Clang file of a specific revision. Returns a string.""" 254 return _ReadGitilesContent(CLANG_FILE_TEMPLATE % (revision, path_below_src)) 255 256 257def ReadUrlContent(url): 258 """Connect to a remote host and read the contents. Returns a list of lines.""" 259 conn = urllib2.urlopen(url) 260 try: 261 return conn.readlines() 262 except IOError as e: 263 logging.exception('Error connecting to %s. Error: %s', url, e) 264 raise 265 finally: 266 conn.close() 267 268 269def GetMatchingDepsEntries(depsentry_dict, dir_path): 270 """Gets all deps entries matching the provided path. 271 272 This list may contain more than one DepsEntry object. 273 Example: dir_path='src/testing' would give results containing both 274 'src/testing/gtest' and 'src/testing/gmock' deps entries for Chromium's DEPS. 275 Example 2: dir_path='src/build' should return 'src/build' but not 276 'src/buildtools'. 277 278 Returns: 279 A list of DepsEntry objects. 280 """ 281 result = [] 282 for path, depsentry in depsentry_dict.iteritems(): 283 if path == dir_path: 284 result.append(depsentry) 285 else: 286 parts = path.split('/') 287 if all(part == parts[i] for i, part in enumerate(dir_path.split('/'))): 288 result.append(depsentry) 289 return result 290 291 292def BuildDepsentryDict(deps_dict): 293 """Builds a dict of paths to DepsEntry objects from a raw parsed deps dict.""" 294 result = {} 295 296 def AddDepsEntries(deps_subdict): 297 for path, dep in deps_subdict.iteritems(): 298 if path in result: 299 continue 300 if not isinstance(dep, dict): 301 dep = {'url': dep} 302 if dep.get('dep_type') == 'cipd': 303 result[path] = CipdDepsEntry(path, dep['packages']) 304 else: 305 if '@' not in dep['url']: 306 continue 307 url, revision = dep['url'].split('@') 308 result[path] = DepsEntry(path, url, revision) 309 310 AddDepsEntries(deps_dict['deps']) 311 for deps_os in ['win', 'mac', 'unix', 'android', 'ios', 'unix']: 312 AddDepsEntries(deps_dict.get('deps_os', {}).get(deps_os, {})) 313 return result 314 315 316def _FindChangedCipdPackages(path, old_pkgs, new_pkgs): 317 pkgs_equal = ({p['package'] for p in old_pkgs} == {p['package'] for p in new_pkgs}) 318 assert pkgs_equal, ('Old: %s\n New: %s.\nYou need to do a manual roll ' 319 'and remove/add entries in DEPS so the old and new ' 320 'list match.' % (old_pkgs, new_pkgs)) 321 for old_pkg in old_pkgs: 322 for new_pkg in new_pkgs: 323 old_version = old_pkg['version'] 324 new_version = new_pkg['version'] 325 if (old_pkg['package'] == new_pkg['package'] and old_version != new_version): 326 logging.debug('Roll dependency %s to %s', path, new_version) 327 yield ChangedCipdPackage(path, old_pkg['package'], old_version, new_version) 328 329 330def _FindNewDeps(old, new): 331 """ Gather dependencies only in |new| and return corresponding paths. """ 332 old_entries = set(BuildDepsentryDict(old)) 333 new_entries = set(BuildDepsentryDict(new)) 334 return [path for path in new_entries - old_entries if path in ANGLE_CHROMIUM_DEPS] 335 336 337def CalculateChangedDeps(angle_deps, new_cr_deps): 338 """ 339 Calculate changed deps entries based on entries defined in the ANGLE DEPS 340 file: 341 - If a shared dependency with the Chromium DEPS file: roll it to the same 342 revision as Chromium (i.e. entry in the new_cr_deps dict) 343 - If it's a Chromium sub-directory, roll it to the HEAD revision (notice 344 this means it may be ahead of the chromium_revision, but generally these 345 should be close). 346 - If it's another DEPS entry (not shared with Chromium), roll it to HEAD 347 unless it's configured to be skipped. 348 349 Returns: 350 A list of ChangedDep objects representing the changed deps. 351 """ 352 353 def ChromeURL(angle_deps_entry): 354 # Perform variable substitutions. 355 # This is a hack to get around the unsupported way this script parses DEPS. 356 # A better fix would be to use the gclient APIs to query and update DEPS. 357 # However this is complicated by how this script downloads DEPS remotely. 358 return angle_deps_entry.url.replace('{chromium_git}', 'https://chromium.googlesource.com') 359 360 result = [] 361 angle_entries = BuildDepsentryDict(angle_deps) 362 new_cr_entries = BuildDepsentryDict(new_cr_deps) 363 for path, angle_deps_entry in angle_entries.iteritems(): 364 if path not in ANGLE_CHROMIUM_DEPS: 365 continue 366 367 # All ANGLE Chromium dependencies are located in src/. 368 chrome_path = 'src/%s' % path 369 cr_deps_entry = new_cr_entries.get(chrome_path) 370 371 if cr_deps_entry: 372 assert type(cr_deps_entry) is type(angle_deps_entry) 373 374 if isinstance(cr_deps_entry, CipdDepsEntry): 375 result.extend( 376 _FindChangedCipdPackages(path, angle_deps_entry.packages, 377 cr_deps_entry.packages)) 378 continue 379 380 # Use the revision from Chromium's DEPS file. 381 new_rev = cr_deps_entry.revision 382 assert ChromeURL(angle_deps_entry) == cr_deps_entry.url, ( 383 'ANGLE DEPS entry %s has a different URL (%s) than Chromium (%s).' % 384 (path, ChromeURL(angle_deps_entry), cr_deps_entry.url)) 385 else: 386 if isinstance(angle_deps_entry, DepsEntry): 387 # Use the HEAD of the deps repo. 388 stdout, _ = _RunCommand(['git', 'ls-remote', ChromeURL(angle_deps_entry), 'HEAD']) 389 new_rev = stdout.strip().split('\t')[0] 390 else: 391 # The dependency has been removed from chromium. 392 # This is handled by FindRemovedDeps. 393 continue 394 395 # Check if an update is necessary. 396 if angle_deps_entry.revision != new_rev: 397 logging.debug('Roll dependency %s to %s', path, new_rev) 398 result.append( 399 ChangedDep(path, ChromeURL(angle_deps_entry), angle_deps_entry.revision, new_rev)) 400 return sorted(result) 401 402 403def CalculateChangedClang(changed_deps, autoroll): 404 mirror_change = [change for change in changed_deps if change.path == CLANG_TOOLS_PATH] 405 if not mirror_change: 406 return None 407 408 mirror_change = mirror_change[0] 409 410 def GetClangRev(lines): 411 for line in lines: 412 match = CLANG_REVISION_RE.match(line) 413 if match: 414 return match.group(1) 415 raise RollError('Could not parse Clang revision!') 416 417 old_clang_update_py = ReadRemoteClangFile(CLANG_UPDATE_SCRIPT_URL_PATH, 418 mirror_change.current_rev).splitlines() 419 old_clang_rev = GetClangRev(old_clang_update_py) 420 logging.debug('Found old clang rev: %s' % old_clang_rev) 421 422 new_clang_update_py = ReadRemoteClangFile(CLANG_UPDATE_SCRIPT_URL_PATH, 423 mirror_change.new_rev).splitlines() 424 new_clang_rev = GetClangRev(new_clang_update_py) 425 logging.debug('Found new clang rev: %s' % new_clang_rev) 426 clang_change = ChangedDep(CLANG_UPDATE_SCRIPT_LOCAL_PATH, None, old_clang_rev, new_clang_rev) 427 return ClangChange(mirror_change, clang_change) 428 429 430def GenerateCommitMessage( 431 rev_update, 432 current_commit_pos, 433 new_commit_pos, 434 changed_deps_list, 435 autoroll, 436 clang_change, 437): 438 current_cr_rev = rev_update.current_chromium_rev[0:10] 439 new_cr_rev = rev_update.new_chromium_rev[0:10] 440 rev_interval = '%s..%s' % (current_cr_rev, new_cr_rev) 441 git_number_interval = '%s:%s' % (current_commit_pos, new_commit_pos) 442 443 commit_msg = [] 444 # Autoroll already adds chromium_revision changes to commit message 445 if not autoroll: 446 commit_msg.extend([ 447 'Roll chromium_revision %s (%s)\n' % (rev_interval, git_number_interval), 448 'Change log: %s' % (CHROMIUM_LOG_TEMPLATE % rev_interval), 449 'Full diff: %s\n' % (CHROMIUM_COMMIT_TEMPLATE % rev_interval) 450 ]) 451 452 def Section(adjective, deps): 453 noun = 'dependency' if len(deps) == 1 else 'dependencies' 454 commit_msg.append('%s %s' % (adjective, noun)) 455 456 tbr_authors = '' 457 if changed_deps_list: 458 Section('Changed', changed_deps_list) 459 460 for c in changed_deps_list: 461 if isinstance(c, ChangedCipdPackage): 462 commit_msg.append('* %s: %s..%s' % (c.path, c.current_version, c.new_version)) 463 else: 464 commit_msg.append('* %s: %s/+log/%s..%s' % 465 (c.path, c.url, c.current_rev[0:10], c.new_rev[0:10])) 466 467 if changed_deps_list: 468 # rev_interval is empty for autoroll, since we are starting from a state 469 # in which chromium_revision is already modified in DEPS 470 if not autoroll: 471 change_url = CHROMIUM_FILE_TEMPLATE % (rev_interval, 'DEPS') 472 commit_msg.append('DEPS diff: %s\n' % change_url) 473 else: 474 commit_msg.append('No dependencies changed.') 475 476 c = clang_change 477 if (c and (c.clang_change.current_rev != c.clang_change.new_rev)): 478 commit_msg.append('Clang version changed %s:%s' % 479 (c.clang_change.current_rev, c.clang_change.new_rev)) 480 481 rev_clang = rev_interval = '%s..%s' % (c.mirror_change.current_rev, 482 c.mirror_change.new_rev) 483 change_url = CLANG_FILE_TEMPLATE % (rev_clang, CLANG_UPDATE_SCRIPT_URL_PATH) 484 commit_msg.append('Details: %s\n' % change_url) 485 else: 486 commit_msg.append('No update to Clang.\n') 487 488 # Autoroll takes care of BUG and TBR in commit message 489 if not autoroll: 490 # TBR needs to be non-empty for Gerrit to process it. 491 git_author = _RunCommand(['git', 'config', 'user.email'], 492 working_dir=CHECKOUT_SRC_DIR)[0].splitlines()[0] 493 tbr_authors = git_author + ',' + tbr_authors 494 495 commit_msg.append('TBR=%s' % tbr_authors) 496 commit_msg.append('BUG=None') 497 498 return '\n'.join(commit_msg) 499 500 501def UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content, autoroll): 502 """Update the DEPS file with the new revision.""" 503 504 with open(deps_filename, 'rb') as deps_file: 505 deps_content = deps_file.read() 506 # Autoroll takes care of updating 'chromium_revision', thus we don't need to. 507 if not autoroll: 508 # Update the chromium_revision variable. 509 deps_content = deps_content.replace(rev_update.current_chromium_rev, 510 rev_update.new_chromium_rev) 511 512 # Add and remove dependencies. For now: only generated android deps. 513 # Since gclient cannot add or remove deps, we rely on the fact that 514 # these android deps are located in one place to copy/paste. 515 deps_re = re.compile(ANDROID_DEPS_START + '.*' + ANDROID_DEPS_END, re.DOTALL) 516 new_deps = deps_re.search(new_cr_content) 517 old_deps = deps_re.search(deps_content) 518 if not new_deps or not old_deps: 519 faulty = 'Chromium' if not new_deps else 'ANGLE' 520 raise RollError('Was expecting to find "%s" and "%s"\n' 521 'in %s DEPS' % (ANDROID_DEPS_START, ANDROID_DEPS_END, faulty)) 522 523 replacement = new_deps.group(0).replace('src/third_party/android_deps', 524 'third_party/android_deps') 525 replacement = replacement.replace('checkout_android', 526 'checkout_android and not build_with_chromium') 527 528 deps_content = deps_re.sub(replacement, deps_content) 529 530 with open(deps_filename, 'wb') as deps_file: 531 deps_file.write(deps_content) 532 533 # Update each individual DEPS entry. 534 for dep in changed_deps: 535 # We don't sync deps on autoroller, so ignore missing local deps 536 if not autoroll: 537 local_dep_dir = os.path.join(CHECKOUT_ROOT_DIR, dep.path) 538 if not os.path.isdir(local_dep_dir): 539 raise RollError('Cannot find local directory %s. Either run\n' 540 'gclient sync --deps=all\n' 541 'or make sure the .gclient file for your solution contains all ' 542 'platforms in the target_os list, i.e.\n' 543 'target_os = ["android", "unix", "mac", "ios", "win"];\n' 544 'Then run "gclient sync" again.' % local_dep_dir) 545 if isinstance(dep, ChangedCipdPackage): 546 package = dep.package.format() # Eliminate double curly brackets 547 update = '%s:%s@%s' % (dep.path, package, dep.new_version) 548 else: 549 update = '%s@%s' % (dep.path, dep.new_rev) 550 gclient_cmd = 'gclient' 551 if platform.system() == 'Windows': 552 gclient_cmd += '.bat' 553 _RunCommand([gclient_cmd, 'setdep', '--revision', update], working_dir=CHECKOUT_SRC_DIR) 554 555 556def _IsTreeClean(): 557 stdout, _ = _RunCommand(['git', 'status', '--porcelain']) 558 if len(stdout) == 0: 559 return True 560 561 logging.error('Dirty/unversioned files:\n%s', stdout) 562 return False 563 564 565def _EnsureUpdatedMainBranch(dry_run): 566 current_branch = _RunCommand(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])[0].splitlines()[0] 567 if current_branch != 'main': 568 logging.error('Please checkout the main branch and re-run this script.') 569 if not dry_run: 570 sys.exit(-1) 571 572 logging.info('Updating main branch...') 573 _RunCommand(['git', 'pull']) 574 575 576def _CreateRollBranch(dry_run): 577 logging.info('Creating roll branch: %s', ROLL_BRANCH_NAME) 578 if not dry_run: 579 _RunCommand(['git', 'checkout', '-b', ROLL_BRANCH_NAME]) 580 581 582def _RemovePreviousRollBranch(dry_run): 583 active_branch, branches = _GetBranches() 584 if active_branch == ROLL_BRANCH_NAME: 585 active_branch = 'main' 586 if ROLL_BRANCH_NAME in branches: 587 logging.info('Removing previous roll branch (%s)', ROLL_BRANCH_NAME) 588 if not dry_run: 589 _RunCommand(['git', 'checkout', active_branch]) 590 _RunCommand(['git', 'branch', '-D', ROLL_BRANCH_NAME]) 591 592 593def _LocalCommit(commit_msg, dry_run): 594 logging.info('Committing changes locally.') 595 if not dry_run: 596 _RunCommand(['git', 'add', '--update', '.']) 597 _RunCommand(['git', 'commit', '-m', commit_msg]) 598 599 600def _LocalCommitAmend(commit_msg, dry_run): 601 logging.info('Amending changes to local commit.') 602 if not dry_run: 603 old_commit_msg = _RunCommand(['git', 'log', '-1', '--pretty=%B'])[0].strip() 604 logging.debug('Existing commit message:\n%s\n', old_commit_msg) 605 606 bug_index = old_commit_msg.rfind('Bug:') 607 if bug_index == -1: 608 logging.error('"Bug:" not found in commit message.') 609 if not dry_run: 610 sys.exit(-1) 611 new_commit_msg = old_commit_msg[:bug_index] + commit_msg + '\n' + old_commit_msg[bug_index:] 612 613 _RunCommand(['git', 'commit', '-a', '--amend', '-m', new_commit_msg]) 614 615 616def ChooseCQMode(skip_cq, cq_over, current_commit_pos, new_commit_pos): 617 if skip_cq: 618 return 0 619 if (new_commit_pos - current_commit_pos) < cq_over: 620 return 1 621 return 2 622 623 624def _UploadCL(commit_queue_mode): 625 """Upload the committed changes as a changelist to Gerrit. 626 627 commit_queue_mode: 628 - 2: Submit to commit queue. 629 - 1: Run trybots but do not submit to CQ. 630 - 0: Skip CQ, upload only. 631 """ 632 cmd = ['git', 'cl', 'upload', '--force', '--bypass-hooks', '--send-mail'] 633 cmd.extend(['--cc', NOTIFY_EMAIL]) 634 if commit_queue_mode >= 2: 635 logging.info('Sending the CL to the CQ...') 636 cmd.extend(['--use-commit-queue']) 637 elif commit_queue_mode >= 1: 638 logging.info('Starting CQ dry run...') 639 cmd.extend(['--cq-dry-run']) 640 extra_env = { 641 'EDITOR': 'true', 642 'SKIP_GCE_AUTH_FOR_GIT': '1', 643 } 644 stdout, stderr = _RunCommand(cmd, extra_env=extra_env) 645 logging.debug('Output from "git cl upload":\nstdout:\n%s\n\nstderr:\n%s', stdout, stderr) 646 647 648def GetRollRevisionRanges(opts, angle_deps): 649 current_cr_rev = angle_deps['vars']['chromium_revision'] 650 new_cr_rev = opts.revision 651 if not new_cr_rev: 652 stdout, _ = _RunCommand(['git', 'ls-remote', CHROMIUM_SRC_URL, 'HEAD']) 653 head_rev = stdout.strip().split('\t')[0] 654 logging.info('No revision specified. Using HEAD: %s', head_rev) 655 new_cr_rev = head_rev 656 657 return ChromiumRevisionUpdate(current_cr_rev, new_cr_rev) 658 659 660def main(): 661 p = argparse.ArgumentParser() 662 p.add_argument( 663 '--clean', 664 action='store_true', 665 default=False, 666 help='Removes any previous local roll branch.') 667 p.add_argument( 668 '-r', 669 '--revision', 670 help=('Chromium Git revision to roll to. Defaults to the ' 671 'Chromium HEAD revision if omitted.')) 672 p.add_argument( 673 '--dry-run', 674 action='store_true', 675 default=False, 676 help=('Calculate changes and modify DEPS, but don\'t create ' 677 'any local branch, commit, upload CL or send any ' 678 'tryjobs.')) 679 p.add_argument( 680 '-i', 681 '--ignore-unclean-workdir', 682 action='store_true', 683 default=False, 684 help=('Ignore if the current branch is not main or if there ' 685 'are uncommitted changes (default: %(default)s).')) 686 grp = p.add_mutually_exclusive_group() 687 grp.add_argument( 688 '--skip-cq', 689 action='store_true', 690 default=False, 691 help='Skip sending the CL to the CQ (default: %(default)s)') 692 grp.add_argument( 693 '--cq-over', 694 type=int, 695 default=1, 696 help=('Commit queue dry run if the revision difference ' 697 'is below this number (default: %(default)s)')) 698 grp.add_argument( 699 '--autoroll', 700 action='store_true', 701 default=False, 702 help='Autoroller mode - amend existing commit, ' 703 'do not create nor upload a CL (default: %(default)s)') 704 p.add_argument( 705 '-v', 706 '--verbose', 707 action='store_true', 708 default=False, 709 help='Be extra verbose in printing of log messages.') 710 opts = p.parse_args() 711 712 if opts.verbose: 713 logging.basicConfig(level=logging.DEBUG) 714 else: 715 logging.basicConfig(level=logging.INFO) 716 717 # We don't have locally sync'ed deps on autoroller, 718 # so trust it to have depot_tools in path 719 if not opts.autoroll: 720 AddDepotToolsToPath() 721 722 if not opts.ignore_unclean_workdir and not _IsTreeClean(): 723 logging.error('Please clean your local checkout first.') 724 return 1 725 726 if opts.clean: 727 _RemovePreviousRollBranch(opts.dry_run) 728 729 if not opts.ignore_unclean_workdir: 730 _EnsureUpdatedMainBranch(opts.dry_run) 731 732 deps_filename = os.path.join(CHECKOUT_SRC_DIR, 'DEPS') 733 angle_deps = ParseLocalDepsFile(deps_filename) 734 735 rev_update = GetRollRevisionRanges(opts, angle_deps) 736 737 current_commit_pos = ParseCommitPosition(ReadRemoteCrCommit(rev_update.current_chromium_rev)) 738 new_commit_pos = ParseCommitPosition(ReadRemoteCrCommit(rev_update.new_chromium_rev)) 739 740 new_cr_content = ReadRemoteCrFile('DEPS', rev_update.new_chromium_rev) 741 new_cr_deps = ParseDepsDict(new_cr_content) 742 changed_deps = CalculateChangedDeps(angle_deps, new_cr_deps) 743 clang_change = CalculateChangedClang(changed_deps, opts.autoroll) 744 commit_msg = GenerateCommitMessage(rev_update, current_commit_pos, new_commit_pos, 745 changed_deps, opts.autoroll, clang_change) 746 logging.debug('Commit message:\n%s', commit_msg) 747 748 # We are updating a commit that autoroll has created, using existing branch 749 if not opts.autoroll: 750 _CreateRollBranch(opts.dry_run) 751 752 if not opts.dry_run: 753 UpdateDepsFile(deps_filename, rev_update, changed_deps, new_cr_content, opts.autoroll) 754 755 if opts.autoroll: 756 _LocalCommitAmend(commit_msg, opts.dry_run) 757 else: 758 if _IsTreeClean(): 759 logging.info("No DEPS changes detected, skipping CL creation.") 760 else: 761 _LocalCommit(commit_msg, opts.dry_run) 762 commit_queue_mode = ChooseCQMode(opts.skip_cq, opts.cq_over, current_commit_pos, 763 new_commit_pos) 764 logging.info('Uploading CL...') 765 if not opts.dry_run: 766 _UploadCL(commit_queue_mode) 767 return 0 768 769 770if __name__ == '__main__': 771 sys.exit(main()) 772