# HG changeset patch # User David Anderson # Date 1236968521 0 # Node ID 3f30b7b14c57ccb42aad1436db825c7919c4ed52 # Parent 0589bf1395c567b61228f947f0bd6377aeea524d Fix indentation to match Melange/Google style. diff -r 0589bf1395c5 -r 3f30b7b14c57 scripts/release/error.py --- a/scripts/release/error.py Fri Mar 13 18:14:54 2009 +0000 +++ b/scripts/release/error.py Fri Mar 13 18:22:01 2009 +0000 @@ -19,4 +19,4 @@ class Error(Exception): - """Base class for release script exceptions.""" + """Base class for release script exceptions.""" diff -r 0589bf1395c5 -r 3f30b7b14c57 scripts/release/release.py --- a/scripts/release/release.py Fri Mar 13 18:14:54 2009 +0000 +++ b/scripts/release/release.py Fri Mar 13 18:22:01 2009 +0000 @@ -64,779 +64,779 @@ class Error(error.Error): - pass + pass class AbortedByUser(Error): - """The operation was aborted by the user.""" + """The operation was aborted by the user.""" class ObstructionError(Error): - """An operation was obstructed by existing data.""" + """An operation was obstructed by existing data.""" class ExpectationFailed(Error): - """An unexpected state was encountered by an automated step.""" + """An unexpected state was encountered by an automated step.""" class FileAccessError(Error): - """An error occured while accessing a file.""" + """An error occured while accessing a file.""" def confirm(prompt, default=False): - """Ask a yes/no question and return the answer. + """Ask a yes/no question and return the answer. - Will reprompt the user until one of "yes", "no", "y" or "n" is - entered. The input is case insensitive. + Will reprompt the user until one of "yes", "no", "y" or "n" is + entered. The input is case insensitive. + + Args: + prompt: The question to ask the user. + default: The answer to return if the user just hits enter. - Args: - prompt: The question to ask the user. - default: The answer to return if the user just hits enter. - - Returns: - True if the user answered affirmatively, False otherwise. - """ - if default: - question = prompt + ' [Yn] ' + Returns: + True if the user answered affirmatively, False otherwise. + """ + if default: + question = prompt + ' [Yn] ' + else: + question = prompt + ' [yN] ' + while True: + try: + answer = raw_input(question).strip().lower() + except EOFError: + raise AbortedByUser('Aborted by ctrl+D') + if not answer: + return default + elif answer in ('y', 'yes'): + return True + elif answer in ('n', 'no'): + return False else: - question = prompt + ' [yN] ' - while True: - try: - answer = raw_input(question).strip().lower() - except EOFError: - raise AbortedByUser('Aborted by ctrl+D') - if not answer: - return default - elif answer in ('y', 'yes'): - return True - elif answer in ('n', 'no'): - return False - else: - log.error('Please answer yes or no.') + log.error('Please answer yes or no.') def getString(prompt): - """Prompt for and return a string.""" - try: - return raw_input(prompt + ' ').strip() - except EOFError: - raise AbortedByUser('Aborted by ctrl+D') + """Prompt for and return a string.""" + try: + return raw_input(prompt + ' ').strip() + except EOFError: + raise AbortedByUser('Aborted by ctrl+D') def getNumber(prompt): - """Prompt for and return a number. + """Prompt for and return a number. - Will reprompt the user until a number is entered. - """ - while True: - value_str = getString(prompt) - try: - return int(value_str) - except ValueError: - log.error('Please enter a number. You entered "%s".' % value_str) + Will reprompt the user until a number is entered. + """ + while True: + value_str = getString(prompt) + try: + return int(value_str) + except ValueError: + log.error('Please enter a number. You entered "%s".' % value_str) def getChoice(intro, prompt, choices, done=None, suggest=None): - """Prompt for and return a choice from a menu. + """Prompt for and return a choice from a menu. - Will reprompt the user until a valid menu entry is chosen. + Will reprompt the user until a valid menu entry is chosen. - Args: - intro: Text to print verbatim before the choice menu. - prompt: The prompt to print right before accepting input. - choices: The list of string choices to display. - done: If not None, the list of indices of previously - selected/completed choices. - suggest: If not None, the index of the choice to highlight as - the suggested choice. + Args: + intro: Text to print verbatim before the choice menu. + prompt: The prompt to print right before accepting input. + choices: The list of string choices to display. + done: If not None, the list of indices of previously + selected/completed choices. + suggest: If not None, the index of the choice to highlight as + the suggested choice. - Returns: - The index in the choices list of the selection the user made. - """ - done = set(done or []) - while True: - print intro - print - for i, entry in enumerate(choices): - done_text = ' (done)' if i in done else '' - indent = '--> ' if i == suggest else ' ' - print '%s%2d. %s%s' % (indent, i+1, entry, done_text) - print - choice = getNumber(prompt) - if 0 < choice <= len(choices): - return choice-1 - log.error('%d is not a valid choice between %d and %d' % - (choice, 1, len(choices))) - print + Returns: + The index in the choices list of the selection the user made. + """ + done = set(done or []) + while True: + print intro + print + for i, entry in enumerate(choices): + done_text = ' (done)' if i in done else '' + indent = '--> ' if i == suggest else ' ' + print '%s%2d. %s%s' % (indent, i+1, entry, done_text) + print + choice = getNumber(prompt) + if 0 < choice <= len(choices): + return choice-1 + log.error('%d is not a valid choice between %d and %d' % + (choice, 1, len(choices))) + print def fileToLines(path): - """Read a file and return it as a list of lines.""" - try: - with file(path) as f: - return f.read().split('\n') - except (IOError, OSError), e: - raise FileAccessError(str(e)) + """Read a file and return it as a list of lines.""" + try: + with file(path) as f: + return f.read().split('\n') + except (IOError, OSError), e: + raise FileAccessError(str(e)) def linesToFile(path, lines): - """Write a list of lines to a file.""" - try: - with file(path, 'w') as f: - f.write('\n'.join(lines)) - except (IOError, OSError), e: - raise FileAccessError(str(e)) + """Write a list of lines to a file.""" + try: + with file(path, 'w') as f: + f.write('\n'.join(lines)) + except (IOError, OSError), e: + raise FileAccessError(str(e)) class Subversion(util.Paths): - """Wrapper for operations on a Subversion working copy. + """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. + 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). + 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. + 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. - """ + 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 __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. + def _unknownAndMissing(self, path): + """Returns lists of unknown and missing files in the working copy. - Args: - path: The working copy path to scan. + Args: + path: The working copy path to scan. - Returns: + 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 + 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. + 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()]) + 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. + 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)]) + 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. + 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)]) + 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] + 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()) + 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) + util.run(['rm', '-rf', '--'] + unknown) - def ls(self, dir=''): - """List the contents of a working copy directory. + 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) + 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. + def copy(self, src, dest): + """Copy a working copy path. - The copy is only scheduled for commit, not committed. + 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)]) + 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. + 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) + 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. + 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. + 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)]) + 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. + def add(self, paths): + """Schedule working copy paths for addition. - The paths are only scheduled for addition, not committed. + 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) + 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. + def remove(self, paths): + """Schedule working copy paths for deletion. - The paths are only scheduled for deletion, not committed. + 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) + 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. + 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. + 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) + 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. + 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. + 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) + 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. + 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)]) + 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. + @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. + 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]) + 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. + @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. + 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. + 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 + 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. + @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. + 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) + 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. # def pristine_wc(f): - """A decorator that cleans up the release repository.""" - @functools.wraps(f) - def revert_wc(self, *args, **kwargs): - self.wc.revert() - return f(self, *args, **kwargs) - return revert_wc + """A decorator that cleans up the release repository.""" + @functools.wraps(f) + def revert_wc(self, *args, **kwargs): + self.wc.revert() + return f(self, *args, **kwargs) + return revert_wc def requires_branch(f): - """A decorator that checks that a release branch is active.""" - @functools.wraps(f) - def check_branch(self, *args, **kwargs): - if self.branch is None: - raise ExpectationFailed( - 'This operation requires an active release branch') - return f(self, *args, **kwargs) - return check_branch + """A decorator that checks that a release branch is active.""" + @functools.wraps(f) + def check_branch(self, *args, **kwargs): + if self.branch is None: + raise ExpectationFailed( + 'This operation requires an active release branch') + return f(self, *args, **kwargs) + return check_branch class ReleaseEnvironment(util.Paths): - """Encapsulates the state of a Melange release rolling environment. + """Encapsulates the state of a Melange release rolling environment. + + This class contains the actual releasing logic, and makes use of + the previously defined utility classes to carry out user commands. - This class contains the actual releasing logic, and makes use of - the previously defined utility classes to carry out user commands. + Attributes: + release_repos: The URL to the Google release repository root. + upstream_repos: The URL to the Melange upstream repository root. + wc: A Subversion object encapsulating a Google SoC working copy. + """ - Attributes: + BRANCH_FILE = 'BRANCH' + + def __init__(self, root, release_repos, upstream_repos): + """Initializer. + + Args: + root: The root of the release environment. release_repos: The URL to the Google release repository root. upstream_repos: The URL to the Melange upstream repository root. - wc: A Subversion object encapsulating a Google SoC working copy. """ + util.Paths.__init__(self, root) + self.wc = Subversion(self.path('google-soc')) + self.release_repos = release_repos.strip('/') + self.upstream_repos = upstream_repos.strip('/') - BRANCH_FILE = 'BRANCH' - - def __init__(self, root, release_repos, upstream_repos): - """Initializer. + if not self.wc.exists(): + self._InitializeWC() + else: + self.wc.revert() - Args: - root: The root of the release environment. - release_repos: The URL to the Google release repository root. - upstream_repos: The URL to the Melange upstream repository root. - """ - util.Paths.__init__(self, root) - self.wc = Subversion(self.path('google-soc')) - self.release_repos = release_repos.strip('/') - self.upstream_repos = upstream_repos.strip('/') + if self.exists(self.BRANCH_FILE): + branch = fileToLines(self.path(self.BRANCH_FILE))[0] + self._switchBranch(branch) + else: + self._switchBranch(None) + + def _InitializeWC(self): + """Check out the initial release repository. - if not self.wc.exists(): - self._InitializeWC() - else: - self.wc.revert() + Will also select the latest release branch, if any, so that + the end state is a fully ready to function release + environment. + """ + log.info('Checking out the release repository') - if self.exists(self.BRANCH_FILE): - branch = fileToLines(self.path(self.BRANCH_FILE))[0] - self._switchBranch(branch) - else: - self._switchBranch(None) - - def _InitializeWC(self): - """Check out the initial release repository. + # Check out a sparse view of the relevant repository paths. + self.wc.checkout(self.release_repos, depth='immediates') + self.wc.update('vendor', depth='immediates') + self.wc.update('branches', depth='immediates') + self.wc.update('tags', depth='immediates') - Will also select the latest release branch, if any, so that - the end state is a fully ready to function release - environment. - """ - log.info('Checking out the release repository') + # Locate the most recent release branch, if any, and switch + # the release environment to it. + branches = self._listBranches() + if not branches: + self._switchBranch(None) + else: + self._switchBranch(branches[-1]) - # Check out a sparse view of the relevant repository paths. - self.wc.checkout(self.release_repos, depth='immediates') - self.wc.update('vendor', depth='immediates') - self.wc.update('branches', depth='immediates') - self.wc.update('tags', depth='immediates') + def _listBranches(self): + """Return a list of available Melange release branches. - # Locate the most recent release branch, if any, and switch - # the release environment to it. - branches = self._listBranches() - if not branches: - self._switchBranch(None) - else: - self._switchBranch(branches[-1]) + Branches are returned in sorted order, from least recent to + most recent in release number ordering. + """ + assert self.wc.exists('branches') + branches = self.wc.ls('branches') - def _listBranches(self): - """Return a list of available Melange release branches. + # Some early release branches used a different naming scheme + # that doesn't sort properly with new-style release names. We + # filter those out here, along with empty lines. + branches = [b.strip('/') for b in branches + if MELANGE_RELEASE_RE.match(b.strip('/'))] - Branches are returned in sorted order, from least recent to - most recent in release number ordering. - """ - assert self.wc.exists('branches') - branches = self.wc.ls('branches') + return sorted(branches) + + def _switchBranch(self, release): + """Activate the branch matching the given release. - # Some early release branches used a different naming scheme - # that doesn't sort properly with new-style release names. We - # filter those out here, along with empty lines. - branches = [b.strip('/') for b in branches - if MELANGE_RELEASE_RE.match(b.strip('/'))] + Once activated, this branch is the target of future release + operations. - return sorted(branches) + None can be passed as the release. The result is that no + branch is active, and all operations that require an active + branch will fail until a branch is activated again. This is + used only at initialization, when it is detected that there + are no available release branches to activate. - def _switchBranch(self, release): - """Activate the branch matching the given release. - - Once activated, this branch is the target of future release - operations. + Args: + release: The version number of a Melange release already + imported in the release repository, or None to + activate no branch. - None can be passed as the release. The result is that no - branch is active, and all operations that require an active - branch will fail until a branch is activated again. This is - used only at initialization, when it is detected that there - are no available release branches to activate. - - Args: - release: The version number of a Melange release already - imported in the release repository, or None to - activate no branch. + """ + if release is None: + self.branch = None + self.branch_dir = None + log.info('No release branch available') + else: + self.wc.update() + assert self.wc.exists('branches/' + release) + linesToFile(self.path(self.BRANCH_FILE), [release]) + self.branch = release + self.branch_dir = 'branches/' + release + self.wc.update(self.branch_dir, depth='infinity') + log.info('Working on branch ' + self.branch) - """ - if release is None: - self.branch = None - self.branch_dir = None - log.info('No release branch available') - else: - self.wc.update() - assert self.wc.exists('branches/' + release) - linesToFile(self.path(self.BRANCH_FILE), [release]) - self.branch = release - self.branch_dir = 'branches/' + release - self.wc.update(self.branch_dir, depth='infinity') - log.info('Working on branch ' + self.branch) + def _branchPath(self, path): + """Return the given path with the release branch path prepended.""" + assert self.branch_dir is not None + return os.path.join(self.branch_dir, path) - def _branchPath(self, path): - """Return the given path with the release branch path prepended.""" - assert self.branch_dir is not None - return os.path.join(self.branch_dir, path) + # + # Release engineering commands. See further down for their + # integration into a commandline interface. + # + @pristine_wc + def update(self): + """Update and clean the release repository""" + self.wc.update() - # - # Release engineering commands. See further down for their - # integration into a commandline interface. - # - @pristine_wc - def update(self): - """Update and clean the release repository""" - self.wc.update() + @pristine_wc + def switchToBranch(self): + """Switch to another Melange release branch""" + branches = self._listBranches() + if not branches: + raise ExpectationFailed( + 'No branches available. Please import one.') - @pristine_wc - def switchToBranch(self): - """Switch to another Melange release branch""" - branches = self._listBranches() - if not branches: - raise ExpectationFailed( - 'No branches available. Please import one.') + choice = getChoice('Available release branches:', + 'Your choice?', + branches, + suggest=len(branches)-1) + self._switchBranch(branches[choice]) + + def _addAppYaml(self): + """Create a Google production app.yaml configuration. - choice = getChoice('Available release branches:', - 'Your choice?', - branches, - suggest=len(branches)-1) - self._switchBranch(branches[choice]) + The file is copied and modified from the upstream + app.yaml.template, configure for Google's Summer of Code App + Engine instance, and committed. + """ + if self.wc.exists(self._branchPath('app/app.yaml')): + raise ObstructionError('app/app.yaml exists already') - def _addAppYaml(self): - """Create a Google production app.yaml configuration. + yaml_path = self._branchPath('app/app.yaml') + self.wc.copy(yaml_path + '.template', yaml_path) - The file is copied and modified from the upstream - app.yaml.template, configure for Google's Summer of Code App - Engine instance, and committed. - """ - if self.wc.exists(self._branchPath('app/app.yaml')): - raise ObstructionError('app/app.yaml exists already') - - yaml_path = self._branchPath('app/app.yaml') - self.wc.copy(yaml_path + '.template', yaml_path) + yaml = fileToLines(self.wc.path(yaml_path)) + out = [] + for i, line in enumerate(yaml): + stripped_line = line.strip() + if 'TODO' in stripped_line: + continue + elif stripped_line == '# application: FIXME': + out.append('application: socghop') + elif stripped_line.startswith('version:'): + out.append(line.lstrip() + 'g0') + out.append('# * initial Google fork of Melange ' + + self.branch) + else: + out.append(line) + linesToFile(self.wc.path(yaml_path), out) - yaml = fileToLines(self.wc.path(yaml_path)) - out = [] - for i, line in enumerate(yaml): - stripped_line = line.strip() - if 'TODO' in stripped_line: - continue - elif stripped_line == '# application: FIXME': - out.append('application: socghop') - elif stripped_line.startswith('version:'): - out.append(line.lstrip() + 'g0') - out.append('# * initial Google fork of Melange ' + - self.branch) - else: - out.append(line) - linesToFile(self.wc.path(yaml_path), out) + self.wc.commit('Create app.yaml with Google patch version g0 ' + 'in branch ' + self.branch) + + def _applyGooglePatches(self): + """Apply Google-specific patches to a vanilla Melange release. - self.wc.commit('Create app.yaml with Google patch version g0 ' - 'in branch ' + self.branch) - - def _applyGooglePatches(self): - """Apply Google-specific patches to a vanilla Melange release. + Each patch is applied and committed in turn. + """ + # Edit the base template to point users to the Google fork + # of the Melange codebase instead of the vanilla release. + tmpl_file = self.wc.path( + self._branchPath('app/soc/templates/soc/base.html')) + tmpl = fileToLines(tmpl_file) + for i, line in enumerate(tmpl): + if 'http://code.google.com/p/soc/source/browse/tags/' in line: + tmpl[i] = line.replace('/p/soc/', '/p/soc-google/') + break + else: + raise ExpectationFailed( + 'No source code link found in base.html') + linesToFile(tmpl_file, tmpl) - Each patch is applied and committed in turn. - """ - # Edit the base template to point users to the Google fork - # of the Melange codebase instead of the vanilla release. - tmpl_file = self.wc.path( - self._branchPath('app/soc/templates/soc/base.html')) - tmpl = fileToLines(tmpl_file) - for i, line in enumerate(tmpl): - if 'http://code.google.com/p/soc/source/browse/tags/' in line: - tmpl[i] = line.replace('/p/soc/', '/p/soc-google/') - break - else: - raise ExpectationFailed( - 'No source code link found in base.html') - linesToFile(tmpl_file, tmpl) + self.wc.commit( + 'Customize the Melange release link in the sidebar menu') - self.wc.commit( - 'Customize the Melange release link in the sidebar menu') + @pristine_wc + def importTag(self): + """Import a new Melange release""" + release = getString('Enter the Melange release to import:') + if not release: + AbortedByUser('No release provided, import aborted') - @pristine_wc - def importTag(self): - """Import a new Melange release""" - release = getString('Enter the Melange release to import:') - if not release: - AbortedByUser('No release provided, import aborted') + branch_dir = 'branches/' + release + if self.wc.exists(branch_dir): + raise ObstructionError('Release %s already imported' % release) - branch_dir = 'branches/' + release - if self.wc.exists(branch_dir): - raise ObstructionError('Release %s already imported' % release) + tag_url = '%s/tags/%s' % (self.upstream_repos, release) + release_rev = Subversion.find_tag_rev(tag_url) - tag_url = '%s/tags/%s' % (self.upstream_repos, release) - release_rev = Subversion.find_tag_rev(tag_url) + if confirm('Confirm import of release %s, tagged at r%d?' % + (release, release_rev)): + # Add an entry to the vendor externals for the Melange + # release. + externals = self.wc.propget('svn:externals', 'vendor/soc') + externals.append('%s -r %d %s' % (release, release_rev, tag_url)) + self.wc.propset('svn:externals', '\n'.join(externals), 'vendor/soc') + self.wc.commit('Add svn:externals entry to pull in Melange ' + 'release %s at r%d.' % (release, release_rev)) - if confirm('Confirm import of release %s, tagged at r%d?' % - (release, release_rev)): - # Add an entry to the vendor externals for the Melange - # release. - externals = self.wc.propget('svn:externals', 'vendor/soc') - externals.append('%s -r %d %s' % (release, release_rev, tag_url)) - self.wc.propset('svn:externals', '\n'.join(externals), 'vendor/soc') - self.wc.commit('Add svn:externals entry to pull in Melange ' - '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)) - # Export the tag into the release repository's branches - 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]) + self.wc.commit('Branch of Melange release %s' % release, + branch_dir) + self._switchBranch(release) - # Add and commit the branch add (very long operation!) - self.wc.add([branch_dir]) - self.wc.commit('Branch of Melange release %s' % release, - branch_dir) - self._switchBranch(release) + # Commit the production GSoC configuration and + # google-specific patches. + self._addAppYaml() + self._applyGooglePatches() + + # All done! + log.info('Melange release %s imported and googlified' % self.branch) - # Commit the production GSoC configuration and - # google-specific patches. - self._addAppYaml() - self._applyGooglePatches() + @requires_branch + @pristine_wc + def cherryPickChange(self): + """Cherry-pick a change from the Melange trunk""" + rev = getNumber('Revision number to cherry-pick:') + bug = getNumber('Issue fixed by this change:') - # All done! - log.info('Melange release %s imported and googlified' % self.branch) - - @requires_branch - @pristine_wc - def cherryPickChange(self): - """Cherry-pick a change from the Melange trunk""" - rev = getNumber('Revision number to cherry-pick:') - bug = getNumber('Issue fixed by this change:') + diff = self.wc.diff(self.upstream_repos + '/trunk', rev) + if not diff.strip(): + raise ExpectationFailed( + 'Retrieved diff is empty. ' + 'Did you accidentally cherry-pick a branch change?') + util.run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), + stdin=diff) + self.wc.addRemove(self.branch_dir) - diff = self.wc.diff(self.upstream_repos + '/trunk', rev) - if not diff.strip(): - raise ExpectationFailed( - 'Retrieved diff is empty. ' - 'Did you accidentally cherry-pick a branch change?') - util.run(['patch', '-p0'], cwd=self.wc.path(self.branch_dir), - stdin=diff) - self.wc.addRemove(self.branch_dir) + yaml_path = self.wc.path(self._branchPath('app/app.yaml')) + out = [] + updated_patchlevel = False + for line in fileToLines(yaml_path): + if line.strip().startswith('version: '): + version = line.strip().split()[-1] + base, patch = line.rsplit('g', 1) + new_version = '%sg%d' % (base, int(patch) + 1) + message = ('Cherry-picked r%d from /p/soc/ to fix issue %d' % + (rev, bug)) + out.append('version: ' + new_version) + out.append('# * ' + message) + updated_patchlevel = True + else: + out.append(line) - yaml_path = self.wc.path(self._branchPath('app/app.yaml')) - out = [] - updated_patchlevel = False - for line in fileToLines(yaml_path): - if line.strip().startswith('version: '): - version = line.strip().split()[-1] - base, patch = line.rsplit('g', 1) - new_version = '%sg%d' % (base, int(patch) + 1) - message = ('Cherry-picked r%d from /p/soc/ to fix issue %d' % - (rev, bug)) - out.append('version: ' + new_version) - out.append('# * ' + message) - updated_patchlevel = True - else: - out.append(line) + if not updated_patchlevel: + log.error('Failed to update Google patch revision') + log.error('Cherry-picking failed') + + linesToFile(yaml_path, out) - if not updated_patchlevel: - log.error('Failed to update Google patch revision') - log.error('Cherry-picking failed') - - linesToFile(yaml_path, out) + log.info('Check the diff about to be committed with:') + log.info('svn diff ' + self.wc.path(self.branch_dir)) + if not confirm('Commit this change?'): + raise AbortedByUser('Cherry-pick aborted') + self.wc.commit(message) + log.info('Cherry-picked r%d from the Melange trunk.' % rev) - log.info('Check the diff about to be committed with:') - log.info('svn diff ' + self.wc.path(self.branch_dir)) - if not confirm('Commit this change?'): - raise AbortedByUser('Cherry-pick aborted') - self.wc.commit(message) - log.info('Cherry-picked r%d from the Melange trunk.' % rev) + MENU_ORDER = [ + update, + switchToBranch, + importTag, + cherryPickChange, + ] + + MENU_STRINGS = [d.__doc__ for d in MENU_ORDER] - MENU_ORDER = [ - update, - switchToBranch, - importTag, - cherryPickChange, - ] - - MENU_STRINGS = [d.__doc__ for d in MENU_ORDER] - - MENU_SUGGESTIONS = { - None: update, - update: cherryPickChange, - switchToBranch: cherryPickChange, - importTag: cherryPickChange, - cherryPickChange: None, - } + MENU_SUGGESTIONS = { + None: update, + update: cherryPickChange, + switchToBranch: cherryPickChange, + importTag: cherryPickChange, + cherryPickChange: None, + } - def interactiveMenu(self): - done = [] - last_choice = None - while True: - # Show the user their previously completed operations and - # a suggested next op, to remind them where they are in - # the release process (useful after long operations that - # may have caused lunch or an extended context switch). - if last_choice is not None: - last_command = self.MENU_ORDER[last_choice] - else: - last_command = None - suggested_next = self.MENU_ORDER.index( - self.MENU_SUGGESTIONS[last_command]) + def interactiveMenu(self): + done = [] + last_choice = None + while True: + # Show the user their previously completed operations and + # a suggested next op, to remind them where they are in + # the release process (useful after long operations that + # may have caused lunch or an extended context switch). + if last_choice is not None: + last_command = self.MENU_ORDER[last_choice] + else: + last_command = None + suggested_next = self.MENU_ORDER.index( + self.MENU_SUGGESTIONS[last_command]) - try: - choice = getChoice('Main menu:', 'Your choice?', - self.MENU_STRINGS, done=done, - suggest=suggested_next) - except (KeyboardInterrupt, AbortedByUser): - log.info('Exiting.') - return - try: - self.MENU_ORDER[choice](self) - except Error, e: - log.error(str(e)) - else: - done.append(choice) - last_choice = choice + try: + choice = getChoice('Main menu:', 'Your choice?', + self.MENU_STRINGS, done=done, + suggest=suggested_next) + except (KeyboardInterrupt, AbortedByUser): + log.info('Exiting.') + return + try: + self.MENU_ORDER[choice](self) + except Error, e: + log.error(str(e)) + else: + done.append(choice) + last_choice = choice def main(argv): - if not (1 <= len(argv) <= 3): - print ('Usage: gsoc-release.py [release repos root URL] ' - '[upstream repos root URL]') - sys.exit(1) + if not (1 <= len(argv) <= 3): + print ('Usage: gsoc-release.py [release repos root URL] ' + '[upstream repos root URL]') + sys.exit(1) - release_repos, upstream_repos = GOOGLE_SOC_REPOS, MELANGE_REPOS - if len(argv) >= 2: - release_repos = argv[1] - if len(argv) == 3: - upstream_repos = argv[2] + release_repos, upstream_repos = GOOGLE_SOC_REPOS, MELANGE_REPOS + if len(argv) >= 2: + release_repos = argv[1] + if len(argv) == 3: + upstream_repos = argv[2] - log.init('release.log') + log.init('release.log') - log.info('Release repository: ' + release_repos) - log.info('Upstream repository: ' + upstream_repos) + log.info('Release repository: ' + release_repos) + log.info('Upstream repository: ' + upstream_repos) - r = ReleaseEnvironment(os.path.abspath('_release_'), - release_repos, - upstream_repos) - r.interactiveMenu() + r = ReleaseEnvironment(os.path.abspath('_release_'), + release_repos, + upstream_repos) + r.interactiveMenu() if __name__ == '__main__': - main(sys.argv) + main(sys.argv) diff -r 0589bf1395c5 -r 3f30b7b14c57 scripts/release/util.py --- a/scripts/release/util.py Fri Mar 13 18:14:54 2009 +0000 +++ b/scripts/release/util.py Fri Mar 13 18:22:01 2009 +0000 @@ -33,11 +33,11 @@ class Error(error.Error): - pass + pass class SubprocessFailed(Error): - """A subprocess returned a non-zero error code.""" + """A subprocess returned a non-zero error code.""" # The magic escape sequence understood by modern terminal emulators to @@ -59,116 +59,116 @@ def _ansi_escape(code): - return _ANSI_ESCAPE % code + return _ANSI_ESCAPE % code def colorize(text, color, bold=False): - """Colorize some text using ANSI color codes. + """Colorize some text using ANSI color codes. - Note that while ANSI color codes look good in a terminal they look - like noise in log files unless viewed in an ANSI color capable - viewer (such as 'less -R'). + Note that while ANSI color codes look good in a terminal they look + like noise in log files unless viewed in an ANSI color capable + viewer (such as 'less -R'). - Args: - text: The text to colorize. - color: One of the color symbols from this module. - bold: If True, make the color brighter. + Args: + text: The text to colorize. + color: One of the color symbols from this module. + bold: If True, make the color brighter. - Returns: - The input text string, appropriately sprinkled with color - codes. Colors are reset to terminal defaults after the input - text. - """ - bold = _ansi_escape(_BOLD) if bold else '' - return '%s%s%s%s' % (bold, _ansi_escape(color), - text, _ansi_escape(_RESET)) + Returns: + The input text string, appropriately sprinkled with color + codes. Colors are reset to terminal defaults after the input + text. + """ + bold = _ansi_escape(_BOLD) if bold else '' + return '%s%s%s%s' % (bold, _ansi_escape(color), + text, _ansi_escape(_RESET)) def decolorize(text): - """Remove ANSI color codes from text.""" - return _ANSI_ESCAPE_RE.sub('', text) + """Remove ANSI color codes from text.""" + return _ANSI_ESCAPE_RE.sub('', text) class Paths(object): - """A helper to construct and check paths under a given root.""" + """A helper to construct and check paths under a given root.""" - def __init__(self, root): - """Initializer. + def __init__(self, root): + """Initializer. - Args: - root: The root of all paths this instance will consider. - """ - self._root = os.path.abspath( - os.path.expandvars(os.path.expanduser(root))) + Args: + root: The root of all paths this instance will consider. + """ + self._root = os.path.abspath( + os.path.expandvars(os.path.expanduser(root))) - def path(self, path=''): - """Construct and return a path under the path root. + def path(self, path=''): + """Construct and return a path under the path root. - Args: - path: The desired path string relative to the root. + Args: + path: The desired path string relative to the root. - Returns: - The absolute path corresponding to the relative input path. - """ - assert not os.path.isabs(path) - return os.path.abspath(os.path.join(self._root, path)) + Returns: + The absolute path corresponding to the relative input path. + """ + assert not os.path.isabs(path) + return os.path.abspath(os.path.join(self._root, path)) - def exists(self, path=''): - """Check for the existence of a path under the path root. + def exists(self, path=''): + """Check for the existence of a path under the path root. - Does not discriminate on the path type (ie. it could be a - directory, a file, a symbolic link...), just checks for the - existence of the path. + Does not discriminate on the path type (ie. it could be a + directory, a file, a symbolic link...), just checks for the + existence of the path. - Args: - path: The path string relative to the root. + Args: + path: The path string relative to the root. - Returns: - True if the path exists, False otherwise. - """ - return os.path.exists(self.path(path)) + Returns: + True if the path exists, False otherwise. + """ + return os.path.exists(self.path(path)) def run(argv, cwd=None, capture=False, split_capture=True, stdin=''): - """Run the given command and optionally return its output. + """Run the given command and optionally return its output. - Note that if you set capture=True, the command's output is - buffered in memory. Output capture should only be used with - commands that output small amounts of data. O(kB) is fine, O(MB) - is starting to push it a little. + Note that if you set capture=True, the command's output is + buffered in memory. Output capture should only be used with + commands that output small amounts of data. O(kB) is fine, O(MB) + is starting to push it a little. - Args: - argv: A list containing the name of the program to run, followed - by its argument vector. - cwd: Run the program from this directory. - capture: If True, capture the program's stdout stream. If False, - stdout will output to sys.stdout. - split_capture: If True, return the captured output as a list of - lines. Else, return as a single unaltered string. - stdin: The string to feed to the program's stdin stream. + Args: + argv: A list containing the name of the program to run, followed + by its argument vector. + cwd: Run the program from this directory. + capture: If True, capture the program's stdout stream. If False, + stdout will output to sys.stdout. + split_capture: If True, return the captured output as a list of + lines. Else, return as a single unaltered string. + stdin: The string to feed to the program's stdin stream. - Returns: - If capture is True, a string containing the combined - stdout/stderr output of the program. If capture is False, - nothing is returned. + Returns: + If capture is True, a string containing the combined + stdout/stderr output of the program. If capture is False, + nothing is returned. - Raises: - SubprocessFailed: The subprocess exited with a non-zero exit - code. - """ - print colorize('# ' + ' '.join(argv), WHITE, bold=True) + Raises: + SubprocessFailed: The subprocess exited with a non-zero exit + code. + """ + print colorize('# ' + ' '.join(argv), WHITE, bold=True) - process = subprocess.Popen(argv, - shell=False, - cwd=cwd, - stdin=subprocess.PIPE, - stdout=(subprocess.PIPE if capture else None), - stderr=None) - output, _ = process.communicate(input=stdin) - if process.returncode != 0: - raise SubprocessFailed('Process %s failed with output: %s' % - (argv[0], output)) - if output is not None and split_capture: - return output.strip().split('\n') - else: - return output + process = subprocess.Popen(argv, + shell=False, + cwd=cwd, + stdin=subprocess.PIPE, + stdout=(subprocess.PIPE if capture else None), + stderr=None) + output, _ = process.communicate(input=stdin) + if process.returncode != 0: + raise SubprocessFailed('Process %s failed with output: %s' % + (argv[0], output)) + if output is not None and split_capture: + return output.strip().split('\n') + else: + return output