Skip to content
Open
217 changes: 177 additions & 40 deletions bugz/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

"""

import datetime
import getpass
import mimetypes
import os
import re
import subprocess
Expand All @@ -36,7 +38,7 @@
from bugz.settings import Settings
from bugz.exceptions import BugzError
from bugz.log import log_error, log_info
from bugz.utils import block_edit, get_content_type
from bugz.utils import block_edit


def check_bugz_token():
Expand Down Expand Up @@ -230,6 +232,14 @@ def prompt_for_bug(settings):
log_info('Append command (optional): %s' % settings.append_command)


def parsetime(when):
return datetime.datetime.strptime(str(when), '%Y%m%dT%H:%M:%S')


def printtime(dt, settings):
return dt.strftime(settings.timeformat)


def show_bug_info(bug, settings):
FieldMap = {
'alias': 'Alias',
Expand All @@ -245,20 +255,23 @@ def show_bug_info(bug, settings):
'severity': 'Severity',
'target_milestone': 'TargetMilestone',
'assigned_to': 'AssignedTo',
'assigned_to_detail': 'AssignedTo',
'url': 'URL',
'whiteboard': 'Whiteboard',
'keywords': 'Keywords',
'depends_on': 'dependsOn',
'blocks': 'Blocks',
'creation_time': 'Reported',
'creator': 'Reporter',
'creator_detail': 'Reporter',
'last_change_time': 'Updated',
'cc': 'CC',
'cc_detail': 'CC',
'see_also': 'See Also',
}
SkipFields = ['assigned_to_detail', 'cc_detail', 'creator_detail', 'id',
'is_confirmed', 'is_creator_accessible', 'is_cc_accessible',
'is_open', 'update_token']
SkipFields = ['assigned_to', 'cc', 'creator', 'id', 'is_confirmed',
'is_creator_accessible', 'is_cc_accessible', 'is_open',
'update_token']
TimeFields = ['last_change_time', 'creation_time']
user_detail = {}

for field in bug:
if field in SkipFields:
Expand All @@ -267,8 +280,18 @@ def show_bug_info(bug, settings):
desc = FieldMap[field]
else:
desc = field
value = bug[field]
if field in ['cc', 'see_also']:
if field in TimeFields:
value = printtime(parsetime(bug[field]), settings)
else:
value = bug[field]
if field in ['assigned_to_detail', 'creator_detail']:
print('%-12s: %s <%s>' % (desc, value['real_name'], value['email']))
user_detail[value['email']] = value
elif field == 'cc_detail':
for cc in value:
print('%-12s: %s <%s>' % (desc, cc['real_name'], cc['email']))
user_detail[cc['email']] = cc
elif field == 'see_also':
for x in value:
print('%-12s: %s' % (desc, x))
elif isinstance(value, list):
Expand All @@ -294,19 +317,71 @@ def show_bug_info(bug, settings):
params = {'ids': [bug['id']]}
bug_comments = settings.call_bz(settings.bz.Bug.comments, params)
bug_comments = bug_comments['bugs']['%s' % bug['id']]['comments']
print('%-12s: %d' % ('Comments', len(bug_comments)))
for comment in bug_comments:
comment['when'] = parsetime(comment['time'])
del comment['time']
comment['who'] = comment['creator']
del comment['creator']
bug_history = settings.call_bz(settings.bz.Bug.history, params)
assert(bug_history['bugs'][0]['id'] == bug['id'])
bug_history = bug_history['bugs'][0]['history']
for change in bug_history:
change['when'] = parsetime(change['when'])
bug_comments += bug_history
bug_comments.sort(key=lambda c: (c['when'], 'changes' in c))
print()
i = 0
wrapper = textwrap.TextWrapper(width=settings.columns,
break_long_words=False,
break_on_hyphens=False)
for comment in bug_comments:
who = comment['creator']
when = comment['time']
# Header, who & when
if comment == bug_comments[0] or \
prev['when'] != comment['when'] or \
prev['who'] != comment['who']:
if comment['who'] in user_detail:
who = '%s <%s>' % (
user_detail[comment['who']]['real_name'],
comment['who'])
else:
who = comment['who']
when = comment['when']
header_left = '%s %s' % (who, printtime(when, settings))
if i == 0:
header_right = 'Description'
elif 'changes' in comment:
header_right = ''
else:
header_right = '[Comment %d]' % i
space = settings.columns - len(header_left) - \
len(header_right) - 3
if space < 0:
space = 0
print(header_left, ' ' * space, header_right)
print('-' * (settings.columns - 1))

# A change from Bug.history
if 'changes' in comment:
for change in comment['changes']:
if change['field_name'] in FieldMap:
desc = FieldMap[change['field_name']]
else:
desc = change['field_name']
if change['removed'] and change['added']:
print('%s: %s → %s' % (desc, change['removed'],
change['added']))
elif change['added']:
print('%s: %s' % (desc, change['added']))
elif change['removed']:
print('REMOVED %s: %s ' % (desc, change['removed']))
else:
print(change)
prev = comment
print()
continue

# A comment from Bug.comments
what = comment['text']
print('[Comment #%d] %s : %s' % (i, who, when))
print('-' * (settings.columns - 1))

if what is None:
what = ''

Expand All @@ -318,6 +393,7 @@ def show_bug_info(bug, settings):
for shortline in wrapper.wrap(line):
print(shortline)
print()
prev = comment
i += 1


Expand All @@ -333,8 +409,19 @@ def attach(settings):
if not os.path.exists(filename):
raise BugzError('File not found: %s' % filename)

if is_patch is None and \
(filename.endswith('.diff') or filename.endswith('.patch')):
content_type = 'text/plain'
is_patch = 1

if content_type is None:
content_type = get_content_type(filename)
content_type = mimetypes.guess_type(filename)[0]

if content_type is None:
if is_patch is None:
content_type = 'application/octet-stream'
else:
content_type = 'text/plain'

if comment is None:
comment = block_edit('Enter optional long description of attachment')
Expand Down Expand Up @@ -363,33 +450,55 @@ def attach(settings):


def attachment(settings):
""" Download or view an attachment given the id."""
log_info('Getting attachment %s' % settings.attachid)
""" Download or view an attachment(s) given the attachment or bug id."""

params = {}
params['attachment_ids'] = [settings.attachid]
if hasattr(settings, 'bug'):
params['ids'] = [settings.id]
log_info('Getting attachment(s) for bug %s' % settings.id)
else:
params['attachment_ids'] = [settings.id]
log_info('Getting attachment %s' % settings.id)

check_auth(settings)
results = settings.call_bz(settings.bz.Bug.attachments, params)

result = settings.call_bz(settings.bz.Bug.attachments, params)
result = result['attachments'][settings.attachid]
view = hasattr(settings, 'view')
if hasattr(settings, 'bug'):
results = results['bugs'][settings.id]
else:
results = [ results['attachments'][settings.id] ]

if hasattr(settings, 'patch_only'):
results = list(filter(lambda x : x['is_patch'], results))

if hasattr(settings, 'skip_obsolete'):
results = list(filter(lambda x : not x['is_obsolete'], results))

if not results:
return

if hasattr(settings, 'most_recent'):
results = [ results[-1] ]

view = hasattr(settings, 'view')
action = {True: 'Viewing', False: 'Saving'}
log_info('%s attachment: "%s"' %
(action[view], result['file_name']))
safe_filename = os.path.basename(re.sub(r'\.\.', '',

for result in results:
log_info('%s%s attachment: "%s"' % (action[view],
' obsolete' if result['is_obsolete'] else '',
result['file_name']))
safe_filename = os.path.basename(re.sub(r'\.\.', '',
result['file_name']))

if view:
print(result['data'].data.decode('utf-8'))
else:
if os.path.exists(result['file_name']):
raise RuntimeError('Filename already exists')
if view:
print(result['data'].data.decode('utf-8'))
else:
if os.path.exists(result['file_name']):
raise RuntimeError('Filename already exists')

fd = open(safe_filename, 'wb')
fd.write(result['data'].data)
fd.close()
fd = open(safe_filename, 'wb')
fd.write(result['data'].data)
fd.close()


def get(settings):
Expand All @@ -415,14 +524,13 @@ def modify(settings):
except IOError as error:
raise BugzError('unable to read file: %s: %s' %
(settings.comment_from, error))
else:
settings.comment = ''

if hasattr(settings, 'assigned_to') and \
hasattr(settings, 'reset_assigned_to'):
raise BugzError('--assigned-to and --unassign cannot be used together')

if hasattr(settings, 'comment_editor'):
settings.comment = block_edit('Enter comment:')

params = {}
params['ids'] = [settings.bugid]
if hasattr(settings, 'alias'):
Expand Down Expand Up @@ -453,10 +561,6 @@ def modify(settings):
if 'cc' not in params:
params['cc'] = {}
params['cc']['remove'] = settings.cc_remove
if hasattr(settings, 'comment'):
if 'comment' not in params:
params['comment'] = {}
params['comment']['body'] = settings.comment
if hasattr(settings, 'component'):
params['component'] = settings.component
if hasattr(settings, 'dupe_of'):
Expand Down Expand Up @@ -522,9 +626,42 @@ def modify(settings):
params['status'] = 'RESOLVED'
params['resolution'] = 'INVALID'

check_auth(settings)

if hasattr(settings, 'comment_editor'):
quotes=''
if hasattr(settings, 'quote'):
bug_comments = settings.call_bz(settings.bz.Bug.comments, params)
bug_comments = bug_comments['bugs']['%s' % settings.bugid]\
['comments'][-settings.quote:]
wrapper = textwrap.TextWrapper(width=settings.columns,
break_long_words=False,
break_on_hyphens=False)
for comment in bug_comments:
what = comment['text']
if what is None:
continue
who = comment['creator']
when = parsetime(comment['time'])
quotes += 'On %s, %s wrote:\n' % (printtime(when, settings),
who)
for line in what.splitlines():
if len(line) < settings.columns:
quotes += '> %s\n' % line
else:
for shortline in wrapper.wrap(line):
quotes += '> %s\n' % shortline
settings.comment = block_edit('Enter comment:',
comment_from=settings.comment,
quotes=quotes)

if hasattr(settings, 'comment'):
if 'comment' not in params:
params['comment'] = {}
params['comment']['body'] = settings.comment

if len(params) < 2:
raise BugzError('No changes were specified')
check_auth(settings)
result = settings.call_bz(settings.bz.Bug.update, params)
for bug in result['bugs']:
changes = bug['changes']
Expand Down
25 changes: 21 additions & 4 deletions bugz/cli_argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def make_arg_parser():
'configuration file')
parser.add_argument('-b', '--base',
help='base URL of Bugzilla')
parser.add_argument('-t', '--timeformat',
help='Time format (default: %%+ UTC), see strftime(3)')
parser.add_argument('-u', '--user',
help='username')
parser.add_argument('-p', '--password',
Expand Down Expand Up @@ -78,13 +80,25 @@ def make_arg_parser():

attachment_parser = subparsers.add_parser('attachment',
argument_default=argparse.SUPPRESS,
help='get an attachment '
help='get an attachment(s) '
'from Bugzilla')
attachment_parser.add_argument('attachid',
help='the ID of the attachment')
attachment_parser.add_argument('id',
help='the ID of the attachment or bug')
attachment_parser.add_argument('-b', '--bug',
action='store_true',
help='the ID is a bug')
attachment_parser.add_argument('-r', '--most-recent',
action='store_true',
help='get only most recent attachment')
attachment_parser.add_argument('-p', '--patch-only',
action='store_true',
help='get only patch attachment(s)')
attachment_parser.add_argument('-o', '--skip-obsolete',
action='store_true',
help='get only not obsolete attachment(s)')
attachment_parser.add_argument('-v', '--view',
action="store_true",
help='print attachment rather than save')
help='print attachment(s) rather than save')
attachment_parser.set_defaults(func=bugz.cli.attachment)

connections_parser = subparsers.add_parser('connections',
Expand Down Expand Up @@ -190,6 +204,9 @@ def make_arg_parser():
help='change the priority for this bug')
modify_parser.add_argument('--product',
help='change the product for this bug')
modify_parser.add_argument('-Q', '--quote',
action='count',
help='quote most recent comment(s) with -C')
modify_parser.add_argument('-r', '--resolution',
help='set new resolution '
'(if status = RESOLVED)')
Expand Down
8 changes: 8 additions & 0 deletions bugz/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ def __init__(self, args, config):
self.connection,
'component')

if not hasattr(self, 'timeformat'):
if config.has_option(self.connection, 'timeformat'):
self.timeformat = get_config_option(config.get,
self.connection,
'timeformat')
else:
self.timeformat = '%+ UTC'

if not hasattr(self, 'user'):
if config.has_option(self.connection, 'user'):
self.user = get_config_option(config.get,
Expand Down
Loading