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