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