Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/python
      2 #
      3 # Copyright (C) 2012 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 
     17 """Merge Chromium into the Android tree."""
     18 
     19 import contextlib
     20 import logging
     21 import optparse
     22 import os
     23 import re
     24 import shutil
     25 import sys
     26 import urllib2
     27 
     28 import merge_common
     29 
     30 
     31 # We need to import this *after* merging from upstream to get the latest
     32 # version. Set it to none here to catch uses before it's imported.
     33 webview_licenses = None
     34 
     35 
     36 AUTOGEN_MESSAGE = 'This commit was generated by merge_from_chromium.py.'
     37 SRC_GIT_BRANCH = 'refs/remotes/history/upstream-master'
     38 
     39 
     40 def _ReadGitFile(sha1, path, git_url=None, git_branch=None):
     41   """Reads a file from a (possibly remote) git project at a specific revision.
     42 
     43   Args:
     44     sha1: The SHA1 at which to read.
     45     path: The relative path of the file to read.
     46     git_url: The URL of the git server, if reading a remote project.
     47     git_branch: The branch to fetch, if reading a remote project.
     48   Returns:
     49     The contents of the specified file.
     50   """
     51   if git_url:
     52     merge_common.GetCommandStdout(['git', 'fetch', '-f', git_url, git_branch])
     53   return merge_common.GetCommandStdout(['git', 'show', '%s:%s' % (sha1, path)])
     54 
     55 
     56 def _ParseDEPS(deps_content):
     57   """Parses the .DEPS.git file from Chromium and returns its contents.
     58 
     59   Args:
     60     deps_content: The contents of the .DEPS.git file as text.
     61   Returns:
     62     A dictionary of the contents of .DEPS.git at the specified revision
     63   """
     64 
     65   class FromImpl(object):
     66     """Used to implement the From syntax."""
     67 
     68     def __init__(self, module_name):
     69       self.module_name = module_name
     70 
     71     def __str__(self):
     72       return 'From("%s")' % self.module_name
     73 
     74   class _VarImpl(object):
     75     def __init__(self, custom_vars, local_scope):
     76       self._custom_vars = custom_vars
     77       self._local_scope = local_scope
     78 
     79     def Lookup(self, var_name):
     80       """Implements the Var syntax."""
     81       if var_name in self._custom_vars:
     82         return self._custom_vars[var_name]
     83       elif var_name in self._local_scope.get('vars', {}):
     84         return self._local_scope['vars'][var_name]
     85       raise Exception('Var is not defined: %s' % var_name)
     86 
     87   tmp_locals = {}
     88   var = _VarImpl({}, tmp_locals)
     89   tmp_globals = {'From': FromImpl, 'Var': var.Lookup, 'deps_os': {}}
     90   exec(deps_content) in tmp_globals, tmp_locals
     91   return tmp_locals
     92 
     93 
     94 def _GetProjectMergeInfo(projects, deps_vars):
     95   """Gets the git URL and SHA1 for each project based on .DEPS.git.
     96 
     97   Args:
     98     projects: The list of projects to consider.
     99     deps_vars: The dictionary of dependencies from .DEPS.git.
    100   Returns:
    101     A dictionary from project to git URL and SHA1 - 'path: (url, sha1)'
    102   Raises:
    103     TemporaryMergeError: if a project to be merged is not found in .DEPS.git.
    104   """
    105   deps_fallback_order = [
    106       deps_vars['deps'],
    107       deps_vars['deps_os']['unix'],
    108       deps_vars['deps_os']['android'],
    109   ]
    110   result = {}
    111   for path in projects:
    112     for deps in deps_fallback_order:
    113       if len(path) > 0:
    114         upstream_path = os.path.join('src', path)
    115       else:
    116         upstream_path = 'src'
    117       url_plus_sha1 = deps.get(upstream_path)
    118       if url_plus_sha1:
    119         break
    120     else:
    121       raise merge_common.TemporaryMergeError(
    122           'Could not find .DEPS.git entry for project %s. This probably '
    123           'means that the project list in merge_from_chromium.py needs to be '
    124           'updated.' % path)
    125     match = re.match('(.*?)@(.*)', url_plus_sha1)
    126     url = match.group(1)
    127     sha1 = match.group(2)
    128     logging.debug('  Got URL %s and SHA1 %s for project %s', url, sha1, path)
    129     result[path] = {'url': url, 'sha1': sha1}
    130   return result
    131 
    132 
    133 def _MergeProjects(version, root_sha1, target, unattended, buildspec_url):
    134   """Merges each required Chromium project into the Android repository.
    135 
    136   .DEPS.git is consulted to determine which revision each project must be merged
    137   at. Only a whitelist of required projects are merged.
    138 
    139   Args:
    140     version: The version to mention in generated commit messages.
    141     root_sha1: The git hash to merge in the root repository.
    142     target: The target branch to merge to.
    143     unattended: Run in unattended mode.
    144     buildspec_url: URL for buildspec repository, when merging a branch.
    145   Raises:
    146     TemporaryMergeError: If incompatibly licensed code is left after pruning.
    147   """
    148   # The logic for this step lives here, in the Android tree, as it makes no
    149   # sense for a Chromium tree to know about this merge.
    150 
    151   if unattended:
    152     branch_create_flag = '-B'
    153   else:
    154     branch_create_flag = '-b'
    155   branch_name = 'merge-from-chromium-%s' % version
    156 
    157   logging.debug('Parsing DEPS ...')
    158   if root_sha1:
    159     deps_content = _ReadGitFile(root_sha1, '.DEPS.git')
    160   else:
    161     deps_content = _ReadGitFile('FETCH_HEAD', version + '/DEPS',
    162                                 buildspec_url, 'master')
    163 
    164   deps_vars = _ParseDEPS(deps_content)
    165 
    166   merge_info = _GetProjectMergeInfo(merge_common.THIRD_PARTY_PROJECTS,
    167                                     deps_vars)
    168 
    169   for path in merge_info:
    170     # webkit needs special handling as we have a local mirror
    171     local_mirrored = path == 'third_party/WebKit'
    172     url = merge_info[path]['url']
    173     sha1 = merge_info[path]['sha1']
    174     dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    175     if local_mirrored:
    176       remote = 'history'
    177     else:
    178       remote = 'goog'
    179     merge_common.GetCommandStdout(['git', 'checkout',
    180                                    branch_create_flag, branch_name,
    181                                    '-t', remote + '/' + target],
    182                                   cwd=dest_dir)
    183     if not local_mirrored or not root_sha1:
    184       logging.debug('Fetching project %s at %s ...', path, sha1)
    185       fetch_args = ['git', 'fetch', url]
    186       if not root_sha1:
    187         # Only try to fetch the specific SHA1 when merging a branch.
    188         # Older versions of git cannot fetch SHA1s directly, and trunk merges
    189         # should be using versions that are available on the default branch.
    190         fetch_args.append(sha1)
    191       merge_common.GetCommandStdout(fetch_args, cwd=dest_dir)
    192     if merge_common.GetCommandStdout(['git', 'rev-list', '-1', 'HEAD..' + sha1],
    193                                      cwd=dest_dir):
    194       logging.debug('Merging project %s at %s ...', path, sha1)
    195       # Merge conflicts make git merge return 1, so ignore errors
    196       merge_common.GetCommandStdout(['git', 'merge', '--no-commit', sha1],
    197                                     cwd=dest_dir, ignore_errors=True)
    198       merge_common.CheckNoConflictsAndCommitMerge(
    199           'Merge %s from %s at %s\n\n%s' % (path, url, sha1, AUTOGEN_MESSAGE),
    200           cwd=dest_dir, unattended=unattended)
    201     else:
    202       logging.debug('No new commits to merge in project %s', path)
    203 
    204   # Handle root repository separately.
    205   merge_common.GetCommandStdout(['git', 'checkout',
    206                                  branch_create_flag, branch_name,
    207                                  '-t', 'history/' + target])
    208   if not root_sha1:
    209     merge_info = _GetProjectMergeInfo([''], deps_vars)
    210     url = merge_info['']['url']
    211     root_sha1 = merge_info['']['sha1']
    212     merge_common.GetCommandStdout(['git', 'fetch', url, root_sha1])
    213   logging.debug('Merging Chromium at %s ...', root_sha1)
    214   # Merge conflicts make git merge return 1, so ignore errors
    215   merge_common.GetCommandStdout(['git', 'merge', '--no-commit', root_sha1],
    216                                 ignore_errors=True)
    217   merge_common.CheckNoConflictsAndCommitMerge(
    218       'Merge Chromium at %s (%s)\n\n%s'
    219       % (version, root_sha1, AUTOGEN_MESSAGE), unattended=unattended)
    220 
    221   logging.debug('Getting directories to exclude ...')
    222 
    223   # We import this now that we have merged the latest version.
    224   # It imports to a global in order that it can be used to generate NOTICE
    225   # later. We also disable writing bytecode to keep the source tree clean.
    226   sys.path.append(os.path.join(merge_common.REPOSITORY_ROOT, 'android_webview',
    227                                'tools'))
    228   sys.dont_write_bytecode = True
    229   global webview_licenses
    230   import webview_licenses
    231   import known_issues
    232 
    233   for path, exclude_list in known_issues.KNOWN_INCOMPATIBLE.iteritems():
    234     logging.debug('  %s', '\n  '.join(os.path.join(path, x) for x in
    235                                       exclude_list))
    236     dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    237     merge_common.GetCommandStdout(['git', 'rm', '-rf', '--ignore-unmatch'] +
    238                                   exclude_list, cwd=dest_dir)
    239     if _ModifiedFilesInIndex(dest_dir):
    240       merge_common.GetCommandStdout(['git', 'commit', '-m',
    241                                      'Exclude unwanted directories'],
    242                                     cwd=dest_dir)
    243 
    244 
    245 def _CheckLicenses():
    246   """Check that no incompatibly licensed directories exist."""
    247   directories_left_over = webview_licenses.GetIncompatibleDirectories()
    248   if directories_left_over:
    249     raise merge_common.TemporaryMergeError(
    250         'Incompatibly licensed directories remain: ' +
    251         '\n'.join(directories_left_over))
    252 
    253 
    254 def _GenerateMakefiles(version, unattended):
    255   """Run gyp to generate the Android build system makefiles.
    256 
    257   Args:
    258     version: The version to mention in generated commit messages.
    259     unattended: Run in unattended mode.
    260   """
    261   logging.debug('Generating makefiles ...')
    262 
    263   # TODO(torne): come up with a way to deal with hooks from DEPS properly
    264   # Download linux GN from google storage as per hook in DEPS.
    265   merge_common.GetCommandStdout(['download_from_google_storage',
    266                                  '--no_resume',
    267                                  '--platform=linux*',
    268                                  '--no_auth',
    269                                  '--bucket',
    270                                  'chromium-gn',
    271                                  '-s',
    272                                  'tools/gn/bin/linux/gn.sha1'])
    273 
    274   # TODO(torne): The .tmp files are generated by
    275   # third_party/WebKit/Source/WebCore/WebCore.gyp/WebCore.gyp into the source
    276   # tree. We should avoid this, or at least use a more specific name to avoid
    277   # accidentally removing or adding other files.
    278   for path in merge_common.ALL_PROJECTS:
    279     dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    280     merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch',
    281                                    'GypAndroid.*.mk', '*.target.*.mk',
    282                                    '*.host.*.mk', '*.tmp'], cwd=dest_dir)
    283 
    284   try:
    285     merge_common.GetCommandStdout(['android_webview/tools/gyp_webview', 'all'])
    286   except merge_common.MergeError as e:
    287     if not unattended:
    288       raise
    289     else:
    290       for path in merge_common.ALL_PROJECTS:
    291         merge_common.GetCommandStdout(
    292             ['git', 'reset', '--hard'],
    293             cwd=os.path.join(merge_common.REPOSITORY_ROOT, path))
    294       raise merge_common.TemporaryMergeError('Makefile generation failed: ' +
    295                                              str(e))
    296 
    297   # Copy ARM makefile to ARM64 to allow multiarch builds
    298   for host in ['linux', 'darwin']:
    299     shutil.copy(os.path.join(merge_common.REPOSITORY_ROOT,
    300                              'GypAndroid.%s-arm.mk' % host),
    301                 os.path.join(merge_common.REPOSITORY_ROOT,
    302                              'GypAndroid.%s-arm64.mk' % host))
    303 
    304   for path in merge_common.ALL_PROJECTS:
    305     dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    306     # git add doesn't have an --ignore-unmatch so we have to do this instead:
    307     merge_common.GetCommandStdout(['git', 'add', '-f', 'GypAndroid.*.mk'],
    308                                   ignore_errors=True, cwd=dest_dir)
    309     merge_common.GetCommandStdout(['git', 'add', '-f', '*.target.*.mk'],
    310                                   ignore_errors=True, cwd=dest_dir)
    311     merge_common.GetCommandStdout(['git', 'add', '-f', '*.host.*.mk'],
    312                                   ignore_errors=True, cwd=dest_dir)
    313     merge_common.GetCommandStdout(['git', 'add', '-f', '*.tmp'],
    314                                   ignore_errors=True, cwd=dest_dir)
    315     # Only try to commit the makefiles if something has actually changed.
    316     if _ModifiedFilesInIndex(dest_dir):
    317       merge_common.GetCommandStdout(
    318           ['git', 'commit', '-m',
    319            'Update makefiles after merge of Chromium at %s\n\n%s' %
    320            (version, AUTOGEN_MESSAGE)], cwd=dest_dir)
    321 
    322 
    323 def _ModifiedFilesInIndex(cwd=merge_common.REPOSITORY_ROOT):
    324   """Returns true if git's index contains any changes."""
    325   status = merge_common.GetCommandStdout(['git', 'status', '--porcelain'],
    326                                          cwd=cwd)
    327   return re.search(r'^[MADRC]', status, flags=re.MULTILINE) is not None
    328 
    329 
    330 def _GenerateNoticeFile(version):
    331   """Generates and commits a NOTICE file containing code licenses.
    332 
    333   This covers all third-party code (from Android's perspective) that lives in
    334   the Chromium tree.
    335 
    336   Args:
    337     version: The version to mention in generated commit messages.
    338   """
    339   logging.debug('Regenerating NOTICE file ...')
    340 
    341   contents = webview_licenses.GenerateNoticeFile()
    342 
    343   with open(os.path.join(merge_common.REPOSITORY_ROOT, 'NOTICE'), 'w') as f:
    344     f.write(contents)
    345   merge_common.GetCommandStdout(['git', 'add', 'NOTICE'])
    346   # Only try to commit the NOTICE update if the file has actually changed.
    347   if _ModifiedFilesInIndex():
    348     merge_common.GetCommandStdout([
    349         'git', 'commit', '-m',
    350         'Update NOTICE file after merge of Chromium at %s\n\n%s'
    351         % (version, AUTOGEN_MESSAGE)])
    352 
    353 
    354 def _GenerateLastChange(version):
    355   """Write a build/util/LASTCHANGE file containing the current revision.
    356 
    357   The revision number is compiled into the binary at build time from this file.
    358 
    359   Args:
    360     version: The version to mention in generated commit messages.
    361   """
    362   logging.debug('Updating LASTCHANGE ...')
    363   svn_revision, sha1 = _GetSVNRevisionAndSHA1('HEAD', 'HEAD')
    364   with open(os.path.join(merge_common.REPOSITORY_ROOT, 'build/util/LASTCHANGE'),
    365             'w') as f:
    366     f.write('LASTCHANGE=%s\n' % svn_revision)
    367   merge_common.GetCommandStdout(['git', 'add', '-f', 'build/util/LASTCHANGE'])
    368   logging.debug('Updating LASTCHANGE.blink ...')
    369   with open(os.path.join(merge_common.REPOSITORY_ROOT,
    370                          'build/util/LASTCHANGE.blink'), 'w') as f:
    371     f.write('LASTCHANGE=%s\n' % _GetBlinkRevision())
    372   merge_common.GetCommandStdout(['git', 'add', '-f',
    373                                  'build/util/LASTCHANGE.blink'])
    374   if _ModifiedFilesInIndex():
    375     merge_common.GetCommandStdout([
    376         'git', 'commit', '-m',
    377         'Update LASTCHANGE file after merge of Chromium at %s\n\n%s'
    378         % (version, AUTOGEN_MESSAGE)])
    379 
    380 
    381 def GetLKGR():
    382   """Fetch the last known good release from Chromium's dashboard.
    383 
    384   Returns:
    385     The last known good SVN revision.
    386   """
    387   with contextlib.closing(
    388       urllib2.urlopen('https://chromium-status.appspot.com/lkgr')) as lkgr:
    389     return int(lkgr.read())
    390 
    391 
    392 def GetHEAD():
    393   """Fetch the latest HEAD revision from the git mirror of the Chromium svn
    394   repo.
    395 
    396   Returns:
    397     The latest HEAD SVN revision.
    398   """
    399   (svn_revision, root_sha1) = _GetSVNRevisionAndSHA1(SRC_GIT_BRANCH,
    400                                                      'HEAD')
    401   return int(svn_revision)
    402 
    403 
    404 def _ParseSvnRevisionFromGitCommitMessage(commit_message):
    405   return re.search(r'^git-svn-id: .*@([0-9]+)', commit_message,
    406                    flags=re.MULTILINE).group(1)
    407 
    408 
    409 def _GetSVNRevisionFromSha(sha1):
    410   commit = merge_common.GetCommandStdout([
    411       'git', 'show', '--format=%H%n%b', sha1])
    412   return _ParseSvnRevisionFromGitCommitMessage(commit)
    413 
    414 
    415 def _GetSVNRevisionAndSHA1(git_branch, svn_revision):
    416   logging.debug('Getting SVN revision and SHA1 ...')
    417 
    418   if svn_revision == 'HEAD':
    419     # Just use the latest commit.
    420     commit = merge_common.GetCommandStdout([
    421         'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b',
    422         git_branch])
    423     sha1 = commit.split()[0]
    424     svn_revision = _ParseSvnRevisionFromGitCommitMessage(commit)
    425     return (svn_revision, sha1)
    426 
    427   if svn_revision is None:
    428     # Fetch LKGR from upstream.
    429     svn_revision = GetLKGR()
    430   output = merge_common.GetCommandStdout([
    431       'git', 'log', '--grep=git-svn-id: .*@%s' % svn_revision,
    432       '--format=%H', git_branch])
    433   if not output:
    434     raise merge_common.TemporaryMergeError('Revision %s not found in git repo.'
    435                                            % svn_revision)
    436   # The log grep will sometimes match reverts/reapplies of commits. We take the
    437   # oldest (last) match because the first time it appears in history is
    438   # overwhelmingly likely to be the correct commit.
    439   sha1 = output.split()[-1]
    440   return (svn_revision, sha1)
    441 
    442 
    443 def _GetBlinkRevision():
    444   commit = merge_common.GetCommandStdout([
    445       'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b'],
    446       cwd=os.path.join(merge_common.REPOSITORY_ROOT, 'third_party', 'WebKit'))
    447   return _ParseSvnRevisionFromGitCommitMessage(commit)
    448 
    449 
    450 def Snapshot(svn_revision, root_sha1, release, target, unattended,
    451              buildspec_url):
    452   """Takes a snapshot of the Chromium tree and merges it into Android.
    453 
    454   Android makefiles and a top-level NOTICE file are generated and committed
    455   after the merge.
    456 
    457   Args:
    458     svn_revision: The SVN revision in the Chromium repository to merge from.
    459     root_sha1: The sha1 in the Chromium git mirror to merge from.
    460     release: The Chromium release version to merge from (e.g. "30.0.1599.20").
    461              Only one of svn_revision, root_sha1 and release should be
    462              specified.
    463     target: The target branch to merge to.
    464     unattended: Run in unattended mode.
    465     buildspec_url: URL for buildspec repository, used when merging a release.
    466 
    467   Returns:
    468     True if new commits were merged; False if no new commits were present.
    469   """
    470   if svn_revision:
    471     svn_revision, root_sha1 = _GetSVNRevisionAndSHA1(SRC_GIT_BRANCH,
    472                                                      svn_revision)
    473   elif root_sha1:
    474     svn_revision = _GetSVNRevisionFromSha(root_sha1)
    475 
    476   if svn_revision and root_sha1:
    477     version = svn_revision
    478     if not merge_common.GetCommandStdout(['git', 'rev-list', '-1',
    479                                           'HEAD..' + root_sha1]):
    480       logging.info('No new commits to merge at %s (%s)',
    481                    svn_revision, root_sha1)
    482       return False
    483   elif release:
    484     version = release
    485     root_sha1 = None
    486   else:
    487     raise merge_common.MergeError('No merge source specified')
    488 
    489   logging.info('Snapshotting Chromium at %s (%s)', version, root_sha1)
    490 
    491   # 1. Merge, accounting for excluded directories
    492   _MergeProjects(version, root_sha1, target, unattended, buildspec_url)
    493 
    494   # 2. Generate Android makefiles
    495   _GenerateMakefiles(version, unattended)
    496 
    497   # 3. Check for incompatible licenses
    498   _CheckLicenses()
    499 
    500   # 4. Generate Android NOTICE file
    501   _GenerateNoticeFile(version)
    502 
    503   # 5. Generate LASTCHANGE file
    504   _GenerateLastChange(version)
    505 
    506   return True
    507 
    508 
    509 def Push(version, target):
    510   """Push the finished snapshot to the Android repository."""
    511   src = 'merge-from-chromium-%s' % version
    512   # Use forced pushes ('+' prefix) for the temporary and archive branches in
    513   # case they already got updated by a previous (possibly failed?) merge, but
    514   # do not force push to the real master-chromium branch as this could erase
    515   # downstream changes.
    516   refspecs = ['%s:%s' % (src, target),
    517               '+%s:refs/archive/chromium-%s' % (src, version)]
    518   if target == 'master-chromium':
    519     refspecs.insert(0, '+%s:master-chromium-merge' % src)
    520   for refspec in refspecs:
    521     logging.debug('Pushing to server (%s) ...' % refspec)
    522     for path in merge_common.ALL_PROJECTS:
    523       if path in merge_common.PROJECTS_WITH_FLAT_HISTORY:
    524         remote = 'history'
    525       else:
    526         remote = 'goog'
    527       logging.debug('Pushing %s', path)
    528       dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    529       merge_common.GetCommandStdout(['git', 'push', remote, refspec],
    530                                     cwd=dest_dir)
    531 
    532 
    533 def main():
    534   parser = optparse.OptionParser(usage='%prog [options]')
    535   parser.epilog = ('Takes a snapshot of the Chromium tree at the specified '
    536                    'Chromium SVN revision and merges it into this repository. '
    537                    'Paths marked as excluded for license reasons are removed '
    538                    'as part of the merge. Also generates Android makefiles and '
    539                    'generates a top-level NOTICE file suitable for use in the '
    540                    'Android build.')
    541   parser.add_option(
    542       '', '--svn_revision',
    543       default=None,
    544       help=('Merge to the specified chromium SVN revision, rather than using '
    545             'the current LKGR. Can also pass HEAD to merge from tip of tree. '
    546             'Only one of svn_revision, sha1 and release should be specified'))
    547   parser.add_option(
    548       '', '--sha1',
    549       default=None,
    550       help=('Merge to the specified chromium sha1 revision from ' + SRC_GIT_BRANCH
    551             + ' branch, rather than using the current LKGR. Only one of'
    552             'svn_revision, sha1 and release should be specified.'))
    553   parser.add_option(
    554       '', '--release',
    555       default=None,
    556       help=('Merge to the specified chromium release buildspec (e.g. '
    557             '"30.0.1599.20"). Only one of svn_revision, sha1 and release '
    558             'should be specified.'))
    559   parser.add_option(
    560       '', '--buildspec_url',
    561       default=None,
    562       help=('Git URL for buildspec repository.'))
    563   parser.add_option(
    564       '', '--target',
    565       default='master-chromium', metavar='BRANCH',
    566       help=('Target branch to push to. Defaults to master-chromium.'))
    567   parser.add_option(
    568       '', '--push',
    569       default=False, action='store_true',
    570       help=('Push the result of a previous merge to the server. Note '
    571             'svn_revision must be given.'))
    572   parser.add_option(
    573       '', '--get_lkgr',
    574       default=False, action='store_true',
    575       help=('Just print the current LKGR on stdout and exit.'))
    576   parser.add_option(
    577       '', '--get_head',
    578       default=False, action='store_true',
    579       help=('Just print the current HEAD revision on stdout and exit.'))
    580   parser.add_option(
    581       '', '--unattended',
    582       default=False, action='store_true',
    583       help=('Run in unattended mode.'))
    584   parser.add_option(
    585       '', '--no_changes_exit',
    586       default=0, type='int',
    587       help=('Exit code to use if there are no changes to merge, for scripts.'))
    588   (options, args) = parser.parse_args()
    589   if args:
    590     parser.print_help()
    591     return 1
    592 
    593   if 'ANDROID_BUILD_TOP' not in os.environ:
    594     print >>sys.stderr, 'You need to run the Android envsetup.sh and lunch.'
    595     return 1
    596 
    597   logging.basicConfig(format='%(message)s', level=logging.DEBUG,
    598                       stream=sys.stdout)
    599 
    600   if options.get_lkgr:
    601     print GetLKGR()
    602   elif options.get_head:
    603     logging.disable(logging.CRITICAL)  # Prevent log messages
    604     print GetHEAD()
    605   elif options.push:
    606     if options.release:
    607       Push(options.release, options.target)
    608     elif options.svn_revision:
    609       Push(options.svn_revision, options.target)
    610     else:
    611       print >>sys.stderr, 'You need to pass the version to push.'
    612       return 1
    613   else:
    614     if not Snapshot(options.svn_revision, options.sha1, options.release,
    615                     options.target, options.unattended, options.buildspec_url):
    616       return options.no_changes_exit
    617 
    618   return 0
    619 
    620 if __name__ == '__main__':
    621   sys.exit(main())
    622