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
19import contextlib
20import logging
21import optparse
22import os
23import re
24import shutil
25import sys
26import urllib2
27
28import 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.
33webview_licenses = None
34
35
36AUTOGEN_MESSAGE = 'This commit was generated by merge_from_chromium.py.'
37SRC_GIT_BRANCH = 'refs/remotes/history/upstream-master'
38
39
40def _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
56def _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
94def _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
133def _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
245def _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
254def _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
323def _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
330def _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
354def _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
381def 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
392def 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
404def _ParseSvnRevisionFromGitCommitMessage(commit_message):
405  return re.search(r'^git-svn-id: .*@([0-9]+)', commit_message,
406                   flags=re.MULTILINE).group(1)
407
408
409def _GetSVNRevisionFromSha(sha1):
410  commit = merge_common.GetCommandStdout([
411      'git', 'show', '--format=%H%n%b', sha1])
412  return _ParseSvnRevisionFromGitCommitMessage(commit)
413
414
415def _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
443def _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
450def 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
509def 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
533def 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
620if __name__ == '__main__':
621  sys.exit(main())
622