diff --git a/.gitignore b/.gitignore index 8f3ee2a..13d1421 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *~ tags build +.dir-locals.el diff --git a/bugz/cli.py b/bugz/cli.py index 6cfe8d3..82cfaa5 100644 --- a/bugz/cli.py +++ b/bugz/cli.py @@ -18,48 +18,53 @@ """ import getpass +import json import os import re import subprocess import sys import textwrap -import xmlrpc.client - -try: - import readline -except ImportError: - pass +import xmlrpc.client +import urllib.parse from bugz.cli_argparser import make_arg_parser from bugz.configfile import load_config -from bugz.settings import Settings from bugz.exceptions import BugzError from bugz.log import log_error, log_info +from bugz.settings import Settings from bugz.utils import block_edit, get_content_type +try: + import readline +except ImportError: + pass def list_bugs(buglist, settings): - for bug in buglist: - bugid = bug['id'] - status = bug['status'] - priority = bug['priority'] - severity = bug['severity'] - assignee = bug['assigned_to'].split('@')[0] - desc = bug['summary'] - line = '%s' % (bugid) - if hasattr(settings, 'show_status'): - line = '%s %-12s' % (line, status) - if hasattr(settings, 'show_priority'): - line = '%s %-12s' % (line, priority) - if hasattr(settings, 'show_severity'): - line = '%s %-12s' % (line, severity) - line = '%s %-20s' % (line, assignee) - line = '%s %s' % (line, desc) - print(line[:settings.columns]) - - log_info("%i bug(s) found." % len(buglist)) - + fmt = settings.format + if fmt is None: + fmt = '{bug[id]}' + if hasattr(settings, 'show_status'): + fmt += ' {bug[status]:>12}' + if hasattr(settings, 'show_priority'): + fmt += ' {bug[priority]:>12}' + if hasattr(settings, 'show_severity'): + fmt += ' {bug[severity]:>12}' + fmt += ' {bug[short_assigned_to]:>20}' + fmt += ' {bug[summary]}' + + for bug in buglist: + bug['short_assigned_to'] = bug['assigned_to'].split('@')[0] + + print(fmt.format(bug=bug)[:settings.columns]) + log_info("%i bug(s) found." % len(buglist)) + +def json_records(buglist): + for bug in buglist: + for k, v in list(bug.items()): + if isinstance(v, xmlrpc.client.DateTime): + bug[k] = str(v) + print(json.dumps(bug)) def prompt_for_bug(settings): """ Prompt for the information for a bug @@ -418,6 +423,9 @@ def modify(settings): raise BugzError('unable to read file: %s: %s' % (settings.comment_from, error)) + if hasattr(settings, 'assigned_to') and hasattr(settings, 'unassign'): + raise BugzError('--assigned-to and --unassign cannot be used together') + if hasattr(settings, 'comment_editor'): settings.comment = block_edit('Enter comment:') @@ -427,6 +435,8 @@ def modify(settings): params['alias'] = settings.alias if hasattr(settings, 'assigned_to'): params['assigned_to'] = settings.assigned_to + if hasattr(settings, 'unassign'): + params['reset_assigned_to'] = True if hasattr(settings, 'blocks_add'): if 'blocks' not in params: params['blocks'] = {} @@ -630,6 +640,32 @@ def post(settings): result = settings.call_bz(settings.bz.Bug.create, params) log_info('Bug %d submitted' % result['id']) +def products(settings): + products = fetch_products(settings) + fmt = settings.format + if fmt is None: + fmt = '{product[name]}' + if settings.json: + json_records(products) + else: + for product in products: + print(fmt.format(product=product)[:settings.columns]) + +def components(settings): + products = fetch_products(settings) + fmt = settings.format + if fmt is None: + fmt = '{product[name]:>20} {component[name]:>20} {component[description]:>20}' + if settings.json: + json_records(products) + else: + for product in products: + for component in product['components']: + print(fmt.format(product=product, component=component)[:settings.columns]) + +def fetch_products(settings): + product_ids = settings.call_bz(settings.bz.Product.get_accessible_products, dict())['ids'] + return settings.call_bz(settings.bz.Product.get, dict(ids=product_ids))['products'] def search(settings): """Performs a search on the bugzilla database with @@ -649,13 +685,11 @@ def search(settings): if 'all' not in d['status']: params['status'] = d['status'] elif 'search_statuses' in d: - params['status'] = d['search_statuses'] + params['status'] = [urllib.parse.unquote(s) + for s in d['search_statuses']] if 'terms' in d: params['summary'] = d['terms'] - if not params: - raise BugzError('Please give search terms or options.') - log_info('Searching for bugs meeting the following criteria:') for key in params: log_info(' {0:<20} = {1}'.format(key, params[key])) @@ -665,8 +699,13 @@ def search(settings): result = settings.call_bz(settings.bz.Bug.search, params)['bugs'] + if hasattr(settings, 'not_status'): + result = list(b for b in result if b['status'] not in settings.not_status) + if not len(result): log_info('No bugs found.') + elif settings.json: + json_records(result) else: list_bugs(result, settings) diff --git a/bugz/cli_argparser.py b/bugz/cli_argparser.py index a3c3ad0..dca9125 100644 --- a/bugz/cli_argparser.py +++ b/bugz/cli_argparser.py @@ -192,6 +192,9 @@ def make_arg_parser(): modify_parser.add_argument('-t', '--title', dest='summary', help='set title of bug') + modify_parser.add_argument('-u', '--unassign', + dest='unassign', action='store_true', + help='Reassign the bug to default owner') modify_parser.add_argument('-U', '--url', help='set URL field of bug') modify_parser.add_argument('-v', '--version', @@ -249,6 +252,34 @@ def make_arg_parser(): help='default answer to confirmation question') post_parser.set_defaults(func=bugz.cli.post) + products_parser = subparsers.add_parser('products', + argument_default=argparse.SUPPRESS, help='list available products') + products_parser.set_defaults(func=bugz.cli.products) + products_parser.add_argument( + '--json', + action='store_true', + help='format results as newline separated json records', + default=False) + products_parser.add_argument( + '--format', + type=str, + help='custom format. Format: {product[field]} (see --json)', + default=None) + + components_parser = subparsers.add_parser('components', + argument_default=argparse.SUPPRESS, help='list available components') + components_parser.set_defaults(func=bugz.cli.components) + components_parser.add_argument( + '--json', + action='store_true', + help='format results as newline separated json records', + default=False) + components_parser.add_argument( + '--format', + type=str, + help='custom format. Format: {product[field]} (see --json)', + default=None) + search_parser = subparsers.add_parser('search', argument_default=argparse.SUPPRESS, help='search for bugs in bugzilla') @@ -294,6 +325,10 @@ def make_arg_parser(): action='append', help='restrict by status ' '(one or more, use all for all statuses)') + search_parser.add_argument('-S', '--not-status', + action='append', + help='exclude by status ' + '(one or more, use all for all statuses)') search_parser.add_argument('-v', '--version', action='append', help='restrict by version (one or more)') @@ -308,6 +343,17 @@ def make_arg_parser(): search_parser.add_argument('--show-severity', action='store_true', help='show severity of bugs') + search_parser.add_argument( + '--format', + type=str, + help='custom format found bugs. Format: {bug[field]} (see --json)', + default=None) + search_parser.add_argument( + '--json', + action='store_true', + help='format results as newline separated json records', + default=False) + search_parser.set_defaults(func=bugz.cli.search) return parser diff --git a/lbugz b/lbugz index 77e3b62..0f49754 100755 --- a/lbugz +++ b/lbugz @@ -12,7 +12,7 @@ This is based on a patch from Mike Frysinger . import os import sys - + sys.path.insert(0, os.path.dirname(__file__)) from bugz.cli import main diff --git a/pybugz.d/freebsd.conf b/pybugz.d/freebsd.conf new file mode 100644 index 0000000..9d56fef --- /dev/null +++ b/pybugz.d/freebsd.conf @@ -0,0 +1,3 @@ +[FreeBSD] +base = https://bugs.freebsd.org/bugzilla/xmlrpc.cgi +search_statuses = New Open In%%20Progress UNCONFIRMED