changeset 0:b5671297a0db

initial commit of hq
author Jeff Hammel <jhammel@mozilla.com>
date Fri, 30 Apr 2010 14:31:35 -0700
parents
children 67e06ca7c56e
files hq/__init__.py hq/command.py hq/main.py setup.py
diffstat 4 files changed, 280 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hq/__init__.py	Fri Apr 30 14:31:35 2010 -0700
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hq/command.py	Fri Apr 30 14:31:35 2010 -0700
@@ -0,0 +1,180 @@
+"""
+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__
+        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.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
+        _object = self._class(self, self.options)
+
+        # 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 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
+        self.commands[name] = { 'doc': doc,
+                                'args': args, 
+                                'optional': optional,
+                                'varargs': argspec.varargs
+                                }
+        return function # XXX to restructure???
+
+    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):
+        doc, argdict = self.doc2arghelp(self.commands[command]['doc'])
+        parser = OptionParser('%%prog %s %s' % (command, self.commandargs2str(command)),
+                              description=doc, add_help_option=False)
+        if self.commands[command]['optional']:
+            for key, value in self.commands[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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hq/main.py	Fri Apr 30 14:31:35 2010 -0700
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+"""
+mercurial queue extension front-end policy manager
+"""
+
+import os
+import subprocess
+import sys
+
+from command import CommandParser
+
+def call(command, *args, **kw):
+    code = subprocess.call(command, *args, **kw)
+    if code:
+        if isinstance(command, basestring):
+            cmdstr = command
+        else:
+            cmdstr = ' '.join(command)
+        raise SystemExit("Command `%s` exited with code %d" % (cmdstr, code))
+
+class HQ(object):
+    """
+    mercurial queue extension front-end policy manager
+    """
+
+    def __init__(self, parser, options):
+        """initialize global options"""
+        # TODO: look at hgrc file
+
+    def clone(self, repo, patch=None):
+        """clone the repository and begin a patch queue"""
+        directory = repo.rsplit('/', 1)
+        call(['hg', 'clone', repo, directory])
+        os.chdir(directory)
+        call(['hg', 'qinit', '-c'])
+        if patch:
+            call(['hg', 'qnew', patch])
+
+    def edit(self, patch=None):
+        """
+        edit a patch
+        - patch: the patch to edit
+        """
+
+    def commit(self, message):
+        """
+        commit a patch and push it to the master repository
+        """
+        call(['hg', 'qrefresh'])
+        call(['hg', 'qcommit', '-m', message])
+        root = subprocess.Popen(['hg', 'root'], stdout=subprocess.PIPE).communicate()[0]
+        os.chdir(os.path.join(root, '.hg', 'patches'))
+        call(['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 main(args=sys.argv[1:]):
+    parser = CommandParser(HQ)
+    options, args = parser.parse_args(args)
+    parser.invoke(args)
+
+if __name__ == '__main__':
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Fri Apr 30 14:31:35 2010 -0700
@@ -0,0 +1,28 @@
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.0'
+
+setup(name='hq',
+      version=version,
+      description='mercurial queue extension front-end policy manager',
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='hg mercurial patch',
+      author='Jeff Hammel',
+      author_email='jhammel@mozilla.com',
+      url='http://k0s.org',
+      license='MPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      hq = hq.main:main
+      """,
+      )