Michal Koutný ccf7f1
#!/usr/bin/env python3
Jeff Mahoney 3dff52
#
Jeff Mahoney 3dff52
# bugzilla - a commandline frontend for the python bugzilla module
Jeff Mahoney 3dff52
#
Jeff Mahoney 3dff52
# Copyright (C) 2007-2017 Red Hat Inc.
Jeff Mahoney 3dff52
# Author: Will Woods <wwoods@redhat.com>
Jeff Mahoney 3dff52
# Author: Cole Robinson <crobinso@redhat.com>
Jeff Mahoney 3dff52
#
Michal Koutný ccf7f1
# This work is licensed under the GNU GPLv2 or later.
Michal Koutný ccf7f1
# See the COPYING file in the top-level directory.
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
import argparse
Michal Koutný ccf7f1
import base64
Michal Koutný ccf7f1
import datetime
Michal Koutný ccf7f1
import errno
Michal Koutný ccf7f1
import json
Jeff Mahoney 3dff52
import locale
Jeff Mahoney 3dff52
from logging import getLogger, DEBUG, INFO, WARN, StreamHandler, Formatter
Jeff Mahoney 3dff52
import os
Jeff Mahoney 3dff52
import re
Jeff Mahoney 3dff52
import socket
Jeff Mahoney 3dff52
import sys
Jeff Mahoney 3dff52
import tempfile
Michal Koutný ccf7f1
import urllib.parse
Michal Koutný ccf7f1
import xmlrpc.client
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
import requests.exceptions
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
import bugzilla
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
Michal Koutný 4e69d7
DEFAULT_BZ = 'https://apibugzilla.suse.com'
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
log = getLogger(bugzilla.__name__)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
################
Jeff Mahoney 3dff52
# Util helpers #
Jeff Mahoney 3dff52
################
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _is_unittest_debug():
Jeff Mahoney 3dff52
    return bool(os.getenv("__BUGZILLA_UNITTEST_DEBUG"))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def open_without_clobber(name, *args):
Michal Koutný ccf7f1
    """
Michal Koutný ccf7f1
    Try to open the given file with the given mode; if that filename exists,
Michal Koutný ccf7f1
    try "name.1", "name.2", etc. until we find an unused filename.
Michal Koutný ccf7f1
    """
Jeff Mahoney 3dff52
    fd = None
Jeff Mahoney 3dff52
    count = 1
Jeff Mahoney 3dff52
    orig_name = name
Jeff Mahoney 3dff52
    while fd is None:
Jeff Mahoney 3dff52
        try:
Jeff Mahoney 3dff52
            fd = os.open(name, os.O_CREAT | os.O_EXCL, 0o666)
Jeff Mahoney 3dff52
        except OSError as err:
Michal Koutný ccf7f1
            if err.errno == errno.EEXIST:
Jeff Mahoney 3dff52
                name = "%s.%i" % (orig_name, count)
Jeff Mahoney 3dff52
                count += 1
Michal Koutný ccf7f1
            else:  # pragma: no cover
Michal Koutný ccf7f1
                raise IOError(err.errno, err.strerror, err.filename) from None
Jeff Mahoney 3dff52
    fobj = open(name, *args)
Jeff Mahoney 3dff52
    if fd != fobj.fileno():
Jeff Mahoney 3dff52
        os.close(fd)
Jeff Mahoney 3dff52
    return fobj
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def setup_logging(debug, verbose):
Jeff Mahoney 3dff52
    handler = StreamHandler(sys.stderr)
Jeff Mahoney 3dff52
    handler.setFormatter(Formatter(
Jeff Mahoney 3dff52
        "[%(asctime)s] %(levelname)s (%(module)s:%(lineno)d) %(message)s",
Jeff Mahoney 3dff52
        "%H:%M:%S"))
Jeff Mahoney 3dff52
    log.addHandler(handler)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if debug:
Jeff Mahoney 3dff52
        log.setLevel(DEBUG)
Jeff Mahoney 3dff52
    elif verbose:
Jeff Mahoney 3dff52
        log.setLevel(INFO)
Jeff Mahoney 3dff52
    else:
Jeff Mahoney 3dff52
        log.setLevel(WARN)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if _is_unittest_debug():
Michal Koutný ccf7f1
        log.setLevel(DEBUG)  # pragma: no cover
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
##################
Jeff Mahoney 3dff52
# Option parsing #
Jeff Mahoney 3dff52
##################
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_root_parser():
Jeff Mahoney 3dff52
    epilog = 'Try "bugzilla COMMAND --help" for command-specific help.'
Jeff Mahoney 3dff52
    p = argparse.ArgumentParser(epilog=epilog)
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    default_url = bugzilla.Bugzilla.get_rcfile_default_url()
Michal Koutný ccf7f1
    if not default_url:
Michal Koutný ccf7f1
        default_url = DEFAULT_BZ
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # General bugzilla connection options
Jeff Mahoney 3dff52
    p.add_argument('--bugzilla', default=default_url,
Michal Koutný ccf7f1
            help="bugzilla URI. default: %s" % default_url)
Jeff Mahoney 3dff52
    p.add_argument("--nosslverify", dest="sslverify",
Jeff Mahoney 3dff52
                 action="store_false", default=True,
Jeff Mahoney 3dff52
                 help="Don't error on invalid bugzilla SSL certificate")
Jeff Mahoney 3dff52
    p.add_argument('--cert',
Jeff Mahoney 3dff52
            help="client side certificate file needed by the webserver")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('--login', action="store_true",
Jeff Mahoney 3dff52
        help='Run interactive "login" before performing the '
Jeff Mahoney 3dff52
             'specified command.')
Jeff Mahoney 3dff52
    p.add_argument('--username', help="Log in with this username")
Jeff Mahoney 3dff52
    p.add_argument('--password', help="Log in with this password")
Michal Koutný ccf7f1
    p.add_argument('--restrict-login', action="store_true",
Michal Koutný ccf7f1
                   help="The session (login token) will be restricted to "
Michal Koutný ccf7f1
                        "the current IP address.")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('--ensure-logged-in', action="store_true",
Jeff Mahoney 3dff52
        help="Raise an error if we aren't logged in to bugzilla. "
Jeff Mahoney 3dff52
             "Consider using this if you are depending on "
Jeff Mahoney 3dff52
             "cached credentials, to ensure that when they expire the "
Jeff Mahoney 3dff52
             "tool errors, rather than subtly change output.")
Jeff Mahoney 3dff52
    p.add_argument('--no-cache-credentials',
Jeff Mahoney 3dff52
        action='store_false', default=True, dest='cache_credentials',
Jeff Mahoney 3dff52
        help="Don't save any bugzilla cookies or tokens to disk, and "
Jeff Mahoney 3dff52
             "don't use any pre-existing credentials.")
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    p.add_argument('--cookiefile', default=None, help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('--tokenfile', default=None,
Jeff Mahoney 3dff52
            help="token file to use for bugzilla authentication")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('--verbose', action='store_true',
Jeff Mahoney 3dff52
            help="give more info about what's going on")
Jeff Mahoney 3dff52
    p.add_argument('--debug', action='store_true',
Jeff Mahoney 3dff52
            help="output bunches of debugging info")
Jeff Mahoney 3dff52
    p.add_argument('--version', action='version',
Jeff Mahoney 3dff52
                   version=bugzilla.__version__)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Allow user to specify BZClass to initialize. Kinda weird for the
Jeff Mahoney 3dff52
    # CLI, I'd rather people file bugs about this so we can fix our detection.
Jeff Mahoney 3dff52
    # So hide it from the help output but keep it for back compat
Jeff Mahoney 3dff52
    p.add_argument('--bztype', default='auto', help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    return p
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _parser_add_output_options(p):
Jeff Mahoney 3dff52
    outg = p.add_argument_group("Output format options")
Jeff Mahoney 3dff52
    outg.add_argument('--full', action='store_const', dest='output',
Jeff Mahoney 3dff52
            const='full', default='normal',
Jeff Mahoney 3dff52
            help="output detailed bug info")
Jeff Mahoney 3dff52
    outg.add_argument('-i', '--ids', action='store_const', dest='output',
Jeff Mahoney 3dff52
            const='ids', help="output only bug IDs")
Jeff Mahoney 3dff52
    outg.add_argument('-e', '--extra', action='store_const',
Jeff Mahoney 3dff52
            dest='output', const='extra',
Jeff Mahoney 3dff52
            help="output additional bug information "
Jeff Mahoney 3dff52
                 "(keywords, Whiteboards, etc.)")
Jeff Mahoney 3dff52
    outg.add_argument('--oneline', action='store_const', dest='output',
Jeff Mahoney 3dff52
            const='oneline',
Jeff Mahoney 3dff52
            help="one line summary of the bug (useful for scripts)")
Michal Koutný ccf7f1
    outg.add_argument('--json', action='store_const', dest='output',
Michal Koutný ccf7f1
            const='json', help="output contents in json format")
Michal Koutný ccf7f1
    outg.add_argument("--includefield", action="append",
Michal Koutný ccf7f1
            help="Pass the field name to bugzilla include_fields list. "
Michal Koutný ccf7f1
                 "Only the fields passed to include_fields are returned "
Michal Koutný ccf7f1
                 "by the bugzilla server. "
Michal Koutný ccf7f1
                 "This can be specified multiple times.")
Michal Koutný ccf7f1
    outg.add_argument("--extrafield", action="append",
Michal Koutný ccf7f1
            help="Pass the field name to bugzilla extra_fields list. "
Michal Koutný ccf7f1
                 "When used with --json this can be used to request "
Michal Koutný ccf7f1
                 "bugzilla to return values for non-default fields. "
Michal Koutný ccf7f1
                 "This can be specified multiple times.")
Michal Koutný ccf7f1
    outg.add_argument("--excludefield", action="append",
Michal Koutný ccf7f1
            help="Pass the field name to bugzilla exclude_fields list. "
Michal Koutný ccf7f1
                 "When used with --json this can be used to request "
Michal Koutný ccf7f1
                 "bugzilla to not return values for a field. "
Michal Koutný ccf7f1
                 "This can be specified multiple times.")
Jeff Mahoney 3dff52
    outg.add_argument('--raw', action='store_const', dest='output',
Michal Koutný ccf7f1
            const='raw', help="raw output of the bugzilla contents. This "
Michal Koutný ccf7f1
            "format is unstable and difficult to parse. Use --json instead.")
Jeff Mahoney 3dff52
    outg.add_argument('--outputformat',
Jeff Mahoney 3dff52
            help="Print output in the form given. "
Jeff Mahoney 3dff52
                 "You can use RPM-style tags that match bug "
Jeff Mahoney 3dff52
                 "fields, e.g.: '%%{id}: %%{summary}'. See the man page "
Jeff Mahoney 3dff52
                 "section 'Output options' for more details.")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _parser_add_bz_fields(rootp, command):
Jeff Mahoney 3dff52
    cmd_new = (command == "new")
Jeff Mahoney 3dff52
    cmd_query = (command == "query")
Jeff Mahoney 3dff52
    cmd_modify = (command == "modify")
Jeff Mahoney 3dff52
    if cmd_new:
Jeff Mahoney 3dff52
        comment_help = "Set initial bug comment/description"
Jeff Mahoney 3dff52
    elif cmd_query:
Jeff Mahoney 3dff52
        comment_help = "Search all bug comments"
Jeff Mahoney 3dff52
    else:
Jeff Mahoney 3dff52
        comment_help = "Add new bug comment"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p = rootp.add_argument_group("Standard bugzilla options")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('-p', '--product', help="Product name")
Jeff Mahoney 3dff52
    p.add_argument('-v', '--version', help="Product version")
Jeff Mahoney 3dff52
    p.add_argument('-c', '--component', help="Component name")
Jeff Mahoney 3dff52
    p.add_argument('-t', '--summary', '--short_desc', help="Bug summary")
Jeff Mahoney 3dff52
    p.add_argument('-l', '--comment', '--long_desc', help=comment_help)
Jeff Mahoney 3dff52
    if not cmd_query:
Jeff Mahoney 3dff52
        p.add_argument("--comment-tag", action="append",
Jeff Mahoney 3dff52
                help="Comment tag for the new comment")
Jeff Mahoney 3dff52
    p.add_argument("--sub-component", action="append",
Jeff Mahoney 3dff52
        help="RHBZ sub component field")
Jeff Mahoney 3dff52
    p.add_argument('-o', '--os', help="Operating system")
Jeff Mahoney 3dff52
    p.add_argument('--arch', help="Arch this bug occurs on")
Jeff Mahoney 3dff52
    p.add_argument('-x', '--severity', help="Bug severity")
Jeff Mahoney 3dff52
    p.add_argument('-z', '--priority', help="Bug priority")
Jeff Mahoney 3dff52
    p.add_argument('--alias', help='Bug alias (name)')
Jeff Mahoney 3dff52
    p.add_argument('-s', '--status', '--bug_status',
Jeff Mahoney 3dff52
        help='Bug status (NEW, ASSIGNED, etc.)')
Jeff Mahoney 3dff52
    p.add_argument('-u', '--url', help="URL field")
Jeff Mahoney 3dff52
    p.add_argument('-m', '--target_milestone', help="Target milestone")
Jeff Mahoney 3dff52
    p.add_argument('--target_release', help="RHBZ Target release")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('--blocked', action="append",
Jeff Mahoney 3dff52
        help="Bug IDs that this bug blocks")
Jeff Mahoney 3dff52
    p.add_argument('--dependson', action="append",
Jeff Mahoney 3dff52
        help="Bug IDs that this bug depends on")
Jeff Mahoney 3dff52
    p.add_argument('--keywords', action="append",
Jeff Mahoney 3dff52
        help="Bug keywords")
Jeff Mahoney 3dff52
    p.add_argument('--groups', action="append",
Jeff Mahoney 3dff52
        help="Which user groups can view this bug")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument('--cc', action="append", help="CC list")
Jeff Mahoney 3dff52
    p.add_argument('-a', '--assigned_to', '--assignee', help="Bug assignee")
Jeff Mahoney 3dff52
    p.add_argument('-q', '--qa_contact', help='QA contact')
Michal Koutný ccf7f1
    if cmd_modify:
Michal Koutný ccf7f1
        p.add_argument("--minor-update", action="store_true",
Michal Koutný ccf7f1
                help="Request bugzilla to not send any "
Michal Koutný ccf7f1
                     "email about this change")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if not cmd_new:
Jeff Mahoney 3dff52
        p.add_argument('-f', '--flag', action='append',
Jeff Mahoney 3dff52
            help="Bug flags state. Ex:\n"
Jeff Mahoney 3dff52
                 "  --flag needinfo?\n"
Jeff Mahoney 3dff52
                 "  --flag dev_ack+ \n"
Jeff Mahoney 3dff52
                 "  clear with --flag needinfoX")
Jeff Mahoney 3dff52
        p.add_argument("--tags", action="append",
Jeff Mahoney 3dff52
                help="Tags/Personal Tags field.")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
        p.add_argument('-w', "--whiteboard", '--status_whiteboard',
Jeff Mahoney 3dff52
            action="append", help='Whiteboard field')
Jeff Mahoney 3dff52
        p.add_argument("--devel_whiteboard", action="append",
Jeff Mahoney 3dff52
            help='RHBZ devel whiteboard field')
Jeff Mahoney 3dff52
        p.add_argument("--internal_whiteboard", action="append",
Jeff Mahoney 3dff52
            help='RHBZ internal whiteboard field')
Jeff Mahoney 3dff52
        p.add_argument("--qa_whiteboard", action="append",
Jeff Mahoney 3dff52
            help='RHBZ QA whiteboard field')
Jeff Mahoney 3dff52
        p.add_argument('-F', '--fixed_in',
Jeff Mahoney 3dff52
            help="RHBZ 'Fixed in version' field")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Put this at the end, so it sticks out more
Jeff Mahoney 3dff52
    p.add_argument('--field',
Jeff Mahoney 3dff52
        metavar="FIELD=VALUE", action="append", dest="fields",
Michal Koutný ccf7f1
        help="Manually specify a bugzilla API field. FIELD is "
Michal Koutný ccf7f1
        "the raw name used by the bugzilla instance. For example, if your "
Jeff Mahoney 3dff52
        "bugzilla instance has a custom field cf_my_field, do:\n"
Jeff Mahoney 3dff52
        "  --field cf_my_field=VALUE")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if not cmd_modify:
Jeff Mahoney 3dff52
        _parser_add_output_options(rootp)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_new_parser(subparsers):
Jeff Mahoney 3dff52
    description = ("Create a new bug report. "
Jeff Mahoney 3dff52
        "--product, --component, --version, --summary, and --comment "
Jeff Mahoney 3dff52
        "must be specified. "
Jeff Mahoney 3dff52
        "Options that take multiple values accept comma separated lists, "
Jeff Mahoney 3dff52
        "including --cc, --blocks, --dependson, --groups, and --keywords.")
Jeff Mahoney 3dff52
    p = subparsers.add_parser("new", description=description)
Michal Koutný 029c1e
    p.add_argument('--no-refresh', action='store_true',
Michal Koutný 029c1e
                   help='Do not refresh bug after creating')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    _parser_add_bz_fields(p, "new")
Michal Koutný ccf7f1
    g = p.add_argument_group("'new' specific options")
Michal Koutný ccf7f1
    g.add_argument('--private', action='store_true', default=False,
Michal Koutný ccf7f1
        help='Mark new comment as private')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_query_parser(subparsers):
Jeff Mahoney 3dff52
    description = ("List bug reports that match the given criteria. "
Jeff Mahoney 3dff52
        "Certain options can accept a comma separated list to query multiple "
Jeff Mahoney 3dff52
        "values, including --status, --component, --product, --version, --id.")
Jeff Mahoney 3dff52
    epilog = ("Note: querying via explicit command line options will only "
Jeff Mahoney 3dff52
        "get you so far. See the --from-url option for a way to use powerful "
Jeff Mahoney 3dff52
        "Web UI queries from the command line.")
Jeff Mahoney 3dff52
    p = subparsers.add_parser("query",
Jeff Mahoney 3dff52
        description=description, epilog=epilog)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    _parser_add_bz_fields(p, "query")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    g = p.add_argument_group("'query' specific options")
Jeff Mahoney 3dff52
    g.add_argument('-b', '--id', '--bug_id',
Jeff Mahoney 3dff52
        help="specify individual bugs by IDs, separated with commas")
Jeff Mahoney 3dff52
    g.add_argument('-r', '--reporter',
Jeff Mahoney 3dff52
        help="Email: search reporter email for given address")
Jeff Mahoney 3dff52
    g.add_argument('--quicksearch',
Jeff Mahoney 3dff52
        help="Search using bugzilla's quicksearch functionality.")
Jeff Mahoney 3dff52
    g.add_argument('--savedsearch',
Jeff Mahoney 3dff52
        help="Name of a bugzilla saved search. If you don't own this "
Jeff Mahoney 3dff52
            "saved search, you must passed --savedsearch_sharer_id.")
Jeff Mahoney 3dff52
    g.add_argument('--savedsearch-sharer-id',
Jeff Mahoney 3dff52
        help="Owner ID of the --savedsearch. You can get this ID from "
Jeff Mahoney 3dff52
            "the URL bugzilla generates when running the saved search "
Jeff Mahoney 3dff52
            "from the web UI.")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Keep this at the end so it sticks out more
Jeff Mahoney 3dff52
    g.add_argument('--from-url', metavar="WEB_QUERY_URL",
Jeff Mahoney 3dff52
        help="Make a working query via bugzilla's 'Advanced search' web UI, "
Jeff Mahoney 3dff52
             "grab the url from your browser (the string with query.cgi or "
Jeff Mahoney 3dff52
             "buglist.cgi in it), and --from-url will run it via the "
Jeff Mahoney 3dff52
             "bugzilla API. Don't forget to quote the string! "
Jeff Mahoney 3dff52
             "This only works for Bugzilla 5 and Red Hat bugzilla")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Deprecated options
Jeff Mahoney 3dff52
    p.add_argument('-E', '--emailtype', help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('--components_file', help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('-U', '--url_type',
Jeff Mahoney 3dff52
            help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('-K', '--keywords_type',
Jeff Mahoney 3dff52
            help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('-W', '--status_whiteboard_type',
Jeff Mahoney 3dff52
            help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
    p.add_argument('--fixed_in_type', help=argparse.SUPPRESS)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_info_parser(subparsers):
Jeff Mahoney 3dff52
    description = ("List products or component information about the "
Jeff Mahoney 3dff52
        "bugzilla server.")
Jeff Mahoney 3dff52
    p = subparsers.add_parser("info", description=description)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    x = p.add_mutually_exclusive_group(required=True)
Jeff Mahoney 3dff52
    x.add_argument('-p', '--products', action='store_true',
Jeff Mahoney 3dff52
            help='Get a list of products')
Jeff Mahoney 3dff52
    x.add_argument('-c', '--components', metavar="PRODUCT",
Jeff Mahoney 3dff52
            help='List the components in the given product')
Jeff Mahoney 3dff52
    x.add_argument('-o', '--component_owners', metavar="PRODUCT",
Jeff Mahoney 3dff52
            help='List components (and their owners)')
Jeff Mahoney 3dff52
    x.add_argument('-v', '--versions', metavar="PRODUCT",
Jeff Mahoney 3dff52
            help='List the versions for the given product')
Jeff Mahoney 3dff52
    p.add_argument('--active-components', action="store_true",
Jeff Mahoney 3dff52
            help='Only show active components. Combine with --components*')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_modify_parser(subparsers):
Jeff Mahoney 3dff52
    usage = ("bugzilla modify [options] BUGID [BUGID...]\n"
Jeff Mahoney 3dff52
        "Fields that take multiple values have a special input format.\n"
Jeff Mahoney 3dff52
        "Append:    --cc=foo@example.com\n"
Jeff Mahoney 3dff52
        "Overwrite: --cc==foo@example.com\n"
Jeff Mahoney 3dff52
        "Remove:    --cc=-foo@example.com\n"
Jeff Mahoney 3dff52
        "Options that accept this format: --cc, --blocked, --dependson,\n"
Jeff Mahoney 3dff52
        "    --groups, --tags, whiteboard fields.")
Jeff Mahoney 3dff52
    p = subparsers.add_parser("modify", usage=usage)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    _parser_add_bz_fields(p, "modify")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    g = p.add_argument_group("'modify' specific options")
Jeff Mahoney 3dff52
    g.add_argument("ids", nargs="+", help="Bug IDs to modify")
Jeff Mahoney 3dff52
    g.add_argument('-k', '--close', metavar="RESOLUTION",
Jeff Mahoney 3dff52
        help='Close with the given resolution (WONTFIX, NOTABUG, etc.)')
Jeff Mahoney 3dff52
    g.add_argument('-d', '--dupeid', metavar="ORIGINAL",
Jeff Mahoney 3dff52
        help='ID of original bug. Implies --close DUPLICATE')
Jeff Mahoney 3dff52
    g.add_argument('--private', action='store_true', default=False,
Jeff Mahoney 3dff52
        help='Mark new comment as private')
Jeff Mahoney 3dff52
    g.add_argument('--reset-assignee', action="store_true",
Jeff Mahoney 3dff52
        help='Reset assignee to component default')
Jeff Mahoney 3dff52
    g.add_argument('--reset-qa-contact', action="store_true",
Jeff Mahoney 3dff52
        help='Reset QA contact to component default')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_attach_parser(subparsers):
Jeff Mahoney 3dff52
    usage = """
Jeff Mahoney 3dff52
bugzilla attach --file=FILE --desc=DESC [--type=TYPE] BUGID [BUGID...]
Michal Koutný ccf7f1
bugzilla attach --get=ATTACHID --getall=BUGID [--ignore-obsolete] [...]
Jeff Mahoney 3dff52
bugzilla attach --type=TYPE BUGID [BUGID...]"""
Jeff Mahoney 3dff52
    description = "Attach files or download attachments."
Jeff Mahoney 3dff52
    p = subparsers.add_parser("attach", description=description, usage=usage)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    p.add_argument("ids", nargs="*", help="BUGID references")
Jeff Mahoney 3dff52
    p.add_argument('-f', '--file', metavar="FILENAME",
Jeff Mahoney 3dff52
            help='File to attach, or filename for data provided on stdin')
Jeff Mahoney 3dff52
    p.add_argument('-d', '--description', '--summary',
Jeff Mahoney 3dff52
            metavar="SUMMARY", dest='desc',
Jeff Mahoney 3dff52
            help="A short summary of the file being attached")
Jeff Mahoney 3dff52
    p.add_argument('-t', '--type', metavar="MIMETYPE",
Jeff Mahoney 3dff52
            help="Mime-type for the file being attached")
Jeff Mahoney 3dff52
    p.add_argument('-g', '--get', metavar="ATTACHID", action="append",
Jeff Mahoney 3dff52
            default=[], help="Download the attachment with the given ID")
Jeff Mahoney 3dff52
    p.add_argument("--getall", "--get-all", metavar="BUGID", action="append",
Jeff Mahoney 3dff52
            default=[], help="Download all attachments on the given bug")
Michal Koutný ccf7f1
    p.add_argument('--ignore-obsolete', action="store_true",
Michal Koutný ccf7f1
        help='Do not download attachments marked as obsolete.')
Michal Koutný ccf7f1
    p.add_argument('-l', '--comment', '--long_desc',
Michal Koutný ccf7f1
            help="Add comment with attachment")
Michal Koutný ccf7f1
    p.add_argument('--private', action='store_true', default=False,
Michal Koutný ccf7f1
        help='Mark new comment as private')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _setup_action_login_parser(subparsers):
Michal Koutný ccf7f1
    usage = 'bugzilla login [--api-key] [username [password]]'
Michal Koutný ccf7f1
    description = """Log into bugzilla and save a login cookie or token.
Michal Koutný ccf7f1
Note: These tokens are short-lived, and future Bugzilla versions will no
Michal Koutný ccf7f1
longer support token authentication at all. Please use a
Michal Koutný ccf7f1
~/.config/python-bugzilla/bugzillarc file with an API key instead, or
Michal Koutný ccf7f1
use 'bugzilla login --api-key' and we will save it for you."""
Jeff Mahoney 3dff52
    p = subparsers.add_parser("login", description=description, usage=usage)
Michal Koutný ccf7f1
    p.add_argument('--api-key', action='store_true', default=False,
Michal Koutný ccf7f1
                   help='Prompt for and save an API key into bugzillarc, '
Michal Koutný ccf7f1
                        'rather than prompt for username and password.')
Michal Koutný ccf7f1
    p.add_argument("pos_username", nargs="?", help="Optional username ",
Michal Koutný ccf7f1
                   metavar="username")
Michal Koutný ccf7f1
    p.add_argument("pos_password", nargs="?", help="Optional password ",
Michal Koutný ccf7f1
                   metavar="password")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def setup_parser():
Jeff Mahoney 3dff52
    rootparser = _setup_root_parser()
Jeff Mahoney 3dff52
    subparsers = rootparser.add_subparsers(dest="command")
Jeff Mahoney 3dff52
    subparsers.required = True
Jeff Mahoney 3dff52
    _setup_action_new_parser(subparsers)
Jeff Mahoney 3dff52
    _setup_action_query_parser(subparsers)
Jeff Mahoney 3dff52
    _setup_action_info_parser(subparsers)
Jeff Mahoney 3dff52
    _setup_action_modify_parser(subparsers)
Jeff Mahoney 3dff52
    _setup_action_attach_parser(subparsers)
Jeff Mahoney 3dff52
    _setup_action_login_parser(subparsers)
Jeff Mahoney 3dff52
    return rootparser
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
####################
Jeff Mahoney 3dff52
# Command routines #
Jeff Mahoney 3dff52
####################
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
def _merge_field_opts(query, fields, parser):
Jeff Mahoney 3dff52
    # Add any custom fields if specified
Michal Koutný ccf7f1
    for f in fields:
Jeff Mahoney 3dff52
        try:
Jeff Mahoney 3dff52
            f, v = f.split('=', 1)
Jeff Mahoney 3dff52
            query[f] = v
Jeff Mahoney 3dff52
        except Exception:
Jeff Mahoney 3dff52
            parser.error("Invalid field argument provided: %s" % (f))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_query(bz, opt, parser):
Jeff Mahoney 3dff52
    q = {}
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Parse preconstructed queries.
Jeff Mahoney 3dff52
    u = opt.from_url
Jeff Mahoney 3dff52
    if u:
Jeff Mahoney 3dff52
        q = bz.url_to_query(u)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if opt.components_file:
Jeff Mahoney 3dff52
        # Components slurped in from file (one component per line)
Jeff Mahoney 3dff52
        # This can be made more robust
Jeff Mahoney 3dff52
        clist = []
Jeff Mahoney 3dff52
        f = open(opt.components_file, 'r')
Jeff Mahoney 3dff52
        for line in f.readlines():
Jeff Mahoney 3dff52
            line = line.rstrip("\n")
Jeff Mahoney 3dff52
            clist.append(line)
Jeff Mahoney 3dff52
        opt.component = clist
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if opt.status:
Jeff Mahoney 3dff52
        val = opt.status
Jeff Mahoney 3dff52
        stat = val
Jeff Mahoney 3dff52
        if val == 'ALL':
Jeff Mahoney 3dff52
            # leaving this out should return bugs of any status
Jeff Mahoney 3dff52
            stat = None
Jeff Mahoney 3dff52
        elif val == 'DEV':
Jeff Mahoney 3dff52
            # Alias for all development bug statuses
Jeff Mahoney 3dff52
            stat = ['NEW', 'ASSIGNED', 'NEEDINFO', 'ON_DEV',
Jeff Mahoney 3dff52
                'MODIFIED', 'POST', 'REOPENED']
Jeff Mahoney 3dff52
        elif val == 'QE':
Jeff Mahoney 3dff52
            # Alias for all QE relevant bug statuses
Jeff Mahoney 3dff52
            stat = ['ASSIGNED', 'ON_QA', 'FAILS_QA', 'PASSES_QA']
Jeff Mahoney 3dff52
        elif val == 'EOL':
Jeff Mahoney 3dff52
            # Alias for EndOfLife bug statuses
Michal Koutný 029c1e
            stat = ['VERIFIED', 'RELEASE_PENDING', 'RESOLVED']
Jeff Mahoney 3dff52
        elif val == 'OPEN':
Michal Koutný 029c1e
            # non-RESOLVED statuses
Jeff Mahoney 3dff52
            stat = ['NEW', 'ASSIGNED', 'MODIFIED', 'ON_DEV', 'ON_QA',
Jeff Mahoney 3dff52
                'VERIFIED', 'RELEASE_PENDING', 'POST']
Jeff Mahoney 3dff52
        opt.status = stat
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Convert all comma separated list parameters to actual lists,
Jeff Mahoney 3dff52
    # which is what bugzilla wants
Jeff Mahoney 3dff52
    # According to bugzilla docs, any parameter can be a list, but
Jeff Mahoney 3dff52
    # let's only do this for options we explicitly mention can be
Jeff Mahoney 3dff52
    # comma separated.
Jeff Mahoney 3dff52
    for optname in ["severity", "id", "status", "component",
Jeff Mahoney 3dff52
                    "priority", "product", "version"]:
Jeff Mahoney 3dff52
        val = getattr(opt, optname, None)
Jeff Mahoney 3dff52
        if not isinstance(val, str):
Jeff Mahoney 3dff52
            continue
Jeff Mahoney 3dff52
        setattr(opt, optname, val.split(","))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    include_fields = None
Michal Koutný ccf7f1
    if opt.output in ['raw', 'json']:
Jeff Mahoney 3dff52
        # 'raw' always does a getbug() call anyways, so just ask for ID back
Jeff Mahoney 3dff52
        include_fields = ['id']
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif opt.outputformat:
Jeff Mahoney 3dff52
        include_fields = []
Jeff Mahoney 3dff52
        for fieldname, rest in format_field_re.findall(opt.outputformat):
Jeff Mahoney 3dff52
            if fieldname == "whiteboard" and rest:
Jeff Mahoney 3dff52
                fieldname = rest + "_" + fieldname
Jeff Mahoney 3dff52
            elif fieldname == "flag":
Jeff Mahoney 3dff52
                fieldname = "flags"
Jeff Mahoney 3dff52
            elif fieldname == "cve":
Jeff Mahoney 3dff52
                fieldname = ["keywords", "blocks"]
Jeff Mahoney 3dff52
            elif fieldname == "__unicode__":
Jeff Mahoney 3dff52
                # Needs to be in sync with bug.__unicode__
Jeff Mahoney 3dff52
                fieldname = ["id", "status", "assigned_to", "summary"]
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
            flist = isinstance(fieldname, list) and fieldname or [fieldname]
Jeff Mahoney 3dff52
            for f in flist:
Jeff Mahoney 3dff52
                if f not in include_fields:
Jeff Mahoney 3dff52
                    include_fields.append(f)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if include_fields is not None:
Jeff Mahoney 3dff52
        include_fields.sort()
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    kwopts = {}
Michal Koutný ccf7f1
    if opt.product:
Michal Koutný ccf7f1
        kwopts["product"] = opt.product
Michal Koutný ccf7f1
    if opt.component:
Michal Koutný ccf7f1
        kwopts["component"] = opt.component
Michal Koutný ccf7f1
    if opt.sub_component:
Michal Koutný ccf7f1
        kwopts["sub_component"] = opt.sub_component
Michal Koutný ccf7f1
    if opt.version:
Michal Koutný ccf7f1
        kwopts["version"] = opt.version
Michal Koutný ccf7f1
    if opt.reporter:
Michal Koutný ccf7f1
        kwopts["reporter"] = opt.reporter
Michal Koutný ccf7f1
    if opt.id:
Michal Koutný ccf7f1
        kwopts["bug_id"] = opt.id
Michal Koutný ccf7f1
    if opt.summary:
Michal Koutný ccf7f1
        kwopts["short_desc"] = opt.summary
Michal Koutný ccf7f1
    if opt.comment:
Michal Koutný ccf7f1
        kwopts["long_desc"] = opt.comment
Michal Koutný ccf7f1
    if opt.cc:
Michal Koutný ccf7f1
        kwopts["cc"] = opt.cc
Michal Koutný ccf7f1
    if opt.assigned_to:
Michal Koutný ccf7f1
        kwopts["assigned_to"] = opt.assigned_to
Michal Koutný ccf7f1
    if opt.qa_contact:
Michal Koutný ccf7f1
        kwopts["qa_contact"] = opt.qa_contact
Michal Koutný ccf7f1
    if opt.status:
Michal Koutný ccf7f1
        kwopts["status"] = opt.status
Michal Koutný ccf7f1
    if opt.blocked:
Michal Koutný ccf7f1
        kwopts["blocked"] = opt.blocked
Michal Koutný ccf7f1
    if opt.dependson:
Michal Koutný ccf7f1
        kwopts["dependson"] = opt.dependson
Michal Koutný ccf7f1
    if opt.keywords:
Michal Koutný ccf7f1
        kwopts["keywords"] = opt.keywords
Michal Koutný ccf7f1
    if opt.keywords_type:
Michal Koutný ccf7f1
        kwopts["keywords_type"] = opt.keywords_type
Michal Koutný ccf7f1
    if opt.url:
Michal Koutný ccf7f1
        kwopts["url"] = opt.url
Michal Koutný ccf7f1
    if opt.url_type:
Michal Koutný ccf7f1
        kwopts["url_type"] = opt.url_type
Michal Koutný ccf7f1
    if opt.whiteboard:
Michal Koutný ccf7f1
        kwopts["status_whiteboard"] = opt.whiteboard
Michal Koutný ccf7f1
    if opt.status_whiteboard_type:
Michal Koutný ccf7f1
        kwopts["status_whiteboard_type"] = opt.status_whiteboard_type
Michal Koutný ccf7f1
    if opt.fixed_in:
Michal Koutný ccf7f1
        kwopts["fixed_in"] = opt.fixed_in
Michal Koutný ccf7f1
    if opt.fixed_in_type:
Michal Koutný ccf7f1
        kwopts["fixed_in_type"] = opt.fixed_in_type
Michal Koutný ccf7f1
    if opt.flag:
Michal Koutný ccf7f1
        kwopts["flag"] = opt.flag
Michal Koutný ccf7f1
    if opt.alias:
Michal Koutný ccf7f1
        kwopts["alias"] = opt.alias
Michal Koutný ccf7f1
    if opt.qa_whiteboard:
Michal Koutný ccf7f1
        kwopts["qa_whiteboard"] = opt.qa_whiteboard
Michal Koutný ccf7f1
    if opt.devel_whiteboard:
Michal Koutný ccf7f1
        kwopts["devel_whiteboard"] = opt.devel_whiteboard
Michal Koutný ccf7f1
    if opt.severity:
Michal Koutný ccf7f1
        kwopts["bug_severity"] = opt.severity
Michal Koutný ccf7f1
    if opt.priority:
Michal Koutný ccf7f1
        kwopts["priority"] = opt.priority
Michal Koutný ccf7f1
    if opt.target_release:
Michal Koutný ccf7f1
        kwopts["target_release"] = opt.target_release
Michal Koutný ccf7f1
    if opt.target_milestone:
Michal Koutný ccf7f1
        kwopts["target_milestone"] = opt.target_milestone
Michal Koutný ccf7f1
    if opt.emailtype:
Michal Koutný ccf7f1
        kwopts["emailtype"] = opt.emailtype
Michal Koutný ccf7f1
    if include_fields:
Michal Koutný ccf7f1
        kwopts["include_fields"] = include_fields
Michal Koutný ccf7f1
    if opt.quicksearch:
Michal Koutný ccf7f1
        kwopts["quicksearch"] = opt.quicksearch
Michal Koutný ccf7f1
    if opt.savedsearch:
Michal Koutný ccf7f1
        kwopts["savedsearch"] = opt.savedsearch
Michal Koutný ccf7f1
    if opt.savedsearch_sharer_id:
Michal Koutný ccf7f1
        kwopts["savedsearch_sharer_id"] = opt.savedsearch_sharer_id
Michal Koutný ccf7f1
    if opt.tags:
Michal Koutný ccf7f1
        kwopts["tags"] = opt.tags
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    built_query = bz.build_query(**kwopts)
Michal Koutný ccf7f1
    if opt.fields:
Michal Koutný ccf7f1
        _merge_field_opts(built_query, opt.fields, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    built_query.update(q)
Jeff Mahoney 3dff52
    q = built_query
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    if not q:  # pragma: no cover
Jeff Mahoney 3dff52
        parser.error("'query' command requires additional arguments")
Jeff Mahoney 3dff52
    return bz.query(q)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_info(bz, opt):
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    Handle the 'info' subcommand
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    # All these commands call getproducts internally, so do it up front
Jeff Mahoney 3dff52
    # with minimal include_fields for speed
Jeff Mahoney 3dff52
    def _filter_components(compdetails):
Jeff Mahoney 3dff52
        ret = {}
Jeff Mahoney 3dff52
        for k, v in compdetails.items():
Jeff Mahoney 3dff52
            if v.get("is_active", True):
Jeff Mahoney 3dff52
                ret[k] = v
Jeff Mahoney 3dff52
        return ret
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    productname = (opt.components or opt.component_owners or opt.versions)
Jeff Mahoney 3dff52
    fastcomponents = (opt.components and not opt.active_components)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    include_fields = ["name", "id"]
Michal Koutný ccf7f1
    if opt.components or opt.component_owners:
Michal Koutný ccf7f1
        include_fields += ["components.name"]
Michal Koutný ccf7f1
        if opt.component_owners:
Michal Koutný ccf7f1
            include_fields += ["components.default_assigned_to"]
Michal Koutný ccf7f1
        if opt.active_components:
Michal Koutný ccf7f1
            include_fields += ["components.is_active"]
Michal Koutný ccf7f1
Jeff Mahoney 3dff52
    if opt.versions:
Jeff Mahoney 3dff52
        include_fields += ["versions"]
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    bz.refresh_products(names=productname and [productname] or None,
Jeff Mahoney 3dff52
            include_fields=include_fields)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if opt.products:
Jeff Mahoney 3dff52
        for name in sorted([p["name"] for p in bz.getproducts()]):
Jeff Mahoney 3dff52
            print(name)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif fastcomponents:
Jeff Mahoney 3dff52
        for name in sorted(bz.getcomponents(productname)):
Jeff Mahoney 3dff52
            print(name)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif opt.components:
Jeff Mahoney 3dff52
        details = bz.getcomponentsdetails(productname)
Jeff Mahoney 3dff52
        for name in sorted(_filter_components(details)):
Jeff Mahoney 3dff52
            print(name)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif opt.versions:
Jeff Mahoney 3dff52
        proddict = bz.getproducts()[0]
Jeff Mahoney 3dff52
        for v in proddict['versions']:
Michal Koutný ccf7f1
            print(str(v["name"] or ''))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif opt.component_owners:
Jeff Mahoney 3dff52
        details = bz.getcomponentsdetails(productname)
Jeff Mahoney 3dff52
        for c in sorted(_filter_components(details)):
Michal Koutný ccf7f1
            print("%s: %s" % (c, details[c]['default_assigned_to']))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _convert_to_outputformat(output):
Jeff Mahoney 3dff52
    fmt = ""
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if output == "normal":
Jeff Mahoney 3dff52
        fmt = "%{__unicode__}"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif output == "ids":
Jeff Mahoney 3dff52
        fmt = "%{id}"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif output == 'full':
Jeff Mahoney 3dff52
        fmt += "%{__unicode__}\n"
Jeff Mahoney 3dff52
        fmt += "Component: %{component}\n"
Jeff Mahoney 3dff52
        fmt += "CC: %{cc}\n"
Jeff Mahoney 3dff52
        fmt += "Blocked: %{blocks}\n"
Jeff Mahoney 3dff52
        fmt += "Depends: %{depends_on}\n"
Jeff Mahoney 3dff52
        fmt += "%{comments}\n"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif output == 'extra':
Jeff Mahoney 3dff52
        fmt += "%{__unicode__}\n"
Jeff Mahoney 3dff52
        fmt += " +Keywords: %{keywords}\n"
Jeff Mahoney 3dff52
        fmt += " +QA Whiteboard: %{qa_whiteboard}\n"
Jeff Mahoney 3dff52
        fmt += " +Status Whiteboard: %{status_whiteboard}\n"
Jeff Mahoney 3dff52
        fmt += " +Devel Whiteboard: %{devel_whiteboard}\n"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif output == 'oneline':
Jeff Mahoney 3dff52
        fmt += "#%{bug_id} %{status} %{assigned_to} %{component}\t"
Jeff Mahoney 3dff52
        fmt += "[%{target_milestone}] %{flags} %{cve}"
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    else:  # pragma: no cover
Jeff Mahoney 3dff52
        raise RuntimeError("Unknown output type '%s'" % output)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    return fmt
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
def _xmlrpc_converter(obj):
Michal Koutný ccf7f1
    if "DateTime" in str(obj.__class__):
Michal Koutný ccf7f1
        # xmlrpc DateTime object. Convert to date format that
Michal Koutný ccf7f1
        # bugzilla REST API outputs
Michal Koutný ccf7f1
        dobj = datetime.datetime.strptime(str(obj), '%Y%m%dT%H:%M:%S')
Michal Koutný ccf7f1
        return dobj.isoformat() + "Z"
Michal Koutný ccf7f1
    if "Binary" in str(obj.__class__):
Michal Koutný ccf7f1
        # xmlrpc Binary object. Convert to base64
Michal Koutný ccf7f1
        return base64.b64encode(obj.data).decode("utf-8")
Michal Koutný ccf7f1
    raise RuntimeError(
Michal Koutný ccf7f1
        "Unexpected JSON conversion class=%s" % obj.__class__)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
Michal Koutný ccf7f1
def _format_output_json(buglist):
Michal Koutný ccf7f1
    out = {"bugs": [b.get_raw_data() for b in buglist]}
Michal Koutný ccf7f1
    s = json.dumps(out, default=_xmlrpc_converter, indent=2, sort_keys=True)
Michal Koutný ccf7f1
    print(s)
Michal Koutný ccf7f1
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
def _format_output_raw(buglist):
Michal Koutný ccf7f1
    for b in buglist:
Michal Koutný ccf7f1
        print("Bugzilla %s: " % b.bug_id)
Michal Koutný ccf7f1
        SKIP_NAMES = ["bugzilla"]
Michal Koutný ccf7f1
        for attrname in sorted(b.__dict__):
Michal Koutný ccf7f1
            if attrname in SKIP_NAMES:
Michal Koutný ccf7f1
                continue
Michal Koutný ccf7f1
            if attrname.startswith("_"):
Michal Koutný ccf7f1
                continue
Michal Koutný ccf7f1
            print("ATTRIBUTE[%s]: %s" % (attrname, b.__dict__[attrname]))
Michal Koutný ccf7f1
        print("\n\n")
Michal Koutný ccf7f1
Michal Koutný ccf7f1
Michal Koutný ccf7f1
def _bug_field_repl_cb(bz, b, matchobj):
Michal Koutný ccf7f1
    # whiteboard and flag allow doing
Michal Koutný ccf7f1
    #   %{whiteboard:devel} and %{flag:needinfo}
Michal Koutný ccf7f1
    # That's what 'rest' matches
Michal Koutný ccf7f1
    (fieldname, rest) = matchobj.groups()
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    if fieldname == "whiteboard" and rest:
Michal Koutný ccf7f1
        fieldname = rest + "_" + fieldname
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    if fieldname == "flag" and rest:
Michal Koutný ccf7f1
        val = b.get_flag_status(rest)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    elif fieldname in ["flags", "flags_requestee"]:
Michal Koutný ccf7f1
        tmpstr = []
Michal Koutný ccf7f1
        for f in getattr(b, "flags", []):
Michal Koutný ccf7f1
            requestee = f.get('requestee', "")
Michal Koutný ccf7f1
            if fieldname == "flags":
Michal Koutný ccf7f1
                requestee = ""
Michal Koutný ccf7f1
            if fieldname == "flags_requestee":
Michal Koutný ccf7f1
                if requestee == "":
Jeff Mahoney 3dff52
                    continue
Michal Koutný ccf7f1
                tmpstr.append("%s" % requestee)
Michal Koutný ccf7f1
            else:
Michal Koutný ccf7f1
                tmpstr.append("%s%s%s" %
Michal Koutný ccf7f1
                        (f['name'], f['status'], requestee))
Michal Koutný ccf7f1
Michal Koutný ccf7f1
        val = ",".join(tmpstr)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    elif fieldname == "cve":
Michal Koutný ccf7f1
        cves = []
Michal Koutný ccf7f1
        for key in getattr(b, "keywords", []):
Michal Koutný ccf7f1
            # grab CVE from keywords and blockers
Michal Koutný ccf7f1
            if key.find("Security") == -1:
Michal Koutný ccf7f1
                continue
Michal Koutný ccf7f1
            for bl in b.blocks:
Michal Koutný ccf7f1
                cvebug = bz.getbug(bl)
Michal Koutný ccf7f1
                for cb in cvebug.alias:
Michal Koutný ccf7f1
                    if (cb.find("CVE") != -1 and
Michal Koutný ccf7f1
                        cb.strip() not in cves):
Michal Koutný ccf7f1
                        cves.append(cb)
Michal Koutný ccf7f1
        val = ",".join(cves)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    elif fieldname == "comments":
Michal Koutný ccf7f1
        val = ""
Michal Koutný ccf7f1
        for c in getattr(b, "comments", []):
Michal Koutný ccf7f1
            val += ("\n* %s - %s:\n%s\n" % (c['time'],
Michal Koutný ccf7f1
                     c.get("creator", c.get("author", "")), c['text']))
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    elif fieldname == "external_bugs":
Michal Koutný ccf7f1
        val = ""
Michal Koutný ccf7f1
        for e in getattr(b, "external_bugs", []):
Michal Koutný ccf7f1
            url = e["type"]["full_url"].replace("%id%", e["ext_bz_bug_id"])
Michal Koutný ccf7f1
            if not val:
Michal Koutný ccf7f1
                val += "\n"
Michal Koutný ccf7f1
            val += "External bug: %s\n" % url
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    elif fieldname == "__unicode__":
Michal Koutný ccf7f1
        val = b.__unicode__()
Michal Koutný ccf7f1
    else:
Michal Koutný ccf7f1
        val = getattr(b, fieldname, "")
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    vallist = isinstance(val, list) and val or [val]
Michal Koutný ccf7f1
    val = ','.join([str(v or '') for v in vallist])
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    return val
Michal Koutný ccf7f1
Michal Koutný ccf7f1
Michal Koutný ccf7f1
def _format_output(bz, opt, buglist):
Michal Koutný ccf7f1
    if opt.output in ['raw', 'json']:
Michal Koutný ccf7f1
        include_fields = None
Michal Koutný ccf7f1
        exclude_fields = None
Michal Koutný ccf7f1
        extra_fields = None
Michal Koutný ccf7f1
Michal Koutný ccf7f1
        if opt.includefield:
Michal Koutný ccf7f1
            include_fields = opt.includefield
Michal Koutný ccf7f1
        if opt.excludefield:
Michal Koutný ccf7f1
            exclude_fields = opt.excludefield
Michal Koutný ccf7f1
        if opt.extrafield:
Michal Koutný ccf7f1
            extra_fields = opt.extrafield
Michal Koutný ccf7f1
Michal Koutný ccf7f1
        buglist = bz.getbugs([b.bug_id for b in buglist],
Michal Koutný ccf7f1
                include_fields=include_fields,
Michal Koutný ccf7f1
                exclude_fields=exclude_fields,
Michal Koutný ccf7f1
                extra_fields=extra_fields)
Michal Koutný ccf7f1
        if opt.output == 'json':
Michal Koutný ccf7f1
            _format_output_json(buglist)
Michal Koutný ccf7f1
        if opt.output == 'raw':
Michal Koutný ccf7f1
            _format_output_raw(buglist)
Michal Koutný ccf7f1
        return
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    for b in buglist:
Michal Koutný ccf7f1
        # pylint: disable=cell-var-from-loop
Michal Koutný ccf7f1
        def cb(matchobj):
Michal Koutný ccf7f1
            return _bug_field_repl_cb(bz, b, matchobj)
Michal Koutný ccf7f1
        print(format_field_re.sub(cb, opt.outputformat))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _parse_triset(vallist, checkplus=True, checkminus=True, checkequal=True,
Jeff Mahoney 3dff52
                  splitcomma=False):
Jeff Mahoney 3dff52
    add_val = []
Jeff Mahoney 3dff52
    rm_val = []
Jeff Mahoney 3dff52
    set_val = None
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    def make_list(v):
Jeff Mahoney 3dff52
        if not v:
Jeff Mahoney 3dff52
            return []
Jeff Mahoney 3dff52
        if splitcomma:
Jeff Mahoney 3dff52
            return v.split(",")
Jeff Mahoney 3dff52
        return [v]
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    for val in isinstance(vallist, list) and vallist or [vallist]:
Jeff Mahoney 3dff52
        val = val or ""
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
        if val.startswith("+") and checkplus:
Jeff Mahoney 3dff52
            add_val += make_list(val[1:])
Jeff Mahoney 3dff52
        elif val.startswith("-") and checkminus:
Jeff Mahoney 3dff52
            rm_val += make_list(val[1:])
Jeff Mahoney 3dff52
        elif val.startswith("=") and checkequal:
Jeff Mahoney 3dff52
            # Intentionally overwrite this
Jeff Mahoney 3dff52
            set_val = make_list(val[1:])
Jeff Mahoney 3dff52
        else:
Jeff Mahoney 3dff52
            add_val += make_list(val)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    return add_val, rm_val, set_val
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_new(bz, opt, parser):
Jeff Mahoney 3dff52
    # Parse options that accept comma separated list
Jeff Mahoney 3dff52
    def parse_multi(val):
Jeff Mahoney 3dff52
        return _parse_triset(val, checkplus=False, checkminus=False,
Jeff Mahoney 3dff52
                             checkequal=False, splitcomma=True)[0]
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    kwopts = {}
Michal Koutný ccf7f1
    if opt.blocked:
Michal Koutný ccf7f1
        kwopts["blocks"] = parse_multi(opt.blocked)
Michal Koutný ccf7f1
    if opt.cc:
Michal Koutný ccf7f1
        kwopts["cc"] = parse_multi(opt.cc)
Michal Koutný ccf7f1
    if opt.component:
Michal Koutný ccf7f1
        kwopts["component"] = opt.component
Michal Koutný ccf7f1
    if opt.dependson:
Michal Koutný ccf7f1
        kwopts["depends_on"] = parse_multi(opt.dependson)
Michal Koutný ccf7f1
    if opt.comment:
Michal Koutný ccf7f1
        kwopts["description"] = opt.comment
Michal Koutný ccf7f1
    if opt.groups:
Michal Koutný ccf7f1
        kwopts["groups"] = parse_multi(opt.groups)
Michal Koutný ccf7f1
    if opt.keywords:
Michal Koutný ccf7f1
        kwopts["keywords"] = parse_multi(opt.keywords)
Michal Koutný ccf7f1
    if opt.os:
Michal Koutný ccf7f1
        kwopts["op_sys"] = opt.os
Michal Koutný ccf7f1
    if opt.arch:
Michal Koutný ccf7f1
        kwopts["platform"] = opt.arch
Michal Koutný ccf7f1
    if opt.priority:
Michal Koutný ccf7f1
        kwopts["priority"] = opt.priority
Michal Koutný ccf7f1
    if opt.product:
Michal Koutný ccf7f1
        kwopts["product"] = opt.product
Michal Koutný ccf7f1
    if opt.severity:
Michal Koutný ccf7f1
        kwopts["severity"] = opt.severity
Michal Koutný ccf7f1
    if opt.summary:
Michal Koutný ccf7f1
        kwopts["summary"] = opt.summary
Michal Koutný ccf7f1
    if opt.url:
Michal Koutný ccf7f1
        kwopts["url"] = opt.url
Michal Koutný ccf7f1
    if opt.version:
Michal Koutný ccf7f1
        kwopts["version"] = opt.version
Michal Koutný ccf7f1
    if opt.assigned_to:
Michal Koutný ccf7f1
        kwopts["assigned_to"] = opt.assigned_to
Michal Koutný ccf7f1
    if opt.qa_contact:
Michal Koutný ccf7f1
        kwopts["qa_contact"] = opt.qa_contact
Michal Koutný ccf7f1
    if opt.sub_component:
Michal Koutný ccf7f1
        kwopts["sub_component"] = opt.sub_component
Michal Koutný ccf7f1
    if opt.alias:
Michal Koutný ccf7f1
        kwopts["alias"] = opt.alias
Michal Koutný ccf7f1
    if opt.comment_tag:
Michal Koutný ccf7f1
        kwopts["comment_tags"] = opt.comment_tag
Michal Koutný ccf7f1
    if opt.private:
Michal Koutný ccf7f1
        kwopts["comment_private"] = opt.private
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    ret = bz.build_createbug(**kwopts)
Michal Koutný ccf7f1
    if opt.fields:
Michal Koutný ccf7f1
        _merge_field_opts(ret, opt.fields, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    b = bz.createbug(ret)
Michal Koutný 029c1e
    if not opt.no_refresh:
Michal Koutný 029c1e
        b.refresh()
Jeff Mahoney 3dff52
    return [b]
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_modify(bz, parser, opt):
Jeff Mahoney 3dff52
    bugid_list = [bugid for a in opt.ids for bugid in a.split(',')]
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    add_wb, rm_wb, set_wb = _parse_triset(opt.whiteboard)
Jeff Mahoney 3dff52
    add_devwb, rm_devwb, set_devwb = _parse_triset(opt.devel_whiteboard)
Jeff Mahoney 3dff52
    add_intwb, rm_intwb, set_intwb = _parse_triset(opt.internal_whiteboard)
Jeff Mahoney 3dff52
    add_qawb, rm_qawb, set_qawb = _parse_triset(opt.qa_whiteboard)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    add_blk, rm_blk, set_blk = _parse_triset(opt.blocked, splitcomma=True)
Jeff Mahoney 3dff52
    add_deps, rm_deps, set_deps = _parse_triset(opt.dependson, splitcomma=True)
Jeff Mahoney 3dff52
    add_key, rm_key, set_key = _parse_triset(opt.keywords)
Jeff Mahoney 3dff52
    add_cc, rm_cc, ignore = _parse_triset(opt.cc,
Jeff Mahoney 3dff52
                                          checkplus=False,
Jeff Mahoney 3dff52
                                          checkequal=False)
Jeff Mahoney 3dff52
    add_groups, rm_groups, ignore = _parse_triset(opt.groups,
Jeff Mahoney 3dff52
                                                  checkequal=False,
Jeff Mahoney 3dff52
                                                  splitcomma=True)
Jeff Mahoney 3dff52
    add_tags, rm_tags, ignore = _parse_triset(opt.tags, checkequal=False)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    status = opt.status or None
Jeff Mahoney 3dff52
    if opt.dupeid is not None:
Jeff Mahoney 3dff52
        opt.close = "DUPLICATE"
Jeff Mahoney 3dff52
    if opt.close:
Michal Koutný 029c1e
        status = "RESOLVED"
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    flags = []
Jeff Mahoney 3dff52
    if opt.flag:
Jeff Mahoney 3dff52
        # Convert "foo+" to tuple ("foo", "+")
Jeff Mahoney 3dff52
        for f in opt.flag:
Jeff Mahoney 3dff52
            flags.append({"name": f[:-1], "status": f[-1]})
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    update_opts = {}
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    if opt.assigned_to:
Michal Koutný ccf7f1
        update_opts["assigned_to"] = opt.assigned_to
Michal Koutný ccf7f1
    if opt.comment:
Michal Koutný ccf7f1
        update_opts["comment"] = opt.comment
Michal Koutný ccf7f1
    if opt.private:
Michal Koutný ccf7f1
        update_opts["comment_private"] = opt.private
Michal Koutný ccf7f1
    if opt.component:
Michal Koutný ccf7f1
        update_opts["component"] = opt.component
Michal Koutný ccf7f1
    if opt.product:
Michal Koutný ccf7f1
        update_opts["product"] = opt.product
Michal Koutný ccf7f1
    if add_blk:
Michal Koutný ccf7f1
        update_opts["blocks_add"] = add_blk
Michal Koutný ccf7f1
    if rm_blk:
Michal Koutný ccf7f1
        update_opts["blocks_remove"] = rm_blk
Michal Koutný ccf7f1
    if set_blk is not None:
Michal Koutný ccf7f1
        update_opts["blocks_set"] = set_blk
Michal Koutný ccf7f1
    if opt.url:
Michal Koutný ccf7f1
        update_opts["url"] = opt.url
Michal Koutný ccf7f1
    if add_cc:
Michal Koutný ccf7f1
        update_opts["cc_add"] = add_cc
Michal Koutný ccf7f1
    if rm_cc:
Michal Koutný ccf7f1
        update_opts["cc_remove"] = rm_cc
Michal Koutný ccf7f1
    if add_deps:
Michal Koutný ccf7f1
        update_opts["depends_on_add"] = add_deps
Michal Koutný ccf7f1
    if rm_deps:
Michal Koutný ccf7f1
        update_opts["depends_on_remove"] = rm_deps
Michal Koutný ccf7f1
    if set_deps is not None:
Michal Koutný ccf7f1
        update_opts["depends_on_set"] = set_deps
Michal Koutný ccf7f1
    if add_groups:
Michal Koutný ccf7f1
        update_opts["groups_add"] = add_groups
Michal Koutný ccf7f1
    if rm_groups:
Michal Koutný ccf7f1
        update_opts["groups_remove"] = rm_groups
Michal Koutný ccf7f1
    if add_key:
Michal Koutný ccf7f1
        update_opts["keywords_add"] = add_key
Michal Koutný ccf7f1
    if rm_key:
Michal Koutný ccf7f1
        update_opts["keywords_remove"] = rm_key
Michal Koutný ccf7f1
    if set_key is not None:
Michal Koutný ccf7f1
        update_opts["keywords_set"] = set_key
Michal Koutný ccf7f1
    if opt.os:
Michal Koutný ccf7f1
        update_opts["op_sys"] = opt.os
Michal Koutný ccf7f1
    if opt.arch:
Michal Koutný ccf7f1
        update_opts["platform"] = opt.arch
Michal Koutný ccf7f1
    if opt.priority:
Michal Koutný ccf7f1
        update_opts["priority"] = opt.priority
Michal Koutný ccf7f1
    if opt.qa_contact:
Michal Koutný ccf7f1
        update_opts["qa_contact"] = opt.qa_contact
Michal Koutný ccf7f1
    if opt.severity:
Michal Koutný ccf7f1
        update_opts["severity"] = opt.severity
Michal Koutný ccf7f1
    if status:
Michal Koutný ccf7f1
        update_opts["status"] = status
Michal Koutný ccf7f1
    if opt.summary:
Michal Koutný ccf7f1
        update_opts["summary"] = opt.summary
Michal Koutný ccf7f1
    if opt.version:
Michal Koutný ccf7f1
        update_opts["version"] = opt.version
Michal Koutný ccf7f1
    if opt.reset_assignee:
Michal Koutný ccf7f1
        update_opts["reset_assigned_to"] = opt.reset_assignee
Michal Koutný ccf7f1
    if opt.reset_qa_contact:
Michal Koutný ccf7f1
        update_opts["reset_qa_contact"] = opt.reset_qa_contact
Michal Koutný ccf7f1
    if opt.close:
Michal Koutný ccf7f1
        update_opts["resolution"] = opt.close
Michal Koutný ccf7f1
    if opt.target_release:
Michal Koutný ccf7f1
        update_opts["target_release"] = opt.target_release
Michal Koutný ccf7f1
    if opt.target_milestone:
Michal Koutný ccf7f1
        update_opts["target_milestone"] = opt.target_milestone
Michal Koutný ccf7f1
    if opt.dupeid:
Michal Koutný ccf7f1
        update_opts["dupe_of"] = opt.dupeid
Michal Koutný ccf7f1
    if opt.fixed_in:
Michal Koutný ccf7f1
        update_opts["fixed_in"] = opt.fixed_in
Michal Koutný ccf7f1
    if set_wb and set_wb[0]:
Michal Koutný ccf7f1
        update_opts["whiteboard"] = set_wb and set_wb[0]
Michal Koutný ccf7f1
    if set_devwb and set_devwb[0]:
Michal Koutný ccf7f1
        update_opts["devel_whiteboard"] = set_devwb and set_devwb[0]
Michal Koutný ccf7f1
    if set_intwb and set_intwb[0]:
Michal Koutný ccf7f1
        update_opts["internal_whiteboard"] = set_intwb and set_intwb[0]
Michal Koutný ccf7f1
    if set_qawb and set_qawb[0]:
Michal Koutný ccf7f1
        update_opts["qa_whiteboard"] = set_qawb and set_qawb[0]
Michal Koutný ccf7f1
    if opt.sub_component:
Michal Koutný ccf7f1
        update_opts["sub_component"] = opt.sub_component
Michal Koutný ccf7f1
    if opt.alias:
Michal Koutný ccf7f1
        update_opts["alias"] = opt.alias
Michal Koutný ccf7f1
    if flags:
Michal Koutný ccf7f1
        update_opts["flags"] = flags
Michal Koutný ccf7f1
    if opt.comment_tag:
Michal Koutný ccf7f1
        update_opts["comment_tags"] = opt.comment_tag
Michal Koutný ccf7f1
    if opt.minor_update:
Michal Koutný ccf7f1
        update_opts["minor_update"] = opt.minor_update
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    update = bz.build_update(**update_opts)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # We make this a little convoluted to facilitate unit testing
Jeff Mahoney 3dff52
    wbmap = {
Jeff Mahoney 3dff52
        "whiteboard": (add_wb, rm_wb),
Jeff Mahoney 3dff52
        "internal_whiteboard": (add_intwb, rm_intwb),
Jeff Mahoney 3dff52
        "qa_whiteboard": (add_qawb, rm_qawb),
Jeff Mahoney 3dff52
        "devel_whiteboard": (add_devwb, rm_devwb),
Jeff Mahoney 3dff52
    }
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    for k, v in wbmap.copy().items():
Jeff Mahoney 3dff52
        if not v[0] and not v[1]:
Jeff Mahoney 3dff52
            del(wbmap[k])
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    if opt.fields:
Michal Koutný ccf7f1
        _merge_field_opts(update, opt.fields, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    log.debug("update bug dict=%s", update)
Jeff Mahoney 3dff52
    log.debug("update whiteboard dict=%s", wbmap)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if not any([update, wbmap, add_tags, rm_tags]):
Jeff Mahoney 3dff52
        parser.error("'modify' command requires additional arguments")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if add_tags or rm_tags:
Jeff Mahoney 3dff52
        ret = bz.update_tags(bugid_list,
Jeff Mahoney 3dff52
            tags_add=add_tags, tags_remove=rm_tags)
Jeff Mahoney 3dff52
        log.debug("bz.update_tags returned=%s", ret)
Jeff Mahoney 3dff52
    if update:
Jeff Mahoney 3dff52
        ret = bz.update_bugs(bugid_list, update)
Jeff Mahoney 3dff52
        log.debug("bz.update_bugs returned=%s", ret)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if not wbmap:
Jeff Mahoney 3dff52
        return
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Now for the things we can't blindly batch.
Jeff Mahoney 3dff52
    # Being able to prepend/append to whiteboards, which are just
Jeff Mahoney 3dff52
    # plain string values, is an old rhbz semantic that we try to maintain
Michal Koutný ccf7f1
    # here. This is a bit weird for traditional bugzilla API
Jeff Mahoney 3dff52
    log.debug("Adjusting whiteboard fields one by one")
Jeff Mahoney 3dff52
    for bug in bz.getbugs(bugid_list):
Michal Koutný ccf7f1
        update_kwargs = {}
Michal Koutný ccf7f1
        for wbkey, (add_list, rm_list) in wbmap.items():
Michal Koutný ccf7f1
            bugval = getattr(bug, wbkey) or ""
Jeff Mahoney 3dff52
            for tag in add_list:
Michal Koutný ccf7f1
                if bugval:
Michal Koutný ccf7f1
                    bugval += " "
Michal Koutný ccf7f1
                bugval += tag
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
            for tag in rm_list:
Michal Koutný ccf7f1
                bugsplit = bugval.split()
Michal Koutný ccf7f1
                for t in bugsplit[:]:
Jeff Mahoney 3dff52
                    if t == tag:
Michal Koutný ccf7f1
                        bugsplit.remove(t)
Michal Koutný ccf7f1
                bugval = " ".join(bugsplit)
Michal Koutný ccf7f1
Michal Koutný ccf7f1
            update_kwargs[wbkey] = bugval
Michal Koutný ccf7f1
Michal Koutný ccf7f1
        bz.update_bugs([bug.id], bz.build_update(**update_kwargs))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_get_attach(bz, opt):
Michal Koutný ccf7f1
    data = {}
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    def _process_attachment_data(_attlist):
Michal Koutný ccf7f1
        for _att in _attlist:
Michal Koutný ccf7f1
            data[_att["id"]] = _att
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    if opt.getall:
Michal Koutný ccf7f1
        for attlist in bz.get_attachments(opt.getall, None)["bugs"].values():
Michal Koutný ccf7f1
            _process_attachment_data(attlist)
Michal Koutný ccf7f1
    if opt.get:
Michal Koutný ccf7f1
        _process_attachment_data(
Michal Koutný ccf7f1
            bz.get_attachments(None, opt.get)["attachments"].values())
Michal Koutný ccf7f1
Michal Koutný ccf7f1
    for attdata in data.values():
Michal Koutný ccf7f1
        is_obsolete = attdata.get("is_obsolete", None) == 1
Michal Koutný ccf7f1
        if opt.ignore_obsolete and is_obsolete:
Michal Koutný ccf7f1
            continue
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
        att = bz.openattachment_data(attdata)
Jeff Mahoney 3dff52
        outfile = open_without_clobber(att.name, "wb")
Jeff Mahoney 3dff52
        data = att.read(4096)
Jeff Mahoney 3dff52
        while data:
Jeff Mahoney 3dff52
            outfile.write(data)
Jeff Mahoney 3dff52
            data = att.read(4096)
Jeff Mahoney 3dff52
        print("Wrote %s" % outfile.name)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _do_set_attach(bz, opt, parser):
Jeff Mahoney 3dff52
    if not opt.ids:
Jeff Mahoney 3dff52
        parser.error("Bug ID must be specified for setting attachments")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if sys.stdin.isatty():
Jeff Mahoney 3dff52
        if not opt.file:
Jeff Mahoney 3dff52
            parser.error("--file must be specified")
Jeff Mahoney 3dff52
        fileobj = open(opt.file, "rb")
Jeff Mahoney 3dff52
    else:
Jeff Mahoney 3dff52
        # piped input on stdin
Jeff Mahoney 3dff52
        if not opt.desc:
Jeff Mahoney 3dff52
            parser.error("--description must be specified if passing "
Jeff Mahoney 3dff52
                         "file on stdin")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
        fileobj = tempfile.NamedTemporaryFile(prefix="bugzilla-attach.")
Jeff Mahoney 3dff52
        data = sys.stdin.read(4096)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
        while data:
Jeff Mahoney 3dff52
            fileobj.write(data.encode(locale.getpreferredencoding()))
Jeff Mahoney 3dff52
            data = sys.stdin.read(4096)
Jeff Mahoney 3dff52
        fileobj.seek(0)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    kwargs = {}
Jeff Mahoney 3dff52
    if opt.file:
Jeff Mahoney 3dff52
        kwargs["filename"] = os.path.basename(opt.file)
Jeff Mahoney 3dff52
    if opt.type:
Jeff Mahoney 3dff52
        kwargs["contenttype"] = opt.type
Jeff Mahoney 3dff52
    if opt.type in ["text/x-patch"]:
Jeff Mahoney 3dff52
        kwargs["ispatch"] = True
Jeff Mahoney 3dff52
    if opt.comment:
Jeff Mahoney 3dff52
        kwargs["comment"] = opt.comment
Michal Koutný ccf7f1
    if opt.private:
Michal Koutný ccf7f1
        kwargs["is_private"] = True
Jeff Mahoney 3dff52
    desc = opt.desc or os.path.basename(fileobj.name)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Upload attachments
Jeff Mahoney 3dff52
    for bugid in opt.ids:
Jeff Mahoney 3dff52
        attid = bz.attachfile(bugid, fileobj, desc, **kwargs)
Jeff Mahoney 3dff52
        print("Created attachment %i on bug %s" % (attid, bugid))
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
#################
Jeff Mahoney 3dff52
# Main handling #
Jeff Mahoney 3dff52
#################
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _make_bz_instance(opt):
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    Build the Bugzilla instance we will use
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    if opt.bztype != 'auto':
Jeff Mahoney 3dff52
        log.info("Explicit --bztype is no longer supported, ignoring")
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    cookiefile = None
Jeff Mahoney 3dff52
    tokenfile = None
Michal Koutný ccf7f1
    use_creds = False
Jeff Mahoney 3dff52
    if opt.cache_credentials:
Jeff Mahoney 3dff52
        cookiefile = opt.cookiefile or -1
Jeff Mahoney 3dff52
        tokenfile = opt.tokenfile or -1
Michal Koutný ccf7f1
        use_creds = True
Jeff Mahoney 3dff52
Michal Koutný ccf7f1
    return bugzilla.Bugzilla(
Jeff Mahoney 3dff52
        url=opt.bugzilla,
Jeff Mahoney 3dff52
        cookiefile=cookiefile,
Jeff Mahoney 3dff52
        tokenfile=tokenfile,
Jeff Mahoney 3dff52
        sslverify=opt.sslverify,
Michal Koutný ccf7f1
        use_creds=use_creds,
Jeff Mahoney 3dff52
        cert=opt.cert)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _handle_login(opt, action, bz):
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    Handle all login related bits
Jeff Mahoney 3dff52
    """
Jeff Mahoney 3dff52
    is_login_command = (action == 'login')
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    do_interactive_login = (is_login_command or
Jeff Mahoney 3dff52
        opt.login or opt.username or opt.password)
Jeff Mahoney 3dff52
    username = getattr(opt, "pos_username", None) or opt.username
Jeff Mahoney 3dff52
    password = getattr(opt, "pos_password", None) or opt.password
Michal Koutný ccf7f1
    use_key = getattr(opt, "api_key", False)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    try:
Michal Koutný ccf7f1
        if use_key:
Michal Koutný ccf7f1
            bz.interactive_save_api_key()
Michal Koutný ccf7f1
        elif do_interactive_login:
Michal Koutný ccf7f1
            if bz.api_key:
Michal Koutný ccf7f1
                print("You already have an API key configured for %s" % bz.url)
Michal Koutný ccf7f1
                print("There is no need to cache a login token. Exiting.")
Michal Koutný ccf7f1
                sys.exit(0)
Michal Koutný ccf7f1
            print("Logging into %s" % urllib.parse.urlparse(bz.url)[1])
Michal Koutný ccf7f1
            bz.interactive_login(username, password,
Michal Koutný ccf7f1
                    restrict_login=opt.restrict_login)
Jeff Mahoney 3dff52
    except bugzilla.BugzillaError as e:
Jeff Mahoney 3dff52
        print(str(e))
Jeff Mahoney 3dff52
        sys.exit(1)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if opt.ensure_logged_in and not bz.logged_in:
Jeff Mahoney 3dff52
        print("--ensure-logged-in passed but you aren't logged in to %s" %
Jeff Mahoney 3dff52
            bz.url)
Jeff Mahoney 3dff52
        sys.exit(1)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if is_login_command:
Jeff Mahoney 3dff52
        sys.exit(0)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def _main(unittest_bz_instance):
Jeff Mahoney 3dff52
    parser = setup_parser()
Jeff Mahoney 3dff52
    opt = parser.parse_args()
Jeff Mahoney 3dff52
    action = opt.command
Jeff Mahoney 3dff52
    setup_logging(opt.debug, opt.verbose)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    log.debug("Launched with command line: %s", " ".join(sys.argv))
Michal Koutný ccf7f1
    log.debug("Bugzilla module: %s", bugzilla)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if unittest_bz_instance:
Jeff Mahoney 3dff52
        bz = unittest_bz_instance
Jeff Mahoney 3dff52
    else:
Jeff Mahoney 3dff52
        bz = _make_bz_instance(opt)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # Handle login options
Jeff Mahoney 3dff52
    _handle_login(opt, action, bz)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    ###########################
Jeff Mahoney 3dff52
    # Run the actual commands #
Jeff Mahoney 3dff52
    ###########################
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    if hasattr(opt, "outputformat"):
Michal Koutný ccf7f1
        if not opt.outputformat and opt.output not in ['raw', 'json', None]:
Jeff Mahoney 3dff52
            opt.outputformat = _convert_to_outputformat(opt.output)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    buglist = []
Jeff Mahoney 3dff52
    if action == 'info':
Jeff Mahoney 3dff52
        _do_info(bz, opt)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif action == 'query':
Jeff Mahoney 3dff52
        buglist = _do_query(bz, opt, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif action == 'new':
Jeff Mahoney 3dff52
        buglist = _do_new(bz, opt, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif action == 'attach':
Jeff Mahoney 3dff52
        if opt.get or opt.getall:
Jeff Mahoney 3dff52
            if opt.ids:
Jeff Mahoney 3dff52
                parser.error("Bug IDs '%s' not used for "
Jeff Mahoney 3dff52
                    "getting attachments" % opt.ids)
Jeff Mahoney 3dff52
            _do_get_attach(bz, opt)
Jeff Mahoney 3dff52
        else:
Jeff Mahoney 3dff52
            _do_set_attach(bz, opt, parser)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    elif action == 'modify':
Michal Koutný ccf7f1
        _do_modify(bz, parser, opt)
Michal Koutný ccf7f1
    else:  # pragma: no cover
Jeff Mahoney 3dff52
        raise RuntimeError("Unexpected action '%s'" % action)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
    # If we're doing new/query/modify, output our results
Jeff Mahoney 3dff52
    if action in ['new', 'query']:
Jeff Mahoney 3dff52
        _format_output(bz, opt, buglist)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def main(unittest_bz_instance=None):
Jeff Mahoney 3dff52
    try:
Jeff Mahoney 3dff52
        try:
Jeff Mahoney 3dff52
            return _main(unittest_bz_instance)
Jeff Mahoney 3dff52
        except (Exception, KeyboardInterrupt):
Jeff Mahoney 3dff52
            log.debug("", exc_info=True)
Jeff Mahoney 3dff52
            raise
Michal Koutný ccf7f1
    except KeyboardInterrupt:
Michal Koutný ccf7f1
        print("\nExited at user request.")
Michal Koutný ccf7f1
        sys.exit(1)
Michal Koutný ccf7f1
    except (xmlrpc.client.Fault, bugzilla.BugzillaError) as e:
Jeff Mahoney 3dff52
        print("\nServer error: %s" % str(e))
Jeff Mahoney 3dff52
        sys.exit(3)
Jeff Mahoney 3dff52
    except requests.exceptions.SSLError as e:
Jeff Mahoney 3dff52
        # Give SSL recommendations
Jeff Mahoney 3dff52
        print("SSL error: %s" % e)
Jeff Mahoney 3dff52
        print("\nIf you trust the remote server, you can work "
Jeff Mahoney 3dff52
              "around this error with:\n"
Jeff Mahoney 3dff52
              "  bugzilla --nosslverify ...")
Jeff Mahoney 3dff52
        sys.exit(4)
Jeff Mahoney 3dff52
    except (socket.error,
Jeff Mahoney 3dff52
            requests.exceptions.HTTPError,
Jeff Mahoney 3dff52
            requests.exceptions.ConnectionError,
Michal Koutný ccf7f1
            requests.exceptions.InvalidURL,
Michal Koutný ccf7f1
            xmlrpc.client.ProtocolError) as e:
Jeff Mahoney 3dff52
        print("\nConnection lost/failed: %s" % str(e))
Jeff Mahoney 3dff52
        sys.exit(2)
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
Jeff Mahoney 3dff52
def cli():
Michal Koutný ccf7f1
    main()