Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several improvements to get and modify. #115

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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