diff --git a/bin/bugz b/bin/bugz index 39229ac..afaa4f8 100755 --- a/bin/bugz +++ b/bin/bugz @@ -4,79 +4,28 @@ import locale import sys import traceback -from optparse import OptionParser - -from bugz import cli, __version__ from bugz.cli import BugzError, PrettyBugz +from bugz.clparser import make_parser, get_kwds def main(): - if len(sys.argv) < 2: - PrettyBugz.help() - sys.exit(-1) - - cmd = sys.argv[1] - - parser = OptionParser(version = '%%prog %s' % __version__) - - global_opts = PrettyBugz.options - for x in global_opts.values(): - parser.add_option(x) - - if cmd == '--help' or cmd == '-h' or cmd == 'help': - PrettyBugz.help() - sys.exit(0) - elif cmd == '--version' : - parser.print_version() - sys.exit(0) - - cmd_func = getattr(PrettyBugz, cmd, None) - if cmd_func is None: - print '!! Error: Unable to recognise command: %s' % cmd - print - PrettyBugz.help() - sys.exit(-1) - - cmd_options = getattr(cmd_func, "options", {}) - cmd_args = getattr(cmd_func, "args", "[options]") - parser.set_description(cmd_func.__doc__) - parser.set_usage('%%prog %s %s' % (cmd, cmd_args)) - - for x in cmd_options.values(): - parser.add_option(x) + parser = make_parser() # parse options - opts, args = parser.parse_args(sys.argv[2:]) - - # separate bugz options and cmd options - bugz_kwds = {} - for name, opt in global_opts.items(): - try: - bugz_kwds[name] = getattr(opts, name) - except AttributeError: - pass - - cmd_kwds = {} - for name, opt in cmd_options.items(): - try: - cmd_kwds[name] = getattr(opts, name) - except AttributeError: - pass + args = parser.parse_args() + (bugz_kwds, cmd_kwds) = get_kwds(args) try: bugz = PrettyBugz(**bugz_kwds) - getattr(bugz, cmd)(*args, **cmd_kwds) + args.func(bugz, **cmd_kwds) except BugzError, e: print ' ! Error: %s' % e - print - parser.print_help() sys.exit(-1) except TypeError, e: print ' ! Error: Incorrect number of arguments supplied' print traceback.print_exc() - parser.print_help() sys.exit(-1) except RuntimeError, e: diff --git a/bugz/bugzilla.py b/bugz/bugzilla.py index 7e1a2a9..2134de6 100644 --- a/bugz/bugzilla.py +++ b/bugz/bugzilla.py @@ -7,6 +7,7 @@ import locale import mimetypes import os import re +import sys from cookielib import LWPCookieJar, CookieJar from cStringIO import StringIO @@ -84,6 +85,8 @@ def get_content_type(filename): # # Override the behaviour of elementtree and allow us to # force the encoding to utf-8 +# Not needed in Python 2.7, since ElementTree.XMLTreeBuilder uses the forced +# encoding. # class ForcedEncodingXMLTreeBuilder(ElementTree.XMLTreeBuilder): @@ -235,7 +238,7 @@ class Bugz: base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1] req.add_header("Authorization", "Basic %s" % base64string) resp = self.opener.open(req) - re_request_login = re.compile(r'.*Log in to Bugzilla') + re_request_login = re.compile(r'.*Log in to .*') if not re_request_login.search(resp.read()): self.log('Already logged in.') self.authenticated = True @@ -255,6 +258,8 @@ class Bugz: qparams = config.params['auth'].copy() qparams['Bugzilla_login'] = self.user qparams['Bugzilla_password'] = self.password + if not self.forget: + qparams['Bugzilla_remember'] = 'on' req_url = urljoin(self.base, config.urls['auth']) req = Request(req_url, urlencode(qparams), config.headers) @@ -285,7 +290,7 @@ class Bugz: self.log('Unknown field: ' + field) columns.append(field) for row in rows[1:]: - if row[0].find("Missing Search") != -1: + if "Missing Search" in row[0]: self.log('Bugzilla error (Missing search found)') return None fields = {} @@ -439,9 +444,25 @@ class Bugz: req.add_header("Authorization", "Basic %s" % base64string) resp = self.opener.open(req) - fd = StringIO(resp.read()) + data = resp.read() + # Get rid of control characters. + data = re.sub('[\x00-\x08\x0e-\x1f\x0b\x0c]', '', data) + fd = StringIO(data) + # workaround for ill-defined XML templates in bugzilla 2.20.2 - parser = ForcedEncodingXMLTreeBuilder(encoding = 'utf-8') + (major_version, minor_version) = \ + (sys.version_info[0], sys.version_info[1]) + if major_version > 2 or \ + (major_version == 2 and minor_version >= 7): + # If this is 2.7 or greater, then XMLTreeBuilder + # does what we want. + parser_class = ElementTree.XMLParser + else: + # Running under Python 2.6, so we need to use our + # subclass of XMLTreeBuilder instead. + parser_class = ForcedEncodingXMLTreeBuilder + parser = parser_class(encoding = 'utf-8') + etree = ElementTree.parse(fd, parser) bug = etree.find('.//bug') if bug and bug.attrib.has_key('error'): @@ -517,7 +538,7 @@ class Bugz: FIELDS = ('bug_file_loc', 'bug_severity', 'short_desc', 'bug_status', 'status_whiteboard', 'keywords', 'op_sys', 'priority', 'version', 'target_milestone', - 'assigned_to', 'rep_platform', 'product', 'component') + 'assigned_to', 'rep_platform', 'product', 'component', 'token') FIELDS_MULTI = ('blocked', 'dependson') @@ -637,6 +658,11 @@ class Bugz: try: resp = self.opener.open(req) + re_error = re.compile(r'id="error_msg".*>([^<]+)<') + error = re_error.search(resp.read()) + if error: + print error.group(1) + return [] return modified except: return [] @@ -753,7 +779,7 @@ class Bugz: return 0 def attach(self, bugid, title, description, filename, - content_type = 'text/plain'): + content_type = 'text/plain', ispatch = False): """Attach a file to a bug. @param bugid: bug id @@ -777,7 +803,11 @@ class Bugz: qparams['bugid'] = bugid qparams['description'] = title qparams['comment'] = description - qparams['contenttypeentry'] = content_type + if ispatch: + qparams['ispatch'] = '1' + qparams['contenttypeentry'] = 'text/plain' + else: + qparams['contenttypeentry'] = content_type filedata = [('data', filename, open(filename).read())] content_type, body = encode_multipart_formdata(qparams.items(), diff --git a/bugz/cli.py b/bugz/cli.py index e97da1b..8f25450 100644 --- a/bugz/cli.py +++ b/bugz/cli.py @@ -8,8 +8,6 @@ import sys import tempfile import textwrap -from optparse import OptionParser -from optparse import make_option from urlparse import urljoin try: @@ -122,43 +120,7 @@ def block_edit(comment, comment_from = ''): class BugzError(Exception): pass -# -# ugly optparse callbacks (really need to integrate this somehow) -# - -def modify_opt_fixed(opt, opt_str, val, parser): - parser.values.status = 'RESOLVED' - parser.values.resolution = 'FIXED' - -def modify_opt_invalid(opt, opt_str, val, parser): - parser.values.status = 'RESOLVED' - parser.values.resolution = 'INVALID' - class PrettyBugz(Bugz): - options = { - 'base': make_option('-b', '--base', type='string', - default = 'https://bugs.gentoo.org/', - help = 'Base URL of Bugzilla'), - 'user': make_option('-u', '--user', type='string', - help = 'Username for commands requiring authentication'), - 'password': make_option('-p', '--password', type='string', - help = 'Password for commands requiring authentication'), - 'httpuser': make_option('-H', '--httpuser', type='string', - help = 'Username for basic http auth'), - 'httppassword': make_option('-P', '--httppassword', type='string', - help = 'Password for basic http auth'), - 'forget': make_option('-f', '--forget', action='store_true', - help = 'Forget login after execution'), - 'columns': make_option('--columns', type='int', default = 0, - help = 'Maximum number of columns output should use'), - 'encoding': make_option('--encoding', - help = 'Output encoding (default: utf-8).'), - 'skip_auth': make_option('--skip-auth', action='store_true', - default = False, help = 'Skip Authentication.'), - 'quiet': make_option('-q', '--quiet', action='store_true', - default = False, help = 'Quiet mode'), - } - def __init__(self, base, user = None, password =None, forget = False, columns = 0, encoding = '', skip_auth = False, quiet = False, httpuser = None, httppassword = None ): @@ -195,10 +157,11 @@ class PrettyBugz(Bugz): def get_input(self, prompt): return raw_input(prompt) - def search(self, *args, **kwds): + def search(self, **kwds): """Performs a search on the bugzilla database with the keywords given on the title (or the body if specified). """ - search_term = ' '.join(args).strip() + search_term = ' '.join(kwds['terms']).strip() + del kwds['terms'] show_status = kwds['show_status'] del kwds['show_status'] show_url = kwds['show_url'] @@ -232,41 +195,6 @@ class PrettyBugz(Bugz): self.listbugs(result, show_url, show_status) - search.args = " [options..]" - search.options = { - 'order': make_option('-o', '--order', type='choice', - choices = config.choices['order'].keys(), - default = 'number'), - 'assigned_to': make_option('-a', '--assigned-to', - help = 'email the bug is assigned to'), - 'reporter': make_option('-r', '--reporter', - help = 'email the bug was reported by'), - 'cc': make_option('--cc',help = 'Restrict by CC email address'), - 'commenter': make_option('--commenter',help = 'email that commented the bug'), - 'status': make_option('-s', '--status', action='append', - help = 'Bug status (for multiple choices,' - 'use --status=NEW --status=ASSIGNED) or --status=all for all statuses'), - 'severity': make_option('--severity', action='append', - choices = config.choices['severity'], - help = 'Restrict by severity.'), - 'priority': make_option('--priority', action='append', - choices = config.choices['priority'].values(), - help = 'Restrict by priority (1 or more)'), - 'comments': make_option('-c', '--comments', action='store_true', - help = 'Search comments instead of title'), - 'product': make_option('--product', action='append', - help = 'Restrict by product (1 or more)'), - 'component': make_option('-C', '--component', action='append', - help = 'Restrict by component (1 or more)'), - 'keywords': make_option('-k', '--keywords', help = 'Bug keywords'), - 'whiteboard': make_option('-w', '--whiteboard', - help = 'Status whiteboard'), - 'show_status': make_option('--show-status', help='show status of bugs', - action = 'store_true', default = False), - 'show_url': make_option('--show-url', help='Show bug id as a url.', - action = 'store_true', default = False), - } - def namedcmd(self, command, show_status=False, show_url=False): """Run a command stored in Bugzilla by name.""" log_msg = 'Running namedcmd \'%s\''%command @@ -280,14 +208,6 @@ class PrettyBugz(Bugz): self.listbugs(result, show_url, show_status) - namedcmd.args = "" - namedcmd.options = { - 'show_status': make_option('--show-status', help='show status of bugs', - action = 'store_true', default = False), - 'show_url': make_option('--show-url', help='Show bug id as a url.', - action = 'store_true', default = False), - } - def get(self, bugid, comments = True, attachments = True): """ Fetch bug details given the bug id """ self.log('Getting bug %s ..' % bugid) @@ -384,13 +304,6 @@ class PrettyBugz(Bugz): i += 1 print - get.args = " [options..]" - get.options = { - 'comments': make_option("-n", "--no-comments", dest = 'comments', - action="store_false", default = True, - help = 'Do not show comments'), - } - def post(self, product = None, component = None, title = None, description = None, assigned_to = None, cc = None, url = None, keywords = None, @@ -579,42 +492,6 @@ class PrettyBugz(Bugz): else: raise RuntimeError('Failed to submit bug') - post.args = "[options]" - post.options = { - 'product': make_option('--product', help = 'Product'), - 'component': make_option('--component', help = 'Component'), - 'prodversion': make_option('--prodversion', - help = 'Version of the product'), - 'title': make_option('-t', '--title', help = 'Title of bug'), - 'description': make_option('-d', '--description', - help = 'Description of the bug'), - 'description_from': make_option('-F' , '--description-from', - help = 'Description from contents of' - ' file'), - 'append_command': make_option('--append-command', - help = 'Append the output of a command to the description.'), - 'assigned_to': make_option('-a', '--assigned-to', - help = 'Assign bug to someone other than ' - 'the default assignee'), - 'cc': make_option('--cc', help = 'Add a list of emails to CC list'), - 'url': make_option('-U', '--url', - help = 'URL associated with the bug'), - 'dependson': make_option('--depends-on', dest='dependson', help = 'Add a list of bug dependencies'), - 'blocked': make_option('--blocked', help = 'Add a list of blocker bugs'), - 'keywords': make_option('-k', '--keywords', help = 'List of bugzilla keywords'), - 'batch': make_option('--batch', action="store_true", - help = 'do not prompt for any values'), - 'default_confirm': make_option('--default-confirm', - choices = ['y','Y','n','N'], - default = 'y', - help = 'default answer to confirmation question (y/n)'), - 'priority': make_option('--priority', - choices=config.choices['priority'].values()), - 'severity': make_option('-S', '--severity', - choices=config.choices['severity']), - } - - def modify(self, bugid, **kwds): """Modify an existing bug (eg. adding a comment or changing resolution.)""" if 'comment_from' in kwds: @@ -637,6 +514,15 @@ class PrettyBugz(Bugz): kwds['comment'] = block_edit('Enter comment:') del kwds['comment_editor'] + if kwds['fixed']: + kwds['STATUS'] = 'CLOSED' + kwds['resolution'] = 'FIXED' + del kwds['fixed'] + + if kwds['invalid']: + kwds['STATUS'] = 'CLOSED' + kwds['resolution'] = 'INVALID' + del kwds['invalid'] result = Bugz.modify(self, bugid, **kwds) if not result: raise RuntimeError('Failed to modify bug') @@ -645,53 +531,6 @@ class PrettyBugz(Bugz): for field, value in result: self.log(' %-12s: %s' % (field, value)) - - modify.args = " [options..]" - modify.options = { - 'title': make_option('-t', '--title', help = 'Set title of bug'), - 'comment_from': make_option('-F', '--comment-from', - help = 'Add comment from file. If -C is also specified, the editor will be opened with this file as its contents.'), - 'comment_editor': make_option('-C', '--comment-editor', - action='store_true', default = False, - help = 'Add comment via default editor'), - 'comment': make_option('-c', '--comment', help = 'Add comment to bug'), - 'url': make_option('-U', '--url', help = 'Set URL field of bug'), - 'status': make_option('-s', '--status', - choices=config.choices['status'].values(), - help = 'Set new status of bug (eg. RESOLVED)'), - 'resolution': make_option('-r', '--resolution', - choices=config.choices['resolution'].values(), - help = 'Set new resolution (only if status = RESOLVED)'), - 'assigned_to': make_option('-a', '--assigned-to'), - 'duplicate': make_option('-d', '--duplicate', type='int', default=0), - 'priority': make_option('--priority', - choices=config.choices['priority'].values()), - 'severity': make_option('-S', '--severity', - choices=config.choices['severity']), - 'fixed': make_option('--fixed', action='callback', - callback = modify_opt_fixed, - help = "Mark bug as RESOLVED, FIXED"), - 'invalid': make_option('--invalid', action='callback', - callback = modify_opt_invalid, - help = "Mark bug as RESOLVED, INVALID"), - 'add_cc': make_option('--add-cc', action = 'append', - help = 'Add an email to the CC list'), - 'remove_cc': make_option('--remove-cc', action = 'append', - help = 'Remove an email from the CC list'), - 'add_dependson': make_option('--add-dependson', action = 'append', - help = 'Add a bug to the depends list'), - 'remove_dependson': make_option('--remove-dependson', action = 'append', - help = 'Remove a bug from the depends list'), - 'add_blocked': make_option('--add-blocked', action = 'append', - help = 'Add a bug to the blocked list'), - 'remove_blocked': make_option('--remove-blocked', action = 'append', - help = 'Remove a bug from the blocked list'), - 'whiteboard': make_option('-w', '--whiteboard', - help = 'Set Status whiteboard'), - 'keywords': make_option('-k', '--keywords', - help = 'Set bug keywords'), - } - def attachment(self, attachid, view = False): """ Download or view an attachment given the id.""" self.log('Getting attachment %s' % attachid) @@ -713,30 +552,14 @@ class PrettyBugz(Bugz): open(safe_filename, 'wb').write(result['fd'].read()) - attachment.args = " [-v]" - attachment.options = { - 'view': make_option('-v', '--view', action="store_true", - default = False, - help = "Print attachment rather than save") - } - - def attach(self, bugid, filename, content_type = 'text/plain', description = None): + def attach(self, bugid, filename, content_type = 'text/plain', patch = False, description = None): """ Attach a file to a bug given a filename. """ if not os.path.exists(filename): raise BugzError('File not found: %s' % filename) if not description: description = block_edit('Enter description (optional)') result = Bugz.attach(self, bugid, filename, description, filename, - content_type) - - attach.args = " [-c=] [-d=]" - attach.options = { - 'content_type': make_option('-c', '--content-type', - default='text/plain', - help = 'Mimetype of the file (default: text/plain)'), - 'description': make_option('-d', '--description', - help = 'A description of the attachment.') - } + content_type, patch) def listbugs(self, buglist, show_url=False, show_status=False): for row in buglist: @@ -760,27 +583,3 @@ class PrettyBugz(Bugz): print line[:self.columns] self.log("%i bug(s) found." % len(buglist)) - - @classmethod - def help(self): - print 'Usage: bugz [parameter(s)] [options..]' - print - print 'Subcommands:' - print ' search Search for bugs in bugzilla' - print ' get Get a bug from bugzilla' - print ' attachment Get an attachment from bugzilla' - print ' post Post a new bug into bugzilla' - print ' modify Modify a bug (eg. post a comment)' - print ' attach Attach file to a bug' - print ' namedcmd Run a stored search,' - print - print 'Examples:' - print ' bugz get 12345' - print ' bugz search python --assigned-to liquidx@gentoo.org' - print ' bugz attachment 5000 --view' - print ' bugz attach 140574 python-2.4.3.ebuild' - print ' bugz modify 140574 -c "Me too"' - print ' bugz namedcmd "Amd64 stable"' - print - print 'For more information on subcommands, run:' - print ' bugz --help' diff --git a/bugz/clparser.py b/bugz/clparser.py new file mode 100644 index 0000000..f9bdca9 --- /dev/null +++ b/bugz/clparser.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python + +import argparse + +from bugz import __version__ +from bugz.cli import PrettyBugz +from bugz.config import config + +def make_attach_parser(subparsers): + attach_parser = subparsers.add_parser('attach', + help = 'attach file to a bug') + attach_parser.add_argument('bugid', + help = 'the ID of the bug where the file should be attached') + attach_parser.add_argument('filename', + help = 'the name of the file to attach') + attach_parser.add_argument('-c', '--content-type', + default='text/plain', + help = 'mimetype of the file (default: text/plain)') + attach_parser.add_argument('-d', '--description', + help = 'a description of the attachment.') + attach_parser.add_argument('-p', '--patch', + action='store_true', + help = 'attachment is a patch') + attach_parser.set_defaults(func = PrettyBugz.attach) + +def make_attachment_parser(subparsers): + attachment_parser = subparsers.add_parser('attachment', + help = 'get an attachment from bugzilla') + attachment_parser.add_argument('attachid', + help = 'the ID of the attachment') + attachment_parser.add_argument('-v', '--view', + action="store_true", + default = False, + help = 'print attachment rather than save') + attachment_parser.set_defaults(func = PrettyBugz.attachment) + +def make_get_parser(subparsers): + get_parser = subparsers.add_parser('get', + help = 'get a bug from bugzilla') + get_parser.add_argument('bugid', + help = 'the ID of the bug to retrieve.') + get_parser.add_argument("-a", "--no-attachments", + action="store_false", + default = True, + help = 'do not show attachments', + dest = 'attachments') + get_parser.add_argument("-n", "--no-comments", + action="store_false", + default = True, + help = 'do not show comments', + dest = 'comments') + get_parser.set_defaults(func = PrettyBugz.get) + +def make_modify_parser(subparsers): + modify_parser = subparsers.add_parser('modify', + help = 'modify a bug (eg. post a comment)') + modify_parser.add_argument('bugid', + help = 'the ID of the bug to modify') + modify_parser.add_argument('-a', '--assigned-to', + help = 'change assignee for this bug') + modify_parser.add_argument('-C', '--comment-editor', + action='store_true', + help = 'add comment via default editor') + modify_parser.add_argument('-F', '--comment-from', + help = 'add comment from file. If -C is also specified, the editor will be opened with this file as its contents.') + modify_parser.add_argument('-c', '--comment', + help = 'add comment from command line') + modify_parser.add_argument('-d', '--duplicate', + type = int, + default = 0, + help = 'this bug is a duplicate') + modify_parser.add_argument('-k', '--keywords', + help = 'set bug keywords'), + modify_parser.add_argument('--priority', + choices=config.choices['priority'].values(), + help = 'change the priority for this bug') + modify_parser.add_argument('-r', '--resolution', + choices=config.choices['resolution'].values(), + help = 'set new resolution (only if status = RESOLVED)') + modify_parser.add_argument('-s', '--status', + choices=config.choices['status'].values(), + help = 'set new status of bug (eg. RESOLVED)') + modify_parser.add_argument('-S', '--severity', + choices=config.choices['severity'], + help = 'set severity for this bug') + modify_parser.add_argument('-t', '--title', + help = 'set title of bug') + modify_parser.add_argument('-U', '--url', + help = 'set URL field of bug') + modify_parser.add_argument('-w', '--whiteboard', + help = 'set Status whiteboard'), + modify_parser.add_argument('--add-cc', + action = 'append', + help = 'add an email to the CC list') + modify_parser.add_argument('--remove-cc', + action = 'append', + help = 'remove an email from the CC list') + modify_parser.add_argument('--add-dependson', + action = 'append', + help = 'add a bug to the depends list') + modify_parser.add_argument('--remove-dependson', + action = 'append', + help = 'remove a bug from the depends list') + modify_parser.add_argument('--add-blocked', + action = 'append', + help = 'add a bug to the blocked list') + modify_parser.add_argument('--remove-blocked', + action = 'append', + help = 'remove a bug from the blocked list') + modify_parser.add_argument('--fixed', + action='store_true', + help = 'mark bug as RESOLVED, FIXED') + modify_parser.add_argument('--invalid', + action='store_true', + help = 'mark bug as RESOLVED, INVALID') + modify_parser.set_defaults(func = PrettyBugz.modify) + +def make_namedcmd_parser(subparsers): + namedcmd_parser = subparsers.add_parser('namedcmd', + help = 'run a stored search') + namedcmd_parser.add_argument('command', + help = 'the name of the stored search') + namedcmd_parser.add_argument('--show-status', + action = 'store_true', + help = 'show status of bugs') + namedcmd_parser.add_argument('--show-url', + action = 'store_true', + help = 'show bug id as a url') + namedcmd_parser.set_defaults(func = PrettyBugz.namedcmd) + +def make_post_parser(subparsers): + post_parser = subparsers.add_parser('post', + help = 'post a new bug into bugzilla') + post_parser.add_argument('--product', + help = 'product') + post_parser.add_argument('--component', + help = 'component') + post_parser.add_argument('--prodversion', + help = 'version of the product') + post_parser.add_argument('-t', '--title', + help = 'title of bug') + post_parser.add_argument('-d', '--description', + help = 'description of the bug') + post_parser.add_argument('-F' , '--description-from', + help = 'description from contents of file') + post_parser.add_argument('--append-command', + help = 'append the output of a command to the description') + post_parser.add_argument('-a', '--assigned-to', + help = 'assign bug to someone other than the default assignee') + post_parser.add_argument('--cc', + help = 'add a list of emails to CC list') + post_parser.add_argument('-U', '--url', + help = 'URL associated with the bug') + post_parser.add_argument('--depends-on', + help = 'add a list of bug dependencies', + dest='dependson') + post_parser.add_argument('--blocked', + help = 'add a list of blocker bugs') + post_parser.add_argument('-k', '--keywords', + help = 'list of bugzilla keywords') + post_parser.add_argument('--batch', + action="store_true", + help = 'do not prompt for any values') + post_parser.add_argument('--default-confirm', + choices = ['y','Y','n','N'], + default = 'y', + help = 'default answer to confirmation question') + post_parser.add_argument('--priority', + choices=config.choices['priority'].values(), + help = 'set priority for the new bug') + post_parser.add_argument('-S', '--severity', + choices=config.choices['severity'], + help = 'set the severity for the new bug') + post_parser.set_defaults(func = PrettyBugz.post) + +def make_search_parser(subparsers): + search_parser = subparsers.add_parser('search', + help = 'search for bugs in bugzilla') + search_parser.add_argument('terms', + nargs='*', + help = 'strings to search for in title or body') + search_parser.add_argument('-o', '--order', + choices = config.choices['order'].keys(), + default = 'number', + help = 'display bugs in this order') + search_parser.add_argument('-a', '--assigned-to', + help = 'email the bug is assigned to') + search_parser.add_argument('-r', '--reporter', + help = 'email the bug was reported by') + search_parser.add_argument('--cc', + help = 'restrict by CC email address') + search_parser.add_argument('--commenter', + help = 'email that commented the bug') + search_parser.add_argument('-s', '--status', + action='append', + help = 'restrict by status (one or more, use all for all statuses)') + search_parser.add_argument('--severity', + action='append', + choices = config.choices['severity'], + help = 'restrict by severity (one or more)') + search_parser.add_argument('--priority', + action='append', + choices = config.choices['priority'].values(), + help = 'restrict by priority (one or more)') + search_parser.add_argument('-c', '--comments', + action='store_true', + default=None, + help = 'search comments instead of title') + search_parser.add_argument('--product', + action='append', + help = 'restrict by product (one or more)') + search_parser.add_argument('-C', '--component', + action='append', + help = 'restrict by component (1 or more)') + search_parser.add_argument('-k', '--keywords', + help = 'restrict by keywords') + search_parser.add_argument('-w', '--whiteboard', + help = 'status whiteboard') + search_parser.add_argument('--show-status', + action = 'store_true', + help='show status of bugs') + search_parser.add_argument('--show-url', + action = 'store_true', + help='show bug id as a url.') + search_parser.set_defaults(func = PrettyBugz.search) + +def make_parser(): + parser = argparse.ArgumentParser( + epilog = 'use -h after a sub-command for sub-command specific help') + parser.add_argument('-b', '--base', + default = 'https://bugs.gentoo.org/', + help = 'base URL of Bugzilla') + parser.add_argument('-u', '--user', + help = 'username for commands requiring authentication') + parser.add_argument('-p', '--password', + help = 'password for commands requiring authentication') + parser.add_argument('-H', '--httpuser', + help = 'username for basic http auth') + parser.add_argument('-P', '--httppassword', + help = 'password for basic http auth') + parser.add_argument('-f', '--forget', + action='store_true', + help = 'forget login after execution') + parser.add_argument('-q', '--quiet', + action='store_true', + default = False, + help = 'quiet mode') + parser.add_argument('--columns', + type = int, + default = 0, + help = 'maximum number of columns output should use') + parser.add_argument('--encoding', + help = 'output encoding (default: utf-8).') + parser.add_argument('--skip-auth', + action='store_true', + default = False, + help = 'skip Authentication.') + parser.add_argument('--version', + action='version', + help='show program version and exit', + version='%(prog)s ' + __version__) + subparsers = parser.add_subparsers(help = 'help for sub-commands') + make_attach_parser(subparsers) + make_attachment_parser(subparsers) + make_get_parser(subparsers) + make_modify_parser(subparsers) + make_namedcmd_parser(subparsers) + make_post_parser(subparsers) + make_search_parser(subparsers) + return parser + +def get_kwds(args): + bugz = {} + cmd = {} + global_attrs = ['user', 'password', 'httpuser', 'httppassword', 'forget', + 'base', 'columns', 'encoding', 'quiet', 'skip_auth'] + for attr in dir(args): + if attr[0] != '_' and attr != 'func': + if attr in global_attrs: + bugz[attr] = getattr(args,attr) + else: + cmd[attr] = getattr(args,attr) + return bugz, cmd diff --git a/bugz/config.py b/bugz/config.py index 4083e48..824c2ad 100644 --- a/bugz/config.py +++ b/bugz/config.py @@ -54,6 +54,7 @@ class BugzConfig: 'attach_post': { 'action': 'insert', + 'ispatch': '', 'contenttypemethod': 'manual', 'bugid': '', 'description': '', diff --git a/contrib/zsh-completion b/contrib/zsh-completion index ee4bef6..c88ebff 100644 --- a/contrib/zsh-completion +++ b/contrib/zsh-completion @@ -14,7 +14,7 @@ _bugz() { '(-P --httppassword)'{-P,--httppassword}'[basic http auth password (if required)]:password: ' '(-f --forget)'{-f,--forget}'[do not remember authentication]' '--columns[number of columns to use when displaying output]:number: ' - '(--skip-auth)'{--skip-auth}'[do not authenticate]' + '--skip-auth[do not authenticate]' '(-q --quiet)'{-q,--quiet}'[do not display status messages]' ) _bugz_commands=( .