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