# HG changeset patch # User Jeff Hammel # Date 1368785703 25200 # Node ID 6c4f258fae85ae9e06f4818513bca2446fe67fa3 # Parent 46a68cd554b5dea0e3b17d77fcf693a221630a19 going forward with it diff -r 46a68cd554b5 -r 6c4f258fae85 hq/command.py --- a/hq/command.py Fri May 10 09:23:40 2013 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,191 +0,0 @@ -""" -a command-line interface to the command line, a la pythonpaste -""" - -import inspect -import sys -from optparse import OptionParser -from pprint import pprint - -class CommandParser(OptionParser): - # TODO: add `help` command - - def __init__(self, _class, description=None): - self._class = _class - self.commands = {} - usage = '%prog [options] [command-options]' - description = description or _class.__doc__.strip() - description += ' Use `%prog help ` to display the usage of a command' - - OptionParser.__init__(self, usage=usage, description=description) - commands = [ getattr(_class, i) for i in dir(_class) - if not i.startswith('_') ] - commands = [ method for method in commands - if hasattr(method, '__call__') ] - for command in commands: - self.add_command(command) - self.disable_interspersed_args() - - def print_help(self): - # XXX should probably use the optparse formatters to help out here - - OptionParser.print_help(self) - - # short descriptions for commands - command_descriptions = [dict(name=i, - description=self.commands[i]['doc'].strip().split('\n',1)[0]) - for i in sorted(self.commands.keys())] - max_len = max([len(i['name']) for i in command_descriptions]) - description = "Commands: \n%s" % ('\n'.join([' %s%s %s' % (description['name'], ' ' * (max_len - len(description['name'])), description['description']) - for description in command_descriptions])) - - print - print description - - def parse(self, args=sys.argv[1:]): - """global parse step""" - - self.options, args = self.parse_args(args) - - # help/sanity check -- should probably be separated - if not len(args): - self.print_help() - sys.exit(0) - if args[0] == 'help': - if len(args) == 2: - if args[1] in self.commands: - name = args[1] - commandparser = self.command2parser(name) - commandparser.print_help() - else: - self.error("No command '%s'" % args[1]) - else: - self.print_help() - sys.exit(0) - command = args[0] - if command not in self.commands: - self.error("No command '%s'" % command) - return command, args[1:] - - def invoke(self, args=sys.argv[1:]): - """ - invoke - """ - - # parse - name, args = self.parse(args) - - # setup - options = self.options.__dict__.copy() - _object = self._class(**options) - # XXX should only pass values in options that self._class.__init__ - # needs/wants - - # command specific args - command = self.commands[name] - commandparser = self.command2parser(name) - command_options, command_args = commandparser.parse_args(args) - if len(command_args) < len(command['args']): - commandparser.error("Not enough arguments given") - if len(command_args) != len(command['args']) and not command['varargs']: - commandparser.error("Too many arguments given") - - # invoke the command - retval = getattr(_object, name)(*command_args, **command_options.__dict__) - if isinstance(retval, basestring): - print retval - elif retval is None: - pass - else: - pprint(retval) - return retval - - def add_command(self, function): - command = self.command(function) - self.commands[command['name']] = command - - def command(self, function): - name = function.func_name - if function.__doc__: - doc = inspect.cleandoc(function.__doc__) - else: - doc = '' - argspec = inspect.getargspec(function) - defaults = argspec.defaults - if defaults: - args = argspec.args[1:-len(defaults)] - optional = dict(zip(argspec.args[-len(defaults):], defaults)) - else: - args = argspec.args[1:] - optional = None - return { 'name': name, - 'doc': doc, - 'args': args, - 'optional': optional, - 'varargs': argspec.varargs - } - - def commandargs2str(self, command): - if isinstance(command, basestring): - command = self.commands[command] - retval = [] - retval.extend(['<%s>' % arg for arg in command['args']]) - varargs = command['varargs'] - if varargs: - retval.append('<%s> [%s] [...]' % (varargs, varargs)) - if command['optional']: - retval.append('[options]') - return ' '.join(retval) - - def doc2arghelp(self, docstring, decoration='-', delimeter=':'): - """ - Parse a docstring and get at the section describing arguments - - decoration: decoration character - - delimeter: delimter character - - Yields a tuple of the stripped docstring and the arguments help - dictionary - """ - lines = [ i.strip() for i in docstring.split('\n') ] - argdict = {} - doc = [] - option = None - for line in lines: - if not line and option: # blank lines terminate [???] - break - if line.startswith(decoration) and delimeter in line: - name, description = line.split(delimeter, 1) - name = name.lstrip(decoration).strip() - description = description.strip() - argdict[name] = [ description ] - option = name - else: - if option: - argdict[name].append(line) - else: - doc.append(line) - argdict = dict([(key, ' '.join(value)) - for key, value in argdict.items()]) - return ('\n'.join(doc), argdict) - - def command2parser(self, command): - if isinstance(command, basestring): - command = self.commands[command] - doc, argdict = self.doc2arghelp(command['doc']) - parser = OptionParser('%%prog %s %s' % (command['name'], self.commandargs2str(command['name'])), - description=doc, add_help_option=False) - if command['optional']: - for key, value in command['optional'].items(): - help = argdict.get(key, '') - if value is True: - parser.add_option('--no-%s' % key, dest=key, - action='store_false', default=True, - help=help) - elif value is False: - parser.add_option('--%s' % key, action='store_true', - default=False, help=help) - else: - help += ' [DEFAULT: %s]' % value - parser.add_option('--%s' % key, help=help, default=value) - - return parser diff -r 46a68cd554b5 -r 6c4f258fae85 hq/main.py --- a/hq/main.py Fri May 10 09:23:40 2013 -0700 +++ b/hq/main.py Fri May 17 03:15:03 2013 -0700 @@ -10,7 +10,7 @@ import subprocess import sys -from command import CommandParser +from commandparser import CommandParser call = subprocess.check_output @@ -30,6 +30,7 @@ # repository root self.root = root or call(['hg', 'root']).strip() + assert os.path.isdir(self.root), "'%s': not a directory!" # hg binary self.binary = binary @@ -70,12 +71,26 @@ if self.network: self._patch_command(*['hg', 'push']) - def pull(self, repo=None): - """pull from the root repository""" - # TODO: commit prior to realignment - call(['hg', 'qpop', '--all']) - call(['hg', 'pull'] + (repo and [repo] or [])) - call(['hg', 'qpush', '--all']) + def pull(self, repo=None, mq=True): + """ + pull from the root repository + if mq is true, update the patch queue, if versioned + """ + # check for outstanding changes + output = self._call(['st']).strip() + lines = [line for line in output.splitlines() + if not line.startswith('?')] + if lines: + print "Outstanding changes:" + print output + raise AssertionError + + applied, unapplied = self._series() + self._call(['qpop', '--all']) + self._call(['pull'] + (repo and [repo] or [])) + # TODO: pull queue repo + for patch in applied: + self._call(['qpush']) def goto(self, patch): """ @@ -94,8 +109,7 @@ list the files added by the top patch """ # TODO: should only list top-level directories, otherwise it's silly - _oldcwd = os.getcwd() - process = subprocess.Popen("hg qdiff | grep '^+++ ' | sed 's/+++ b\///'", stdout=subprocess.PIPE) + process = subprocess.Popen("hg qdiff | grep '^+++ ' | sed 's/+++ b\///'", stdout=subprocess.PIPE, cwd=self._root) stdout, stderr = process.communicate() return stdout @@ -103,13 +117,6 @@ """ display status """ - - # TODO: once this is on CommandParser, there should be a utility in - # command parser to allow aliases; e.g. if you do ``st = status`` at - # the class scope, you can run through the methods and only display - # one as canon but allow aliases as such. - # Alternatively, you can allow any short non-ambiguous abbreviation. - return '\n'.join([self._call(i).strip() for i in ('root', 'status', 'qseries')]) def directory(self): @@ -117,16 +124,46 @@ if os.path.isdir(self._patch_repo): return self._patch_repo + def incoming(self): + """are there incoming changes to the patch queue""" + if not self._versioned(): + return False + try: + call([self.binary, 'incoming'], cwd=self.directory()) + return True + except subprocess.CalledProcessError: + return False + ### internals - def _call(self, *args): + def _call(self, *args, **kwargs): command = [self.binary] + list(args) + kwargs.setdefault('cwd', self.root) return call(command, cwd=self.root) - def _patch_command(self, *command): + def _patch_command(self, *command, **kwargs): """perform a command in the patch repository""" - call(command, cwd=self.directory()) + kwargs.setdefault(cwd=self.directory()) + return call(command, **kwargs) + + def _versioned(self): + """is the patch queue versioned?""" + return os.path.isdir(os.path.join(self.directory(), '.hg')) + def _series(self): + """returns a 2-tuple of applied, unapplied""" + lines = self._command(['qseries']).strip() + applied = [] + unapplied = [] + for line in lines: + line.strip() + index, status, name = line.split() + if status == 'A': + applied.append(name) + else: + assert status == 'U' + unapplied.append(name) + return applied, unapplied def main(args=sys.argv[1:]): parser = CommandParser(HQ) diff -r 46a68cd554b5 -r 6c4f258fae85 setup.py --- a/setup.py Fri May 10 09:23:40 2013 -0700 +++ b/setup.py Fri May 17 03:15:03 2013 -0700 @@ -1,7 +1,7 @@ from setuptools import setup, find_packages import sys, os -version = '0.0' +version = '0.1' setup(name='hq', version=version, @@ -12,13 +12,13 @@ keywords='hg mercurial patch', author='Jeff Hammel', author_email='jhammel@mozilla.com', - url='http://k0s.org', + url='http://k0s.org/hg/hq', license='MPL', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, install_requires=[ - # -*- Extra requirements: -*- + 'CommandParser', ], entry_points=""" # -*- Entry points: -*-