From: <sv...@ww...> - 2004-06-03 06:22:36
|
Author: mkrose Date: 2004-06-02 23:22:26 -0700 (Wed, 02 Jun 2004) New Revision: 983 Added: trunk/CSP/__init__.py trunk/CSP/base/ trunk/CSP/base/__init__.py trunk/CSP/base/app.py trunk/CSP/base/applog.py trunk/CSP/tools/ trunk/CSP/tools/__init__.py trunk/CSP/tools/bootstrap.py trunk/CSP/tools/sublib.py trunk/CSP/tools/subset Log: * Create a python package space for CSP in the trunk workspace. * Add tool for changeset management under subversion. * Add helper modules that can be used by other python applications in CSP. The python path bootstrapping is a bit primitive / brittle at the moment. Adding an installer, and/or a one-time workspace setup script would help a lot. Added: trunk/CSP/__init__.py =================================================================== --- trunk/CSP/__init__.py 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/__init__.py 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1 @@ +CSP = 1 Added: trunk/CSP/base/__init__.py =================================================================== Added: trunk/CSP/base/app.py =================================================================== --- trunk/CSP/base/app.py 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/base/app.py 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1,145 @@ +# Copyright 2004 Mark Rose <mk...@us...> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Define a standard framework for running python applications. + +Applicitions using this module should be structured as follows: + + #! /.../pythonX + # copyright notices + docstring (for usage) + imports + classes and functions + def main(args): ... + app.start() + +The docstring serves as the usage text that is printed when --help is +specified. It can include %(prog)s and %(progpath)s substitutions. + +main() is the main entry point for the application. Non-flag command +line arguments are passed in (excluding the program name), and the +return value serves as the exit code for the application. The program +name and absolute path can be accessed using app.programName() and +app.programPath(), respectively. Command line flags are specified +using app.addOption(...) (same arguments as optparse add_option), and +accessed by app.options.<flagname>. + +The call to app.start() can optionally be placed inside an if __name__ +== '__main__' block. This is not necessary, however, since app.start() +does nothing if called from a non-main module. +""" + +import sys +import os.path +import inspect +import optparse +import exceptions +import logging +import applog + + +class CustomUsageFormatter(optparse.IndentedHelpFormatter): + def format_usage(self, usage): + return usage % {'prog' : programName(), 'progpath' : programPath()} + +log = logging.getLogger('app') + +def _fatal(msg, *args, **kw): + """ + Log a critical message and terminate the application, with an optional exit + code. + + Args: + same as logging.critical() + optional keyword argument 'exit_code' to set the exit code (default 1). + """ + log.critical(msg, *args, **kw) + code = kw.get('exit_code', 1) + sys.exit(code) + +# override logging.fatal, which is deprecated and equivalent to critical(). +# the replacement logs a critical message and terminates the application. +log.fatal = _fatal + +opt = optparse.OptionParser(usage='', formatter=CustomUsageFormatter()) +addOption = opt.add_option + +addOption('-v', '--verbose', default='info', type='string', metavar='<level>', + help='set verbosity level (numeric, or by name)') +addOption('--logfile', default='', type='string', help='log file (default stderr)') + +def usage(): + """Print usage to stdout.""" + opt.print_help() + +def programPath(): + """Return the absolute path of the application program.""" + return os.path.abspath(sys.argv[0]) + +def programName(): + """Return the basename of the application program.""" + return os.path.basename(sys.argv[0]) + +def start(disable_interspersed_args=0): + """ + Start the application. + + Parses arguments, sets up logging, and calls main(args). + """ + if disable_interspersed_args: + opt.disable_interspersed_args() + result = 0 + try: + frame = inspect.stack()[-1][0] + name = frame.f_globals.get('__name__', '') + if name == '__main__': + doc = frame.f_globals.get('__doc__', None) + opt.set_usage(doc) + main = frame.f_globals.get('main', None) + global options + options, args = opt.parse_args() + if options.logfile: + logfile = open(options.logfile, 'w') + else: + logfile = sys.stderr + loghandler = logging.StreamHandler(logfile) + loghandler.setFormatter(applog.formatter) + loghandler.setLevel(logging.NOTSET) + log.addHandler(loghandler) + try: + level = int(options.verbose) + except ValueError: + levelname = options.verbose.upper() + level = logging._levelNames.get(levelname, -1) + if level == -1: + levels = logging._levelNames.keys() + levels = [x.lower() for x in levels if not isinstance(x, int)] + levels.sort() + print >>sys.stderr, 'logging verbosity "%s" unrecognized; valid level names are:' % options.verbose + print >>sys.stderr, ' (' + ','.join(levels) + ')' + sys.exit(1) + log.setLevel(level) + if main: result = main(args) + except KeyboardInterrupt: + log.exception('caught keyboard interrept; aborting.') + except exceptions.SystemExit, e: + raise e + except Exception, e: + log.exception('caught exception %s; aborting' % e) + sys.exit(result) + + Added: trunk/CSP/base/applog.py =================================================================== --- trunk/CSP/base/applog.py 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/base/applog.py 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1,31 @@ +# Copyright 2004 Mark Rose <mk...@us...> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Define standard formating for application logs. +""" + +import sys +import logging + +logging.addLevelName(logging.DEBUG, 'D') +logging.addLevelName(logging.INFO, 'I') +logging.addLevelName(logging.WARNING, 'W') +logging.addLevelName(logging.ERROR, 'E') +logging.addLevelName(logging.CRITICAL, 'C') + +formatter = logging.Formatter('%(asctime)s%(levelname)s_%(module)s.%(lineno)d %(message)s', '%m%d%H%M%S') + Added: trunk/CSP/tools/__init__.py =================================================================== Added: trunk/CSP/tools/bootstrap.py =================================================================== --- trunk/CSP/tools/bootstrap.py 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/tools/bootstrap.py 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1,17 @@ +import sys +import os.path + +if not getattr(sys, 'CSP', 0): + + dn = os.path.dirname + root = dn(dn(dn(__file__))) + sys.path.insert(0, root) + + try: + import CSP + except: + print 'Unable to import the main CSP module. Check that you have' + print 'a complete working copy.' + sys.exit(1) + + sys.CSP = root Added: trunk/CSP/tools/sublib.py =================================================================== --- trunk/CSP/tools/sublib.py 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/tools/sublib.py 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1,160 @@ +import os +import os.path +import popen2 +import re +import time + +from CSP.base import app + +class File: + ADD = 'ADD' + DEL = 'DEL' + MOD = 'MOD' + + def __init__(self, path, root, mode): + self.mode = mode + self.root = root + self.path = path + + def __str__(self): + return '(%s) //%s' % (self.mode, self.path) + + def abspath(self): + return os.path.join(self.root, self.path) + + +def svn_info(): + st = os.popen('svn info').readlines() + info = {} + for line in st: + line = line.strip() + idx = line.find(':') + if idx < 0: continue + info[line[:idx]] = line[idx+1:].strip() + return info + +def svn_st(files=None): + if not files: files = [] + st = os.popen('svn st %s' % ' '.join(files)).readlines() + root = svn_root() + files = [] + for line in st: + path = line[1:].strip() + abspath = os.path.abspath(path) + assert(abspath.startswith(root)) + relpath = abspath[len(root):] + if os.path.isabs(relpath): + relpath = relpath[1:] + basename = os.path.basename(path) + if basename.startswith('.svn'): continue + if line.startswith('M'): mode = File.MOD + elif line.startswith('A'): mode = File.ADD + elif line.startswith('D'): mode = File.DEL + else: continue + files.append(File(relpath, root, mode)) + return files + +#def svn_info(path): +# info = os.popen('svn info %s' % path) +# app.log.info(info.readlines()) + +def svn_rootsvn(): + path = '.svn' + rootsvn = None + while os.path.exists(path): + rootsvn = path + path = os.path.join('..', path) + if not rootsvn: + app.log.fatal('must run in a subversion workspace (.svn not found)') + return rootsvn + +def svn_root(): + return os.path.abspath(os.path.dirname(svn_rootsvn())) + + +def runoe(cmd): + process = popen2.Popen3(cmd, capturestderr=1) + process.tochild.close() + out = process.fromchild.readlines() + err = process.childerr.readlines() + exit_code = process.wait() + if exit_code: + app.log.debug('run "%s" failed (%d): err=%s, out=%s' % (cmd, exit_code, err, out)) + return exit_code, out, err + +def runo(cmd): + exit_code, out, err = runoe(cmd) + return exit_code, out + +def run(cmd): + exit_code, out, err = runoe(cmd) + return exit_code + +def svn_submit_review(name, log, contents): + info = svn_info() + url = info.get('URL', '') + if not url: + app.log.fatal('unable to locate repository URL') + root = os.path.abspath(svn_rootsvn()) + targetdir = 'reviews' + target = os.path.join(root, '.%s' % targetdir) + targetfile = os.path.join(target, 'change') + if os.path.exists(targetfile): + os.unlink(targetfile) + while 1: + source = os.path.join(url, targetdir) + #print 'svn co %s %s 2>/dev/null' % (source, target) + exit_code = run('svn co %s %s' % (source, target)) + if not exit_code: break + app.log.debug('checkout of %s to %s failed.' % (source, target)) + parent = os.path.dirname(url) + if parent == url: + app.log.fatal('could not find review path; have you created one?') + url = parent + if not os.path.exists(targetfile): + app.log.fatal('could not find review path; have you created one?') + app.log.debug('checked out review path from %s' % source) + app.log.debug('submitting changelist %s for review' % name) + while 1: + exit_code = run('svn rm %s' % targetfile) + if exit_code: + app.log.error('unable to remove %s' % targetfile) + break + f = open(targetfile, 'w') + f.write(contents) + f.close() + exit_code = run('svn add %s' % targetfile) + if exit_code: + app.log.error('unable to add %s' % targetfile) + break + exit_code, out = runo('svn ci -m "%s" %s' % (log, targetfile)) + if not exit_code: + ci_log = out + break + app.log.info('failed to submit changelist %s for review: %s' % (name, str(ci))) + exit_code = run('svn revert %s' % targetfile) + if exit_code: + app.log.error('unable to revert changes to %s in order to retry commit' % targetfile) + break + time.sleep(1) + exit_code = run('svn up %s' % targetfile) + if exit_code: + app.log.error('unable to update %s in order to retry commit' % targetfile) + break + if exit_code: + print 'Failed to submit changelist %s for review; see log for details.' % name + return 1 + rev = 0 + re_rev = re.compile(r'Committed revision (\d+)') + for line in ci_log: + m = re_rev.match(line) + if m is not None: + rev = int(m.group(1)) + break + if rev: + print "Changelist %s submitted for review; review id %d." % (name, rev) + else: + print 'Changelist %s submitted for review, but could not determine a review number' + print 'Try running "svn log %s"' % source + return 0 + Added: trunk/CSP/tools/subset =================================================================== --- trunk/CSP/tools/subset 2004-05-31 04:10:07 UTC (rev 982) +++ trunk/CSP/tools/subset 2004-06-03 06:22:26 UTC (rev 983) @@ -0,0 +1,789 @@ +#!/usr/bin/python +# +# Copyright 2004 Mark Rose <mk...@us...> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +""" +Changeset manager for subversion (http://tigris.subversion.org). + +Copyright 2004 Mark Rose. This program is free software; you can +redistributed it and/or modify it under the terms of the GNU General +Public License as published by the Free Software Foundation; either +version 2 of the License, or (at your option) any later version. + +Usage: %(prog)s [options] command [args...] + +Run "%(prog)s help <command>" for help on a specific command. +""" + +# TODO +# clean up error reporting of review submit +# default changeset? +# use edit function for changeset descriptions (honor save/nosave) +# a changeset diff viewer (extend describe)... maybe a textbased +# diff chooser. also need diffstats +# work with tkdiff by piping through undiff + + +import sys +import os +import os.path +import glob +import stat +import random +import tempfile +import optparse +import pickle +import zipfile +import StringIO +import time + +# setup CSP +import bootstrap + +from CSP.tools import sublib +from CSP.base import app + + +def sendmail(sender, recipients, subject, body, attachments=None): + import email + import email.MIMEMultipart + import email.MIMEText + import email.MIMEBase + import smtplib + import mimetypes + outer = email.MIMEMultipart.MIMEMultipart() + outer['Subject'] = subject + outer['To'] = ', '.join(recipients) + outer['From'] = sender + outer.preamble = 'You need a MIME-aware mail reader to view this message.\n' + outer.epilogue = '' + outer.attach(email.MIMEText.MIMEText(body)) + if attachments is not None: + for name, content in attachments: + ctype, encoding = mimetypes.guess_type(name) + if ctype is None or encoding is not None: + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/') + attachment = email.MIMEBase.MIMEBase(maintype, subtype) + attachment.set_payload(content) + email.Encoders.encode_base64(attachment) + attachment.add_header('Content-Disposition', 'attachment', filename=name) + outer.attach(attachment) + s = smtplib.SMTP() + s.connect() + try: + s.sendmail(sender, recipients, outer.as_string()) + except smtplib.SMTPException, e: + return Error('error sending message: %s' % e.smtp_error) + s.close() + return Result(0) + +def edit(text, cursor=1): + tmpname = '.svn.cs.%d' % random.randint(0,100000000) + tmppath = os.path.join(tempfile.gettempdir(), tmpname) + tmpfile = os.fdopen(os.open(tmppath, os.O_CREAT|os.O_WRONLY|os.O_EXCL), 'w') + tmpfile.write(text) + tmpfile.close() + mtime = os.stat(tmppath)[stat.ST_MTIME] + os.system('vi +%d %s' % (cursor, tmppath)) + if mtime == os.stat(tmppath)[stat.ST_MTIME]: + return Result(0) + saved = ''.join(open(tmppath).readlines()) + os.unlink(tmppath) + return Result(saved) + + +class Filter: + def __init__(self, path): + self._sub = 0 + if path.endswith('...'): + path = path[:-3] + self._sub = 1 + self._abs = os.path.abspath(path) + def __call__(self, file): + path = file.abspath() + dirname = os.path.dirname(path) + if self._sub: + return dirname.startswith(self._abs) + else: + return dirname == self._abs + +class Result: + + def __init__(self, value, error=''): + self.value = value + self.error = error + self.ok = not error + + def __str__(self): + if self.error: + return 'error: %s' % self.error + else: + return 'result: %s' % self.value + + +def Error(msg): + return Result(None, error=msg) + + +class Changeset: + + def __init__(self, name, files, pending=0): + self._name = name + self._description = [] + self._files = files + self._date = time.time() + self._pending = pending + + def describe(self): + print + print 'changelist: %s' % self._name + print 'created on: %s' % self.date() + print + print '\n'.join(self._description) + print + if self._files: + for file in self._files: print str(file) + print + return Result(0) + + def submit(self): + files = map(lambda x: x.abspath(), self._files) + exitcode = os.system('svn ci -m \'%s\' %s' % + ('\n'.join(self._description), ' '.join(files))) + if exitcode != 0: + return Error('error submitting changelist') + self._pending = 0 + return Result(0) + + def edit(self, new=0): + if not hasattr(self, '_date'): + self._date = time.time() + info = sublib.svn_info() + URL = info.get('URL', '') + rev = info.get('Revision', '') + desc = self._description + cursor = 5 + text = [] + addtext = text.append + if new: + self._pending = 1 + desc = ['<enter a description here>'] + addtext('# changelist : %s' % self._name) + addtext('# created on : %s' % self.date()) + if self._pending: + addtext('# status : pending') + else: + addtext('# status : submitted') + if URL: + addtext('# repository : %s' % URL) + cursor += 1 + if rev: + addtext('# revision : %s' % rev) + cursor += 1 + addtext('') + addtext('[description]') + text += desc + addtext('') + addtext('[files]') + fileref = {} + for file in self._files: + mode = file.mode + path = file.path + addtext(' //%s # %s' % (path, mode.lower())) + fileref[path] = file + text = '\n'.join(text) + result = edit(text, cursor) + if not result.ok: + return Error('error editing description, aborting') + if not result.value: + return Error('description unchanged, aborting') + lines = result.value.split('\n') + state = 'description' + description = [] + fileset = {} + for line in lines: + line = line.strip() + if state == 'description' and line == '[description]': + state = 'desc' + elif state == 'desc': + if line == '[files]': + state = 'files' + else: + description.append(line) + elif state == 'files': + if line: + idx = line.find('#') + if idx >= 0: + line = line[:idx] + path = line.strip() + assert path.startswith('//') + path = path[2:] + file = fileref.get(path, 0) + assert file + assert not path in fileset.keys() + fileset[path] = file + if description == ['<enter a description here>', '']: + return Error('description unchanged, aborting') + idx = 0 + description.reverse() + for line in description: + if line.strip(): break + idx = idx + 1 + description.reverse() + description = map(lambda x: x.rstrip(), description[:-idx]) + self._description = description + self._files = fileset.values() + return Result(0) + + def _fileIndex(self): + index = {} + for file in self._files: + index[file.abspath()] = file + return index + + def removeFile(self, file): + index = self._fileIndex() + abspath = file.abspath() + file = index.get(abspath, None) + if not file: + return Error('file not found in changeset %s' % self._name) + self._files.remove(file) + return Result(file) + + def addFile(self, file): + index = self._fileIndex() + abspath = file.abspath() + if index.get(abspath, None): + return Error('file already in changeset %s' % self._name) + self._files.append(file) + + def isPending(self): + return self._pending + + def name(self): + return self._name + + def date(self): + return time.ctime(self._date) + + def localtime(self): + return time.localtime(self._date) + + def description(self): + return self._description[:] + + def files(self): + return self._files[:] + + def datecmp(x, y): + return cmp(x._date, y._date) + + datecmp = staticmethod(datecmp) + + +class Workspace: + + def __init__(self, rootsvn=''): + self._sets = {} + self._files = {} + if not rootsvn: rootsvn = sublib.svn_rootsvn() + self._load(rootsvn) + + def _load(self, rootsvn): + ws = os.path.join(rootsvn, '.workspace') + if os.path.exists(ws): + file = open(ws) + wsp = pickle.load(file) + self._sets = wsp.sets + for set in self._sets.values(): + for file in set._files: + abspath = file.abspath() + assert(not self._files.get(abspath, 0)) + self._files[abspath] = set + self._rootsvn = rootsvn + self._ws = ws + + def _save(self): + file = open(self._ws, 'w') + pickle.dump(self, file) + + def __getstate__(self): + return {'sets' : self._sets } + + def getChangeset(self, name): + return self._sets.get(name, None) + + def _closed(self, file): + abspath = file.abspath() + return not self._files.has_key(abspath) + + def assign(self, name, files): + if name == 'default': + dest = None + else: + dest = self.getChangeset(name) + if not dest: + return Error('no changeset "%s"' % name) + files = sublib.svn_st(files) + if not files: + return Error('no files found') + for file in files: + set = self._files.get(file.abspath(), None) + if set: + set.removeFile(file) + if dest: dest.addFile(file) + self.save() + return Result(0) + + def abandon(self, name): + cs = self.getChangeset(name) + if not cs: + return Error('no changeset "%s"' % name) + #for file in cs._files: + # del self._files[file.abspath()] + del self._sets[name] + self.save() + return Result(0) + + def opened(self, filters=None): + files = sublib.svn_st() + if filters: + fileset = {} + for path_filter in filters: + filtered = filter(path_filter, files) + for file in filtered: + fileset[file.abspath()] = file + files = fileset.values() + default = filter(self._closed, files) + for file in files: + if file in default: + name = 'default' + else: + set = self._files.get(file.abspath(), 0) + if set: + name = set._name + else: + name = '?' + print '%s (%s)' % (file, name) + return Result(0) + + def change(self, name): + cs = self._sets.get(name, None) + if cs: + result = cs.edit() + if result.ok: self.save() + return result + files = sublib.svn_st() + files = filter(self._closed, files) + cs = Changeset(name, files) + result = cs.edit(new=1) + if result.ok: + self._sets[name] = cs + self.save() + return result + + def changes(self, long=False, short=False): + if short: + long = False + pending = '*P*' + submitted = '-S-' + else: + if self._sets: print + pending = '*PENDING*' + submitted = 'SUBMITTED' + sets = self._sets.values() + sets.sort(Changeset.datecmp) + for set in sets: + date = time.strftime('%Y-%M-%d %H:%M', set.localtime()) + name = set.name() + description = set.description() + if set.isPending(): + status = pending + else: + status = submitted + print '%s %s | %-10s' % (date, status, name), + if short: + desc = description[0][:40] + print ': %s' % desc + else: + print '\n ' + '\n '.join(description) + if long: + for file in set.files(): + print ' - %s (%s)' % (file.path, file.mode) + print + return Result(0) + + def describe(self, name): + cs = self.getChangeset(name) + if not cs: + return Error('no changeset "%s"' % name) + return cs.describe() + + def submit(self, name): + cs = self.getChangeset(name) + if not cs: + result = self.create(name) + if not result.ok: return result + cs = result.value + else: + result = self.edit(name) + if not result.ok: return result + result = cs.submit() + if result.ok: + #del self._sets[name] + self.save() + return result + + def review(self, name, + save='', + mail='', + sender='', + interactive=1, + context=32): + cs = self.getChangeset(name) + if not cs: + return Error('no changeset "%s"' % name) + log = ('Code review of changeset %s (created %s)\n\n' + '%s\n' % (cs.name(), cs.date(), '\n'.join(cs.description()))) + zipbuffer = StringIO.StringIO() + zip = zipfile.ZipFile(zipbuffer, 'a') + svn_root = sublib.svn_root() + for file in cs.files(): + path = file.abspath() + exit_code, out = sublib.runo('svn --non-interactive --diff-cmd=/usr/bin/diff' + ' --extensions=-U%d diff %s' % (context, path)) + if exit_code: + return Error('unable to diff %s' % path) + + # replace absolute paths with repository paths + if len(out) >= 4 and out[2].startswith('--- ') and out[3].startswith('+++ '): + file1 = out[2][4:] + if file1.startswith(svn_root): + out[2] = '--- /' + file1[len(svn_root):] + file2 = out[3][4:] + if file2.startswith(svn_root): + out[3] = '+++ /' + file2[len(svn_root):] + + diff = ''.join(out) + log = "%s\n%s" % (log, str(file)) + zip.writestr(file.path + '.diff', diff) + zip.writestr('README.txt', log) + zip.close() + zipdata = zipbuffer.getvalue() + if save: + open(save, 'w').write(zipdata) + result = Result(0) + if mail: + recipients = mail + subject = 'Code review, changeset %s' % name + body = ('%s\n' + '___________________________________________________________________\n' + '\n' + 'The attached zipfile contains diffs of each file in the changeset\n' + 'With a properly configured graphical mail client and diff tool, it\n' + 'should be possible to open the individual file diffs with a few\n' + 'mouse clicks. For assistance, see http://....\n' % log) + if interactive: + print 'edit and save the message to send.' + result = edit(body) + if result.ok: + body = result.value + if not body: return Error('message not saved; aborting.') + result = sendmail(sender, recipients, subject, body, attachments=[(name + '.zip', zipdata)]) + if result.ok: + size = len(zipdata) + len(body) + if size >= 1000000: + size, unit = '%.1f' % (size/1024.0/1024.0), 'M' + elif size >= 10000: + size, unit = '%.0f' % (size/1024.0), 'K' + elif size >= 1000: + size, unit = '%.1f' % (size/1024.0), 'K' + else: + size, unit = '%d' % size, ' bytes' + print 'message sent (%s%s).' % (size, unit) + return result + + def save(self): + if hasattr(self, '_ws'): + self._save() + + +def run(method, *args, **kw): + ws = Workspace() + method = getattr(ws, method) + result = method(*args, **kw) + if result.ok: return 0 + print result.error + return 1 + + +class Command: + Index = {} + + def __init__(self): + self._options = [] + self._keys = [] + self._define() + + def help(self): + self._makeOpt().print_help() + + def _addKeys(self, *keys): + self._keys = keys + for key in keys: + Command.Index[key] = self + + def _addOption(self, *args, **kw): + self._options.append((args, kw)) + + def _makeOpt(self): + opt = optparse.OptionParser(usage=self._long, + add_help_option=0, + formatter=app.CustomUsageFormatter()) + for optargs, optkw in self._options: + opt.add_option(*optargs, **optkw) + return opt + + def _start(self, args): + opt = self._makeOpt() + options, args = opt.parse_args(args) + self._run(options, args) + + def run(command, options): + handler = Command.Index.get(command, None) + if handler is None: + print 'unknown command "%s"' % command + app.usage() + return 1 + return handler._start(options) + run = staticmethod(run) + + +class Opened(Command): + + def _define(self): + self._long = ('opened (o): Show open files.\n' + 'usage: %prog opened [path]') + self._short = 'show opened files' + self._addKeys('opened', 'o') + + def _run(self, options, args): + filters = map(Filter, args) + return run('opened', filters) + + +class Changes(Command): + + def _define(self): + self._long = ('changes: Show all active changesets.\n' + 'usage: %prog changes [options]') + self._short = 'show active changesets' + self._addKeys('changes') + self._addOption('-l', '--long', default=False, action='store_true', help='list full descriptions and changed files') + self._addOption('-s', '--short', default=False, action='store_true', help='list only abbreviated descriptions') + + def _run(self, options, args): + return run('changes', long=options.long, short=options.short) + + +class Assign(Command): + + def _define(self): + self._long = ('assign (a): assign files to an existing changeset, or move files\n' + ' from one changeset to another\n' + 'usage: %prog assign changeset file [file...]\n' + 'If changeset is "default", the files will be removed from all changesets.') + self._short = 'reassign open files to a different changeset' + self._addKeys('assign', 'a') + + def _run(self, options, args): + if len(args) < 2: + self.help() + return 1 + name = args[0] + files = args[1:] + return run('assign', name, files) + + +class Change(Command): + + def _define(self): + self._long = ('change: create or edit a changeset\n' + 'usage: %prog change [options] changeset') + self._short = 'create or edit a changeset' + self._addKeys('change') + self._addOption('-d', '--delete', default=False, action='store_true', help='delete the changeset') + + def _run(self, options, args): + if len(args) != 1: + self.help() + return 1 + name = args[0] + if options.delete: + return run('abandon', name) + else: + return run('change', name) + + +class Review(Command): + + def _define(self): + self._long = ('review: distribute a changeset for review\n' + 'usage: %prog review [options] changeset\n' + '\n' + 'Creates a zipfile containing diffs of all files in the changeset.\n' + 'The zipfile can be saved locally or emailed to reviews along with\n' + 'the changeset description.\n') + self._short = 'submit a changeset for review' + self._addKeys('review') + self._addOption('--context', default='32', metavar='<n>', help='lines of context in diffs') + self._addOption('--from', default='', metavar='<addr>', help='return email address') + self._addOption('--mail', default='', metavar='<addr>', help='recipients (comma separated)') + self._addOption('--noedit', default=False, action='store_true', help='immediate send (no edit)') + self._addOption('--save', default='', metavar='<path>', help='save a copy of the review') + + def _run(self, options, args): + if len(args) != 1: + self.help() + return 1 + name = args[0] + mail = [] + sender = getattr(options, 'from') + if options.mail: + mail = options.mail.split(',') + if not sender: + sender = os.environ.get('SVN_FROM', '') + if not sender: + print 'could not determine your email address; set SVN_FROM or the --from option' + return 1 + try: + context = int(options.context) + except ValueError: + context = 0 + if context < 1: + print 'invalid value (%s) for --context option' % options.context + return 1 + interactive = not options.noedit + save = options.save + return run('review', name, mail=mail, save=save, sender=sender, interactive=interactive, context=context) + + +class Submit(Command): + + def _define(self): + self._long = ('submit: submit (commit) a changeset to the repository\n' + 'usage: %prog submit changeset') + self._short = 'submit a changeset to the repository' + self._addKeys('submit') + + def _run(self, options, args): + if len(args) != 1: + self.help() + return 1 + name = args[0] + return run('submit', name) + + +class Abandon(Command): + + def _define(self): + self._long = ('abandon: discard an existing changeset. open files will be reassigned\n' + 'to the default changeset.\n' + 'usage: %prog abandon changeset') + self._short = 'abandon a changeset' + self._addKeys('abandon') + + def _run(self, options, args): + if len(args) != 1: + self.help() + return 1 + name = args[0] + return run('abandon', name) + + +class Describe(Command): + + def _define(self): + self._long = ('describe (d): show information about a changeset.\n' + 'usage: %prog describe changeset') + self._short = 'describe a changeset' + self._addKeys('describe', 'd') + + def _run(self, options, args): + if len(args) != 1: + self.help() + return 1 + name = args[0] + return run('describe', name) + + +class Help(Command): + + def _define(self): + self._long = ('help (?, h): Describe the usage of this program or its subcommands.\n' + 'usage: %prog help [SUBCOMMAND...]') + self._short = 'describe subcommands' + self._addKeys('help', 'h', '?') + self._addOption('-r') + + def _run(self, options, args): + if len(args) == 1: + key = args[0] + command = Command.Index.get(key, None) + if command is None: + print 'unknown subcommand "%s"' % key + return 1 + command.help() + return 0 + self.help() + return 0 + + def help(self): + print __doc__ % {'prog': app.programName()} + print 'Available commands:' + commands = {} + for command in Command.Index.values(): + primary = command._keys[0] + commands[primary] = command + primaries = commands.keys() + primaries.sort() + for primary in primaries: + command = commands[primary] + aliases = command._keys[1:] + aliases = ', '.join(aliases) + subcommand = primary + if aliases: + subcommand = subcommand + ' (%s)' % aliases + print ' %-20s %s' % (subcommand, command._short) + print + + +# register subcommands +Help(), Opened(), Changes(), Assign(), Describe(), Abandon(), Change(), Submit(), Review() + +def main(args): + if len(args) < 1: + app.usage() + return 0 + command = args[0] + options = args[1:] + return Command.run(command, options) + +app.start(disable_interspersed_args=1) + Property changes on: trunk/CSP/tools/subset ___________________________________________________________________ Name: svn:executable + * |