1 --- a/README Fri Apr 15 22:57:55 2011 -0700
2 +++ b/README Fri Apr 15 23:10:12 2011 -0700
3 @@ -27,7 +27,7 @@
4
5 In the ``[extensions]`` section of your ``~/.hgrc`` add::
6
7 - artemis = /path/to/artemis.py
8 + artemis = /path/to/Artemis/artemis
9
10 Optionally, provide a section ``[artemis]``, and specify an alternative path for
11 the issues subdirectory (instead of the default ``.issues``)::
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/artemis/__init__.py Fri Apr 15 23:10:12 2011 -0700
1.3 @@ -0,0 +1,1 @@
1.4 +from artemis import *
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/artemis/artemis.py Fri Apr 15 23:10:12 2011 -0700
2.3 @@ -0,0 +1,487 @@
2.4 +# Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
2.5 +
2.6 +"""A very simple and lightweight issue tracker for Mercurial."""
2.7 +
2.8 +from mercurial import hg, util, commands
2.9 +from mercurial.i18n import _
2.10 +import os, time, random, mailbox, glob, socket, ConfigParser
2.11 +import mimetypes
2.12 +from email import encoders
2.13 +from email.generator import Generator
2.14 +from email.mime.audio import MIMEAudio
2.15 +from email.mime.base import MIMEBase
2.16 +from email.mime.image import MIMEImage
2.17 +from email.mime.multipart import MIMEMultipart
2.18 +from email.mime.text import MIMEText
2.19 +
2.20 +from termcolor import colored
2.21 +
2.22 +
2.23 +state = { 'new': ['new'],
2.24 + 'fixed': ['fixed', 'resolved'] }
2.25 +annotation = { 'resolved': 'resolution' }
2.26 +default_state = 'new'
2.27 +default_issues_dir = ".issues"
2.28 +filter_prefix = ".filter"
2.29 +date_format = '%a, %d %b %Y %H:%M:%S %1%2'
2.30 +maildir_dirs = ['new','cur','tmp']
2.31 +
2.32 +
2.33 +def ilist(ui, repo, **opts):
2.34 + """List issues associated with the project"""
2.35 +
2.36 + # Process options
2.37 + show_all = opts['all']
2.38 + properties = []
2.39 + match_date, date_match = False, lambda x: True
2.40 + if opts['date']:
2.41 + match_date, date_match = True, util.matchdate(opts['date'])
2.42 + order = 'new'
2.43 + if opts['order']:
2.44 + order = opts['order']
2.45 +
2.46 + # Colors
2.47 + colors = _read_colors(ui)
2.48 +
2.49 + # Find issues
2.50 + issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
2.51 + issues_path = os.path.join(repo.root, issues_dir)
2.52 + if not os.path.exists(issues_path): return
2.53 +
2.54 + issues = glob.glob(os.path.join(issues_path, '*'))
2.55 +
2.56 + _create_all_missing_dirs(issues_path, issues)
2.57 +
2.58 + # Process filter
2.59 + if opts['filter']:
2.60 + filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
2.61 + config = ConfigParser.SafeConfigParser()
2.62 + config.read(filters)
2.63 + if not config.has_section(opts['filter']):
2.64 + ui.write('No filter %s defined\n' % opts['filter'])
2.65 + else:
2.66 + properties += config.items(opts['filter'])
2.67 +
2.68 + cmd_properties = _get_properties(opts['property'])
2.69 + list_properties = [p[0] for p in cmd_properties if len(p) == 1]
2.70 + list_properties_dict = {}
2.71 + properties += filter(lambda p: len(p) > 1, cmd_properties)
2.72 +
2.73 + summaries = []
2.74 + for issue in issues:
2.75 + mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
2.76 + root = _find_root_key(mbox)
2.77 + if not root: continue
2.78 + property_match = True
2.79 + for property,value in properties:
2.80 + if value:
2.81 + property_match = property_match and (mbox[root][property] == value)
2.82 + else:
2.83 + property_match = property_match and (property not in mbox[root])
2.84 +
2.85 + if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['fixed']]): continue
2.86 + if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
2.87 +
2.88 + if not list_properties:
2.89 + summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], colors), # +1 for trailing /
2.90 + _find_mbox_date(mbox, root, order)))
2.91 + else:
2.92 + for lp in list_properties:
2.93 + if lp in mbox[root]: list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
2.94 +
2.95 + if not list_properties:
2.96 + summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
2.97 + for s,d in summaries:
2.98 + ui.write(s)
2.99 + else:
2.100 + for lp in list_properties_dict.keys():
2.101 + ui.write("%s:\n" % lp)
2.102 + for value in sorted(list_properties_dict[lp]):
2.103 + ui.write(" %s\n" % value)
2.104 +
2.105 +
2.106 +def iadd(ui, repo, id = None, comment = 0, **opts):
2.107 + """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
2.108 +
2.109 + comment = int(comment)
2.110 +
2.111 + # First, make sure issues have a directory
2.112 + issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
2.113 + issues_path = os.path.join(repo.root, issues_dir)
2.114 + if not os.path.exists(issues_path): os.mkdir(issues_path)
2.115 +
2.116 + if id:
2.117 + issue_fn, issue_id = _find_issue(ui, repo, id)
2.118 + if not issue_fn:
2.119 + ui.warn('No such issue\n')
2.120 + return
2.121 + _create_missing_dirs(issues_path, issue_id)
2.122 +
2.123 + user = ui.username()
2.124 +
2.125 + default_issue_text = "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
2.126 + if not id:
2.127 + default_issue_text += "State: %s\n" % default_state
2.128 + default_issue_text += "Subject: brief description\n\n"
2.129 + default_issue_text += "Detailed description."
2.130 +
2.131 + # Get properties, and figure out if we need an explicit comment
2.132 + properties = _get_properties(opts['property'])
2.133 + no_comment = id and properties and opts['no_property_comment']
2.134 + message = opts['message']
2.135 +
2.136 + # Create the text
2.137 + if message:
2.138 + if not id:
2.139 + state_str = 'State: %s\n' % default_state
2.140 + else:
2.141 + state_str = ''
2.142 + issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
2.143 + (user, util.datestr(format=date_format), message, state_str)
2.144 + elif not no_comment:
2.145 + issue = ui.edit(default_issue_text, user)
2.146 +
2.147 + if issue.strip() == '':
2.148 + ui.warn('Empty issue, ignoring\n')
2.149 + return
2.150 + if issue.strip() == default_issue_text:
2.151 + ui.warn('Unchanged issue text, ignoring\n')
2.152 + return
2.153 + else:
2.154 + # Write down a comment about updated properties
2.155 + properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
2.156 +
2.157 + issue = "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
2.158 + (user, util.datestr(format = date_format), properties_subject)
2.159 +
2.160 + # Create the message
2.161 + msg = mailbox.MaildirMessage(issue)
2.162 + if opts['attach']:
2.163 + outer = _attach_files(msg, opts['attach'])
2.164 + else:
2.165 + outer = msg
2.166 +
2.167 + # Pick random filename
2.168 + if not id:
2.169 + issue_fn = issues_path
2.170 + while os.path.exists(issue_fn):
2.171 + issue_id = _random_id()
2.172 + issue_fn = os.path.join(issues_path, issue_id)
2.173 + # else: issue_fn already set
2.174 +
2.175 + # Add message to the mailbox
2.176 + mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
2.177 + keys = _order_keys_date(mbox)
2.178 + mbox.lock()
2.179 + if id and comment >= len(mbox):
2.180 + ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
2.181 +
2.182 + if not id:
2.183 + outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
2.184 + else:
2.185 + root = keys[0]
2.186 + outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
2.187 + outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
2.188 + outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
2.189 + new_bug_path = issue_fn[(len(repo.root)+1):] + '/new/' + mbox.add(outer) # + 1 for the trailing /
2.190 + commands.add(ui, repo, new_bug_path)
2.191 +
2.192 + # Fix properties in the root message
2.193 + if properties:
2.194 + root = _find_root_key(mbox)
2.195 + msg = mbox[root]
2.196 + for property, value in properties:
2.197 + if property in msg:
2.198 + msg.replace_header(property, value)
2.199 + else:
2.200 + msg.add_header(property, value)
2.201 + mbox[root] = msg
2.202 +
2.203 + mbox.close()
2.204 +
2.205 + if opts['commit']:
2.206 + commands.commit(ui, repo, issue_fn)
2.207 +
2.208 + # If adding issue, add the new mailbox to the repository
2.209 + if not id:
2.210 + ui.status('Added new issue %s\n' % issue_id)
2.211 + else:
2.212 + _show_mbox(ui, mbox, 0)
2.213 +
2.214 +def ishow(ui, repo, id, comment = 0, **opts):
2.215 + """Shows issue ID, or possibly its comment COMMENT"""
2.216 +
2.217 + comment = int(comment)
2.218 + issue, id = _find_issue(ui, repo, id)
2.219 + if not issue:
2.220 + return ui.warn('No such issue\n')
2.221 +
2.222 + issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
2.223 + _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
2.224 +
2.225 + if opts.get('mutt'):
2.226 + return util.system('mutt -R -f %s' % issue)
2.227 +
2.228 + mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
2.229 +
2.230 + if opts['all']:
2.231 + ui.write('='*70 + '\n')
2.232 + i = 0
2.233 + keys = _order_keys_date(mbox)
2.234 + for k in keys:
2.235 + _write_message(ui, mbox[k], i, skip = opts['skip'])
2.236 + ui.write('-'*70 + '\n')
2.237 + i += 1
2.238 + return
2.239 +
2.240 + _show_mbox(ui, mbox, comment, skip = opts['skip'])
2.241 +
2.242 + if opts['extract']:
2.243 + attachment_numbers = map(int, opts['extract'])
2.244 + keys = _order_keys_date(mbox)
2.245 + msg = mbox[keys[comment]]
2.246 + counter = 1
2.247 + for part in msg.walk():
2.248 + ctype = part.get_content_type()
2.249 + maintype, subtype = ctype.split('/', 1)
2.250 + if maintype == 'multipart' or ctype == 'text/plain': continue
2.251 + if counter in attachment_numbers:
2.252 + filename = part.get_filename()
2.253 + if not filename:
2.254 + ext = mimetypes.guess_extension(part.get_content_type()) or ''
2.255 + filename = 'attachment-%03d%s' % (counter, ext)
2.256 + fp = open(filename, 'wb')
2.257 + fp.write(part.get_payload(decode = True))
2.258 + fp.close()
2.259 + counter += 1
2.260 +
2.261 +
2.262 +def _find_issue(ui, repo, id):
2.263 + issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
2.264 + issues_path = os.path.join(repo.root, issues_dir)
2.265 + if not os.path.exists(issues_path): return False
2.266 +
2.267 + issues = glob.glob(os.path.join(issues_path, id + '*'))
2.268 +
2.269 + if len(issues) == 0:
2.270 + return False, 0
2.271 + elif len(issues) > 1:
2.272 + ui.status("Multiple choices:\n")
2.273 + for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n')
2.274 + return False, 0
2.275 +
2.276 + return issues[0], issues[0][len(issues_path)+1:]
2.277 +
2.278 +def _get_properties(property_list):
2.279 + return [p.split('=') for p in property_list]
2.280 +
2.281 +def _write_message(ui, message, index = 0, skip = None):
2.282 + if index: ui.write("Comment: %d\n" % index)
2.283 + if ui.verbose:
2.284 + _show_text(ui, message.as_string().strip(), skip)
2.285 + else:
2.286 + if 'From' in message: ui.write('From: %s\n' % message['From'])
2.287 + if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
2.288 + if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
2.289 + if 'State' in message: ui.write('State: %s\n' % message['State'])
2.290 + counter = 1
2.291 + for part in message.walk():
2.292 + ctype = part.get_content_type()
2.293 + maintype, subtype = ctype.split('/', 1)
2.294 + if maintype == 'multipart': continue
2.295 + if ctype == 'text/plain':
2.296 + ui.write('\n')
2.297 + _show_text(ui, part.get_payload().strip(), skip)
2.298 + else:
2.299 + filename = part.get_filename()
2.300 + ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
2.301 + counter += 1
2.302 +
2.303 +def _show_text(ui, text, skip = None):
2.304 + for line in text.splitlines():
2.305 + if not skip or not line.startswith(skip):
2.306 + ui.write(line + '\n')
2.307 + ui.write('\n')
2.308 +
2.309 +def _show_mbox(ui, mbox, comment, **opts):
2.310 + # Output the issue (or comment)
2.311 + if comment >= len(mbox):
2.312 + comment = 0
2.313 + ui.warn('Comment out of range, showing the issue itself\n')
2.314 + keys = _order_keys_date(mbox)
2.315 + root = keys[0]
2.316 + msg = mbox[keys[comment]]
2.317 + ui.write('='*70 + '\n')
2.318 + if comment:
2.319 + ui.write('Subject: %s\n' % mbox[root]['Subject'])
2.320 + ui.write('State: %s\n' % mbox[root]['State'])
2.321 + ui.write('-'*70 + '\n')
2.322 + _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
2.323 + ui.write('-'*70 + '\n')
2.324 +
2.325 + # Read the mailbox into the messages and children dictionaries
2.326 + messages = {}
2.327 + children = {}
2.328 + i = 0
2.329 + for k in keys:
2.330 + m = mbox[k]
2.331 + messages[m['Message-Id']] = (i,m)
2.332 + children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
2.333 + i += 1
2.334 + children[None] = [] # Safeguard against infinte loop on empty Message-Id
2.335 +
2.336 + # Iterate over children
2.337 + id = msg['Message-Id']
2.338 + id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
2.339 + if not id_stack: return
2.340 + ui.write('Comments:\n')
2.341 + while id_stack:
2.342 + id,offset = id_stack.pop()
2.343 + id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
2.344 + index, msg = messages[id]
2.345 + ui.write(' '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
2.346 + ui.write('-'*70 + '\n')
2.347 +
2.348 +def _find_root_key(maildir):
2.349 + for k,m in maildir.iteritems():
2.350 + if 'in-reply-to' not in m:
2.351 + return k
2.352 +
2.353 +def _order_keys_date(mbox):
2.354 + keys = mbox.keys()
2.355 + root = _find_root_key(mbox)
2.356 + keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
2.357 + return keys
2.358 +
2.359 +def _find_mbox_date(mbox, root, order):
2.360 + if order == 'latest':
2.361 + keys = _order_keys_date(mbox)
2.362 + msg = mbox[keys[-1]]
2.363 + else: # new
2.364 + msg = mbox[root]
2.365 + return util.parsedate(msg['date'])
2.366 +
2.367 +def _random_id():
2.368 + return "%x" % random.randint(2**63, 2**64-1)
2.369 +
2.370 +def _create_missing_dirs(issues_path, issue):
2.371 + for d in maildir_dirs:
2.372 + path = os.path.join(issues_path,issue,d)
2.373 + if not os.path.exists(path): os.mkdir(path)
2.374 +
2.375 +def _create_all_missing_dirs(issues_path, issues):
2.376 + for i in issues:
2.377 + _create_missing_dirs(issues_path, i)
2.378 +
2.379 +def _humanreadable(size):
2.380 + if size > 1024*1024:
2.381 + return '%5.1fM' % (float(size) / (1024*1024))
2.382 + elif size > 1024:
2.383 + return '%5.1fK' % (float(size) / 1024)
2.384 + else:
2.385 + return '%dB' % size
2.386 +
2.387 +def _attach_files(msg, filenames):
2.388 + outer = MIMEMultipart()
2.389 + for k in msg.keys(): outer[k] = msg[k]
2.390 + outer.attach(MIMEText(msg.get_payload()))
2.391 +
2.392 + for filename in filenames:
2.393 + ctype, encoding = mimetypes.guess_type(filename)
2.394 + if ctype is None or encoding is not None:
2.395 + # No guess could be made, or the file is encoded (compressed), so
2.396 + # use a generic bag-of-bits type.
2.397 + ctype = 'application/octet-stream'
2.398 + maintype, subtype = ctype.split('/', 1)
2.399 + if maintype == 'text':
2.400 + fp = open(filename)
2.401 + # Note: we should handle calculating the charset
2.402 + attachment = MIMEText(fp.read(), _subtype=subtype)
2.403 + fp.close()
2.404 + elif maintype == 'image':
2.405 + fp = open(filename, 'rb')
2.406 + attachment = MIMEImage(fp.read(), _subtype=subtype)
2.407 + fp.close()
2.408 + elif maintype == 'audio':
2.409 + fp = open(filename, 'rb')
2.410 + attachment = MIMEAudio(fp.read(), _subtype=subtype)
2.411 + fp.close()
2.412 + else:
2.413 + fp = open(filename, 'rb')
2.414 + attachment = MIMEBase(maintype, subtype)
2.415 + attachment.set_payload(fp.read())
2.416 + fp.close()
2.417 + # Encode the payload using Base64
2.418 + encoders.encode_base64(attachment)
2.419 + # Set the filename parameter
2.420 + attachment.add_header('Content-Disposition', 'attachment', filename=filename)
2.421 + outer.attach(attachment)
2.422 + return outer
2.423 +
2.424 +def _status_msg(msg):
2.425 + s = msg['State']
2.426 + if s in annotation:
2.427 + return '%s=%s' % (s, msg[annotation[s]])
2.428 + else:
2.429 + return s
2.430 +
2.431 +def _read_colors(ui):
2.432 + colors = {}
2.433 + # defaults
2.434 + colors['new.color'] = 'red'
2.435 + colors['new.on_color'] = 'on_grey'
2.436 + colors['new.attrs'] = 'bold'
2.437 + colors['resolved.color'] = 'white'
2.438 + colors['resolved.on_color'] = ''
2.439 + colors['resolved.attrs'] = ''
2.440 + for v in colors:
2.441 + colors[v] = ui.config('artemis', v, colors[v])
2.442 + if v.endswith('attrs'): colors[v] = colors[v].split()
2.443 + return colors
2.444 +
2.445 +def _color_summary(line, msg, colors):
2.446 + if msg['State'] == 'new':
2.447 + return colored(line, colors['new.color'], attrs = colors['new.attrs'])
2.448 + elif msg['State'] in state['fixed']:
2.449 + return colored(line, colors['resolved.color'], attrs = colors['resolved.attrs'])
2.450 + else:
2.451 + return line
2.452 +
2.453 +def _summary_line(mbox, root, issue, colors):
2.454 + line = "%s (%3d) [%s]: %s\n" % (issue,
2.455 + len(mbox)-1, # number of replies (-1 for self)
2.456 + _status_msg(mbox[root]),
2.457 + mbox[root]['Subject'])
2.458 + return _color_summary(line, mbox[root], colors)
2.459 +
2.460 +cmdtable = {
2.461 + 'ilist': (ilist,
2.462 + [('a', 'all', False,
2.463 + 'list all issues (by default only those with state new)'),
2.464 + ('p', 'property', [],
2.465 + 'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
2.466 + ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
2.467 + ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
2.468 + ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
2.469 + _('hg ilist [OPTIONS]')),
2.470 + 'iadd': (iadd,
2.471 + [('a', 'attach', [],
2.472 + 'attach file(s) (e.g., -a filename1 -a filename2)'),
2.473 + ('p', 'property', [],
2.474 + 'update properties (e.g., -p state=fixed)'),
2.475 + ('n', 'no-property-comment', None,
2.476 + 'do not add a comment about changed properties'),
2.477 + ('m', 'message', '',
2.478 + 'use <text> as an issue subject'),
2.479 + ('c', 'commit', False,
2.480 + 'perform a commit after the addition')],
2.481 + _('hg iadd [OPTIONS] [ID] [COMMENT]')),
2.482 + 'ishow': (ishow,
2.483 + [('a', 'all', None, 'list all comments'),
2.484 + ('s', 'skip', '>', 'skip lines starting with a substring'),
2.485 + ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
2.486 + ('', 'mutt', False, 'use mutt to show issue')],
2.487 + _('hg ishow [OPTIONS] ID [COMMENT]')),
2.488 +}
2.489 +
2.490 +# vim: expandtab
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/artemis/termcolor.py Fri Apr 15 23:10:12 2011 -0700
3.3 @@ -0,0 +1,168 @@
3.4 +# coding: utf-8
3.5 +# Copyright (c) 2008-2011 Volvox Development Team
3.6 +#
3.7 +# Permission is hereby granted, free of charge, to any person obtaining a copy
3.8 +# of this software and associated documentation files (the "Software"), to deal
3.9 +# in the Software without restriction, including without limitation the rights
3.10 +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
3.11 +# copies of the Software, and to permit persons to whom the Software is
3.12 +# furnished to do so, subject to the following conditions:
3.13 +#
3.14 +# The above copyright notice and this permission notice shall be included in
3.15 +# all copies or substantial portions of the Software.
3.16 +#
3.17 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3.18 +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3.19 +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3.20 +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3.21 +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3.22 +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
3.23 +# THE SOFTWARE.
3.24 +#
3.25 +# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
3.26 +
3.27 +"""ANSII Color formatting for output in terminal."""
3.28 +
3.29 +from __future__ import print_function
3.30 +import os
3.31 +
3.32 +
3.33 +__ALL__ = [ 'colored', 'cprint' ]
3.34 +
3.35 +VERSION = (1, 1, 0)
3.36 +
3.37 +ATTRIBUTES = dict(
3.38 + list(zip([
3.39 + 'bold',
3.40 + 'dark',
3.41 + '',
3.42 + 'underline',
3.43 + 'blink',
3.44 + '',
3.45 + 'reverse',
3.46 + 'concealed'
3.47 + ],
3.48 + list(range(1, 9))
3.49 + ))
3.50 + )
3.51 +del ATTRIBUTES['']
3.52 +
3.53 +
3.54 +HIGHLIGHTS = dict(
3.55 + list(zip([
3.56 + 'on_grey',
3.57 + 'on_red',
3.58 + 'on_green',
3.59 + 'on_yellow',
3.60 + 'on_blue',
3.61 + 'on_magenta',
3.62 + 'on_cyan',
3.63 + 'on_white'
3.64 + ],
3.65 + list(range(40, 48))
3.66 + ))
3.67 + )
3.68 +
3.69 +
3.70 +COLORS = dict(
3.71 + list(zip([
3.72 + 'grey',
3.73 + 'red',
3.74 + 'green',
3.75 + 'yellow',
3.76 + 'blue',
3.77 + 'magenta',
3.78 + 'cyan',
3.79 + 'white',
3.80 + ],
3.81 + list(range(30, 38))
3.82 + ))
3.83 + )
3.84 +
3.85 +
3.86 +RESET = '\033[0m'
3.87 +
3.88 +
3.89 +def colored(text, color=None, on_color=None, attrs=None):
3.90 + """Colorize text.
3.91 +
3.92 + Available text colors:
3.93 + red, green, yellow, blue, magenta, cyan, white.
3.94 +
3.95 + Available text highlights:
3.96 + on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white.
3.97 +
3.98 + Available attributes:
3.99 + bold, dark, underline, blink, reverse, concealed.
3.100 +
3.101 + Example:
3.102 + colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
3.103 + colored('Hello, World!', 'green')
3.104 + """
3.105 + if os.getenv('ANSI_COLORS_DISABLED') is None:
3.106 + fmt_str = '\033[%dm%s'
3.107 + if color is not None:
3.108 + text = fmt_str % (COLORS[color], text)
3.109 +
3.110 + if on_color is not None:
3.111 + text = fmt_str % (HIGHLIGHTS[on_color], text)
3.112 +
3.113 + if attrs is not None:
3.114 + for attr in attrs:
3.115 + text = fmt_str % (ATTRIBUTES[attr], text)
3.116 +
3.117 + text += RESET
3.118 + return text
3.119 +
3.120 +
3.121 +def cprint(text, color=None, on_color=None, attrs=None, **kwargs):
3.122 + """Print colorize text.
3.123 +
3.124 + It accepts arguments of print function.
3.125 + """
3.126 +
3.127 + print((colored(text, color, on_color, attrs)), **kwargs)
3.128 +
3.129 +
3.130 +if __name__ == '__main__':
3.131 + print('Current terminal type: %s' % os.getenv('TERM'))
3.132 + print('Test basic colors:')
3.133 + cprint('Grey color', 'grey')
3.134 + cprint('Red color', 'red')
3.135 + cprint('Green color', 'green')
3.136 + cprint('Yellow color', 'yellow')
3.137 + cprint('Blue color', 'blue')
3.138 + cprint('Magenta color', 'magenta')
3.139 + cprint('Cyan color', 'cyan')
3.140 + cprint('White color', 'white')
3.141 + print(('-' * 78))
3.142 +
3.143 + print('Test highlights:')
3.144 + cprint('On grey color', on_color='on_grey')
3.145 + cprint('On red color', on_color='on_red')
3.146 + cprint('On green color', on_color='on_green')
3.147 + cprint('On yellow color', on_color='on_yellow')
3.148 + cprint('On blue color', on_color='on_blue')
3.149 + cprint('On magenta color', on_color='on_magenta')
3.150 + cprint('On cyan color', on_color='on_cyan')
3.151 + cprint('On white color', color='grey', on_color='on_white')
3.152 + print('-' * 78)
3.153 +
3.154 + print('Test attributes:')
3.155 + cprint('Bold grey color', 'grey', attrs=['bold'])
3.156 + cprint('Dark red color', 'red', attrs=['dark'])
3.157 + cprint('Underline green color', 'green', attrs=['underline'])
3.158 + cprint('Blink yellow color', 'yellow', attrs=['blink'])
3.159 + cprint('Reversed blue color', 'blue', attrs=['reverse'])
3.160 + cprint('Concealed Magenta color', 'magenta', attrs=['concealed'])
3.161 + cprint('Bold underline reverse cyan color', 'cyan',
3.162 + attrs=['bold', 'underline', 'reverse'])
3.163 + cprint('Dark blink concealed white color', 'white',
3.164 + attrs=['dark', 'blink', 'concealed'])
3.165 + print(('-' * 78))
3.166 +
3.167 + print('Test mixing:')
3.168 + cprint('Underline red on grey color', 'red', 'on_grey',
3.169 + ['underline'])
3.170 + cprint('Reversed green on red color', 'green', 'on_red', ['reverse'])
3.171 +
4.1 --- a/artemis.py Fri Apr 15 22:57:55 2011 -0700
4.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
4.3 @@ -1,487 +0,0 @@
4.4 -# Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
4.5 -
4.6 -"""A very simple and lightweight issue tracker for Mercurial."""
4.7 -
4.8 -from mercurial import hg, util, commands
4.9 -from mercurial.i18n import _
4.10 -import os, time, random, mailbox, glob, socket, ConfigParser
4.11 -import mimetypes
4.12 -from email import encoders
4.13 -from email.generator import Generator
4.14 -from email.mime.audio import MIMEAudio
4.15 -from email.mime.base import MIMEBase
4.16 -from email.mime.image import MIMEImage
4.17 -from email.mime.multipart import MIMEMultipart
4.18 -from email.mime.text import MIMEText
4.19 -
4.20 -from termcolor import colored
4.21 -
4.22 -
4.23 -state = { 'new': ['new'],
4.24 - 'fixed': ['fixed', 'resolved'] }
4.25 -annotation = { 'resolved': 'resolution' }
4.26 -default_state = 'new'
4.27 -default_issues_dir = ".issues"
4.28 -filter_prefix = ".filter"
4.29 -date_format = '%a, %d %b %Y %H:%M:%S %1%2'
4.30 -maildir_dirs = ['new','cur','tmp']
4.31 -
4.32 -
4.33 -def ilist(ui, repo, **opts):
4.34 - """List issues associated with the project"""
4.35 -
4.36 - # Process options
4.37 - show_all = opts['all']
4.38 - properties = []
4.39 - match_date, date_match = False, lambda x: True
4.40 - if opts['date']:
4.41 - match_date, date_match = True, util.matchdate(opts['date'])
4.42 - order = 'new'
4.43 - if opts['order']:
4.44 - order = opts['order']
4.45 -
4.46 - # Colors
4.47 - colors = _read_colors(ui)
4.48 -
4.49 - # Find issues
4.50 - issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
4.51 - issues_path = os.path.join(repo.root, issues_dir)
4.52 - if not os.path.exists(issues_path): return
4.53 -
4.54 - issues = glob.glob(os.path.join(issues_path, '*'))
4.55 -
4.56 - _create_all_missing_dirs(issues_path, issues)
4.57 -
4.58 - # Process filter
4.59 - if opts['filter']:
4.60 - filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
4.61 - config = ConfigParser.SafeConfigParser()
4.62 - config.read(filters)
4.63 - if not config.has_section(opts['filter']):
4.64 - ui.write('No filter %s defined\n' % opts['filter'])
4.65 - else:
4.66 - properties += config.items(opts['filter'])
4.67 -
4.68 - cmd_properties = _get_properties(opts['property'])
4.69 - list_properties = [p[0] for p in cmd_properties if len(p) == 1]
4.70 - list_properties_dict = {}
4.71 - properties += filter(lambda p: len(p) > 1, cmd_properties)
4.72 -
4.73 - summaries = []
4.74 - for issue in issues:
4.75 - mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
4.76 - root = _find_root_key(mbox)
4.77 - if not root: continue
4.78 - property_match = True
4.79 - for property,value in properties:
4.80 - if value:
4.81 - property_match = property_match and (mbox[root][property] == value)
4.82 - else:
4.83 - property_match = property_match and (property not in mbox[root])
4.84 -
4.85 - if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['fixed']]): continue
4.86 - if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
4.87 -
4.88 - if not list_properties:
4.89 - summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], colors), # +1 for trailing /
4.90 - _find_mbox_date(mbox, root, order)))
4.91 - else:
4.92 - for lp in list_properties:
4.93 - if lp in mbox[root]: list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
4.94 -
4.95 - if not list_properties:
4.96 - summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
4.97 - for s,d in summaries:
4.98 - ui.write(s)
4.99 - else:
4.100 - for lp in list_properties_dict.keys():
4.101 - ui.write("%s:\n" % lp)
4.102 - for value in sorted(list_properties_dict[lp]):
4.103 - ui.write(" %s\n" % value)
4.104 -
4.105 -
4.106 -def iadd(ui, repo, id = None, comment = 0, **opts):
4.107 - """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
4.108 -
4.109 - comment = int(comment)
4.110 -
4.111 - # First, make sure issues have a directory
4.112 - issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
4.113 - issues_path = os.path.join(repo.root, issues_dir)
4.114 - if not os.path.exists(issues_path): os.mkdir(issues_path)
4.115 -
4.116 - if id:
4.117 - issue_fn, issue_id = _find_issue(ui, repo, id)
4.118 - if not issue_fn:
4.119 - ui.warn('No such issue\n')
4.120 - return
4.121 - _create_missing_dirs(issues_path, issue_id)
4.122 -
4.123 - user = ui.username()
4.124 -
4.125 - default_issue_text = "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
4.126 - if not id:
4.127 - default_issue_text += "State: %s\n" % default_state
4.128 - default_issue_text += "Subject: brief description\n\n"
4.129 - default_issue_text += "Detailed description."
4.130 -
4.131 - # Get properties, and figure out if we need an explicit comment
4.132 - properties = _get_properties(opts['property'])
4.133 - no_comment = id and properties and opts['no_property_comment']
4.134 - message = opts['message']
4.135 -
4.136 - # Create the text
4.137 - if message:
4.138 - if not id:
4.139 - state_str = 'State: %s\n' % default_state
4.140 - else:
4.141 - state_str = ''
4.142 - issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
4.143 - (user, util.datestr(format=date_format), message, state_str)
4.144 - elif not no_comment:
4.145 - issue = ui.edit(default_issue_text, user)
4.146 -
4.147 - if issue.strip() == '':
4.148 - ui.warn('Empty issue, ignoring\n')
4.149 - return
4.150 - if issue.strip() == default_issue_text:
4.151 - ui.warn('Unchanged issue text, ignoring\n')
4.152 - return
4.153 - else:
4.154 - # Write down a comment about updated properties
4.155 - properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
4.156 -
4.157 - issue = "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
4.158 - (user, util.datestr(format = date_format), properties_subject)
4.159 -
4.160 - # Create the message
4.161 - msg = mailbox.MaildirMessage(issue)
4.162 - if opts['attach']:
4.163 - outer = _attach_files(msg, opts['attach'])
4.164 - else:
4.165 - outer = msg
4.166 -
4.167 - # Pick random filename
4.168 - if not id:
4.169 - issue_fn = issues_path
4.170 - while os.path.exists(issue_fn):
4.171 - issue_id = _random_id()
4.172 - issue_fn = os.path.join(issues_path, issue_id)
4.173 - # else: issue_fn already set
4.174 -
4.175 - # Add message to the mailbox
4.176 - mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
4.177 - keys = _order_keys_date(mbox)
4.178 - mbox.lock()
4.179 - if id and comment >= len(mbox):
4.180 - ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
4.181 -
4.182 - if not id:
4.183 - outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
4.184 - else:
4.185 - root = keys[0]
4.186 - outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
4.187 - outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
4.188 - outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
4.189 - new_bug_path = issue_fn[(len(repo.root)+1):] + '/new/' + mbox.add(outer) # + 1 for the trailing /
4.190 - commands.add(ui, repo, new_bug_path)
4.191 -
4.192 - # Fix properties in the root message
4.193 - if properties:
4.194 - root = _find_root_key(mbox)
4.195 - msg = mbox[root]
4.196 - for property, value in properties:
4.197 - if property in msg:
4.198 - msg.replace_header(property, value)
4.199 - else:
4.200 - msg.add_header(property, value)
4.201 - mbox[root] = msg
4.202 -
4.203 - mbox.close()
4.204 -
4.205 - if opts['commit']:
4.206 - commands.commit(ui, repo, issue_fn)
4.207 -
4.208 - # If adding issue, add the new mailbox to the repository
4.209 - if not id:
4.210 - ui.status('Added new issue %s\n' % issue_id)
4.211 - else:
4.212 - _show_mbox(ui, mbox, 0)
4.213 -
4.214 -def ishow(ui, repo, id, comment = 0, **opts):
4.215 - """Shows issue ID, or possibly its comment COMMENT"""
4.216 -
4.217 - comment = int(comment)
4.218 - issue, id = _find_issue(ui, repo, id)
4.219 - if not issue:
4.220 - return ui.warn('No such issue\n')
4.221 -
4.222 - issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
4.223 - _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
4.224 -
4.225 - if opts.get('mutt'):
4.226 - return util.system('mutt -R -f %s' % issue)
4.227 -
4.228 - mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
4.229 -
4.230 - if opts['all']:
4.231 - ui.write('='*70 + '\n')
4.232 - i = 0
4.233 - keys = _order_keys_date(mbox)
4.234 - for k in keys:
4.235 - _write_message(ui, mbox[k], i, skip = opts['skip'])
4.236 - ui.write('-'*70 + '\n')
4.237 - i += 1
4.238 - return
4.239 -
4.240 - _show_mbox(ui, mbox, comment, skip = opts['skip'])
4.241 -
4.242 - if opts['extract']:
4.243 - attachment_numbers = map(int, opts['extract'])
4.244 - keys = _order_keys_date(mbox)
4.245 - msg = mbox[keys[comment]]
4.246 - counter = 1
4.247 - for part in msg.walk():
4.248 - ctype = part.get_content_type()
4.249 - maintype, subtype = ctype.split('/', 1)
4.250 - if maintype == 'multipart' or ctype == 'text/plain': continue
4.251 - if counter in attachment_numbers:
4.252 - filename = part.get_filename()
4.253 - if not filename:
4.254 - ext = mimetypes.guess_extension(part.get_content_type()) or ''
4.255 - filename = 'attachment-%03d%s' % (counter, ext)
4.256 - fp = open(filename, 'wb')
4.257 - fp.write(part.get_payload(decode = True))
4.258 - fp.close()
4.259 - counter += 1
4.260 -
4.261 -
4.262 -def _find_issue(ui, repo, id):
4.263 - issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
4.264 - issues_path = os.path.join(repo.root, issues_dir)
4.265 - if not os.path.exists(issues_path): return False
4.266 -
4.267 - issues = glob.glob(os.path.join(issues_path, id + '*'))
4.268 -
4.269 - if len(issues) == 0:
4.270 - return False, 0
4.271 - elif len(issues) > 1:
4.272 - ui.status("Multiple choices:\n")
4.273 - for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n')
4.274 - return False, 0
4.275 -
4.276 - return issues[0], issues[0][len(issues_path)+1:]
4.277 -
4.278 -def _get_properties(property_list):
4.279 - return [p.split('=') for p in property_list]
4.280 -
4.281 -def _write_message(ui, message, index = 0, skip = None):
4.282 - if index: ui.write("Comment: %d\n" % index)
4.283 - if ui.verbose:
4.284 - _show_text(ui, message.as_string().strip(), skip)
4.285 - else:
4.286 - if 'From' in message: ui.write('From: %s\n' % message['From'])
4.287 - if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
4.288 - if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
4.289 - if 'State' in message: ui.write('State: %s\n' % message['State'])
4.290 - counter = 1
4.291 - for part in message.walk():
4.292 - ctype = part.get_content_type()
4.293 - maintype, subtype = ctype.split('/', 1)
4.294 - if maintype == 'multipart': continue
4.295 - if ctype == 'text/plain':
4.296 - ui.write('\n')
4.297 - _show_text(ui, part.get_payload().strip(), skip)
4.298 - else:
4.299 - filename = part.get_filename()
4.300 - ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
4.301 - counter += 1
4.302 -
4.303 -def _show_text(ui, text, skip = None):
4.304 - for line in text.splitlines():
4.305 - if not skip or not line.startswith(skip):
4.306 - ui.write(line + '\n')
4.307 - ui.write('\n')
4.308 -
4.309 -def _show_mbox(ui, mbox, comment, **opts):
4.310 - # Output the issue (or comment)
4.311 - if comment >= len(mbox):
4.312 - comment = 0
4.313 - ui.warn('Comment out of range, showing the issue itself\n')
4.314 - keys = _order_keys_date(mbox)
4.315 - root = keys[0]
4.316 - msg = mbox[keys[comment]]
4.317 - ui.write('='*70 + '\n')
4.318 - if comment:
4.319 - ui.write('Subject: %s\n' % mbox[root]['Subject'])
4.320 - ui.write('State: %s\n' % mbox[root]['State'])
4.321 - ui.write('-'*70 + '\n')
4.322 - _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
4.323 - ui.write('-'*70 + '\n')
4.324 -
4.325 - # Read the mailbox into the messages and children dictionaries
4.326 - messages = {}
4.327 - children = {}
4.328 - i = 0
4.329 - for k in keys:
4.330 - m = mbox[k]
4.331 - messages[m['Message-Id']] = (i,m)
4.332 - children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
4.333 - i += 1
4.334 - children[None] = [] # Safeguard against infinte loop on empty Message-Id
4.335 -
4.336 - # Iterate over children
4.337 - id = msg['Message-Id']
4.338 - id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
4.339 - if not id_stack: return
4.340 - ui.write('Comments:\n')
4.341 - while id_stack:
4.342 - id,offset = id_stack.pop()
4.343 - id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
4.344 - index, msg = messages[id]
4.345 - ui.write(' '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
4.346 - ui.write('-'*70 + '\n')
4.347 -
4.348 -def _find_root_key(maildir):
4.349 - for k,m in maildir.iteritems():
4.350 - if 'in-reply-to' not in m:
4.351 - return k
4.352 -
4.353 -def _order_keys_date(mbox):
4.354 - keys = mbox.keys()
4.355 - root = _find_root_key(mbox)
4.356 - keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
4.357 - return keys
4.358 -
4.359 -def _find_mbox_date(mbox, root, order):
4.360 - if order == 'latest':
4.361 - keys = _order_keys_date(mbox)
4.362 - msg = mbox[keys[-1]]
4.363 - else: # new
4.364 - msg = mbox[root]
4.365 - return util.parsedate(msg['date'])
4.366 -
4.367 -def _random_id():
4.368 - return "%x" % random.randint(2**63, 2**64-1)
4.369 -
4.370 -def _create_missing_dirs(issues_path, issue):
4.371 - for d in maildir_dirs:
4.372 - path = os.path.join(issues_path,issue,d)
4.373 - if not os.path.exists(path): os.mkdir(path)
4.374 -
4.375 -def _create_all_missing_dirs(issues_path, issues):
4.376 - for i in issues:
4.377 - _create_missing_dirs(issues_path, i)
4.378 -
4.379 -def _humanreadable(size):
4.380 - if size > 1024*1024:
4.381 - return '%5.1fM' % (float(size) / (1024*1024))
4.382 - elif size > 1024:
4.383 - return '%5.1fK' % (float(size) / 1024)
4.384 - else:
4.385 - return '%dB' % size
4.386 -
4.387 -def _attach_files(msg, filenames):
4.388 - outer = MIMEMultipart()
4.389 - for k in msg.keys(): outer[k] = msg[k]
4.390 - outer.attach(MIMEText(msg.get_payload()))
4.391 -
4.392 - for filename in filenames:
4.393 - ctype, encoding = mimetypes.guess_type(filename)
4.394 - if ctype is None or encoding is not None:
4.395 - # No guess could be made, or the file is encoded (compressed), so
4.396 - # use a generic bag-of-bits type.
4.397 - ctype = 'application/octet-stream'
4.398 - maintype, subtype = ctype.split('/', 1)
4.399 - if maintype == 'text':
4.400 - fp = open(filename)
4.401 - # Note: we should handle calculating the charset
4.402 - attachment = MIMEText(fp.read(), _subtype=subtype)
4.403 - fp.close()
4.404 - elif maintype == 'image':
4.405 - fp = open(filename, 'rb')
4.406 - attachment = MIMEImage(fp.read(), _subtype=subtype)
4.407 - fp.close()
4.408 - elif maintype == 'audio':
4.409 - fp = open(filename, 'rb')
4.410 - attachment = MIMEAudio(fp.read(), _subtype=subtype)
4.411 - fp.close()
4.412 - else:
4.413 - fp = open(filename, 'rb')
4.414 - attachment = MIMEBase(maintype, subtype)
4.415 - attachment.set_payload(fp.read())
4.416 - fp.close()
4.417 - # Encode the payload using Base64
4.418 - encoders.encode_base64(attachment)
4.419 - # Set the filename parameter
4.420 - attachment.add_header('Content-Disposition', 'attachment', filename=filename)
4.421 - outer.attach(attachment)
4.422 - return outer
4.423 -
4.424 -def _status_msg(msg):
4.425 - s = msg['State']
4.426 - if s in annotation:
4.427 - return '%s=%s' % (s, msg[annotation[s]])
4.428 - else:
4.429 - return s
4.430 -
4.431 -def _read_colors(ui):
4.432 - colors = {}
4.433 - # defaults
4.434 - colors['new.color'] = 'red'
4.435 - colors['new.on_color'] = 'on_grey'
4.436 - colors['new.attrs'] = 'bold'
4.437 - colors['resolved.color'] = 'white'
4.438 - colors['resolved.on_color'] = ''
4.439 - colors['resolved.attrs'] = ''
4.440 - for v in colors:
4.441 - colors[v] = ui.config('artemis', v, colors[v])
4.442 - if v.endswith('attrs'): colors[v] = colors[v].split()
4.443 - return colors
4.444 -
4.445 -def _color_summary(line, msg, colors):
4.446 - if msg['State'] == 'new':
4.447 - return colored(line, colors['new.color'], attrs = colors['new.attrs'])
4.448 - elif msg['State'] in state['fixed']:
4.449 - return colored(line, colors['resolved.color'], attrs = colors['resolved.attrs'])
4.450 - else:
4.451 - return line
4.452 -
4.453 -def _summary_line(mbox, root, issue, colors):
4.454 - line = "%s (%3d) [%s]: %s\n" % (issue,
4.455 - len(mbox)-1, # number of replies (-1 for self)
4.456 - _status_msg(mbox[root]),
4.457 - mbox[root]['Subject'])
4.458 - return _color_summary(line, mbox[root], colors)
4.459 -
4.460 -cmdtable = {
4.461 - 'ilist': (ilist,
4.462 - [('a', 'all', False,
4.463 - 'list all issues (by default only those with state new)'),
4.464 - ('p', 'property', [],
4.465 - 'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
4.466 - ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
4.467 - ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
4.468 - ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
4.469 - _('hg ilist [OPTIONS]')),
4.470 - 'iadd': (iadd,
4.471 - [('a', 'attach', [],
4.472 - 'attach file(s) (e.g., -a filename1 -a filename2)'),
4.473 - ('p', 'property', [],
4.474 - 'update properties (e.g., -p state=fixed)'),
4.475 - ('n', 'no-property-comment', None,
4.476 - 'do not add a comment about changed properties'),
4.477 - ('m', 'message', '',
4.478 - 'use <text> as an issue subject'),
4.479 - ('c', 'commit', False,
4.480 - 'perform a commit after the addition')],
4.481 - _('hg iadd [OPTIONS] [ID] [COMMENT]')),
4.482 - 'ishow': (ishow,
4.483 - [('a', 'all', None, 'list all comments'),
4.484 - ('s', 'skip', '>', 'skip lines starting with a substring'),
4.485 - ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
4.486 - ('', 'mutt', False, 'use mutt to show issue')],
4.487 - _('hg ishow [OPTIONS] ID [COMMENT]')),
4.488 -}
4.489 -
4.490 -# vim: expandtab
5.1 --- a/termcolor.py Fri Apr 15 22:57:55 2011 -0700
5.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
5.3 @@ -1,168 +0,0 @@
5.4 -# coding: utf-8
5.5 -# Copyright (c) 2008-2011 Volvox Development Team
5.6 -#
5.7 -# Permission is hereby granted, free of charge, to any person obtaining a copy
5.8 -# of this software and associated documentation files (the "Software"), to deal
5.9 -# in the Software without restriction, including without limitation the rights
5.10 -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5.11 -# copies of the Software, and to permit persons to whom the Software is
5.12 -# furnished to do so, subject to the following conditions:
5.13 -#
5.14 -# The above copyright notice and this permission notice shall be included in
5.15 -# all copies or substantial portions of the Software.
5.16 -#
5.17 -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
5.18 -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
5.19 -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
5.20 -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
5.21 -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
5.22 -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
5.23 -# THE SOFTWARE.
5.24 -#
5.25 -# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
5.26 -
5.27 -"""ANSII Color formatting for output in terminal."""
5.28 -
5.29 -from __future__ import print_function
5.30 -import os
5.31 -
5.32 -
5.33 -__ALL__ = [ 'colored', 'cprint' ]
5.34 -
5.35 -VERSION = (1, 1, 0)
5.36 -
5.37 -ATTRIBUTES = dict(
5.38 - list(zip([
5.39 - 'bold',
5.40 - 'dark',
5.41 - '',
5.42 - 'underline',
5.43 - 'blink',
5.44 - '',
5.45 - 'reverse',
5.46 - 'concealed'
5.47 - ],
5.48 - list(range(1, 9))
5.49 - ))
5.50 - )
5.51 -del ATTRIBUTES['']
5.52 -
5.53 -
5.54 -HIGHLIGHTS = dict(
5.55 - list(zip([
5.56 - 'on_grey',
5.57 - 'on_red',
5.58 - 'on_green',
5.59 - 'on_yellow',
5.60 - 'on_blue',
5.61 - 'on_magenta',
5.62 - 'on_cyan',
5.63 - 'on_white'
5.64 - ],
5.65 - list(range(40, 48))
5.66 - ))
5.67 - )
5.68 -
5.69 -
5.70 -COLORS = dict(
5.71 - list(zip([
5.72 - 'grey',
5.73 - 'red',
5.74 - 'green',
5.75 - 'yellow',
5.76 - 'blue',
5.77 - 'magenta',
5.78 - 'cyan',
5.79 - 'white',
5.80 - ],
5.81 - list(range(30, 38))
5.82 - ))
5.83 - )
5.84 -
5.85 -
5.86 -RESET = '\033[0m'
5.87 -
5.88 -
5.89 -def colored(text, color=None, on_color=None, attrs=None):
5.90 - """Colorize text.
5.91 -
5.92 - Available text colors:
5.93 - red, green, yellow, blue, magenta, cyan, white.
5.94 -
5.95 - Available text highlights:
5.96 - on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white.
5.97 -
5.98 - Available attributes:
5.99 - bold, dark, underline, blink, reverse, concealed.
5.100 -
5.101 - Example:
5.102 - colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
5.103 - colored('Hello, World!', 'green')
5.104 - """
5.105 - if os.getenv('ANSI_COLORS_DISABLED') is None:
5.106 - fmt_str = '\033[%dm%s'
5.107 - if color is not None:
5.108 - text = fmt_str % (COLORS[color], text)
5.109 -
5.110 - if on_color is not None:
5.111 - text = fmt_str % (HIGHLIGHTS[on_color], text)
5.112 -
5.113 - if attrs is not None:
5.114 - for attr in attrs:
5.115 - text = fmt_str % (ATTRIBUTES[attr], text)
5.116 -
5.117 - text += RESET
5.118 - return text
5.119 -
5.120 -
5.121 -def cprint(text, color=None, on_color=None, attrs=None, **kwargs):
5.122 - """Print colorize text.
5.123 -
5.124 - It accepts arguments of print function.
5.125 - """
5.126 -
5.127 - print((colored(text, color, on_color, attrs)), **kwargs)
5.128 -
5.129 -
5.130 -if __name__ == '__main__':
5.131 - print('Current terminal type: %s' % os.getenv('TERM'))
5.132 - print('Test basic colors:')
5.133 - cprint('Grey color', 'grey')
5.134 - cprint('Red color', 'red')
5.135 - cprint('Green color', 'green')
5.136 - cprint('Yellow color', 'yellow')
5.137 - cprint('Blue color', 'blue')
5.138 - cprint('Magenta color', 'magenta')
5.139 - cprint('Cyan color', 'cyan')
5.140 - cprint('White color', 'white')
5.141 - print(('-' * 78))
5.142 -
5.143 - print('Test highlights:')
5.144 - cprint('On grey color', on_color='on_grey')
5.145 - cprint('On red color', on_color='on_red')
5.146 - cprint('On green color', on_color='on_green')
5.147 - cprint('On yellow color', on_color='on_yellow')
5.148 - cprint('On blue color', on_color='on_blue')
5.149 - cprint('On magenta color', on_color='on_magenta')
5.150 - cprint('On cyan color', on_color='on_cyan')
5.151 - cprint('On white color', color='grey', on_color='on_white')
5.152 - print('-' * 78)
5.153 -
5.154 - print('Test attributes:')
5.155 - cprint('Bold grey color', 'grey', attrs=['bold'])
5.156 - cprint('Dark red color', 'red', attrs=['dark'])
5.157 - cprint('Underline green color', 'green', attrs=['underline'])
5.158 - cprint('Blink yellow color', 'yellow', attrs=['blink'])
5.159 - cprint('Reversed blue color', 'blue', attrs=['reverse'])
5.160 - cprint('Concealed Magenta color', 'magenta', attrs=['concealed'])
5.161 - cprint('Bold underline reverse cyan color', 'cyan',
5.162 - attrs=['bold', 'underline', 'reverse'])
5.163 - cprint('Dark blink concealed white color', 'white',
5.164 - attrs=['dark', 'blink', 'concealed'])
5.165 - print(('-' * 78))
5.166 -
5.167 - print('Test mixing:')
5.168 - cprint('Underline red on grey color', 'red', 'on_grey',
5.169 - ['underline'])
5.170 - cprint('Reversed green on red color', 'green', 'on_red', ['reverse'])
5.171 -