summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPavel Raiskup <pavel@raiskup.cz>2013-01-20 13:10:10 +0100
committerPavel Raiskup <pavel@raiskup.cz>2013-01-20 13:11:58 +0100
commit1d48cf76bdb00db6caa75506c1bc32855de73b3d (patch)
treef72370392ace37dcbff498d13c581403de7a6a11
downloadpybugz-1d48cf76bdb00db6caa75506c1bc32855de73b3d.tar.gz
pybugz-1d48cf76bdb00db6caa75506c1bc32855de73b3d.tar.xz
pybugz-1d48cf76bdb00db6caa75506c1bc32855de73b3d.zip
Initial commit
-rw-r--r--.gitignore2
-rwxr-xr-xprepare-tarball.sh22
-rw-r--r--pybugz-0.10git69cd7-bash-complete.patch10
-rw-r--r--pybugz-0.10git69cd7-downstream.patch1154
-rw-r--r--pybugz-0.10git69cd7.tar.gzbin0 -> 23093 bytes
-rw-r--r--pybugz.spec94
6 files changed, 1282 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..55dd4c1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+pybugz-0.10git69cd7/
+noarch
diff --git a/prepare-tarball.sh b/prepare-tarball.sh
new file mode 100755
index 0000000..a75c61a
--- /dev/null
+++ b/prepare-tarball.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# grep the spec file for version number
+VERSION=$( cat pybugz.spec | grep ^Version: | cut -d' ' -f 2- | tr -d ' ')
+
+BASE=pybugz-$VERSION
+TARBALL=$BASE.tar.gz
+DIR=$( mktemp -d )
+GIT=https://github.com/williamh/pybugz.git
+
+REVISION=`echo $VERSION | sed 's/.*git//'`
+
+echo == preparing tarball for pybugz-$VERSION ==
+
+pushd $DIR > /dev/null && \
+git clone $GIT pybugz && \
+cd pybugz && \
+git archive --prefix pybugz-$VERSION/ $REVISION | gzip > $TARBALL && \
+popd > /dev/null && \
+cp $DIR/pybugz/$TARBALL . && \
+echo == DONE == && \
+rm -rf $DIR
diff --git a/pybugz-0.10git69cd7-bash-complete.patch b/pybugz-0.10git69cd7-bash-complete.patch
new file mode 100644
index 0000000..3163c18
--- /dev/null
+++ b/pybugz-0.10git69cd7-bash-complete.patch
@@ -0,0 +1,10 @@
+diff --git a/contrib/bash-completion b/contrib/bash-completion
+index a51ee10..8530291 100644
+--- a/contrib/bash-completion
++++ b/contrib/bash-completion
+@@ -79,4 +79,4 @@ _bugz() {
+ ;;
+ esac
+ }
+-complete -F _bugz bugz
++complete -F _bugz pybugz
diff --git a/pybugz-0.10git69cd7-downstream.patch b/pybugz-0.10git69cd7-downstream.patch
new file mode 100644
index 0000000..167493f
--- /dev/null
+++ b/pybugz-0.10git69cd7-downstream.patch
@@ -0,0 +1,1154 @@
+From f456eed82305019b8bd31146d4368e10ee33106a Mon Sep 17 00:00:00 2001
+From: Pavel Raiskup <pavel@raiskup.cz>
+Date: Sun, 20 Jan 2013 10:33:59 +0100
+Subject: [PATCH] Downstream patch to follow
+ https://github.com/praiskup/pybugz
+
+---
+ bin/bugz | 30 +++----
+ bugz/argparsers.py | 12 ++-
+ bugz/cli.py | 212 ++++++++++++++++++++++++++++-------------------
+ bugz/configfile.py | 214 +++++++++++++++++++++++++++++++++---------------
+ 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 | 122 +++++++++++++++++++++++++++
+ lbugz | 2 +-
+ man/bugz.1 | 12 ++-
+ setup.py | 4 +
+ 13 files changed, 521 insertions(+), 232 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 94de59f..6f72fa1 100755
+--- a/bin/bugz
++++ b/bin/bugz
+@@ -25,43 +25,43 @@ 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)
+ args.func(bugz, args)
++ return 0
+
+ except BugzError, e:
+- print ' ! Error: %s' % e
+- sys.exit(-1)
++ 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()
+- sys.exit(-1)
++ return 1
+
+ except RuntimeError, e:
+- print ' ! Error: %s' % e
+- sys.exit(-1)
++ log_error(e)
++ return 1
+
+ except KeyboardInterrupt:
+ print
+ print 'Stopped.'
+- sys.exit(-1)
++ return 1
+
+ except:
+ raise
+
+ if __name__ == "__main__":
+- main()
++ sys.exit(main())
+diff --git a/bugz/argparsers.py b/bugz/argparsers.py
+index 2dbdb1a..e287352 100644
+--- a/bugz/argparsers.py
++++ b/bugz/argparsers.py
+@@ -256,11 +256,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')
+@@ -270,10 +271,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('--columns',
++ 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 a533da9..5075c87 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,36 +217,43 @@ 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.connection.password:
+ if not self.passwordcmd:
+- self.log('No password given.')
+- self.password = getpass.getpass()
++ log_info('No password given.')
++ self.connection.password = getpass.getpass()
+ else:
+ process = subprocess.Popen(self.passwordcmd.split(), shell=False,
+ stdout=subprocess.PIPE)
+ 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')
+- self.bz.User.login(params)
++ log_info('Logging in')
++ try:
++ self.bz.User.login(params)
++ except xmlrpclib.Fault as fault:
++ raise BugzError("Can't login: " + fault.faultString)
+
+ if args is not None:
+ self.cookiejar.save()
+ os.chmod(self.cookiejar.filename, 0600)
+
+ def logout(self, args):
+- self.log('logging out')
+- self.bz.User.logout()
++ log_info('logging out')
++ try:
++ self.bz.User.logout()
++ except xmlrpclib.Fault as fault:
++ raise BugzError("Failed to logout: " + fault.faultString)
+
+ def search(self, args):
+ """Performs a search on the bugzilla database with the keywords given on the title (or the body if specified).
+@@ -245,29 +283,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)
+@@ -287,7 +335,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.
+@@ -300,14 +348,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.
+@@ -318,14 +366,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:
+@@ -333,7 +381,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:
+@@ -342,7 +390,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:
+@@ -351,7 +399,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:
+@@ -360,7 +408,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:
+@@ -369,7 +417,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:
+@@ -378,7 +426,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:
+@@ -387,7 +435,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:
+@@ -396,7 +444,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)
+
+ # fixme: groups
+
+@@ -407,7 +455,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:
+@@ -454,7 +502,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={}
+@@ -480,7 +528,7 @@ class PrettyBugz:
+ params['cc'] = args.cc
+
+ 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.)"""
+@@ -586,16 +634,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]
+@@ -603,7 +651,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']))
+@@ -653,7 +701,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:
+@@ -672,7 +720,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..9440217 100644
+--- a/bugz/configfile.py
++++ b/bugz/configfile.py
+@@ -1,70 +1,152 @@
+ 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 homeConf:
++ # the command-line option must win
++ settings['homeconf'] = homeConf
++
++ # parse home configs
++ stack = [ settings['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..ff21972
+--- /dev/null
++++ b/conf/pybugz.conf
+@@ -0,0 +1,122 @@
++# ===========================================================================
++# 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.
++#
++# 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/
+diff --git a/lbugz b/lbugz
+index f1b1b7a..2329859 100755
+--- a/lbugz
++++ b/lbugz
+@@ -25,4 +25,4 @@ if os.path.exists(pkg) and os.path.exists(script):
+ os.environ['PYTHONPATH'] = path + ':' + os.environ['PYTHONPATH']
+ else:
+ os.environ['PYTHONPATH'] = path + ':'
+- subprocess.call(args)
++ sys.exit(subprocess.call(args))
+diff --git a/man/bugz.1 b/man/bugz.1
+index 628eae9..97638f2 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 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
+@@ -32,8 +32,12 @@ 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.
++
+ .SH AUTHOR
+ .PP
+ The original author is Alastair Tse <alastair@liquidx.net>.
+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.10git69cd7.tar.gz b/pybugz-0.10git69cd7.tar.gz
new file mode 100644
index 0000000..f121ab9
--- /dev/null
+++ b/pybugz-0.10git69cd7.tar.gz
Binary files differ
diff --git a/pybugz.spec b/pybugz.spec
new file mode 100644
index 0000000..56e6c5d
--- /dev/null
+++ b/pybugz.spec
@@ -0,0 +1,94 @@
+Name: pybugz
+Summary: Command line interface for Bugzilla written in Python
+Version: 0.10git69cd7
+Release: 1%{?dist}
+Group: Applications/Communications
+License: GPLv2
+URL: https://github.com/williamh/pybugz
+BuildArch: noarch
+
+Requires: python2
+BuildRequires: python2
+
+# we don't need debuginfo package
+%global debug_package %{nil}
+
+%if ! 0%{?rhel}
+# no bash-completion for RHEL
+%define 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.
+Source0: %{name}-%{version}.tar.gz
+
+# use the 'pybugz' binary in bash completion
+Patch0: %{name}-%{version}-bash-complete.patch
+# follow https://github.com/praiskup/pybugz changes (until accepted by upstream)
+Patch1: %{name}-%{version}-downstream.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
+%patch0 -p1 -b .bash-complete
+%patch1 -p1 -b .downstream
+
+%build
+%{__python} setup.py build
+
+%install
+# use rather 'pybugz' than just 'bugz'
+%define pybugz_binary pybugz
+
+# default install process
+%{__python} setup.py install --root=%{buildroot}
+
+mv %{buildroot}%{_bindir}/bugz %{buildroot}%{_bindir}/%{pybugz_binary}
+%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}-%{version}/contrib/bash-completion \
+ %{buildroot}%{bash_cmpl_dir}/pybugz
+%endif
+
+mkdir -p %{buildroot}%{_mandir}/man1
+mv man/bugz.1 %{buildroot}%{_mandir}/man1/pybugz.1
+mkdir -p %{buildroot}%{_docdir}
+
+%clean
+
+%files
+%{_bindir}/%{pybugz_binary}
+%{python_sitelib}/bugz
+%if %{?bash_completion}
+ %{bash_cmpl_dir}/pybugz
+%endif
+%{python_sitelib}/%{name}-*.egg-info
+%{_mandir}/man1/pybugz.1.gz
+%config %{_sysconfdir}/pybugz
+%doc README LICENSE
+
+%changelog
+* Sun Jan 20 2013 Pavel Raiskup <praiskup@redhat.com> - 0.10-2
+- 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
+
+* Mon Oct 01 2012 Pavel Raiskup <praiskup@redhat.com> - 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 <prc@redhat.com> - 0.8.0-1
+- Initial packaging