Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

787 lines (677 sloc) 25.606 kb
#!/usr/bin/env python
import commands
import locale
import os
import re
import sys
import tempfile
import textwrap
from optparse import OptionParser
from optparse import make_option
from urlparse import urljoin
import readline
except ImportError:
readline = None
from bugzilla import Bugz
from config import config
BUGZ: ---------------------------------------------------
BUGZ: Any line beginning with 'BUGZ:' will be ignored.
BUGZ: ---------------------------------------------------
# Auxiliary functions
def raw_input_block():
""" Allows multiple line input until a Ctrl+D is detected.
@rtype: string
target = ''
while True:
line = raw_input()
target += line + '\n'
except EOFError:
return target
# This function was lifted from Bazaar 1.9.
def terminal_width():
"""Return estimated terminal width."""
if sys.platform == 'win32':
return win32utils.get_console_size()[0]
import struct, fcntl, termios
s = struct.pack('HHHH', 0, 0, 0, 0)
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
width = struct.unpack('HHHH', x)[1]
except IOError:
if width <= 0:
width = int(os.environ['COLUMNS'])
if width <= 0:
return width
def launch_editor(initial_text, comment_from = '',comment_prefix = 'BUGZ:'):
"""Launch an editor with some default text.
Lifted from Mercurial 0.9.
@rtype: string
(fd, name) = tempfile.mkstemp("bugz")
f = os.fdopen(fd, "w")
editor = (os.environ.get("BUGZ_EDITOR") or
if editor:
result = os.system("%s \"%s\"" % (editor, name))
if result != 0:
raise RuntimeError('Unable to launch editor: %s' % editor)
new_text = open(name).read()
new_text = re.sub('(?m)^%s.*\n' % comment_prefix, '', new_text)
return new_text
return ''
def block_edit(comment, comment_from = ''):
editor = (os.environ.get('BUGZ_EDITOR') or
if not editor:
print comment + ': (Press Ctrl+D to end)'
new_text = raw_input_block()
return new_text
initial_text = '\n'.join(['BUGZ: %s'%line for line in comment.split('\n')])
new_text = launch_editor(BUGZ_COMMENT_TEMPLATE % initial_text, comment_from)
if new_text.strip():
return new_text
return ''
# Bugz specific exceptions
class BugzError(Exception):
# 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 = '',
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 ):
self.quiet = quiet
self.columns = columns or terminal_width()
Bugz.__init__(self, base, user, password, forget, skip_auth, httpuser, httppassword)
self.log("Using %s " % self.base)
if not encoding:
self.enc = locale.getdefaultlocale()[1]
self.enc = 'utf-8'
if not self.enc:
self.enc = 'utf-8'
self.enc = encoding
def log(self, status_msg, newline = True):
if not self.quiet:
if newline:
print ' * %s' % status_msg
print ' * %s' % status_msg,
def warn(self, warn_msg):
if not self.quiet:
print ' ! Warning: %s' % warn_msg
def get_input(self, prompt):
return raw_input(prompt)
def search(self, *args, **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()
show_status = kwds['show_status']
del kwds['show_status']
show_url = kwds['show_url']
del kwds['show_url']
search_opts = sorted([(opt, val) for opt, val in kwds.items()
if val != None and opt != 'order'])
if not (search_term or search_opts):
raise BugzError('Please give search terms or options.')
if search_term:
log_msg = 'Searching for \'%s\' ' % search_term
log_msg = 'Searching for bugs '
if search_opts:
self.log(log_msg + 'with the following options:')
for opt, val in search_opts:
self.log(' %-20s = %s' % (opt, val))
result =, search_term, **kwds)
if result == None:
raise RuntimeError('Failed to perform search')
if len(result) == 0:
self.log('No bugs found.')
self.listbugs(result, show_url, show_status)
search.args = "<search term> [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
result = Bugz.namedcmd(self, command)
if result == None:
raise RuntimeError('Failed to run command\nWrong namedcmd perhaps?')
if len(result) == 0:
self.log('No result from command')
self.listbugs(result, show_url, show_status)
namedcmd.args = "<command name>"
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)
result = Bugz.get(self, bugid)
if result == None:
raise RuntimeError('Bug %s not found' % bugid)
# Print out all the fields below by extract the text
# directly from the tag, and just ignore if we don't
# see the tag.
('short_desc', 'Title'),
('assigned_to', 'Assignee'),
('creation_ts', 'Reported'),
('delta_ts', 'Updated'),
('bug_status', 'Status'),
('resolution', 'Resolution'),
('bug_file_loc', 'URL'),
('bug_severity', 'Severity'),
('priority', 'Priority'),
('reporter', 'Reporter'),
('product', 'Product'),
('component', 'Component'),
('status_whiteboard', 'Whiteboard'),
('keywords', 'Keywords'),
for field, name in FIELDS + MORE_FIELDS:
value = result.find('//%s' % field).text
except AttributeError:
print '%-12s: %s' % (name, value.encode(self.enc))
# Print out the cc'ed people
cced = result.findall('.//cc')
for cc in cced:
print '%-12s: %s' % ('CC', cc.text)
# print out depends
dependson = ', '.join([d.text for d in result.findall('.//dependson')])
blocked = ', '.join([d.text for d in result.findall('.//blocked')])
if dependson:
print '%-12s: %s' % ('DependsOn', dependson)
if blocked:
print '%-12s: %s' % ('Blocked', blocked)
bug_comments = result.findall('//long_desc')
bug_attachments = result.findall('//attachment')
print '%-12s: %d' % ('Comments', len(bug_comments))
print '%-12s: %d' % ('Attachments', len(bug_attachments))
print '%-12s: %s' % ('URL', '%s?id=%s' % (urljoin(self.base,
if attachments:
for attachment in bug_attachments:
aid = attachment.find('.//attachid').text
desc = attachment.find('.//desc').text
when = attachment.find('.//date').text
print '[Attachment] [%s] [%s]' % (aid, desc.encode(self.enc))
if comments:
i = 0
wrapper = textwrap.TextWrapper(width = self.columns)
for comment in bug_comments:
who = comment.find('.//who').text.encode(self.enc)
except AttributeError:
# Novell doesn't use 'who' on xml
who = ""
when = comment.find('.//bug_when').text.encode(self.enc)
what = comment.find('.//thetext').text
print '\n[Comment #%d] %s : %s' % (i, who, when)
print '-' * (self.columns - 1)
if what is None:
what = ''
# print wrapped version
for line in what.split('\n'):
if len(line) < self.columns:
print line.encode(self.enc)
for shortline in wrapper.wrap(line):
print shortline.encode(self.enc)
i += 1
get.args = "<bug_id> [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,
description_from = None, prodversion = None, append_command = None,
dependson = None, blocked = None, batch = False,
default_confirm = 'y', priority = None, severity = None):
"""Post a new bug"""
# load description from file if possible
if description_from:
description = open(description_from, 'r').read()
except IOError, e:
raise BugzError('Unable to read from file: %s: %s' % \
(description_from, e))
if not batch:
self.log('Press Ctrl+C at any time to abort.')
# Check all bug fields.
# XXX: We use "if not <field>" for mandatory fields
# and "if <field> is None" for optional ones.
# check for product
if not product:
while not product or len(product) < 1:
product = self.get_input('Enter product: ')
self.log('Enter product: %s' % product)
# check for version
# FIXME: This default behaviour is not too nice.
if prodversion is None:
prodversion = self.get_input('Enter version (default: unspecified): ')
self.log('Enter version: %s' % prodversion)
# check for component
if not component:
while not component or len(component) < 1:
component = self.get_input('Enter component: ')
self.log('Enter component: %s' % component)
# check for default priority
if priority is None:
priority_msg ='Enter priority (eg. P2) (optional): '
priority = self.get_input(priority_msg)
self.log('Enter priority (optional): %s' % priority)
# check for default severity
if severity is None:
severity_msg ='Enter severity (eg. normal) (optional): '
severity = self.get_input(severity_msg)
self.log('Enter severity (optional): %s' % severity)
# check for default assignee
if assigned_to is None:
assigned_msg ='Enter assignee (eg. (optional): '
assigned_to = self.get_input(assigned_msg)
self.log('Enter assignee (optional): %s' % assigned_to)
# check for CC list
if cc is None:
cc_msg = 'Enter a CC list (comma separated) (optional): '
cc = self.get_input(cc_msg)
self.log('Enter a CC list (optional): %s' % cc)
# check for optional URL
if url is None:
url = self.get_input('Enter URL (optional): ')
self.log('Enter URL (optional): %s' % url)
# check for title
if not title:
while not title or len(title) < 1:
title = self.get_input('Enter title: ')
self.log('Enter title: %s' % title)
# check for description
if not description:
description = block_edit('Enter bug description: ')
self.log('Enter bug description: %s' % description)
if append_command is None:
append_command = self.get_input('Append the output of the following command (leave blank for none): ')
self.log('Append command (optional): %s' % append_command)
# check for Keywords list
if keywords is None:
kwd_msg = 'Enter a Keywords list (comma separated) (optional): '
keywords = self.get_input(kwd_msg)
self.log('Enter a Keywords list (optional): %s' % keywords)
# check for bug dependencies
if dependson is None:
dependson_msg = 'Enter a list of bug dependencies (comma separated) (optional): '
dependson = self.get_input(dependson_msg)
self.log('Enter a list of bug dependencies (optional): %s' % dependson)
# check for blocker bugs
if blocked is None:
blocked_msg = 'Enter a list of blocker bugs (comma separated) (optional): '
blocked = self.get_input(blocked_msg)
self.log('Enter a list of blocker bugs (optional): %s' % blocked)
# append the output from append_command to the description
if append_command is not None and append_command != '':
append_command_output = commands.getoutput(append_command)
description = description + '\n\n' + '$ ' + append_command + '\n' + append_command_output
# raise an exception if mandatory fields are not specified.
if product is None:
raise RuntimeError('Product not specified')
if component is None:
raise RuntimeError('Component not specified')
if title is None:
raise RuntimeError('Title not specified')
if description is None:
raise RuntimeError('Description not specified')
# set optional fields to their defaults if they are not set.
if prodversion is None:
prodversion = ''
if priority is None:
priority = ''
if severity is None:
severity = ''
if assigned_to is None:
assigned_to = ''
if cc is None:
cc = ''
if url is None:
url = ''
if keywords is None:
keywords = ''
if dependson is None:
dependson = ''
if blocked is None:
blocked = ''
# print submission confirmation
print '-' * (self.columns - 1)
print 'Product : ' + product
print 'Version : ' + prodversion
print 'Component : ' + component
print 'priority : ' + priority
print 'severity : ' + severity
print 'Assigned to : ' + assigned_to
print 'CC : ' + cc
print 'URL : ' + url
print 'Title : ' + title
print 'Description : ' + description
print 'Keywords : ' + keywords
print 'Depends on : ' + dependson
print 'Blocks : ' + blocked
print '-' * (self.columns - 1)
if not batch:
if default_confirm in ['Y','y']:
confirm = raw_input('Confirm bug submission (Y/n)? ')
confirm = raw_input('Confirm bug submission (y/N)? ')
if len(confirm) < 1:
confirm = default_confirm
if confirm[0] not in ('y', 'Y'):
self.log('Submission aborted')
result =, product, component, title, description, url, assigned_to, cc, keywords, prodversion, dependson, blocked, priority, severity)
if result != None and result != 0:
self.log('Bug %d submitted' % result)
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',
'severity': make_option('-S', '--severity',
def modify(self, bugid, **kwds):
"""Modify an existing bug (eg. adding a comment or changing resolution.)"""
if 'comment_from' in kwds:
if kwds['comment_from']:
kwds['comment'] = open(kwds['comment_from'], 'r').read()
except IOError, e:
raise BugzError('Failed to get read from file: %s: %s' % \
(comment_from, e))
if 'comment_editor' in kwds:
if kwds['comment_editor']:
kwds['comment'] = block_edit('Enter comment:', kwds['comment'])
del kwds['comment_editor']
del kwds['comment_from']
if 'comment_editor' in kwds:
if kwds['comment_editor']:
kwds['comment'] = block_edit('Enter comment:')
del kwds['comment_editor']
result = Bugz.modify(self, bugid, **kwds)
if not result:
raise RuntimeError('Failed to modify bug')
self.log('Modified bug %s with the following fields:' % bugid)
for field, value in result:
self.log(' %-12s: %s' % (field, value))
modify.args = "<bug_id> [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',
help = 'Set new status of bug (eg. RESOLVED)'),
'resolution': make_option('-r', '--resolution',
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',
'severity': make_option('-S', '--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)
result = Bugz.attachment(self, attachid)
if not result:
raise RuntimeError('Unable to get attachment')
action = {True:'Viewing', False:'Saving'}
self.log('%s attachment: "%s"' % (action[view], result['filename']))
safe_filename = os.path.basename(re.sub(r'\.\.', '',
if view:
print result['fd'].read()
if os.path.exists(result['filename']):
raise RuntimeError('Filename already exists')
open(safe_filename, 'wb').write(result['fd'].read())
attachment.args = "<attachid> [-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):
""" 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,
attach.args = "<bugid> <filename> [-c=<mimetype>] [-d=<description>]"
attach.options = {
'content_type': make_option('-c', '--content-type',
help = 'Mimetype of the file (default: text/plain)'),
'description': make_option('-d', '--description',
help = 'A description of the attachment.')
def listbugs(self, buglist, show_url=False, show_status=False):
for row in buglist:
bugid = row['bugid']
if show_url:
bugid = '%s%s?id=%s'%(self.base, config.urls['show'], bugid)
status = row['status']
desc = row['desc']
line = '%s' % (bugid)
if show_status:
line = '%s %s' % (line, status)
if row.has_key('assignee'): # Novell does not have 'assignee' field
assignee = row['assignee'].split('@')[0]
line = '%s %-20s' % (line, assignee)
line = '%s %s' % (line, desc)
print line.encode(self.enc)[:self.columns]
except UnicodeDecodeError:
print line[:self.columns]
self.log("%i bug(s) found." % len(buglist))
def help(self):
print 'Usage: bugz <subcommand> [parameter(s)] [options..]'
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 'Examples:'
print ' bugz get 12345'
print ' bugz search python --assigned-to'
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 'For more information on subcommands, run:'
print ' bugz <subcommand> --help'
Jump to Line
Something went wrong with that request. Please try again.