# HG changeset patch # User David Anderson # Date 1237002556 0 # Node ID f8728d5e2e07fc4cd295714a0608e5c3c5a8b473 # Parent a0cae3be1412d046ab17346a91c999066905a609 Factor out the Subversion wrapper to a separate module. diff -r a0cae3be1412 -r f8728d5e2e07 scripts/release/error.py --- a/scripts/release/error.py Sat Mar 14 00:12:22 2009 +0000 +++ b/scripts/release/error.py Sat Mar 14 03:49:16 2009 +0000 @@ -20,3 +20,11 @@ class Error(Exception): """Base class for release script exceptions.""" + + +class ObstructionError(Error): + """An operation was obstructed by existing data.""" + + +class ExpectationFailed(Error): + """An unexpected state was encountered by an automated step.""" diff -r a0cae3be1412 -r f8728d5e2e07 scripts/release/release.py --- a/scripts/release/release.py Sat Mar 14 00:12:22 2009 +0000 +++ b/scripts/release/release.py Sat Mar 14 03:49:16 2009 +0000 @@ -49,6 +49,7 @@ import error import log +import subversion import util @@ -71,14 +72,6 @@ """The operation was aborted by the user.""" -class ObstructionError(Error): - """An operation was obstructed by existing data.""" - - -class ExpectationFailed(Error): - """An unexpected state was encountered by an automated step.""" - - class FileAccessError(Error): """An error occured while accessing a file.""" @@ -191,285 +184,6 @@ raise FileAccessError(str(e)) -class Subversion(util.Paths): - """Wrapper for operations on a Subversion working copy. - - An instance of this class is bound to a specific working copy - directory, and provides an API to perform various Subversion - operations on this working copy. - - Some methods take a 'depth' argument. Depth in Subversion is a - feature that allows the creation of arbitrarily shallow or deep - working copies on a per-directory basis. Possible values are - 'none' (no files or directories), 'files' (only files in .), - 'immediates' (files and directories in ., directories checked out - at depth 'none') or 'infinity' (a normal working copy with - everything). - - This class also provides a few static functions that run the 'svn' - tool against remote repositories to gather information or retrieve - data. - - Note that this wrapper also doubles as a Paths object, offering an - easy way to get or check the existence of paths in the working - copy. - """ - - def __init__(self, wc_dir): - util.Paths.__init__(self, wc_dir) - - def _unknownAndMissing(self, path): - """Returns lists of unknown and missing files in the working copy. - - Args: - path: The working copy path to scan. - - Returns: - - Two lists. The first is a list of all unknown paths - (subversion has no knowledge of them), the second is a list - of missing paths (subversion knows about them, but can't - find them). Paths in either list are relative to the input - path. - """ - assert self.exists() - unknown = [] - missing = [] - for line in self.status(path): - if not line.strip(): - continue - if line[0] == '?': - unknown.append(line[7:]) - elif line[0] == '!': - missing.append(line[7:]) - return unknown, missing - - def checkout(self, url, depth='infinity'): - """Check out a working copy from the given URL. - - Args: - url: The Subversion repository URL to check out. - depth: The depth of the working copy root. - """ - assert not self.exists() - util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()]) - - def update(self, path='', depth=None): - """Update a working copy path, optionally changing depth. - - Args: - path: The working copy path to update. - depth: If set, change the depth of the path before updating. - """ - assert self.exists() - if depth is None: - util.run(['svn', 'update', self.path(path)]) - else: - util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)]) - - def revert(self, path=''): - """Recursively revert a working copy path. - - Note that this command is more zealous than the 'svn revert' - command, as it will also delete any files which subversion - does not know about. - """ - util.run(['svn', 'revert', '-R', self.path(path)]) - - unknown, missing = self._unknownAndMissing(path) - unknown = [os.path.join(self.path(path), p) for p in unknown] - - if unknown: - # rm -rf makes me uneasy. Verify that all paths to be deleted - # are within the release working copy. - for p in unknown: - assert p.startswith(self.path()) - - util.run(['rm', '-rf', '--'] + unknown) - - def ls(self, dir=''): - """List the contents of a working copy directory. - - Note that this returns the contents of the directory as seen - by the server, not constrained by the depth settings of the - local path. - """ - assert self.exists() - return util.run(['svn', 'ls', self.path(dir)], capture=True) - - def copy(self, src, dest): - """Copy a working copy path. - - The copy is only scheduled for commit, not committed. - - Args: - src: The source working copy path. - dst: The destination working copy path. - """ - assert self.exists() - util.run(['svn', 'cp', self.path(src), self.path(dest)]) - - def propget(self, prop_name, path): - """Get the value of a property on a working copy path. - - Args: - prop_name: The property name, eg. 'svn:externals'. - path: The working copy path on which the property is set. - """ - assert self.exists() - return util.run(['svn', 'propget', prop_name, self.path(path)], - capture=True) - - def propset(self, prop_name, prop_value, path): - """Set the value of a property on a working copy path. - - The property change is only scheduled for commit, not committed. - - Args: - prop_name: The property name, eg. 'svn:externals'. - prop_value: The value that should be set. - path: The working copy path on which to set the property. - """ - assert self.exists() - util.run(['svn', 'propset', prop_name, prop_value, self.path(path)]) - - def add(self, paths): - """Schedule working copy paths for addition. - - The paths are only scheduled for addition, not committed. - - Args: - paths: The list of working copy paths to add. - """ - assert self.exists() - paths = [self.path(p) for p in paths] - util.run(['svn', 'add'] + paths) - - def remove(self, paths): - """Schedule working copy paths for deletion. - - The paths are only scheduled for deletion, not committed. - - Args: - paths: The list of working copy paths to delete. - """ - assert self.exists() - paths = [self.path(p) for p in paths] - util.run(['svn', 'rm'] + paths) - - def status(self, path=''): - """Return the status of a working copy path. - - The status returned is the verbatim output of 'svn status' on - the path. - - Args: - path: The path to examine. - """ - assert self.exists() - return util.run(['svn', 'status', self.path(path)], capture=True) - - def addRemove(self, path=''): - """Perform an "addremove" operation a working copy path. - - An "addremove" runs 'svn status' and schedules all the unknown - paths (listed as '?') for addition, and all the missing paths - (listed as '!') for deletion. Its main use is to synchronize - working copy state after applying a patch in unified diff - format. - - Args: - path: The path under which unknown/missing files should be - added/removed. - """ - assert self.exists() - unknown, missing = self._unknownAndMissing(path) - if unknown: - self.add(unknown) - if missing: - self.remove(missing) - - def commit(self, message, path=''): - """Commit scheduled changes to the source repository. - - Args: - message: The commit message to use. - path: The path to commit. - """ - assert self.exists() - util.run(['svn', 'commit', '-m', message, self.path(path)]) - - @staticmethod - def export(url, revision, dest_path): - """Export the contents of a repository to a local path. - - Note that while the underlying 'svn export' only requires a - URL, we require that both a URL and a revision be specified, - to fully qualify the data to export. - - Args: - url: The repository URL to export. - revision: The revision to export. - dest_path: The destination directory for the export. Note - that this is an absolute path, NOT a working copy - relative path. - """ - assert os.path.isabs(dest_path) - if os.path.exists(dest_path): - raise ObstructionError('Cannot export to obstructed path %s' % - dest_path) - util.run(['svn', 'export', '-r', str(revision), url, dest_path]) - - @staticmethod - def find_tag_rev(url): - """Return the revision at which a remote tag was created. - - Since tags are immutable by convention, usually the HEAD of a - tag should be the tag creation revision. However, mistakes can - happen, so this function will walk the history of the given - tag URL, stopping on the first revision that was created by - copy. - - This detection is not foolproof. For example: it will be - fooled by a tag that was created, deleted, and recreated by - copy at a different revision. It is not clear what the desired - behavior in these edge cases are, and no attempt is made to - handle them. You should request user confirmation before using - the result of this function. - - Args: - url: The repository URL of the tag to examine. - """ - try: - output = util.run(['svn', 'log', '-q', '--stop-on-copy', url], - capture=True) - except util.SubprocessFailed: - raise ExpectationFailed('No tag at URL ' + url) - first_rev_line = output[-2] - first_rev = int(first_rev_line.split()[0][1:]) - return first_rev - - @staticmethod - def diff(url, revision): - """Retrieve a revision from a remote repository as a unified diff. - - Args: - url: The repository URL on which to perform the diff. - revision: The revision to extract at the given url. - - Returns: - A string containing the changes extracted from the remote - repository, in unified diff format suitable for application - using 'patch'. - """ - try: - return util.run(['svn', 'diff', '-c', str(revision), url], - capture=True, split_capture=False) - except util.SubprocessFailed: - raise ExpectationFailed('Could not get diff for r%d ' - 'from remote repository' % revision) - - # # Decorators for use in ReleaseEnvironment. # @@ -487,7 +201,7 @@ @functools.wraps(f) def check_branch(self, *args, **kwargs): if self.branch is None: - raise ExpectationFailed( + raise error.ExpectationFailed( 'This operation requires an active release branch') return f(self, *args, **kwargs) return check_branch @@ -516,7 +230,7 @@ upstream_repos: The URL to the Melange upstream repository root. """ util.Paths.__init__(self, root) - self.wc = Subversion(self.path('google-soc')) + self.wc = subversion.WorkingCopy(self.path('google-soc')) self.release_repos = release_repos.strip('/') self.upstream_repos = upstream_repos.strip('/') @@ -621,7 +335,7 @@ """Switch to another Melange release branch""" branches = self._listBranches() if not branches: - raise ExpectationFailed( + raise error.ExpectationFailed( 'No branches available. Please import one.') choice = getChoice('Available release branches:', @@ -677,7 +391,7 @@ tmpl[i] = line.replace('/p/soc/', '/p/soc-google/') break else: - raise ExpectationFailed( + raise error.ExpectationFailed( 'No source code link found in base.html') linesToFile(tmpl_file, tmpl) @@ -696,7 +410,7 @@ raise ObstructionError('Release %s already imported' % release) tag_url = '%s/tags/%s' % (self.upstream_repos, release) - release_rev = Subversion.find_tag_rev(tag_url) + release_rev = subversion.find_tag_rev(tag_url) if confirm('Confirm import of release %s, tagged at r%d?' % (release, release_rev)): @@ -709,7 +423,7 @@ 'release %s at r%d.' % (release, release_rev)) # Export the tag into the release repository's branches - Subversion.export(tag_url, release_rev, self.wc.path(branch_dir)) + subversion.export(tag_url, release_rev, self.wc.path(branch_dir)) # Add and commit the branch add (very long operation!) self.wc.add([branch_dir]) @@ -732,9 +446,9 @@ rev = getNumber('Revision number to cherry-pick:') bug = getNumber('Issue fixed by this change:') - diff = self.wc.diff(self.upstream_repos + '/trunk', rev) + diff = subversion.diff(self.upstream_repos + '/trunk', rev) if not diff.strip(): - raise ExpectationFailed( + raise error.ExpectationFailed( 'Retrieved diff is empty. ' 'Did you accidentally cherry-pick a branch change?') util.run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), @@ -811,7 +525,7 @@ return try: self.MENU_ORDER[choice](self) - except Error, e: + except error.Error, e: log.error(str(e)) else: done.append(choice) diff -r a0cae3be1412 -r f8728d5e2e07 scripts/release/subversion.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/release/subversion.py Sat Mar 14 03:49:16 2009 +0000 @@ -0,0 +1,305 @@ +# Copyright 2009 the Melange authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Subversion commandline wrapper. + +This module provides access to a restricted subset of the Subversion +commandline tool. The main functionality offered is an object wrapping +a working copy, providing version control operations within that +working copy. + +A few standalone commands are also implemented to extract data from +arbitrary remote repositories. +""" + +__authors__ = [ + # alphabetical order by last name, please + '"David Anderson" ', + ] + +import error +import util + + +def export(url, revision, dest_path): + """Export the contents of a repository to a local path. + + Note that while the underlying 'svn export' only requires a URL, we + require that both a URL and a revision be specified, to fully + qualify the data to export. + + Args: + url: The repository URL to export. + revision: The revision to export. + dest_path: The destination directory for the export. Note that + this is an absolute path, NOT a working copy relative + path. + """ + assert os.path.isabs(dest_path) + if os.path.exists(dest_path): + raise error.ObstructionError('Cannot export to obstructed path %s' % + dest_path) + util.run(['svn', 'export', '-r', str(revision), url, dest_path]) + + +def find_tag_rev(url): + """Return the revision at which a remote tag was created. + + Since tags are immutable by convention, usually the HEAD of a tag + should be the tag creation revision. However, mistakes can happen, + so this function will walk the history of the given tag URL, + stopping on the first revision that was created by copy. + + This detection is not foolproof. For example: it will be fooled by a + tag that was created, deleted, and recreated by copy at a different + revision. It is not clear what the desired behavior in these edge + cases are, and no attempt is made to handle them. You should request + user confirmation before using the result of this function. + + Args: + url: The repository URL of the tag to examine. + """ + try: + output = util.run(['svn', 'log', '-q', '--stop-on-copy', url], + capture=True) + except util.SubprocessFailed: + raise error.ExpectationFailed('No tag at URL ' + url) + first_rev_line = output[-2] + first_rev = int(first_rev_line.split()[0][1:]) + return first_rev + + +def diff(url, revision): + """Retrieve a revision from a remote repository as a unified diff. + + Args: + url: The repository URL on which to perform the diff. + revision: The revision to extract at the given url. + + Returns: + A string containing the changes extracted from the remote + repository, in unified diff format suitable for application using + 'patch'. + """ + try: + return util.run(['svn', 'diff', '-c', str(revision), url], + capture=True, split_capture=False) + except util.SubprocessFailed: + raise error.ExpectationFailed('Could not get diff for r%d ' + 'from remote repository' % revision) + + +class WorkingCopy(util.Paths): + """Wrapper for operations on a Subversion working copy. + + An instance of this class is bound to a specific working copy + directory, and provides an API to perform various Subversion + operations on this working copy. + + Some methods take a 'depth' argument. Depth in Subversion is a + feature that allows the creation of arbitrarily shallow or deep + working copies on a per-directory basis. Possible values are + 'none' (no files or directories), 'files' (only files in .), + 'immediates' (files and directories in ., directories checked out + at depth 'none') or 'infinity' (a normal working copy with + everything). + + Note that this wrapper also doubles as a Paths object, offering an + easy way to get or check the existence of paths in the working + copy. + """ + + def __init__(self, wc_dir): + util.Paths.__init__(self, wc_dir) + + def _unknownAndMissing(self, path): + """Returns lists of unknown and missing files in the working copy. + + Args: + path: The working copy path to scan. + + Returns: + + Two lists. The first is a list of all unknown paths + (subversion has no knowledge of them), the second is a list + of missing paths (subversion knows about them, but can't + find them). Paths in either list are relative to the input + path. + """ + assert self.exists() + unknown = [] + missing = [] + for line in self.status(path): + if not line.strip(): + continue + if line[0] == '?': + unknown.append(line[7:]) + elif line[0] == '!': + missing.append(line[7:]) + return unknown, missing + + def checkout(self, url, depth='infinity'): + """Check out a working copy from the given URL. + + Args: + url: The Subversion repository URL to check out. + depth: The depth of the working copy root. + """ + assert not self.exists() + util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()]) + + def update(self, path='', depth=None): + """Update a working copy path, optionally changing depth. + + Args: + path: The working copy path to update. + depth: If set, change the depth of the path before updating. + """ + assert self.exists() + if depth is None: + util.run(['svn', 'update', self.path(path)]) + else: + util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)]) + + def revert(self, path=''): + """Recursively revert a working copy path. + + Note that this command is more zealous than the 'svn revert' + command, as it will also delete any files which subversion + does not know about. + """ + util.run(['svn', 'revert', '-R', self.path(path)]) + + unknown, missing = self._unknownAndMissing(path) + unknown = [os.path.join(self.path(path), p) for p in unknown] + + if unknown: + # rm -rf makes me uneasy. Verify that all paths to be deleted + # are within the release working copy. + for p in unknown: + assert p.startswith(self.path()) + + util.run(['rm', '-rf', '--'] + unknown) + + def ls(self, dir=''): + """List the contents of a working copy directory. + + Note that this returns the contents of the directory as seen + by the server, not constrained by the depth settings of the + local path. + """ + assert self.exists() + return util.run(['svn', 'ls', self.path(dir)], capture=True) + + def copy(self, src, dest): + """Copy a working copy path. + + The copy is only scheduled for commit, not committed. + + Args: + src: The source working copy path. + dst: The destination working copy path. + """ + assert self.exists() + util.run(['svn', 'cp', self.path(src), self.path(dest)]) + + def propget(self, prop_name, path): + """Get the value of a property on a working copy path. + + Args: + prop_name: The property name, eg. 'svn:externals'. + path: The working copy path on which the property is set. + """ + assert self.exists() + return util.run(['svn', 'propget', prop_name, self.path(path)], + capture=True) + + def propset(self, prop_name, prop_value, path): + """Set the value of a property on a working copy path. + + The property change is only scheduled for commit, not committed. + + Args: + prop_name: The property name, eg. 'svn:externals'. + prop_value: The value that should be set. + path: The working copy path on which to set the property. + """ + assert self.exists() + util.run(['svn', 'propset', prop_name, prop_value, self.path(path)]) + + def add(self, paths): + """Schedule working copy paths for addition. + + The paths are only scheduled for addition, not committed. + + Args: + paths: The list of working copy paths to add. + """ + assert self.exists() + paths = [self.path(p) for p in paths] + util.run(['svn', 'add'] + paths) + + def remove(self, paths): + """Schedule working copy paths for deletion. + + The paths are only scheduled for deletion, not committed. + + Args: + paths: The list of working copy paths to delete. + """ + assert self.exists() + paths = [self.path(p) for p in paths] + util.run(['svn', 'rm'] + paths) + + def status(self, path=''): + """Return the status of a working copy path. + + The status returned is the verbatim output of 'svn status' on + the path. + + Args: + path: The path to examine. + """ + assert self.exists() + return util.run(['svn', 'status', self.path(path)], capture=True) + + def addRemove(self, path=''): + """Perform an "addremove" operation a working copy path. + + An "addremove" runs 'svn status' and schedules all the unknown + paths (listed as '?') for addition, and all the missing paths + (listed as '!') for deletion. Its main use is to synchronize + working copy state after applying a patch in unified diff + format. + + Args: + path: The path under which unknown/missing files should be + added/removed. + """ + assert self.exists() + unknown, missing = self._unknownAndMissing(path) + if unknown: + self.add(unknown) + if missing: + self.remove(missing) + + def commit(self, message, path=''): + """Commit scheduled changes to the source repository. + + Args: + message: The commit message to use. + path: The path to commit. + """ + assert self.exists() + util.run(['svn', 'commit', '-m', message, self.path(path)])