• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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