changeset 15:6c4f258fae85

going forward with it
author Jeff Hammel <jhammel@mozilla.com>
date Fri, 17 May 2013 03:15:03 -0700
parents 46a68cd554b5
children b878f4ce93fc
files hq/command.py hq/main.py setup.py
diffstat 3 files changed, 59 insertions(+), 213 deletions(-) [+]
line wrap: on
line diff
--- 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> [command-options]'
-        description = description or _class.__doc__.strip()
-        description += ' Use `%prog help <command>` 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
--- 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)
--- 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: -*-