#!/usr/bin/python
# bugzilla - a commandline frontend for the python bugzilla module
#
# Copyright (C) 2007, 2008, 2009, 2010, 2011 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, re
import logging
import getpass
import bugzilla.util
import locale
import pprint
import socket
import xmlrpclib
from tempfile import NamedTemporaryFile

version = '0.6.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)

cmdlist = ['login','new','query','modify','attach','info']

def to_encoding(ustring):
    if isinstance(ustring, basestring):
        if isinstance(ustring, unicode):
            return ustring.encode(locale.getpreferredencoding(), 'replace')
        return ustring
    return u''

format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}")

def setup_parser():
    u =   "%prog [global options] COMMAND [command-options]"
    u +=  "\nCommands: %s" % ', '.join(cmdlist)
    p = optparse.OptionParser(usage=u)
    p.disable_interspersed_args()
    p.epilog = 'Try "bugzilla COMMAND --help" for command-specific help.'
    # General bugzilla connection options
    p.add_option('--bugzilla',default=default_bz,
            help="bugzilla XMLRPC URI. default: %s" % default_bz)
    p.add_option('--bztype',default='auto',
            help="Bugzilla type. Autodetected if not set. "
                 "Available types: %s" % " ".join(bugzilla.classlist))
    p.add_option('--user',
            help="username")
    p.add_option('--password',
            help="password")
    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="%%prog %s [options]" % action)
    # TODO: product and version could default to current system
    # info (read from /etc/redhat-release?)
    if action == 'new':
        p.set_description("Create a new bug report.")
        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','--short_desc','--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',metavar='CC[,CC,...]',
                help="OPTIONAL: add emails to initial CC list")
        p.add_option('--blocked',metavar='BUGID[,BUGID,...]',
                help="OPTIONAL: add bug_ids blocked by this bug")
        p.add_option('--dependson',metavar='BUGID[,BUGID,...]',
                help="OPTIONAL: add bug_ids that this bug depends on")
        # no API to attach files during bug creation, so no --attach :/

    elif action == 'query':
        p.set_description("List bug reports that match the given criteria.")
        # General bug metadata
        p.add_option('-b','--bug_id',
                help="specify individual bugs by IDs, separated with commas")
        p.add_option('-p','--product',
                help="product name, comma-separated (list with 'bugzilla info -p')")
        p.add_option('-v','--version',
                help="product version")
        p.add_option('-c','--component',
                help="component name(s), comma-separated (list with 'bugzilla info -c PRODUCT')")
        p.add_option('--components_file',
                help="list of component names from a file, one component per line (list with 'bugzilla info -c PRODUCT')")
        p.add_option('-l','--long_desc',
                help="search inside bug comments")
        p.add_option('-m','--target_milestone',
                help="search for a target milestone")
        p.add_option('-s','--short_desc',
                help="search bug summaries")
        p.add_option('-t','--bug_status',default="ALL",
                help="comma-separated list of bug statuses to accept [Default:all] [Available:NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,ON_QA,FAILS_QA,PASSES_QA,REOPENED,VERIFIED,RELEASE_PENDING,CLOSED]")
        p.add_option('-x','--bug_severity','--severity',
                help="search severities, comma-separated")
        p.add_option('-z','--priority',
                help="search priorities, comma-separated")

        # Email
        p.add_option('-E','--emailtype',
                help="Email: specify searching option for emails, ie. substring,notsubstring,exact,... [Default: substring]",default="substring")
        p.add_option('-o','--cc',
                help="Email: search cc lists for given address")
        p.add_option('-r','--reporter',
                help="Email: search reporter email for given address")
        p.add_option('-a','--assigned_to',
                help="Email: search for bugs assigned to this address")
        p.add_option('-q','--qa_contact',
                help="Email: search for bugs which have QA Contact assigned to this address")

        # Strings
        p.add_option('-u','--url',
                help="search keywords field for given url")
        p.add_option('-U','--url_type',
                help="specify searching option for urls, ie. anywords,allwords,nowords")
        p.add_option('-k','--keywords',
                help="search keywords field for specified words")
        p.add_option('-K','--keywords_type',
                help="specify searching option for keywords, ie. anywords,allwords,nowords")
        p.add_option('-w','--status_whiteboard',
                help="search Status Whiteboard field for specified words")
        p.add_option('-W','--status_whiteboard_type',
                help="specify searching option for Status Whiteboard, ie. anywords,allwords,nowords")

        # Boolean Charts
        p.add_option('-B','--booleantype',
                help="specify searching option for booleans, ie. substring,notsubstring,exact,... [Default: substring]",default="substring")
        p.add_option('--boolean_query', action="append",
                help="Boolean:Create your own query. Format: BooleanName-Condition-Parameter &/| ... . ie, keywords-substring-Partner & keywords-notsubstring-OtherQA")
        p.add_option('--blocked', action="append",
                help="Boolean:search for bugs that block this bug ID")
        p.add_option('--dependson', action="append",
                help="Boolean:search for bugs that depend on this bug ID")
        p.add_option('--flag', action='append',
                help="Boolean:search for bugs that have certain flag states present")
        p.add_option('--qa_whiteboard', action="append",
                help="Boolean:search for bugs that have certain QA Whiteboard text present")
        p.add_option('--devel_whiteboard', action="append",
                help="Boolean:search for bugs that have certain Devel Whiteboard text present")
        p.add_option('--alias', action="append",
                help="Boolean:search for bugs that have the provided alias")
        p.add_option('--fixed_in', action="append",
                help="search Status Whiteboard field for specified words")

        # Precomposed queries
        p.add_option('--from-url',
                help="Use the query given by a query.cgi URL. (Use quotes!)")

    elif action == 'info':
        p.set_description("Get information about the bugzilla server.")
        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("%prog modify [options] BUGID [BUGID...]")
        p.set_description("Modify one or more bugs.")
        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')
        p.add_option('-p','--private',action='store_true',default=False,
                help='Mark as private')
        p.add_option('-s','--status',
                help='Change status of bug')
        p.add_option('--assignee',
                help='Assign bugzilla to assignee')
        p.add_option('--qa_contact',
                help='Change QA contact')
        p.add_option('-f','--flag', action='append',
                help='Update bugzilla flags with requested type, ie fedora-cvs?\
 (Use a new option for each flag)')
        p.add_option('--cc', action='append',
                help='Add an email to the cc list')
        p.add_option('-F','--fixed_in',metavar="VERSION",
                help='"Fixed in version" field')
        # TODO: --keyword, --tag, --cc, ...
    elif action == 'attach':
        p.set_usage('''
  %prog attach --file=FILE --desc=DESC [--type=TYPE] BUGID [BUGID...]
  %prog attach --get=ATTACHID --getall=BUGID [...]
  %prog attach --type=TYPE BUGID [BUGID...]''')
        p.set_description("Attach files or download attachments.")
        p.add_option('-f','--file',metavar="FILENAME",
                help='File to attach, or filename for data provided on stdin')
        p.add_option('-d','--description', metavar="DESCRIPTION",dest='desc',
                help="A short description of the file being attached")
        p.add_option('-t','--type', metavar="MIMETYPE",
                help="Mime-type for the file being attached")
        p.add_option('-g','--get', metavar="ATTACHID", action="append",
                default=[], help="Download the attachment with the given ID")
        p.add_option("--getall","--get-all", metavar="BUGID", action="append",
                default=[], help="Download all attachments on the given bug")
    elif action == 'login':
        p.set_usage('%prog login [username [password]]')
        p.set_description("Log into bugzilla and save a login cookie.")

    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('-e','--extra',action='store_const',dest='output',
                const='extra',help="output additional bug information (keywords, Whiteboards, etc.)")
        p.add_option('--oneline', action='store_const', dest='output',
                const='oneline',help="one line summary of the bug (useful for scripts)")
        p.add_option('--raw', action='store_const', dest='output',
                const='raw',help="raw output of the bugzilla contents")
        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

def generate_man_page():
    try:
        from logilab.common.optik_ext import ManHelpFormatter
        import datetime
    except ImportError:
        return
    today = datetime.date.today()
    datestr = today.strftime("%B %d, %Y")
    manpage = \
'''.TH bugzilla 1  "%s" "version %s" "User Commands"
.SH NAME
bugzilla \- command-line interface to Bugzilla over XML-RPC
.SH SYNOPSIS
.B bugzilla
[\\fIoptions\\fR] [\\fIcommand\\fR] [\\fIcommand-options\\fR]
.SH DESCRIPTION
.PP
.BR bugzilla
is a command-line utility that allows access to the XML-RPC interface provided
by Bugzilla.
.PP
\\fIcommand\\fP is one of:
.br
.I \\fR * login - log into the given bugzilla instance
.br
.I \\fR * new - create a new bug
.br
.I \\fR * query - search for bugs matching given criteria
.br
.I \\fR * modify - modify existing bugs
.br
.I \\fR * attach - attach files to existing bugs, or get attachments
.br
.I \\fR * info - get info about the given bugzilla instance
''' % (datestr,version)
    manformatter = ManHelpFormatter()
    parser = setup_parser()
    parser.formatter = manformatter
    opt_section = parser.format_option_help()
    manpage += opt_section.replace("OPTIONS","GLOBAL OPTIONS")
    for action in cmdlist:
        action_parser = setup_action_parser(action)
        action_parser.remove_option("--help")
        action_parser.formatter = manformatter
        opt_section = action_parser.format_option_help()
        manpage += opt_section.replace("OPTIONS",
                                       '\[oq]%s\[cq] OPTIONS' % action.upper())
    manpage += \
'''.SH EXAMPLES
.PP
.RS 0
bugzilla query --bug_id 62037

bugzilla query --version 15 --component python-bugzilla

bugzilla login

bugzilla create -p Fedora -v rawhide -c python-bugzilla \\\\
         --summary "python-bugzilla causes headaches" \\\\
         --comment "python-bugzilla made my brain hurt when I used it."

bugzilla attach --file ~/Pictures/cam1.jpg --desc "me, in pain" $BUGID

bugzilla attach --getall $BUGID

bugzilla modify --close NOTABUG --comment "Actually, you're hungover." $BUGID

.SH EXIT STATUS
.BR bugzilla
normally returns 0 if the requested command was successful.
Otherwise, exit status is 1 if
.BR bugzilla
is interrupted by the user (or a login attempt fails), 2 if a
socket error occurs (e.g. TCP connection timeout), and 3 if the server returns
an XML-RPC fault.
.SH NOTES
Not everything that's exposed in the Web UI is exposed by XML-RPC, and not
everything that's exposed by XML-RPC is used by
.BR bugzilla .
.SH BUGS
Bugs? In a sub-1.0 release? Preposterous.
.SH AUTHOR
Will Woods <wwoods@redhat.com>'''
    print manpage

def 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)

    if global_opt.bztype == 'auto':
        log.info('Autodetecting Bugzilla type')
        # Cheat a little, for the sake of speed
        if 'bugzilla.redhat.com' in global_opt.bugzilla:
            log.info('Using RHBugzilla3 for URL containing bugzilla.redhat.com')
            bzclass = bugzilla.RHBugzilla3
        else:
            bzclass = bugzilla.Bugzilla
    elif global_opt.bztype in bugzilla.classlist:
        log.info('Using Bugzilla class %s' % global_opt.bztype)
        bzclass = getattr(bugzilla,global_opt.bztype)
    else:
        parser.error("bztype must be one of: %s" % str(bugzilla.classlist))
    bz=bzclass(url=global_opt.bugzilla)

    # Handle 'login' action
    if action == 'login':
        if len(args) == 2:
            (global_opt.user, global_opt.password) = args
        elif len(args) == 1:
            (global_opt.user,) = args
        elif len(args) > 2:
            parser.error("Too many arguments for login")
        if not global_opt.user:
            sys.stdout.write('Username: ')
            user = sys.stdin.readline()
            global_opt.user = user.strip()
        if not global_opt.password:
            global_opt.password = getpass.getpass()
        sys.stdout.write('Logging in... ')
        sys.stdout.flush()
        if bz.login(global_opt.user,global_opt.password):
            print 'Authorization cookie received.'
            sys.exit(0)
        else:
            print 'failed.'
            sys.exit(1)

    # Set up authentication
    if global_opt.user:
        if not global_opt.password:
            global_opt.password = getpass.getpass()
        log.info('Using username/password for authentication')
        bz.login(global_opt.user,global_opt.password)
    else:
        if global_opt.cookiefile:
            bz.cookiefile = global_opt.cookiefile
        cookiefile = bz.cookiefile
        if os.path.exists(cookiefile):
            log.info('Using cookies in %s for authentication', cookiefile)
        else:
            # FIXME check to see if .bugzillarc is in use
            log.info('No authentication info provided.')

    # And now we actually execute the given command
    buglist = list() # save the results of query/new/modify here
    if action == 'info':
        if not (opt.products or opt.components or opt.component_owners or opt.versions):
            parser.error("'info' command requires additional arguments")

        if opt.products:
            for k in sorted(bz.products):
                pprint.pprint(k)

        try:
            if opt.components:
                for c in sorted(bz.getcomponents(opt.components)):
                    pprint.pprint(c)
        except ValueError:
            parser.error("No such product: %s" % opt.components)

        if opt.component_owners:
            component_details = bz.getcomponentsdetails(opt.component_owners)
            for c in sorted(component_details):
                print to_encoding(u"%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 to_encoding(v)

    elif action == 'query':
        # Construct the query from the list of queryable options
        q = dict()
        # Parse preconstructed queries.
        u = getattr(opt,'from_url',None)
        if u:
            q = bugzilla.util.url_to_query(u)
        # TODO: flags to save queries, list/perform saved queries
        # TODO: use the given scheme/host for the bugzilla URL
        email_count = 1
        chart_id = 0
        for a in ['product','component','components_file','version','long_desc','bug_id',
                  'short_desc','cc','assigned_to','reporter','qa_contact','bug_status',
                  'blocked','dependson','keywords','keywords_type','url','url_type','status_whiteboard',
                  'status_whiteboard_type','fixed_in','fixed_in_type','flag','alias','qa_whiteboard',
                  'devel_whiteboard','boolean_query','bug_severity','priority','target_milestone']:
            if hasattr(opt,a):
                i = getattr(opt,a)
                if i:
                    if a == 'bug_status': # list args
                        # FIXME: statuses can differ between bugzilla instances..
                        if i == 'ALL':
                            # leaving this out should return bugs of any status
                            pass
                        elif i == 'DEV':
                            # Alias for all development bug statuses
                            q[a] = 'NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,REOPENED'.split(',')
                        elif i == 'QE':
                            # Alias for all QE relevant bug statuses
                            q[a] = 'ASSIGNED,ON_QA,FAILS_QA,PASSES_QA'.split(',')
                        elif i == 'EOL':
                            # Alias for EndOfLife bug statuses
                            q[a] = 'VERIFIED,RELEASE_PENDING,CLOSED'.split(',')
                        elif i == 'OPEN':
                            # non-Closed statuses
                            q[a] = 'NEW,ASSIGNED,MODIFIED,ON_DEV,ON_QA,VERIFIED,RELEASE_PENDING,POST'.split(',')
                        else:
                            q[a] = i.split(',')
                    elif a in ['cc','assigned_to','reporter','qa_contact']:
                        # Emails
                        # ex.: {'email1':'foo@bar.com','emailcc1':True}
                        q['email%i' % email_count] = i
                        q['email%s%i' % (a,email_count)] = True
                        q['emailtype%i' % email_count] = opt.emailtype
                        email_count += 1
                    elif a == 'components_file':
                        # Components slurped in from file (one component per line)
                        # This can be made more robust
                        arr = []
                        f = open (i, 'r')
                        for line in f.readlines():
                            line = line.rstrip("\n")
                            arr.append(line)
                        q['component'] = ",".join(arr)
                    elif a in ['product']:
                        q[a] = [ i ]
                    elif a in ['keywords','keywords_type','url','url_type','status_whiteboard',
                            'status_whiteboard_type','bug_severity','priority']:
                        if a == 'url':
                            q['bug_file_loc'] = i
                        elif a == 'url_type':
                            q['bug_file_loc_type'] = i
                        else:
                            q['%s' % a] = i
                    elif a in ['fixed_in','blocked','dependson','flag','qa_whiteboard','devel_whiteboard','alias']:
                        for b in i:
                            # Boolean Charts
                            and_count = 0
                            or_count = 0
                            x = b.split(' ')
                            for par in x :
                                if par.find('&') != -1:
                                    and_count += 1
                                elif par.find('|') != -1:
                                    or_count += 1
                                elif par.find('!') != -1:
                                     q['negate%i' % chart_id] = 1
                                elif a == 'flag':
                                # Flags have strange parameter name
                                    q['field%i-%i-%i' % (chart_id,and_count,or_count)] = 'flagtypes.name'
                                elif a.endswith('_whiteboard'):
                                    q['field%i-%i-%i' % (chart_id,and_count,or_count)] = 'cf_' + a
                                else:
                                    q['field%i-%i-%i' % (chart_id,and_count,or_count)] = a
                                q['value%i-%i-%i' % (chart_id,and_count,or_count)] = par
                                q['type%i-%i-%i' % (chart_id,and_count,or_count)]  = opt.booleantype
                            chart_id += 1
                    elif a == 'boolean_query':
                        # Custom Boolean Chart query
                        # Format: BooleanName-Condition-Parameter &/| BooleanName-Condition-Parameter &/| ...
                        # ie, keywords-substring-Partner | keywords-notsubstring-PartnerVerified & keywords-notsubstring-OtherQA
                        for b in i:
                            and_count = 0
                            or_count = 0
                            # Manually specified boolean query
                            x = b.split(' ')
                            for par in x :
                                if par.find('&') != -1:
                                    and_count += 1
                                elif par.find('|') != -1:
                                    or_count += 1
                                elif par.find('!') != -1:
                                    q['negate%i' % chart_id] = 1
                                elif par.find('-') != -1:
                                    args = par.split('-',2)
                                    q['field%i-%i-%i' % (chart_id,and_count,or_count)] = args[0]
                                    q['type%i-%i-%i' % (chart_id,and_count,or_count)]  = args[1]
                                    if len(args) == 3:
                                        q['value%i-%i-%i' % (chart_id,and_count,or_count)] = args[2]
                                else:
                                    parser.error('Malformed boolean query: %s' % i)
                            chart_id += 1
                    else:
                        q[a] = i

        #to optimize speed and reduce network traffic through lookups, we
        #specifically tell bugzilla the exact data we want.  This allows us
        #make one call to output the data, rather than have a follow-on
        #getbug() to grab more info than what the default has.
        #Testing has shown this to be a _huge_ time saver.  Unfortunately,
        #this list and the output format have to be in _sync_.  Otherwise,
        #you lose speed by doing a look-up for each new output element.
        if opt.output == 'oneline':
            q['column_list'] = [ 'bug_id', 'bug_status', 'assigned_to', 'component',
              'target_milestone', 'short_desc', 'flags', 'keywords', 'blockedby' ]
        if opt.outputformat:
            aliases = dict(bz.field_aliases)
            req_fields = []
            for fieldname, sub in format_field_re.findall(opt.outputformat):
                req_fields.append(fieldname)
                if fieldname in aliases:
                    req_fields.append(aliases.get(fieldname))
            q['column_list'] = req_fields

        log.debug("bz.query: %s", q)
        if not q:
            parser.error("'query' command requires additional arguments")
        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', 'blocked', 'dependson']
        for a in optional:
            i = getattr(opt,a)
            data[a] = i.split(',')
        for a in required:
            i = getattr(opt,a)
            if i:
                data[a] = i
        missing = [k for k in required if k not in data]
        if missing:
            parser.error("'new' command requires additional arguments: %s" % ",".join(missing))
        log.debug("bz.createbug(%s)", data)
        b = bz.createbug(**data)
        b.refresh()
        buglist = [b]

    elif action == 'attach':
        if opt.get or opt.getall:
            for bug in bz.getbugs(opt.getall):
                opt.get += [a['attach_id'] for a in bug.attachments]
            for attid in set(opt.get):
                att = bz.openattachment(attid)
                outfile = bugzilla.util.open_without_clobber(att.name, "wb")
                data = att.read(4096)
                while data:
                    outfile.write(data)
                    data = att.read(4096)
                print "Wrote %s" % outfile.name
            return
        if not opt.file and opt.desc:
            parser.error("attaching a file requires --file and --desc")
        if not opt.type:
            try:
                import magic
            except ImportError:
                parser.error("specify --type or install python-magic")
            mimemagic = magic.open(magic.MAGIC_MIME_TYPE)
            mimemagic.load()
        if sys.stdin.isatty(): # stdin is a tty -> normal CLI
            if not opt.type:
                opt.type = mimemagic.file(opt.file)
            fileobj = open(opt.file)
        else: # piped input on stdin
            # write it to a tempfile - and get the filetype if we need it
            fileobj = NamedTemporaryFile(prefix="bugzilla-attach.")
            data = sys.stdin.read(4096)
            if not opt.type:
                opt.type = mimemagic.buffer(data)
            while data:
                fileobj.write(data)
                data = sys.stdin.read(4096)
            fileobj.seek(0)
        for bugid in args:
            attid = bz.attachfile(bugid, fileobj, opt.desc,
                                  filename=opt.file,
                                  contenttype=opt.type,
                                  ispatch=(opt.type == "text/x-patch"))
            print "Created attachment %i on bug %s" % (attid, bugid)

    elif action == 'modify':
        if not (opt.status or opt.close or opt.assignee or opt.flag or opt.cc
                or opt.comment or opt.fixed_in):
            parser.error("'modify' command requires additional arguments")

        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)
        log.debug("bz.getbugssimple(%s) -> %s" % (bugid_list, buglist))
        if not buglist:
            parser.error('No bugs were found. Check your bug IDs.')
        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.status:
                log.debug("  set status: %s" % opt.status)
                bug.setstatus(opt.status, opt.comment, private=opt.private)
            elif opt.close:
                log.debug("  close %s" % opt.close)
                bug.close(opt.close,comment=opt.comment,isprivate=opt.private)
            elif opt.assignee:
                log.debug("  assignee %s" % opt.assignee)
                bug.setassignee(assigned_to=opt.assignee,comment=opt.comment)
            elif opt.qa_contact:
                log.debug("  qa_contact %s" % opt.qa_contact)
                bug.setassignee(qa_contact=opt.qa_contact,comment=opt.comment)
            elif opt.flag:
                log.debug("  flag %s" % opt.flag)
                flags=dict()
                for f in opt.flag:
                    (flagname,status) = (f[:-1],f[-1])
                    flags[flagname]=status
                bug.updateflags(flags)
            elif opt.cc:
                ccs=[]
                for c in opt.cc:
                    log.debug("  cc'ing %s" % opt.cc)
                    ccs.append(c)
                bug.addcc(ccs, comment=opt.comment)
            #this has to be last and signifies a stand-alone comment
            elif opt.comment:
                log.debug("  add comment: %s" % opt.comment)
                bug.addcomment(opt.comment, opt.private)

        # stuff that I do in batch mode:
        if opt.fixed_in:
            data = {}
            if opt.fixed_in:
                data['fixed_in'] = opt.fixed_in
            bz._update_bugs(bugid_list, data)
    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:
            special_fields = {
                'flag': lambda b,f: b.get_flag_status(f),
                'whiteboard': lambda b,wb: b.getwhiteboard(wb),
            }
            def bug_field(matchobj):
                (fieldname, rest) = matchobj.groups()

                if special_fields.has_key(fieldname):
                    val = special_fields[fieldname](b, rest)
                else:
                    val = getattr(b,fieldname)
                if type(val) is list:
                    val = ','.join(val)
                try:
                    return str(val)
                except UnicodeEncodeError:
                    return to_encoding(val)
            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 to_encoding(u'#%s %s - %s - %s' %
                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))
                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:
                    if 'email' in c:
                        email = c['email']
                    elif 'safe_email' in c:
                        email = c['safe_email']
                    else:
                        email = 'Unknown'
                    print to_encoding(u"* %s - %s (%s):\n%s\n" % (c['time'],
                        c['author'], email, c['body']))
        elif opt.output == 'normal':
            for b in buglist:
                print to_encoding(u'#%s %8s - %s - %s' %
                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))

        elif opt.output == 'extra':
            print "Grabbing 'extra' bug information. This could take a moment."
            fullbuglist = bz.getbugs([b.bug_id for b in buglist])
            for b in fullbuglist:
                print to_encoding(u'#%s %s - %s - %s' %
                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))
                if hasattr(b, 'keywords') and b.keywords:
                    print to_encoding(u" +Keywords: %s" % b.keywords)
                if hasattr(b, 'qa_whiteboard') and b.qa_whiteboard:
                    print to_encoding(u" +QA Whiteboard: %s" % b.qa_whiteboard)
                if hasattr(b, 'status_whiteboard') and b.status_whiteboard:
                    print to_encoding(u" +Status Whiteboard: %s" %
                            b.status_whiteboard)
                if hasattr(b, 'devel_whiteboard') and b.devel_whiteboard:
                    print to_encoding(u" +Devel Whiteboard: %s" %
                            b.devel_whiteboard)
            print "\nBugs listed: ",len(buglist)
        elif opt.output == 'oneline':
            for b in buglist:
                flags=''
                cve=''
		flags += "".join([str(i) for i in b.flags.split(',')])
                #grab CVEs by searching the keywords and grabbing that bugzilla
                if b.keywords.find("Security") != -1:
                    blockedby="%s" % b.blockedby
                    for bl in blockedby.split(','):
                        cvebug = bz.getbug(bl)
                        for cb in cvebug.alias:
                            if cb.find("CVE") != -1:
                                cve += cb + " "
                # bugzilla.redhat.com has component as a list
                if type(b.component) == list:
                    b.component = ','.join(b.component)
                print to_encoding(u"#%s %8s %22s %s\t[%s] %s %s" %
                        (b.bug_id, b.bug_status, b.assigned_to, b.component,
                            b.target_milestone, flags, cve))
        elif opt.output == 'raw':
            fullbuglist = bz.getbugs([b.bug_id for b in buglist])
            for b in fullbuglist:
                print "Bugzilla %s: " % b.bug_id
                for a in dir(b):
                    print to_encoding(u"ATTRIBUTE[%s]: %s" % (a, getattr(b, a)))
                print "\n\n"
        else:
            parser.error("opt.output was set to something weird.")

if __name__ == '__main__':
    try:
        if '--generate-man' in sys.argv:
            generate_man_page()
        else:
            main()
    except KeyboardInterrupt:
        print "\ninterrupted."
        sys.exit(1)
    except socket.error, e:
        print "\nConnection lost/failed: %s" % str(e)
        sys.exit(2)
    except xmlrpclib.Fault, e:
        print "\nServer error: %s" % str(e)
        sys.exit(3)
