summaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorWill Woods <wwoods@redhat.com>2008-03-25 17:30:30 -0400
committerWill Woods <wwoods@redhat.com>2008-03-25 17:30:30 -0400
commit091e7386f730c17961e8c3b4d80fcf667d29d718 (patch)
tree4e74ad23ae0ea8b74a3fe4fc64cfec1fd9a214bf /bin
parent74a425085fce5b37e192d16a74a2b7de37100a11 (diff)
downloadpython-bugzilla-091e7386f730c17961e8c3b4d80fcf667d29d718.tar.gz
python-bugzilla-091e7386f730c17961e8c3b4d80fcf667d29d718.tar.xz
python-bugzilla-091e7386f730c17961e8c3b4d80fcf667d29d718.zip
Woo doggies a big chunk of abstractification.
Diffstat (limited to 'bin')
-rwxr-xr-xbin/bugzilla302
1 files changed, 302 insertions, 0 deletions
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 <wwoods@redhat.com>
+#
+# 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.")