diff --git a/.gitignore b/.gitignore index e69de29..7a55d68 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +/pybugz-0.10-git89df2.tar.gz diff --git a/prepare-tarball.sh b/prepare-tarball.sh new file mode 100755 index 0000000..84e7b02 --- /dev/null +++ b/prepare-tarball.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# grep the spec file for version number +VERSION=$( cat pybugz.spec | grep ^Version: | cut -d' ' -f 2- | tr -d ' ') +REV=$( cat pybugz.spec | grep '%global gitrev' | cut -d' ' -f 3- | tr -d ' ') + +BASE="pybugz-${VERSION}-git${REV}" +TARBALL=$BASE.tar.gz +DIR=$( mktemp -d ) +GIT=https://github.com/williamh/pybugz.git + +echo == preparing tarball for pybugz-$VERSION == + +pushd $DIR > /dev/null && \ +git clone $GIT pybugz && \ +cd pybugz && \ +git archive --prefix $BASE/ $REV | gzip > $TARBALL && \ +popd > /dev/null && \ +cp $DIR/pybugz/$TARBALL . && \ +echo == DONE == && \ +rm -rf $DIR diff --git a/pybugz-0.10-git89df2-downstream.patch b/pybugz-0.10-git89df2-downstream.patch new file mode 100644 index 0000000..fc95094 --- /dev/null +++ b/pybugz-0.10-git89df2-downstream.patch @@ -0,0 +1,1145 @@ +From ce7591bee38eb3b94f9b68cb5699a597195ff8fa Mon Sep 17 00:00:00 2001 +From: Pavel Raiskup +Date: Sun, 20 Jan 2013 23:20:50 +0100 +Subject: [PATCH] Downstream patch to follow + https://github.com/praiskup/pybugz + +--- + bin/bugz | 19 ++--- + bugz/argparsers.py | 10 ++- + bugz/cli.py | 209 ++++++++++++++++++++++++++++------------------ + bugz/configfile.py | 217 +++++++++++++++++++++++++++++++++--------------- + bugz/errhandling.py | 6 ++ + bugz/log.py | 72 ++++++++++++++++ + bugzrc.example | 61 -------------- + conf/conf.d/gentoo.conf | 3 + + conf/conf.d/redhat.conf | 3 + + conf/pybugz.conf | 126 ++++++++++++++++++++++++++++ + man/bugz.1 | 18 ++-- + setup.py | 4 + + 12 files changed, 520 insertions(+), 228 deletions(-) + create mode 100644 bugz/errhandling.py + create mode 100644 bugz/log.py + delete mode 100644 bugzrc.example + create mode 100644 conf/conf.d/gentoo.conf + create mode 100644 conf/conf.d/redhat.conf + create mode 100644 conf/pybugz.conf + +diff --git a/bin/bugz b/bin/bugz +index 4e61ddf..6f72fa1 100755 +--- a/bin/bugz ++++ b/bin/bugz +@@ -25,17 +25,16 @@ import sys + import traceback + + from bugz.argparsers import make_parser +-from bugz.cli import BugzError, PrettyBugz +-from bugz.configfile import get_config ++from bugz.cli import PrettyBugz ++from bugz.errhandling import BugzError ++from bugz.log import * + + def main(): +- parser = make_parser() + + # parse options ++ args = None ++ parser = make_parser() + args = parser.parse_args() +- get_config(args) +- if getattr(args, 'columns') is None: +- setattr(args, 'columns', 0) + + try: + bugz = PrettyBugz(args) +@@ -43,17 +42,17 @@ def main(): + return 0 + + except BugzError, e: +- print ' ! Error: %s' % e ++ log_error(e) + return 1 + + except TypeError, e: +- print ' ! Error: Incorrect number of arguments supplied' +- print ++ # where this comes from? ++ log_error('Incorrect number of arguments supplied') + traceback.print_exc() + return 1 + + except RuntimeError, e: +- print ' ! Error: %s' % e ++ log_error(e) + return 1 + + except KeyboardInterrupt: +diff --git a/bugz/argparsers.py b/bugz/argparsers.py +index d14dd84..4cf3936 100644 +--- a/bugz/argparsers.py ++++ b/bugz/argparsers.py +@@ -258,11 +258,12 @@ def make_parser(): + parser = argparse.ArgumentParser( + epilog = 'use -h after a sub-command for sub-command specific help') + parser.add_argument('--config-file', ++ default = None, + help = 'read an alternate configuration file') + parser.add_argument('--connection', + help = 'use [connection] section of your configuration file') + parser.add_argument('-b', '--base', +- default = 'https://bugs.gentoo.org/xmlrpc.cgi', ++ default = None, + help = 'base URL of Bugzilla') + parser.add_argument('-u', '--user', + help = 'username for commands requiring authentication') +@@ -272,10 +273,15 @@ def make_parser(): + help = 'password command to evaluate for commands requiring authentication') + parser.add_argument('-q', '--quiet', + action='store_true', ++ default=None, + help = 'quiet mode') ++ parser.add_argument('-d', '--debug', ++ type=int, ++ default=None, ++ help = 'debug level (from 0 to 3)') + parser.add_argument('--columns', + type = int, +- help = 'maximum number of columns output should use') ++ help = 'maximum number of columns output should use (0 = unlimited)') + parser.add_argument('--encoding', + help = 'output encoding (default: utf-8).') + parser.add_argument('--skip-auth', +diff --git a/bugz/cli.py b/bugz/cli.py +index 62ba540..7112387 100644 +--- a/bugz/cli.py ++++ b/bugz/cli.py +@@ -1,5 +1,3 @@ +-#!/usr/bin/env python +- + import commands + import getpass + from cookielib import CookieJar, LWPCookieJar +@@ -12,6 +10,11 @@ import sys + import tempfile + import textwrap + import xmlrpclib ++import pdb ++ ++from bugz.configfile import discover_configs ++from bugz.log import * ++from bugz.errhandling import BugzError + + try: + import readline +@@ -28,8 +31,8 @@ BUGZ: Any line beginning with 'BUGZ:' will be ignored. + BUGZ: --------------------------------------------------- + """ + +-DEFAULT_COOKIE_FILE = '.bugz_cookie' + DEFAULT_NUM_COLS = 80 ++DEFAULT_CONFIG_FILE = '/etc/pybugz/pybugz.conf' + + # + # Auxiliary functions +@@ -119,23 +122,63 @@ def block_edit(comment, comment_from = ''): + else: + return '' + +-# +-# Bugz specific exceptions +-# +- +-class BugzError(Exception): +- pass +- + class PrettyBugz: ++ enc = "utf-8" ++ columns = 0 ++ quiet = None ++ skip_auth = None ++ ++ # TODO: ++ # * make this class more library-like (allow user to script on the python ++ # level using this PrettyBugz class) ++ # * get the "__init__" phase into main() and change parameters to accept ++ # only 'settings' structure + def __init__(self, args): +- self.quiet = args.quiet +- self.columns = args.columns or terminal_width() +- self.user = args.user +- self.password = args.password +- self.passwordcmd = args.passwordcmd +- self.skip_auth = args.skip_auth +- +- cookie_file = os.path.join(os.environ['HOME'], DEFAULT_COOKIE_FILE) ++ ++ sys_config = DEFAULT_CONFIG_FILE ++ home_config = getattr(args, 'config_file') ++ setDebugLvl(getattr(args, 'debug')) ++ settings = discover_configs(sys_config, home_config) ++ ++ # use the default connection name ++ conn_name = settings['default'] ++ ++ # check for redefinition by --connection ++ opt_conn = getattr(args, 'connection') ++ if opt_conn != None: ++ conn_name = opt_conn ++ ++ if not conn_name in settings['connections']: ++ raise BugzError("can't find connection '{0}'".format(conn_name)) ++ ++ # get proper 'Connection' instance ++ connection = settings['connections'][conn_name] ++ ++ def fix_con(con, name,opt): ++ if opt != None: ++ setattr(con, name, opt) ++ con.option_change = True ++ ++ fix_con(connection, "base", args.base) ++ fix_con(connection, "quiet", args.quiet) ++ fix_con(connection, "columns", args.columns) ++ connection.columns = int(connection.columns) or terminal_width() ++ fix_con(connection, "user", args.user) ++ fix_con(connection, "password", args.password) ++ fix_con(connection, "password_cmd", args.passwordcmd) ++ fix_con(connection, "skip_auth", args.skip_auth) ++ fix_con(connection, "encoding", args.encoding) ++ ++ # now must the "connection" be complete ++ ++ # propagate layout settings to 'self' ++ self.enc = connection.encoding ++ self.skip_auth = connection.skip_auth ++ self.columns = connection.columns ++ ++ setQuiet(connection.quiet) ++ ++ cookie_file = os.path.expanduser(connection.cookie_file) + self.cookiejar = LWPCookieJar(cookie_file) + + try: +@@ -143,9 +186,7 @@ class PrettyBugz: + except IOError: + pass + +- if getattr(args, 'encoding'): +- self.enc = args.encoding +- else: ++ if not self.enc: + try: + self.enc = locale.getdefaultlocale()[1] + except: +@@ -153,19 +194,9 @@ class PrettyBugz: + if not self.enc: + self.enc = 'utf-8' + +- self.log("Using %s " % args.base) +- self.bz = BugzillaProxy(args.base, cookiejar=self.cookiejar) +- +- def log(self, status_msg, newline = True): +- if not self.quiet: +- if newline: +- print ' * %s' % status_msg +- else: +- print ' * %s' % status_msg, +- +- def warn(self, warn_msg): +- if not self.quiet: +- print ' ! Warning: %s' % warn_msg ++ self.bz = BugzillaProxy(connection.base, cookiejar=self.cookiejar) ++ connection.dump() ++ self.connection = connection + + def get_input(self, prompt): + return raw_input(prompt) +@@ -186,27 +217,29 @@ class PrettyBugz: + """Authenticate a session. + """ + # prompt for username if we were not supplied with it +- if not self.user: +- self.log('No username given.') +- self.user = self.get_input('Username: ') ++ if not self.connection.user: ++ log_info('No username given.') ++ self.connection.user = self.get_input('Username: ') + + # prompt for password if we were not supplied with it +- if not self.password: +- if not self.passwordcmd: +- self.log('No password given.') +- self.password = getpass.getpass() ++ if not self.connection.password: ++ if not self.connection.password_cmd: ++ log_info('No password given.') ++ self.connection.password = getpass.getpass() + else: +- process = subprocess.Popen(self.passwordcmd.split(), shell=False, +- stdout=subprocess.PIPE) ++ cmd = self.connection.password_cmd.split() ++ stdout = stdout=subprocess.PIPE ++ process = subprocess.Popen(cmd, shell=False, stdout=stdout) + self.password, _ = process.communicate() ++ self.connection.password, _ = process.communicate() + + # perform login + params = {} +- params['login'] = self.user +- params['password'] = self.password ++ params['login'] = self.connection.user ++ params['password'] = self.connection.password + if args is not None: + params['remember'] = True +- self.log('Logging in') ++ log_info('Logging in') + try: + self.bz.User.login(params) + except xmlrpclib.Fault as fault: +@@ -217,7 +250,7 @@ class PrettyBugz: + os.chmod(self.cookiejar.filename, 0600) + + def logout(self, args): +- self.log('logging out') ++ log_info('logging out') + try: + self.bz.User.logout() + except xmlrpclib.Fault as fault: +@@ -251,29 +284,39 @@ class PrettyBugz: + else: + log_msg = 'Searching for bugs ' + +- if search_opts: +- self.log(log_msg + 'with the following options:') +- for opt, val in search_opts: +- self.log(' %-20s = %s' % (opt, val)) +- else: +- self.log(log_msg) +- + if not 'status' in params.keys(): +- params['status'] = ['CONFIRMED', 'IN_PROGRESS', 'UNCONFIRMED'] +- elif 'ALL' in params['status']: ++ if self.connection.query_statuses: ++ params['status'] = self.connection.query_statuses ++ else: ++ # this seems to be most portable among bugzillas as each ++ # bugzilla may have its own set of statuses. ++ params['status'] = ['ALL'] ++ ++ if 'ALL' in params['status']: + del params['status'] + ++ if len(params): ++ log_info(log_msg + 'with the following options:') ++ for opt, val in params.items(): ++ log_info(' %-20s = %s' % (opt, str(val))) ++ else: ++ log_info(log_msg) ++ + result = self.bzcall(self.bz.Bug.search, params)['bugs'] + + if not len(result): +- self.log('No bugs found.') ++ log_info('No bugs found.') + else: + self.listbugs(result, args.show_status) + + def get(self, args): + """ Fetch bug details given the bug id """ +- self.log('Getting bug %s ..' % args.bugid) +- result = self.bzcall(self.bz.Bug.get, {'ids':[args.bugid]}) ++ log_info('Getting bug %s ..' % args.bugid) ++ try: ++ result = self.bzcall(self.bz.Bug.get, {'ids':[args.bugid]}) ++ except xmlrpclib.Fault as fault: ++ raise BugzError("Can't get bug #" + str(args.bugid) + ": " \ ++ + fault.faultString) + + for bug in result['bugs']: + self.showbuginfo(bug, args.attachments, args.comments) +@@ -293,7 +336,7 @@ class PrettyBugz: + (args.description_from, e)) + + if not args.batch: +- self.log('Press Ctrl+C at any time to abort.') ++ log_info('Press Ctrl+C at any time to abort.') + + # + # Check all bug fields. +@@ -306,14 +349,14 @@ class PrettyBugz: + while not args.product or len(args.product) < 1: + args.product = self.get_input('Enter product: ') + else: +- self.log('Enter product: %s' % args.product) ++ log_info('Enter product: %s' % args.product) + + # check for component + if not args.component: + while not args.component or len(args.component) < 1: + args.component = self.get_input('Enter component: ') + else: +- self.log('Enter component: %s' % args.component) ++ log_info('Enter component: %s' % args.component) + + # check for version + # FIXME: This default behaviour is not too nice. +@@ -324,14 +367,14 @@ class PrettyBugz: + else: + args.version = 'unspecified' + else: +- self.log('Enter version: %s' % args.version) ++ log_info('Enter version: %s' % args.version) + + # check for title + if not args.summary: + while not args.summary or len(args.summary) < 1: + args.summary = self.get_input('Enter title: ') + else: +- self.log('Enter title: %s' % args.summary) ++ log_info('Enter title: %s' % args.summary) + + # check for description + if not args.description: +@@ -339,7 +382,7 @@ class PrettyBugz: + if len(line): + args.description = line + else: +- self.log('Enter bug description: %s' % args.description) ++ log_info('Enter bug description: %s' % args.description) + + # check for operating system + if not args.op_sys: +@@ -348,7 +391,7 @@ class PrettyBugz: + if len(line): + args.op_sys = line + else: +- self.log('Enter operating system: %s' % args.op_sys) ++ log_info('Enter operating system: %s' % args.op_sys) + + # check for platform + if not args.platform: +@@ -357,7 +400,7 @@ class PrettyBugz: + if len(line): + args.platform = line + else: +- self.log('Enter hardware platform: %s' % args.platform) ++ log_info('Enter hardware platform: %s' % args.platform) + + # check for default priority + if args.priority is None: +@@ -366,7 +409,7 @@ class PrettyBugz: + if len(line): + args.priority = line + else: +- self.log('Enter priority (optional): %s' % args.priority) ++ log_info('Enter priority (optional): %s' % args.priority) + + # check for default severity + if args.severity is None: +@@ -375,7 +418,7 @@ class PrettyBugz: + if len(line): + args.severity = line + else: +- self.log('Enter severity (optional): %s' % args.severity) ++ log_info('Enter severity (optional): %s' % args.severity) + + # check for default alias + if args.alias is None: +@@ -384,7 +427,7 @@ class PrettyBugz: + if len(line): + args.alias = line + else: +- self.log('Enter alias (optional): %s' % args.alias) ++ log_info('Enter alias (optional): %s' % args.alias) + + # check for default assignee + if args.assigned_to is None: +@@ -393,7 +436,7 @@ class PrettyBugz: + if len(line): + args.assigned_to = line + else: +- self.log('Enter assignee (optional): %s' % args.assigned_to) ++ log_info('Enter assignee (optional): %s' % args.assigned_to) + + # check for CC list + if args.cc is None: +@@ -402,7 +445,7 @@ class PrettyBugz: + if len(line): + args.cc = line.split(', ') + else: +- self.log('Enter a CC list (optional): %s' % args.cc) ++ log_info('Enter a CC list (optional): %s' % args.cc) + + # check for URL + if args.url is None: +@@ -422,7 +465,7 @@ class PrettyBugz: + if args.append_command is None: + args.append_command = self.get_input('Append the output of the following command (leave blank for none): ') + else: +- self.log('Append command (optional): %s' % args.append_command) ++ log_info('Append command (optional): %s' % args.append_command) + + # raise an exception if mandatory fields are not specified. + if args.product is None: +@@ -470,7 +513,7 @@ class PrettyBugz: + if len(confirm) < 1: + confirm = args.default_confirm + if confirm[0] not in ('y', 'Y'): +- self.log('Submission aborted') ++ log_info('Submission aborted') + return + + params={} +@@ -498,7 +541,7 @@ class PrettyBugz: + params['url'] = args.url + + result = self.bzcall(self.bz.Bug.create, params) +- self.log('Bug %d submitted' % result['id']) ++ log_info('Bug %d submitted' % result['id']) + + def modify(self, args): + """Modify an existing bug (eg. adding a comment or changing resolution.)""" +@@ -604,16 +647,16 @@ class PrettyBugz: + for bug in result['bugs']: + changes = bug['changes'] + if not len(changes): +- self.log('Added comment to bug %s' % bug['id']) ++ log_info('Added comment to bug %s' % bug['id']) + else: +- self.log('Modified the following fields in bug %s' % bug['id']) ++ log_info('Modified the following fields in bug %s' % bug['id']) + for key in changes.keys(): +- self.log('%-12s: removed %s' %(key, changes[key]['removed'])) +- self.log('%-12s: added %s' %(key, changes[key]['added'])) ++ log_info('%-12s: removed %s' %(key, changes[key]['removed'])) ++ log_info('%-12s: added %s' %(key, changes[key]['added'])) + + def attachment(self, args): + """ Download or view an attachment given the id.""" +- self.log('Getting attachment %s' % args.attachid) ++ log_info('Getting attachment %s' % args.attachid) + + params = {} + params['attachment_ids'] = [args.attachid] +@@ -621,7 +664,7 @@ class PrettyBugz: + result = result['attachments'][args.attachid] + + action = {True:'Viewing', False:'Saving'} +- self.log('%s attachment: "%s"' % ++ log_info('%s attachment: "%s"' % + (action[args.view], result['file_name'])) + safe_filename = os.path.basename(re.sub(r'\.\.', '', + result['file_name'])) +@@ -671,7 +714,7 @@ class PrettyBugz: + params['comment'] = comment + params['is_patch'] = is_patch + result = self.bzcall(self.bz.Bug.add_attachment, params) +- self.log("'%s' has been attached to bug %s" % (filename, bugid)) ++ log_info("'%s' has been attached to bug %s" % (filename, bugid)) + + def listbugs(self, buglist, show_status=False): + for bug in buglist: +@@ -690,7 +733,7 @@ class PrettyBugz: + except UnicodeDecodeError: + print line[:self.columns] + +- self.log("%i bug(s) found." % len(buglist)) ++ log_info("%i bug(s) found." % len(buglist)) + + def showbuginfo(self, bug, show_attachments, show_comments): + FIELDS = ( +diff --git a/bugz/configfile.py b/bugz/configfile.py +index a900245..43a502c 100644 +--- a/bugz/configfile.py ++++ b/bugz/configfile.py +@@ -1,70 +1,155 @@ + import ConfigParser +-import os ++import os, glob + import sys ++import pdb + +-DEFAULT_CONFIG_FILE = '~/.bugzrc' +- +-def config_option(parser, get, section, option): +- if parser.has_option(section, option): +- try: +- if get(section, option) != '': +- return get(section, option) +- else: +- print " ! Error: "+option+" is not set" +- sys.exit(1) +- except ValueError, e: +- print " ! Error: option "+option+" is not in the right format: "+str(e) +- sys.exit(1) +- +-def fill_config_option(args, parser, get, section, option): +- value = config_option(parser, get, section, option) +- if value is not None: +- setattr(args, option, value) +- +-def fill_config(args, parser, section): +- fill_config_option(args, parser, parser.get, section, 'base') +- fill_config_option(args, parser, parser.get, section, 'user') +- fill_config_option(args, parser, parser.get, section, 'password') +- fill_config_option(args, parser, parser.get, section, 'passwordcmd') +- fill_config_option(args, parser, parser.getint, section, 'columns') +- fill_config_option(args, parser, parser.get, section, 'encoding') +- fill_config_option(args, parser, parser.getboolean, section, 'quiet') +- +-def get_config(args): +- config_file = getattr(args, 'config_file') +- if config_file is None: +- config_file = DEFAULT_CONFIG_FILE +- section = getattr(args, 'connection') +- parser = ConfigParser.ConfigParser() +- config_file_name = os.path.expanduser(config_file) +- +- # try to open config file +- try: +- file = open(config_file_name) +- except IOError: +- if getattr(args, 'config_file') is not None: +- print " ! Error: Can't find user configuration file: "+config_file_name +- sys.exit(1) +- else: +- return +- +- # try to parse config file ++from bugz.errhandling import BugzError ++from bugz.log import * ++ ++class Connection: ++ name = "default" ++ base = 'https://bugs.gentoo.org/xmlrpc.cgi' ++ columns = 0 ++ user = None ++ password = None ++ password_cmd = None ++ dbglvl = 0 ++ quiet = None ++ skip_auth = None ++ encoding = "utf-8" ++ cookie_file = "~/.bugz_cookie" ++ option_change = False ++ query_statuses = [] ++ ++ def dump(self): ++ log_info("Using [{0}] ({1})".format(self.name, self.base)) ++ log_debug("User: '{0}'".format(self.user), 3) ++ # loglvl == 4, only for developers (&& only by hardcoding) ++ log_debug("Pass: '{0}'".format(self.password), 10) ++ log_debug("Columns: {0}".format(self.columns), 3) ++ ++def handle_default(settings, newDef): ++ oldDef = str(settings['default']) ++ if oldDef != newDef: ++ log_debug("redefining default connection from '{0}' to '{1}'". \ ++ format(oldDef, newDef), 2) ++ settings['default'] = newDef ++ ++def handle_settings(settings, context, stack, cp, sec_name): ++ log_debug("contains SETTINGS section named [{0}]".format(sec_name), 3) ++ ++ if cp.has_option(sec_name, 'homeconf'): ++ settings['homeconf'] = cp.get(sec_name, 'homeconf') ++ ++ if cp.has_option(sec_name, 'default'): ++ handle_default(settings, cp.get(sec_name, 'default')) ++ ++ # handle 'confdir' ~> explore and push target files into the stack ++ if cp.has_option(sec_name, 'confdir'): ++ confdir = cp.get(sec_name, 'confdir') ++ full_confdir = os.path.expanduser(confdir) ++ wildcard = os.path.join(full_confdir, '*.conf') ++ log_debug("adding wildcard " + wildcard, 3) ++ for cnffile in glob.glob(wildcard): ++ log_debug(" ++ " + cnffile, 3) ++ if cnffile in context['included']: ++ log_debug("skipping (already included)") ++ break ++ stack.append(cnffile) ++ ++def handle_connection(settings, context, stack, parser, name): ++ log_debug("reading connection '{0}'".format(name), 2) ++ connection = None ++ ++ if name in settings['connections']: ++ log_debug("redefining connection '{0}'".format(name), 2) ++ connection = settings['connections'][name] ++ else: ++ connection = Connection() ++ connection.name = name ++ ++ def fill(conn, id): ++ if parser.has_option(name, id): ++ val = parser.get(name, id) ++ setattr(conn, id, val) ++ log_debug("has {0} - {1}".format(id, val), 3) ++ ++ fill(connection, "base") ++ fill(connection, "user") ++ fill(connection, "password") ++ fill(connection, "encoding") ++ fill(connection, "columns") ++ fill(connection, "quiet") ++ ++ if parser.has_option(name, 'query_statuses'): ++ line = parser.get(name, 'query_statuses') ++ lines = line.split() ++ connection.query_statuses = lines ++ ++ settings['connections'][name] = connection ++ ++def parse_file(settings, context, stack): ++ file_name = stack.pop() ++ full_name = os.path.expanduser(file_name) ++ ++ context['included'][full_name] = None ++ ++ log_debug("parsing '" + file_name + "'", 1) ++ ++ cp = ConfigParser.ConfigParser() ++ parsed = None + try: +- parser.readfp(file) +- sections = parser.sections() +- except ConfigParser.ParsingError, e: +- print " ! Error: Can't parse user configuration file: "+str(e) +- sys.exit(1) +- +- # parse the default section first +- if "default" in sections: +- fill_config(args, parser, "default") +- if section is None: +- section = config_option(parser, parser.get, "default", "connection") +- +- # parse a specific section +- if section in sections: +- fill_config(args, parser, section) +- elif section is not None: +- print " ! Error: Can't find section ["+section+"] in configuration file" +- sys.exit(1) ++ parsed = cp.read(full_name) ++ if parsed != [ full_name ]: ++ raise BugzError("problem with file '" + file_name + "'") ++ except ConfigParser.Error, err: ++ msg = err.message ++ raise BugzError("can't parse: '" + file_name + "'\n" + msg ) ++ ++ # successfully parsed file ++ ++ for sec in cp.sections(): ++ sectype = "connection" ++ ++ if cp.has_option(sec, 'type'): ++ sectype = cp.get(sec, 'type') ++ ++ if sectype == "settings": ++ handle_settings(settings, context, stack, cp, sec) ++ ++ if sectype == "connection": ++ handle_connection(settings, context, stack, cp, sec) ++ ++def discover_configs(file, homeConf=None): ++ settings = { ++ # where to look for user's configuration ++ 'homeconf' : '~/.bugzrc', ++ # list of objects of Connection ++ 'connections' : {}, ++ # the default Connection name ++ 'default' : None, ++ } ++ context = { ++ 'where' : 'sys', ++ 'homeparsed' : False, ++ 'included' : {}, ++ } ++ stack = [ file ] ++ ++ # parse sys configs ++ while len(stack) > 0: ++ parse_file(settings, context, stack) ++ ++ if not homeConf: ++ # the command-line option must win ++ homeConf = settings['homeconf'] ++ ++ if not os.path.isfile(os.path.expanduser(homeConf)): ++ return settings ++ ++ # parse home configs ++ stack = [ homeConf ] ++ while len(stack) > 0: ++ parse_file(settings, context, stack) ++ ++ return settings +diff --git a/bugz/errhandling.py b/bugz/errhandling.py +new file mode 100644 +index 0000000..d3fec06 +--- /dev/null ++++ b/bugz/errhandling.py +@@ -0,0 +1,6 @@ ++# ++# Bugz specific exceptions ++# ++ ++class BugzError(Exception): ++ pass +diff --git a/bugz/log.py b/bugz/log.py +new file mode 100644 +index 0000000..df4bb9a +--- /dev/null ++++ b/bugz/log.py +@@ -0,0 +1,72 @@ ++# TODO: use the python's 'logging' feature? ++ ++dbglvl = 0 ++quiet = False ++ ++LogSettins = { ++ 'W' : { ++ 'symb' : '!', ++ 'word' : 'Warn', ++ }, ++ 'E' : { ++ 'symb' : '#', ++ 'word' : 'Error', ++ }, ++ 'D' : { ++ 'symb' : '~', ++ 'word' : 'Dbg', ++ }, ++ 'I' : { ++ 'symb' : '*', ++ 'word' : 'Info', ++ }, ++ '!' : { ++ 'symb' : '!', ++ 'word' : 'UNKNWN', ++ }, ++} ++ ++def setQuiet(newQuiet): ++ global quiet ++ quiet = newQuiet ++ ++def setDebugLvl(newlvl): ++ global dbglvl ++ if not newlvl: ++ return ++ if newlvl > 3: ++ log_warn("bad debug level '{0}', using '3'".format(str(newlvl))) ++ dbglvl = 3 ++ else: ++ dbglvl = newlvl ++ ++def formatOut(msg, id='!'): ++ lines = str(msg).split('\n') ++ start = True ++ symb=LogSettins[id]['symb'] ++ word=LogSettins[id]['word'] + ":" ++ ++ for line in lines: ++ print ' ' + symb + ' ' + line ++ ++def log_error(string): ++ formatOut(string, 'E') ++ return ++ ++def log_warn(string): ++ formatOut(string, 'W') ++ return ++ ++def log_info(string): ++ global quiet ++ global dbglvl ++ # debug implies info ++ if not quiet or dbglvl: ++ formatOut(string, 'I') ++ return ++ ++def log_debug(string, verboseness=1): ++ global dbglvl ++ if dbglvl >= verboseness: ++ formatOut(string, 'D') ++ return +diff --git a/bugzrc.example b/bugzrc.example +deleted file mode 100644 +index f516bf0..0000000 +--- a/bugzrc.example ++++ /dev/null +@@ -1,61 +0,0 @@ +-# +-# bugzrc.example - an example configuration file for pybugz +-# +-# This file consists of sections which define parameters for each +-# bugzilla you plan to use. +-# +-# Each section begins with a name in square brackets. This is also the +-# name that should be used with the --connection parameter to the bugz +-# command. +-# +-# Each section of this file consists of lines in the form: +-# key: value +-# as listed below. +-# +-# [sectionname] +-# +-# The base url of the bugzilla you wish to use. +-# This must point to the xmlrpc.cgi script on the bugzilla installation. +-# +-# base: http://my.project.com/bugzilla/xmlrpc.cgi +-# +-# It is also possible to encode a username and password into this URL +-# for basic http authentication as follows: +-# +-# base: http://myhttpname:myhttppasswd@my.project.com/bugzilla/xmlrpc.cgi +-# +-# Next are your username and password for this bugzilla. If you do not +-# provide these, you will be prompted for them. +-# +-# user: myname@my.project.com +-# password: secret2 +-# +-# As an alternative to keeping your password in this file you can provide a +-# password command. It is evaluated and pybugz expects this command to output +-# the password to standard out. E.g.: +-# +-# passwordcmd: gpg2 --decrypt /myhome/.my-encrypted-password.gpg +-# +-# The number of columns your terminal can display. +-# Most of the time you should not have to set this. +-# +-# columns: 80 +-# +-# Set the output encoding for pybugz. +-# +-# encoding: utf-8 +-# +-# Run in quiet mode. +-# +-# quiet: True +-# +-# The special section named 'default' may also be used. Other sections will +-# override any values specified here. The optional special key 'connection' is +-# used to name the default connection, to use when no --connection parameter is +-# specified to the bugz command. +-# +-# [default] +-# connection: sectionname +-# +-# All parameters listed above can be used in the default section if you +-# only use one bugzilla installation. +diff --git a/conf/conf.d/gentoo.conf b/conf/conf.d/gentoo.conf +new file mode 100644 +index 0000000..42fae46 +--- /dev/null ++++ b/conf/conf.d/gentoo.conf +@@ -0,0 +1,3 @@ ++[Gentoo] ++base = https://bugs.gentoo.org/xmlrpc.cgi ++query_statuses = CONFIRMED IN_PROGRESS UNCONFIRMED +diff --git a/conf/conf.d/redhat.conf b/conf/conf.d/redhat.conf +new file mode 100644 +index 0000000..0f50fb7 +--- /dev/null ++++ b/conf/conf.d/redhat.conf +@@ -0,0 +1,3 @@ ++[RedHat] ++base = https://bugzilla.redhat.com/xmlrpc.cgi ++query_statuses = NEW ASSIGNED MODIFIED ON_DEV POST +diff --git a/conf/pybugz.conf b/conf/pybugz.conf +new file mode 100644 +index 0000000..3a486b4 +--- /dev/null ++++ b/conf/pybugz.conf +@@ -0,0 +1,126 @@ ++# =========================================================================== ++# The "root" configuration file of PyBugz bugzilla interface. ++# =========================================================================== ++# ++# Overview ++# ======== ++# PyBugz is configured by hierarchy of *.conf files. All the configuration ++# job starts from this file. User specific configuration is by default in ++# file ~/.bugzrc. This is specially usefull to allow user to redefine some ++# system configuration (an example could be adding user's credentials ++# for specific connection — see the following text). ++# ++# Syntax ++# ====== ++# The syntax is similar to Windows INI files. For more info, see the ++# documentation for python's ConfigParser library class — this class is used ++# for parsing configuration files here. Quickly, each file consists of ++# sections (section's name in brackets, e.g. [section]). Each section ++# consists of set of configuration options separated by newlines. ++# ++# [sectionName] ++# optionA = value A ++# optionB = this is value of B # comments are possible ++# ++# Section types ++# ============= ++# Currently, there are implemented two types of sections in PyBugz. Those ++# are 'connection' (default type of section) and 'settings'. ++# Type 'settings' has purpose for setting up some global feature of PyBugz. ++# The type 'connection', however, describes attributes of particular ++# connection to some concrete instance of bugzilla. ++# ++# +------------------------+ ++# | 1. "type = connection" | ++# +------------------------+ ++# ++# Important property of this type is its section identifier (name of ++# section). By passing this name as an argument of --connection option is ++# PyBugz's user able to select which connection will be used. ++# ++# Accepted options / semantics ++# ---------------------------- ++# ++# Note that you may specify each section of type 'connection' multiple ++# times (using the same ID). All settings are combined among same named ++# sections with one rule: the last one wins. This is important when you ++# want to specify some defaults system wide and let particular user ++# redefine (or correct) concrete connection — user's configuration is ++# loaded _later_ than system's. ++# ++# * type ++# May be set optionally to 'connection', but it is the default in each ++# section. ++# ++# * base ++# Sets up the xmlrpc entrance into bugzilla, for example: ++# https://bugzilla.redhat.com/xmlrpc.cgi ++# ++# * user & password ++# These two options let you specify your login information to bugzilla ++# instance (you must be registered there of course). It is also ++# possible to encode a user (usually user's email) and password into ++# base: ++# http://myhttpname:myhttppasswd@my.project.com/bugzilla/xmlrpc.cgi ++# Note that if you don't specify your login information, you will be ++# prompted for them. ++# ++# * passwordcmd ++# As an alternative to keeping your password in this file you can ++# provide a password command. It is evaluated and pybugz expects this ++# command to output the password to standard out. E.g.: ++# ++# passwordcmd = gpg2 --decrypt /myhome/.my-encrypted-password.gpg ++# ++# * columns ++# The number of columns your terminal can display (or you want to be ++# displayed) during using of this connection. Expects integer number. ++# ++# * query_statuses ++# List of bug-statuses to be displayed by default (when *not* redefined by ++# --status option). Accepts list of properly spelled statuses separated ++# by single space, e.g.: query_statuses = ASSIGNED CLOSED ++# ++# * encoding ++# Set the output encoding for PyBugz. Default is utf-8. ++# ++# * quiet ++# Run this connection in quiet mode when: quiet = True. ++# ++# * inherit (to be done in future) ++# ++# +----------------------+ ++# | 2. "type = settings" | ++# +----------------------+ ++# ++# Again, this lets you define PyBugz "global" settings (among all ++# connections). The name of section is not important here. Same as ++# 'connection' type, even this type of section you may define multiple ++# times — options are combined then (and the latest wins). ++# ++# There are several accepted options (now): ++# ++# * type ++# Here the type must be set to 'settings'. This is requirement for pybugz ++# to interpret this section as you want. ++# ++# * default ++# Lets you define the default connection (when the --connection option is ++# not passed). ++# ++# * homeconf ++# Let's you define where to look for user's configuration file. This is ++# by default ~/.bugzrc file. Note that this option makes sense only for ++# system-wide configuration file. ++# ++# * confdir ++# This option lets you define the configuration directory. This directory ++# is searched for *.conf files, and these files (if any) are parsed ++# immediately after specifying configuration file. ++ ++[settings] ++ ++type = settings ++homeconf = ~/.bugzrc ++confdir = /etc/pybugz/conf.d/ ++default = Gentoo +diff --git a/man/bugz.1 b/man/bugz.1 +index 628eae9..fbf2e6c 100644 +--- a/man/bugz.1 ++++ b/man/bugz.1 +@@ -1,8 +1,8 @@ + .\" Hey, Emacs! This is an -*- nroff -*- source file. +-.\" Copyright (c) 2011 William Hubbs ++.\" Copyright (c) 2011, 2012, 2013 William Hubbs + .\" This is free software; see the GNU General Public Licence version 2 + .\" or later for copying conditions. There is NO warranty. +-.TH bugz 1 "17 Feb 2011" "0.9.0" ++.TH bugz 1 "20 Jan 2013" "0.10.2" + .nh + .SH NAME + bugz \(em command line interface to bugzilla +@@ -20,10 +20,10 @@ bugz \(em command line interface to bugzilla + .\" .B \-o value, \-\^\-long=value + .\" Describe the option. + .SH DESCRIPTION +-Bugz is a cprogram which gives you access to the features of the ++Bugz is a program which gives you access to the features of the + bugzilla bug tracking system from the command line. + .PP +-This man page is a stub; the bugs program has extensive built in help. ++This man page is a stub; the bugz program has extensive built in help. + .B bugz -h + will show the help for the global options and + .B bugz [subcommand] -h +@@ -32,8 +32,14 @@ will show the help for a specific subcommand. + .PP + The home page of this project is http://www.github.com/williamh/pybugz. + Bugs should be reported to the bug tracker there. +-.\" .SH SEE ALSO +-.\" .PP ++.SH SEE ALSO ++.PP ++For documentation how to configure PyBugz take a look into distributed ++.B pybugz.conf ++file. User specific configuration may be defined in ++.B ++~/.bugzrc ++file. + .SH AUTHOR + .PP + The original author is Alastair Tse . +diff --git a/setup.py b/setup.py +index e9a8a52..15f004c 100644 +--- a/setup.py ++++ b/setup.py +@@ -18,5 +18,9 @@ setup( + platforms = ['any'], + packages = ['bugz'], + scripts = ['bin/bugz'], ++ data_files = [ ++ ('/etc/pybugz', ['conf/pybugz.conf']), ++ ('/etc/pybugz/conf.d', ['conf/conf.d/redhat.conf', 'conf/conf.d/gentoo.conf']), ++ ], + cmdclass = {'build_py': build_py, 'build_scripts': build_scripts}, + ) +-- +1.7.11.7 + diff --git a/pybugz-0.10-git89df2-rhel-fedora-cust.patch b/pybugz-0.10-git89df2-rhel-fedora-cust.patch new file mode 100644 index 0000000..8a05846 --- /dev/null +++ b/pybugz-0.10-git89df2-rhel-fedora-cust.patch @@ -0,0 +1,22 @@ +From e6ceb40155df600cbc2bba6a593c55fe327a3987 Mon Sep 17 00:00:00 2001 +From: Pavel Raiskup +Date: Sun, 20 Jan 2013 15:20:45 +0100 +Subject: [PATCH] Refine for Fedora purposes + +--- + conf/pybugz.conf | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/conf/pybugz.conf b/conf/pybugz.conf +index 3a486b4..1bd5176 100644 +--- a/conf/pybugz.conf ++++ b/conf/pybugz.conf +@@ -123,4 +123,4 @@ + type = settings + homeconf = ~/.bugzrc + confdir = /etc/pybugz/conf.d/ +-default = Gentoo ++default = RedHat +-- +1.7.11.7 + diff --git a/pybugz.spec b/pybugz.spec new file mode 100644 index 0000000..b404812 --- /dev/null +++ b/pybugz.spec @@ -0,0 +1,102 @@ +%global gitrev 89df2 +%global posttag git%{gitrev} +%global snapshot %{version}-%{posttag} + +Name: pybugz +Summary: Command line interface for Bugzilla written in Python +Version: 0.10 +Release: 1.%{posttag}%{?dist} +Group: Applications/Communications +License: GPLv2 +URL: https://github.com/williamh/pybugz +BuildArch: noarch + +Requires: python2 +BuildRequires: python2-devel + +%if ! 0%{?rhel} +# no bash-completion for RHEL +%global bash_completion 1 +%endif + +%if %{?bash_completion} +BuildRequires: bash-completion pkgconfig +%endif + +# There is possible to download upstream tarball generated by github, but it is +# quite old now. For HOWTO obtain correct tarball see the "prepare-tarball.sh" +# script (in dist-git). +Source0: %{name}-%{snapshot}.tar.gz + +# follow https://github.com/praiskup/pybugz changes (until accepted by upstream) +Patch0: %{name}-%{snapshot}-downstream.patch +# make the installation better satisfy RHEL / Fedora purposes +Patch1: %{name}-%{snapshot}-rhel-fedora-cust.patch + +%description +Pybugz was conceived as a tool to speed up the work-flow for Gentoo Linux +contributors when dealing with bugs using Bugzilla. By avoiding the clunky web +interface, the user can search, isolate and contribute to the project very +quickly. Developers alike can easily extract attachments and close bugs +comfortably from the command line. + +%prep +%setup -q -n %{name}-%{snapshot} +%patch0 -p1 -b .downstream +%patch1 -p1 -b .rhel-fedora-cust + +%build +%{__python} setup.py build + +%install +# default install process +%{__python} setup.py install --root=%{buildroot} + +%global bash_cmpl_dir %(pkg-config --variable=completionsdir bash-completion) +%if %{?bash_completion} + # find the proper directory to install bash-completion script + mkdir -p %{buildroot}%{bash_cmpl_dir} + cp %{_builddir}/%{name}-%{snapshot}/contrib/bash-completion \ + %{buildroot}%{bash_cmpl_dir}/bugz +%endif + +mkdir -p %{buildroot}%{_mandir}/man1 +mv man/bugz.1 %{buildroot}%{_mandir}/man1/bugz.1 +mkdir -p %{buildroot}%{_docdir} + +%clean + +%files +%{_bindir}/bugz +%{python_sitelib}/bugz +%if %{?bash_completion} + %{bash_cmpl_dir}/bugz +%endif +%{python_sitelib}/%{name}-*.egg-info +%{_mandir}/man1/bugz.1.gz +%config(noreplace) %{_sysconfdir}/pybugz +%doc README LICENSE + +%changelog +* Sun Jan 20 2013 Pavel Raiskup - 0.10-1.git89df2 +- changes for problems spotted/fixed by Scott Tsai in merge-review bug: +- important change - move git revision behind the release number +- reflect that ^^^ change in changelog +- remove statement disabling debuginfo (it is not needed) + +* Sun Jan 20 2013 Pavel Raiskup - 0.10git69cd7-1 +- apply downstream patches to reflect https://github.com/praiskup/pybugz + it allows hierarchy of configuration files and a bit better error handling +- update URL as upstream is now on github +- make the RedHat bugzilla default, s/bugz/pybugz/ in manpage +- fedora-review fixes: s/define/global/, BR python2-devel, noreplace +- mention in documentation the ~/.bugzrc file +- switch the binary name to 'bugz' again, it would mislead users (see merge + review bug comment) + +* Mon Oct 01 2012 Pavel Raiskup - 0.10-1 +- rebase to 0.10 +- use the 'pybugz' rather then bugz which collides a little with 'bugzilla' + +* Tue Nov 30 2010 Pierre Carrier - 0.8.0-1 +- Initial packaging diff --git a/sources b/sources index e69de29..8410195 100644 --- a/sources +++ b/sources @@ -0,0 +1 @@ +1b92781ac57ad7cd8b967a8a0ced3557 pybugz-0.10-git89df2.tar.gz