scripts/release/release.py
changeset 1827 c03995a6a88e
parent 1826 12de6d73a908
child 1834 0589bf1395c5
equal deleted inserted replaced
1826:12de6d73a908 1827:c03995a6a88e
    64 
    64 
    65 class Error(error.Error):
    65 class Error(error.Error):
    66     pass
    66     pass
    67 
    67 
    68 
    68 
    69 class SubprocessFailed(Error):
       
    70     """A subprocess returned a non-zero error code."""
       
    71 
       
    72 
       
    73 class AbortedByUser(Error):
    69 class AbortedByUser(Error):
    74     """The operation was aborted by the user."""
    70     """The operation was aborted by the user."""
    75 
    71 
    76 
    72 
    77 class ObstructionError(Error):
    73 class ObstructionError(Error):
    82     """An unexpected state was encountered by an automated step."""
    78     """An unexpected state was encountered by an automated step."""
    83 
    79 
    84 
    80 
    85 class FileAccessError(Error):
    81 class FileAccessError(Error):
    86     """An error occured while accessing a file."""
    82     """An error occured while accessing a file."""
    87 
       
    88 
       
    89 def run(argv, cwd=None, capture=False, split_capture=True, stdin=''):
       
    90     """Run the given command and optionally return its output.
       
    91 
       
    92     Note that if you set capture=True, the command's output is
       
    93     buffered in memory. Output capture should only be used with
       
    94     commands that output small amounts of data. O(kB) is fine, O(MB)
       
    95     is starting to push it a little.
       
    96 
       
    97     Args:
       
    98       argv: A list containing the name of the program to run, followed
       
    99             by its argument vector.
       
   100       cwd: Run the program from this directory.
       
   101       capture: If True, capture the program's stdout stream. If False,
       
   102                stdout will output to sys.stdout.
       
   103       split_capture: If True, return the captured output as a list of
       
   104                      lines. Else, return as a single unaltered string.
       
   105       stdin: The string to feed to the program's stdin stream.
       
   106 
       
   107     Returns:
       
   108       If capture is True, a string containing the combined
       
   109       stdout/stderr output of the program. If capture is False,
       
   110       nothing is returned.
       
   111 
       
   112     Raises:
       
   113       SubprocessFailed: The subprocess exited with a non-zero exit
       
   114                         code.
       
   115     """
       
   116     print util.colorize('# ' + ' '.join(argv), util.WHITE, bold=True)
       
   117 
       
   118     process = subprocess.Popen(argv,
       
   119                                shell=False,
       
   120                                cwd=cwd,
       
   121                                stdin=subprocess.PIPE,
       
   122                                stdout=(subprocess.PIPE if capture else None),
       
   123                                stderr=None)
       
   124     output, _ = process.communicate(input=stdin)
       
   125     if process.returncode != 0:
       
   126         raise SubprocessFailed('Process %s failed with output: %s' %
       
   127                                (argv[0], output))
       
   128     if output is not None and split_capture:
       
   129         return output.strip().split('\n')
       
   130     else:
       
   131         return output
       
   132 
    83 
   133 
    84 
   134 def error(msg):
    85 def error(msg):
   135     """Log an error message."""
    86     """Log an error message."""
   136     print util.colorize(msg, util.RED, bold=True)
    87     print util.colorize(msg, util.RED, bold=True)
   305         Args:
   256         Args:
   306           url: The Subversion repository URL to check out.
   257           url: The Subversion repository URL to check out.
   307           depth: The depth of the working copy root.
   258           depth: The depth of the working copy root.
   308         """
   259         """
   309         assert not self.exists()
   260         assert not self.exists()
   310         run(['svn', 'checkout', '--depth=' + depth, url, self.path()])
   261         util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()])
   311 
   262 
   312     def update(self, path='', depth=None):
   263     def update(self, path='', depth=None):
   313         """Update a working copy path, optionally changing depth.
   264         """Update a working copy path, optionally changing depth.
   314 
   265 
   315         Args:
   266         Args:
   316           path: The working copy path to update.
   267           path: The working copy path to update.
   317           depth: If set, change the depth of the path before updating.
   268           depth: If set, change the depth of the path before updating.
   318         """
   269         """
   319         assert self.exists()
   270         assert self.exists()
   320         if depth is None:
   271         if depth is None:
   321             run(['svn', 'update', self.path(path)])
   272             util.run(['svn', 'update', self.path(path)])
   322         else:
   273         else:
   323             run(['svn', 'update', '--set-depth=' + depth, self.path(path)])
   274             util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)])
   324 
   275 
   325     def revert(self, path=''):
   276     def revert(self, path=''):
   326         """Recursively revert a working copy path.
   277         """Recursively revert a working copy path.
   327 
   278 
   328         Note that this command is more zealous than the 'svn revert'
   279         Note that this command is more zealous than the 'svn revert'
   329         command, as it will also delete any files which subversion
   280         command, as it will also delete any files which subversion
   330         does not know about.
   281         does not know about.
   331         """
   282         """
   332         run(['svn', 'revert', '-R', self.path(path)])
   283         util.run(['svn', 'revert', '-R', self.path(path)])
   333 
   284 
   334         unknown, missing = self._unknownAndMissing(path)
   285         unknown, missing = self._unknownAndMissing(path)
   335         unknown = [os.path.join(self.path(path), p) for p in unknown]
   286         unknown = [os.path.join(self.path(path), p) for p in unknown]
   336 
   287 
   337         if unknown:
   288         if unknown:
   338             # rm -rf makes me uneasy. Verify that all paths to be deleted
   289             # rm -rf makes me uneasy. Verify that all paths to be deleted
   339             # are within the release working copy.
   290             # are within the release working copy.
   340             for p in unknown:
   291             for p in unknown:
   341                 assert p.startswith(self.path())
   292                 assert p.startswith(self.path())
   342 
   293 
   343             run(['rm', '-rf', '--'] + unknown)
   294             util.run(['rm', '-rf', '--'] + unknown)
   344 
   295 
   345     def ls(self, dir=''):
   296     def ls(self, dir=''):
   346         """List the contents of a working copy directory.
   297         """List the contents of a working copy directory.
   347 
   298 
   348         Note that this returns the contents of the directory as seen
   299         Note that this returns the contents of the directory as seen
   349         by the server, not constrained by the depth settings of the
   300         by the server, not constrained by the depth settings of the
   350         local path.
   301         local path.
   351         """
   302         """
   352         assert self.exists()
   303         assert self.exists()
   353         return run(['svn', 'ls', self.path(dir)], capture=True)
   304         return util.run(['svn', 'ls', self.path(dir)], capture=True)
   354 
   305 
   355     def copy(self, src, dest):
   306     def copy(self, src, dest):
   356         """Copy a working copy path.
   307         """Copy a working copy path.
   357 
   308 
   358         The copy is only scheduled for commit, not committed.
   309         The copy is only scheduled for commit, not committed.
   360         Args:
   311         Args:
   361           src: The source working copy path.
   312           src: The source working copy path.
   362           dst: The destination working copy path.
   313           dst: The destination working copy path.
   363         """
   314         """
   364         assert self.exists()
   315         assert self.exists()
   365         run(['svn', 'cp', self.path(src), self.path(dest)])
   316         util.run(['svn', 'cp', self.path(src), self.path(dest)])
   366 
   317 
   367     def propget(self, prop_name, path):
   318     def propget(self, prop_name, path):
   368         """Get the value of a property on a working copy path.
   319         """Get the value of a property on a working copy path.
   369 
   320 
   370         Args:
   321         Args:
   371           prop_name: The property name, eg. 'svn:externals'.
   322           prop_name: The property name, eg. 'svn:externals'.
   372           path: The working copy path on which the property is set.
   323           path: The working copy path on which the property is set.
   373         """
   324         """
   374         assert self.exists()
   325         assert self.exists()
   375         return run(['svn', 'propget', prop_name, self.path(path)], capture=True)
   326         return util.run(['svn', 'propget', prop_name, self.path(path)],
       
   327                         capture=True)
   376 
   328 
   377     def propset(self, prop_name, prop_value, path):
   329     def propset(self, prop_name, prop_value, path):
   378         """Set the value of a property on a working copy path.
   330         """Set the value of a property on a working copy path.
   379 
   331 
   380         The property change is only scheduled for commit, not committed.
   332         The property change is only scheduled for commit, not committed.
   383           prop_name: The property name, eg. 'svn:externals'.
   335           prop_name: The property name, eg. 'svn:externals'.
   384           prop_value: The value that should be set.
   336           prop_value: The value that should be set.
   385           path: The working copy path on which to set the property.
   337           path: The working copy path on which to set the property.
   386         """
   338         """
   387         assert self.exists()
   339         assert self.exists()
   388         run(['svn', 'propset', prop_name, prop_value, self.path(path)])
   340         util.run(['svn', 'propset', prop_name, prop_value, self.path(path)])
   389 
   341 
   390     def add(self, paths):
   342     def add(self, paths):
   391         """Schedule working copy paths for addition.
   343         """Schedule working copy paths for addition.
   392 
   344 
   393         The paths are only scheduled for addition, not committed.
   345         The paths are only scheduled for addition, not committed.
   395         Args:
   347         Args:
   396           paths: The list of working copy paths to add.
   348           paths: The list of working copy paths to add.
   397         """
   349         """
   398         assert self.exists()
   350         assert self.exists()
   399         paths = [self.path(p) for p in paths]
   351         paths = [self.path(p) for p in paths]
   400         run(['svn', 'add'] + paths)
   352         util.run(['svn', 'add'] + paths)
   401 
   353 
   402     def remove(self, paths):
   354     def remove(self, paths):
   403         """Schedule working copy paths for deletion.
   355         """Schedule working copy paths for deletion.
   404 
   356 
   405         The paths are only scheduled for deletion, not committed.
   357         The paths are only scheduled for deletion, not committed.
   407         Args:
   359         Args:
   408           paths: The list of working copy paths to delete.
   360           paths: The list of working copy paths to delete.
   409         """
   361         """
   410         assert self.exists()
   362         assert self.exists()
   411         paths = [self.path(p) for p in paths]
   363         paths = [self.path(p) for p in paths]
   412         run(['svn', 'rm'] + paths)
   364         util.run(['svn', 'rm'] + paths)
   413 
   365 
   414     def status(self, path=''):
   366     def status(self, path=''):
   415         """Return the status of a working copy path.
   367         """Return the status of a working copy path.
   416 
   368 
   417         The status returned is the verbatim output of 'svn status' on
   369         The status returned is the verbatim output of 'svn status' on
   419 
   371 
   420         Args:
   372         Args:
   421           path: The path to examine.
   373           path: The path to examine.
   422         """
   374         """
   423         assert self.exists()
   375         assert self.exists()
   424         return run(['svn', 'status', self.path(path)], capture=True)
   376         return util.run(['svn', 'status', self.path(path)], capture=True)
   425 
   377 
   426     def addRemove(self, path=''):
   378     def addRemove(self, path=''):
   427         """Perform an "addremove" operation a working copy path.
   379         """Perform an "addremove" operation a working copy path.
   428 
   380 
   429         An "addremove" runs 'svn status' and schedules all the unknown
   381         An "addremove" runs 'svn status' and schedules all the unknown
   449         Args:
   401         Args:
   450           message: The commit message to use.
   402           message: The commit message to use.
   451           path: The path to commit.
   403           path: The path to commit.
   452         """
   404         """
   453         assert self.exists()
   405         assert self.exists()
   454         run(['svn', 'commit', '-m', message, self.path(path)])
   406         util.run(['svn', 'commit', '-m', message, self.path(path)])
   455 
   407 
   456     @staticmethod
   408     @staticmethod
   457     def export(url, revision, dest_path):
   409     def export(url, revision, dest_path):
   458         """Export the contents of a repository to a local path.
   410         """Export the contents of a repository to a local path.
   459 
   411 
   470         """
   422         """
   471         assert os.path.isabs(dest_path)
   423         assert os.path.isabs(dest_path)
   472         if os.path.exists(dest_path):
   424         if os.path.exists(dest_path):
   473             raise ObstructionError('Cannot export to obstructed path %s' %
   425             raise ObstructionError('Cannot export to obstructed path %s' %
   474                                    dest_path)
   426                                    dest_path)
   475         run(['svn', 'export', '-r', str(revision), url, dest_path])
   427         util.run(['svn', 'export', '-r', str(revision), url, dest_path])
   476 
   428 
   477     @staticmethod
   429     @staticmethod
   478     def find_tag_rev(url):
   430     def find_tag_rev(url):
   479         """Return the revision at which a remote tag was created.
   431         """Return the revision at which a remote tag was created.
   480 
   432 
   493 
   445 
   494         Args:
   446         Args:
   495           url: The repository URL of the tag to examine.
   447           url: The repository URL of the tag to examine.
   496         """
   448         """
   497         try:
   449         try:
   498             output = run(['svn', 'log', '-q', '--stop-on-copy', url],
   450             output = util.run(['svn', 'log', '-q', '--stop-on-copy', url],
   499                          capture=True)
   451                               capture=True)
   500         except SubprocessFailed:
   452         except util.SubprocessFailed:
   501             raise ExpectationFailed('No tag at URL ' + url)
   453             raise ExpectationFailed('No tag at URL ' + url)
   502         first_rev_line = output[-2]
   454         first_rev_line = output[-2]
   503         first_rev = int(first_rev_line.split()[0][1:])
   455         first_rev = int(first_rev_line.split()[0][1:])
   504         return first_rev
   456         return first_rev
   505 
   457 
   515           A string containing the changes extracted from the remote
   467           A string containing the changes extracted from the remote
   516           repository, in unified diff format suitable for application
   468           repository, in unified diff format suitable for application
   517           using 'patch'.
   469           using 'patch'.
   518         """
   470         """
   519         try:
   471         try:
   520             return run(['svn', 'diff', '-c', str(revision), url],
   472             return util.run(['svn', 'diff', '-c', str(revision), url],
   521                        capture=True, split_capture=False)
   473                             capture=True, split_capture=False)
   522         except SubprocessFailed:
   474         except util.SubprocessFailed:
   523             raise ExpectationFailed('Could not get diff for r%d '
   475             raise ExpectationFailed('Could not get diff for r%d '
   524                                     'from remote repository' % revision)
   476                                     'from remote repository' % revision)
   525 
   477 
   526 
   478 
   527 #
   479 #
   789         diff = self.wc.diff(self.upstream_repos + '/trunk', rev)
   741         diff = self.wc.diff(self.upstream_repos + '/trunk', rev)
   790         if not diff.strip():
   742         if not diff.strip():
   791             raise ExpectationFailed(
   743             raise ExpectationFailed(
   792                 'Retrieved diff is empty. '
   744                 'Retrieved diff is empty. '
   793                 'Did you accidentally cherry-pick a branch change?')
   745                 'Did you accidentally cherry-pick a branch change?')
   794         run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), stdin=diff)
   746         util.run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir),
       
   747                  stdin=diff)
   795         self.wc.addRemove(self.branch_dir)
   748         self.wc.addRemove(self.branch_dir)
   796 
   749 
   797         yaml_path = self.wc.path(self._branchPath('app/app.yaml'))
   750         yaml_path = self.wc.path(self._branchPath('app/app.yaml'))
   798         out = []
   751         out = []
   799         updated_patchlevel = False
   752         updated_patchlevel = False