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