Moved everything into artemis/ subdirectory to be able to import termcolor.py
authorDmitriy Morozov <dmitriy@mrzv.org>
Fri Apr 15 23:10:12 2011 -0700 (13 months ago)
changeset 60c933fa2cd204
parent 5918da6a9fa7b8
child 61c384fa42f8a2
Moved everything into artemis/ subdirectory to be able to import termcolor.py
README
artemis.py
artemis/__init__.py
artemis/artemis.py
artemis/termcolor.py
termcolor.py
       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 -