From 091e7386f730c17961e8c3b4d80fcf667d29d718 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 25 Mar 2008 17:30:30 -0400 Subject: Woo doggies a big chunk of abstractification. --- bin/bugzilla | 302 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100755 bin/bugzilla (limited to 'bin') diff --git a/bin/bugzilla b/bin/bugzilla new file mode 100755 index 0000000..caeda15 --- /dev/null +++ b/bin/bugzilla @@ -0,0 +1,302 @@ +#!/usr/bin/python +# bugzilla - a commandline frontend for the python bugzilla module +# +# Copyright (C) 2007 Red Hat Inc. +# Author: Will Woods +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +import bugzilla, optparse +import os, sys, glob, re +import logging + +version = '0.2' +default_bz = 'https://bugzilla.redhat.com/xmlrpc.cgi' + +# Initial simple logging stuff +logging.basicConfig() +log = logging.getLogger("bugzilla") +if '--debug' in sys.argv: + log.setLevel(logging.DEBUG) +elif '--verbose' in sys.argv: + log.setLevel(logging.INFO) + +def findcookie(): + globs = ['~/.mozilla/firefox/*default*/cookies.txt'] + for g in globs: + log.debug("Looking for cookies.txt in %s", g) + cookiefiles = glob.glob(os.path.expanduser(g)) + if cookiefiles: + # return the first one we find. + # TODO: find all cookiefiles, sort by age, use newest + return cookiefiles[0] +cookiefile = None + +cmdlist = ('info','query','new','modify') +def setup_parser(): + u = "usage: %prog [global options] COMMAND [options]" + u += "\nCommands: %s" % ', '.join(cmdlist) + p = optparse.OptionParser(usage=u) + p.disable_interspersed_args() + # General bugzilla connection options + p.add_option('--bugzilla',default=default_bz, + help="bugzilla XMLRPC URI. default: %s" % default_bz) + p.add_option('--user', + help="username. Will attempt to use browser cookie if not specified.") + p.add_option('--password', + help="password. Will attempt to use browser cookie if not specified.") + p.add_option('--cookiefile', + help="cookie file to use for bugzilla authentication") + p.add_option('--verbose',action='store_true', + help="give more info about what's going on") + p.add_option('--debug',action='store_true', + help="output bunches of debugging info") + return p + +def setup_action_parser(action): + p = optparse.OptionParser(usage="usage: %%prog %s [options]" % action) + # TODO: product and version could default to current system + # info (read from /etc/redhat-release?) + if action == 'new': + p.add_option('-p','--product', + help="REQUIRED: product name (list with 'bugzilla info -p')") + p.add_option('-v','--version', + help="REQUIRED: product version") + p.add_option('-c','--component', + help="REQUIRED: component name (list with 'bugzilla info -c PRODUCT')") + p.add_option('-l','--comment', + help="REQUIRED: initial bug comment") + p.add_option('-s','--summary',dest='short_desc', + help="REQUIRED: bug summary") + p.add_option('-o','--os',default='Linux',dest='op_sys', + help="OPTIONAL: operating system (default: Linux)") + p.add_option('-a','--arch',default='All',dest='rep_platform', + help="OPTIONAL: arch this bug occurs on (default: All)") + p.add_option('--severity',default='medium',dest='bug_severity', + help="OPTIONAL: bug severity (default: medium)") + p.add_option('--priority',default='medium',dest='priority', + help="OPTIONAL: bug priority (default: medium)") + p.add_option('-u','--url',dest='bug_file_loc',default='http://', + help="OPTIONAL: URL for further bug info") + p.add_option('--cc', + help="OPTIONAL: add emails to initial CC list") + # TODO: alias, assigned_to, reporter, qa_contact, dependson, blocked + elif action == 'query': + p.add_option('-p','--product', + help="product name (list with 'bugzilla info -p')") + p.add_option('-v','--version', + help="product version") + p.add_option('-c','--component', + help="component name (list with 'bugzilla info -c PRODUCT')") + p.add_option('-l','--long_desc', + help="search inside bug comments") + p.add_option('-s','--short_desc', + help="search bug summaries") + p.add_option('-o','--cc', + help="search cc lists for given address") + p.add_option('-r','--reporter', + help="search for bugs reported by this address") + p.add_option('-a','--assigned_to', + help="search for bugs assigned to this address") + p.add_option('--blocked', + help="search for bugs that block this bug ID") + p.add_option('--dependson', + help="search for bugs that depend on this bug ID") + p.add_option('-b','--bug_id', + help="specify individual bugs by IDs, separated with commas") + p.add_option('-t','--bug_status','--status', + default="NEW,VERIFIED,ASSIGNED,NEEDINFO,ON_DEV,FAILS_QA,REOPENED", + help="comma-separated list of bug statuses to accept") + elif action == 'info': + p.add_option('-p','--products',action='store_true', + help='Get a list of products') + p.add_option('-c','--components',metavar="PRODUCT", + help='List the components in the given product') + p.add_option('-o','--component_owners',metavar="PRODUCT", + help='List components (and their owners)') + p.add_option('-v','--versions',metavar="PRODUCT", + help='List the versions for the given product') + elif action == 'modify': + p.set_usage("usage: %prog modify [options] BUGID BUGID ...") + p.add_option('-l','--comment', + help='Add a comment') + # FIXME: check value for resolution + p.add_option('-k','--close',metavar="RESOLUTION", + help='Close with the given resolution') + # TODO: --keyword, --flag, --tag, --status, --assignee, --cc, ... + + if action in ('new','query'): + # output modifiers + p.add_option('-f','--full',action='store_const',dest='output', + const='full',default='normal',help="output detailed bug info") + p.add_option('-i','--ids',action='store_const',dest='output', + const='ids',help="output only bug IDs") + p.add_option('--outputformat', + help="Print output in the form given. You can use RPM-style "+ + "tags that match bug fields, e.g.: '%{bug_id}: %{short_desc}'") + return p + +if __name__ == '__main__': + # Set up parser for global args + parser = setup_parser() + # Parse the commandline, woo + (global_opt,args) = parser.parse_args() + # Get our action from these args + if len(args) and args[0] in cmdlist: + action = args.pop(0) + else: + parser.error("command must be one of: %s" % ','.join(cmdlist)) + # Parse action-specific args + action_parser = setup_action_parser(action) + (opt, args) = action_parser.parse_args(args) + + # Connect to bugzilla + log.info('Connecting to %s',global_opt.bugzilla) + bz=bugzilla.Bugzilla(url=global_opt.bugzilla) + if global_opt.user and global_opt.password: + log.info('Using username/password for authentication') + bz.login(global_opt.user,global_opt.password) + elif global_opt.cookiefile: + log.info('Using cookies in %s for authentication', global_opt.cookiefile) + bz.readcookiefile(global_opt.cookiefile) + else: + cookiefile = findcookie() + if cookiefile: + log.info('Using cookies in %s for authentication', cookiefile) + bz.readcookiefile(cookiefile) + else: + parser.error("Could not find a Firefox cookie file. Try --user/--password.") + + # And now we actually execute the given command + buglist = list() # save the results of query/new/modify here + if action == 'info': + if opt.products: + for k in sorted(bz.products): + print k + + if opt.components: + for c in sorted(bz.getcomponents(opt.components)): + print c + + if opt.component_owners: + component_details = bz.getcomponentsdetails(opt.component_owners) + for c in sorted(component_details): + print "%s: %s" % (c, component_details[c]['initialowner']) + + if opt.versions: + for p in bz.querydata['product']: + if p['name'] == opt.versions: + for v in p['versions']: + print v + + elif action == 'query': + # shortcut for specific bug_ids + if opt.bug_id: + log.debug("bz.getbugs(%s)", opt.bug_id.split(',')) + buglist=bz.getbugs(opt.bug_id.split(',')) + else: + # Construct the query from the list of queryable options + q = dict() + email_count = 1 + chart_id = 0 + for a in ('product','component','version','long_desc','bug_id', + 'short_desc','cc','assigned_to','reporter','bug_status', + 'blocked','dependson'): + if hasattr(opt,a): + i = getattr(opt,a) + if i: + if a in ('bug_status'): # list args + q[a] = i.split(',') + elif a in ('cc','assigned_to','reporter'): + # the email query fields are kind of weird - thanks + # to Florian La Roche for figuring this bit out. + # ex.: {'email1':'foo@bar.com','emailcc1':True} + q['email%i' % email_count] = i + q['email%s%i' % (a,email_count)] = True + email_count += 1 + elif a in ('blocked','dependson'): + # Chart args are weird. + q['field%i-0-0' % chart_id] = a + q['type%i-0-0' % chart_id] = 'equals' + q['value%i-0-0' % chart_id] = i + chart_id += 1 + else: + q[a] = i + log.debug("bz.query: %s", q) + buglist = bz.query(q) + + elif action == 'new': + data = dict() + required=['product','component','version','short_desc','comment', + 'rep_platform','bug_severity','op_sys','bug_file_loc','priority'] + optional=['cc'] + for a in required + optional: + i = getattr(opt,a) + if i: + data[a] = i + for k in required: + if k not in data: + parser.error('Missing required argument: %s' % k) + log.debug("bz.createbug(%s)", data) + b = bz.createbug(**data) + buglist = [b] + + elif action == 'modify': + bugid_list = [] + for a in args: + if ',' in a: + for b in a.split(','): + bugid_list.append(b) + else: + bugid_list.append(a) + # Surely there's a simpler way to do that.. + # bail out if no bugs were given + if not bugid_list: + parser.error('No bug IDs given (maybe you forgot an argument somewhere?)') + # Iterate over a list of Bug objects + # FIXME: this should totally use some multicall magic + buglist = bz.getbugssimple(bugid_list) + for id,bug in zip(bugid_list,buglist): + if not bug: + log.error(" failed to load bug %s" % id) + continue + log.debug("modifying bug %s" % bug.bug_id) + if opt.comment: + log.debug(" add comment: %s" % opt.comment) + bug.addcomment(opt.comment) + if opt.close: + log.debug(" close %s" % opt.close) + bug.close(opt.close) + else: + print "Sorry - '%s' not implemented yet." % action + + # If we're doing new/query/modify, output our results + if action in ('new','query'): + if opt.outputformat: + format_field_re = re.compile("%{[a-z0-9_]+}") + def bug_field(matchobj): + fieldname = matchobj.group()[2:-1] + return str(getattr(b,fieldname)) + for b in buglist: + print format_field_re.sub(bug_field,opt.outputformat) + elif opt.output == 'ids': + for b in buglist: + print b.bug_id + elif opt.output == 'full': + fullbuglist = bz.getbugs([b.bug_id for b in buglist]) + for b in fullbuglist: + print b + if b.cc: print "CC: %s" % " ".join(b.cc) + if b.blocked: print "Blocked: %s" % " ".join([str(i) for i in b.blocked]) + if b.dependson: print "Depends: %s" % " ".join([str(i) for i in b.dependson]) + for c in b.longdescs: + print "* %s - %s (%s):\n%s\n" % (c['time'],c['name'],c['email'] or c['safe_email'],c['body']) + elif opt.output == 'normal': + for b in buglist: + print b + else: + parser.error("opt.output was set to something weird.") -- cgit