Back out frostbane's changes, going back to af9913ddbb3d
authorDmitriy Morozov <dmitriy@mrzv.org>
Sat, 08 Apr 2017 13:58:54 -0700
changeset 91 6368495f10a5
parent 90 979145ac3ccc
child 92 c234d2db1e06
Back out frostbane's changes, going back to af9913ddbb3d
.gitignore
.hgignore
README.md
artemis/__init__.py
artemis/add.py
artemis/artemis.py
artemis/find.py
artemis/list.py
artemis/main.py
artemis/properties.py
artemis/show.py
--- a/.gitignore	Wed Mar 15 13:12:28 2017 -0400
+++ b/.gitignore	Sat Apr 08 13:58:54 2017 -0700
@@ -1,7 +1,5 @@
-*.pyo
 *.pyc
 artemis.egg-info/
 build/
 dist/
-.idea/*
-pycharm-debug.egg
+
--- a/.hgignore	Wed Mar 15 13:12:28 2017 -0400
+++ b/.hgignore	Sat Apr 08 13:58:54 2017 -0700
@@ -1,10 +1,5 @@
 syntax:glob
-*.pyo
 *.pyc
 artemis.egg-info/
 build/
 dist/
-.idea/*
-pycharm-debug.egg
-syntax: glob
-artemis.iml
--- a/README.md	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,365 +0,0 @@
-# Artemis #
------------
-
-> [Artemis](http://www.mrzv.org/software/artemis/) is a lightweight distributed issue tracking extension for [Mercurial](http://www.selenic.com/mercurial/).
-
-* [Setup](#markdown-header-setup)
-* [Example](#markdown-header-example)
-* [Commands](#markdown-header-commands)
-* [Filters](#markdown-header-filters)
-* [Format](#markdown-header-format)
-
-
-Individual issues are stored in directories in an `.issues` subdirectory (overridable in a config file). Each one is a `Maildir` and each one is assumed to have a single root message. Various properties of an issue are stored in the headers of that message.
-
-One can obtain Artemis by cloning its [repository](http://hg.mrzv.org/Artemis/):
-
-```
-    $ hg clone http://hg.mrzv.org/Artemis/
-```
-
-or downloading the entire repository as a [tarball](http://hg.mrzv.org/Artemis/archive/tip.tar.gz).
-
-
-
-## Setup ##
------------
-
-In the `[extensions]` section of your `~/.hgrc` add:
-
-```
-    artemis = /path/to/Artemis/artemis
-```
-
-Optionally, provide a section `[artemis]` in your `~/.hgrc` file or the repository local `.hg/hgrc` file, and specify an alternative path for the issues subdirectory (instead of the default `.issues`):
-
-```
-    [artemis]
-    issues = _issues
-```
-
-Additionally, one can specify [filters](#markdown-header-filters) and output [formats](#markdown-header-format).
-
-### Windows ###
----------------
-
-The TortoiseHg for windows comes with a sandboxed Python, that means that thg will not be using the Python installed in the system.
-
-If you get a `No module named mailbox!` error find the `mailbox.py` under `%PYTHON_PATH%\Lib` and add it to the `%HG_PATH%\lib\library.zip`. 
-
-
-[↑ back to top](#markdown-header-artemis)
-
-
-## Example ##
--------------
-
-**Create an issue:**
-
-```
-    $ hg iadd
-    ... enter some text in an editor ...
-```
-.
-```
-    Added new issue 907ab57e04502afd
-```
-
-**List the issues:**
-
-```
-    $ hg ilist
-    907ab57e04502afd (  0) [new]: New issue
-```
-
-**Show an issue:**
-
-```
-    $ hg ishow 907ab57e04502afd
-    ======================================================================
-    From: ...
-    Date: ...
-    Subject: New issue
-    State: new
-
-    Detailed description.
-
-    ----------------------------------------------------------------------
-```
-
-**Add a comment to the issue:**
-
-```
-    $ hg iadd 907ab57e04502afd
-    ... enter the comment text
-```
-.
-```
-    ======================================================================
-    From: ...
-    [snip]
-    Detailed description.
-
-    ----------------------------------------------------------------------
-    Comments:
-      1: [dmitriy] Some comment
-    ----------------------------------------------------------------------
-```
-
-**And a comment to the comment:**
-
-```
-    $ hg iadd 907ab57e04502afd -i 1
-    ... enter the comment text ...
-```
-.
-```
-    ======================================================================
-    From: ...
-    [snip]
-    Detailed description.
-
-    ----------------------------------------------------------------------
-    Comments:
-      1: [dmitriy] Some comment
-        2: [dmitriy] Comment on a comment
-    ----------------------------------------------------------------------
-```
-
-**Close the issue:**
-
-```
-    $ hg iadd 907ab57e04502afd -p state=resolved -p resolution=fixed -n
-```
-.
-```
-    ======================================================================
-    From: ...
-    [snip]
-    Detailed description.
-
-    ----------------------------------------------------------------------
-    Comments:
-      1: [dmitriy] Some comment
-        2: [dmitriy] Comment on a comment
-      3: [dmitriy] changed properties (state=resolved, resolution=fixed)
-    ----------------------------------------------------------------------
-```
-
-**No more new issues, and one resolved issue:**
-
-```
-    $ hg ilist
-    $ hg ilist -a
-    907ab57e04502afd (  3) [resolved=fixed]: New issue
-```
-
-The fact that issues are Maildirs, allows one to look at them in, for example, `mutt` with predictable results:
-
-```
-    mutt -Rf .issues/907ab57e04502afd
-```
-
-**Search the issues:**
-
-Search a property using regular expressions
-```
-    hg ifind -p state -r "fix(ed)?|resolve(d)?"
-    907ab57e04502afd (  3) [resolved]: New issue
-    baca6256d98fb593 (  1) [resolved]: Another issue
-```
-
-Search the message for a string
-```
-    hg ifind -mn "comment" 
-    907ab57e04502afd (  3) [resolved]: New issue
-    baca6256d98fb593 (  1) [resolved]: Another issue
-    ba564b23fcff6358 (  1) [new]: New issue
-```
-
-
-
-[↑ back to top](#markdown-header-artemis)
-
-## Commands ##
---------------
-
-**hg iadd [OPTIONS] [ID]**
-
-> Adds a new issue, or comment to an existing issue ID or its comment COMMENT
-
-
-options:
-
-|     |                       | description                           |
-|-----|-----------------------|---------------------------------------|
-| -a  | --attach VALUE [+]    | attach file(s)                        |
-|     |                       | (e.g., -a filename1 -a filename2)     |
-| -p  | --property VALUE [+]  | update properties                     |
-|     |                       | (e.g. -p state=fixed,                 |
-|     |                       | -p state=resolved                     |
-|     |                       | -p resolution=fixed)                  |
-| -n  | --no-property-comment | do not add a comment about changed    |
-|     |                       | properties                            |
-| -m  | --message VALUE       | use <text> as an issue subject        |
-| -i  | --index VALUE         | 0 based index of the message to show  |
-|     |                       | (default: 0)                          |
-| -c  | --commit              | perform a commit after the addition   |
-
-[+] marked option can be specified multiple times
-
-
-
-
-
-**hg ilist [OPTIONS]**
-
-> List issues associated with the project
-
-options:
-
-|     |                      | description                            |
-|-----|----------------------|----------------------------------------|
-| -a  | --all                | list all issues                        |
-|     |                      | (by default only those with state new) |
-| -p  | --property VALUE [+] | list issues with specific field values |
-|     |                      | (e.g., -p state=fixed,                 |
-|     |                      | -p state=resolved -p category=doc);    |
-|     |                      | lists all possible values of a         |
-|     |                      | property if no = sign is provided.     |
-|     |                      | (e.g. -p category)                     |
-|     | --all-properties     | list all available properties          |
-| -o  | --order VALUE        | order of the issues; choices: "new"    |
-|     |                      | (date submitted), "latest"             |
-|     |                      | (date of the last message)             |
-|     |                      | (default: new)                         |
-| -d  | --date VALUE         | restrict to issues matching the date   |
-|     |                      | (e.g., -d ">12/28/2007)"               |
-| -f  | --filter VALUE       | restrict to pre-defined filter         |
-|     |                      | (in .issues/.filter*)                  |
-
-[+] marked option can be specified multiple times
-
-
-
-**hg ishow [OPTIONS] ID**
-
-> Shows issue ID, or possibly its comment COMMENT
-
-options:
-
-|     |                     | description                             |
-|-----|---------------------|-----------------------------------------|
-| -a  | --all               | list all comments                       |
-| -s  | --skip VALUE        | skip lines starting with a substring    |
-|     |                     | (default: >)                            |
-| -x  | --extract VALUE [+] | extract attachments                     |
-|     |                     | (provide attachment number as argument) |
-| -i  | --index VALUE       | 0 based index of the message to show    |
-|     |                     | (default: 0)                            |
-| -o  | --output VALUE      | extract output directory                |
-|     | --mutt              | use mutt to show issue                  |
-
-[+] marked option can be specified multiple times
-
-
-
-**hg ifind [OPTIONS] QUERY**
-
-> Shows a list of issues matching the specified QUERY
-
-options:
-
-|     |                   | description                               |
-|-----|-------------------|-------------------------------------------|
-| -p  | --property VALUE  | issue property to match                   |
-|     |                   | [state, from, subject, date, priority,    |
-|     |                   | [resolution, ticket, etc..]               |
-|     |                   | (default: subject)                        |
-| -n  | --no-property     | Do not match the property. Use with       |
-|     |                   | --message to search only the message. The |
-|     |                   | --property will be ignored.               |
-| -m  | --message         | Search the message. If no match is found  |
-|     |                   | it will then search the property for a    |
-|     |                   | match. Use a blank property to ignore the | 
-|     |                   | property search.                          | 
-| -c  | --case-sensitive  | case sensitive search                     |
-| -r  | --regex           | use regular expressions                   |
-|     |                   | (exact option will be ignored)            |
-| -e  | --exact           | use exact comparison                      |
-|     |                   | like comparison is used if uspecified     |
-
-[+] marked option can be specified multiple times
-
-
-
-
-
-
-
-[↑ back to top](#markdown-header-artemis)
-
-
-## Filters ##
--------------
-
-Artemis scans all files of the form `.issues/.filter*`, and processes them as config files. Section names become filter names, and the individual settings become properties. For example the following:
-
-```
-    [olddoc]
-    category=documentation
-    state=resolved
-```
-
-placed in a file `.issues/.filterMyCustom` creates a filter `olddoc` which can be invoked with the `ilist` command:
-
-```
-    hg ilist -f olddoc
-```
-
-
-[↑ back to top](#markdown-header-artemis)
-
-
-
-## Format ##
-------------
-
-One can specify the output format for the `ilist` command in the global hg configuration `~/.hgrc` or the repository local configuration `.hg/hgrc`.
-
-```
-    hg config --edit
-```
-
-The default looks like:
-
-```
-    [artemis]
-    format = %(id)s (%(len)3d) [%(state)s]: %(subject)s
-```
-
-Artemis passes a dictionary with the issue properties to the format string. (Plus `id` contains the issue id, and `len` contains the number of replies.)
-
-It's possible to specify different output formats depending on the properties of the issue. The conditions are encoded in the config variable names as follows:
-
-```
-    format:state*resolved&resolution*fixed  = %(id)s (%(len)3d) [fixed]: %(Subject)s
-    format:state*resolved                   = %(id)s (%(len)3d) [%(state)s=%(resolution)s]: %(Subject)s
-```
-
-The first rule matches issues with the state property set to resolved and resolution set to fixed; it abridges the output. The secod rule matches all the resolved issues (not matched by the first rule); it annotates the issue's state with its resolution.
-
-Finally, the dictionary passed to the format string contains a subset of [ANSI codes](http://en.wikipedia.org/wiki/ANSI_escape_code), so one could color the summary lines:
-
-```
-    format:state*new = %(red)s%(bold)s%(id)s (%(len)3d) [%(state)s]: %(Subject)s%(reset)s
-```
-
-
-[↑ back to top](#markdown-header-artemis)
-
-
-
---------------------------------------------------------------------
-
-[http://www.mrzv.org/software/artemis/](http://www.mrzv.org/software/artemis/)
--- a/artemis/__init__.py	Wed Mar 15 13:12:28 2017 -0400
+++ b/artemis/__init__.py	Sat Apr 08 13:58:54 2017 -0700
@@ -1,3 +1,3 @@
-from main import *
+from artemis import *
 
-__version__ = '0.5.1'
+__version__ = '0.5.0'
--- a/artemis/add.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-
-from mercurial import hg, util, commands
-from mercurial.i18n import _
-import sys, os, time, random, mailbox, glob, socket, ConfigParser
-import mimetypes
-from email import encoders
-from email.generator import Generator
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from itertools import izip
-from artemis import Artemis
-
-__author__ = 'frostbane'
-__date__ = '2016/03/04'
-
-
-class ArtemisAdd:
-    commands = [
-        ('a', 'attach', [], 'Attach file(s) '
-                            '(e.g., -a filename1 -a filename2,'
-                            '-a ~/Desktop/file.zip)'),
-        ('p', 'property', [], 'Update properties '
-                              '(e.g., -p state=fixed)'),
-        ('n', 'no-property-comment', None, 'Do not add a comment '
-                                           'about changed properties'),
-        ('m', 'message', '', 'Use <text> as an issue subject'),
-        ('i', 'index', '0', 'Index of the message to comment.'),
-        ('c', 'commit', False, 'Perform a commit after the addition')
-    ]
-    usage = 'hg iadd [OPTIONS] [ID] [COMMENT]'
-
-    def __init__(self):
-        pass
-
-    def attach_files(self, msg, filenames):
-        outer = MIMEMultipart()
-        for k in msg.keys():
-            outer[k] = msg[k]
-
-        outer.attach(MIMEText(msg.get_payload()))
-
-        for filename in filenames:
-            ctype, encoding = mimetypes.guess_type(filename)
-            if ctype is None or encoding is not None:
-                # No guess could be made, or the file is encoded
-                # (compressed), so use a generic bag-of-bits type.
-                ctype = 'application/octet-stream'
-            maintype, subtype = ctype.split('/', 1)
-            if maintype == 'text':
-                fp = open(filename)
-                # Note: we should handle calculating the charset
-                attachment = MIMEText(fp.read(), _subtype=subtype)
-                fp.close()
-            elif maintype == 'image':
-                fp = open(filename, 'rb')
-                attachment = MIMEImage(fp.read(), _subtype=subtype)
-                fp.close()
-            elif maintype == 'audio':
-                fp = open(filename, 'rb')
-                attachment = MIMEAudio(fp.read(), _subtype=subtype)
-                fp.close()
-            else:
-                fp = open(filename, 'rb')
-                attachment = MIMEBase(maintype, subtype)
-                attachment.set_payload(fp.read())
-                fp.close()
-                # Encode the payload using Base64
-                encoders.encode_base64(attachment)
-            # Set the filename parameter
-            attachment.add_header('Content-Disposition', 'attachment',
-                                  filename=os.path.basename(filename))
-            outer.attach(attachment)
-        return outer
-
-    def random_id(self):
-        return "%x" % random.randint(2 ** 63, 2 ** 64 - 1)
-
-    def add(self, ui, repo, id=None, **opts):
-        """Adds a new issue, or comment to an existing issue ID or its
-        comment COMMENT"""
-
-        comment = int(opts["index"])
-
-        # First, make sure issues have a directory
-        issues_dir = ui.config('artemis', 'issues',
-                               default=Artemis.default_issues_dir)
-        issues_path = os.path.join(repo.root, issues_dir)
-        if not os.path.exists(issues_path):
-            os.mkdir(issues_path)
-
-        if id:
-            issue_fn, issue_id = Artemis.find_issue(ui, repo, id)
-            if not issue_fn:
-                ui.warn('No such issue\n')
-                return
-            Artemis.create_missing_dirs(issues_path, issue_id)
-            mbox = mailbox.Maildir(issue_fn,
-                                   factory=mailbox.MaildirMessage)
-            keys = Artemis.order_keys_date(mbox)
-            root = keys[0]
-
-        user = ui.username()
-
-        default_issue_text = "From: %s\nDate: %s\n" % (
-            user, util.datestr(format=Artemis.date_format))
-        if not id:
-            default_issue_text += "State: %s\n" % Artemis.default_state
-            default_issue_text += "Subject: brief description\n\n"
-        else:
-            subject = \
-                mbox[(comment < len(mbox) and keys[comment]) or root][
-                    'Subject']
-            if not subject.startswith('Re: '):
-                subject = 'Re: ' + subject
-            default_issue_text += "Subject: %s\n\n" % subject
-        default_issue_text += "Detailed description."
-
-        # Get properties, and figure out if we need
-        # an explicit comment
-        properties = Artemis.get_properties(opts['property'])
-        no_comment = id and properties and opts['no_property_comment']
-        message = opts['message']
-
-        # Create the text
-        if message:
-            if not id:
-                state_str = 'State: %s\n' % Artemis.default_state
-            else:
-                state_str = ''
-            issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
-                    (user, util.datestr(format=Artemis.date_format),
-                     message,
-                     state_str)
-        elif not no_comment:
-            issue = ui.edit(default_issue_text, user)
-
-            if issue.strip() == '':
-                ui.warn('Empty issue, ignoring\n')
-                return
-            if issue.strip() == default_issue_text:
-                ui.warn('Unchanged issue text, ignoring\n')
-                return
-        else:
-            # Write down a comment about updated properties
-            properties_subject = ', '.join(
-                    ['%s=%s' % (property, value) for (property, value)
-                     in
-                     properties])
-
-            issue = "From: %s\nDate: %s\nSubject: changed " \
-                    "properties (%s)\n" % \
-                    (user, util.datestr(format=Artemis.date_format),
-                     properties_subject)
-
-        # Create the message
-        msg = mailbox.MaildirMessage(issue)
-        if opts['attach']:
-            outer = self.attach_files(msg, opts['attach'])
-
-        else:
-            outer = msg
-
-        # Pick random filename
-        if not id:
-            issue_fn = issues_path
-            while os.path.exists(issue_fn):
-                issue_id = self.random_id()
-                issue_fn = os.path.join(issues_path, issue_id)
-            mbox = mailbox.Maildir(issue_fn,
-                                   factory=mailbox.MaildirMessage)
-            keys = Artemis.order_keys_date(mbox)
-        # else: issue_fn already set
-
-        # Add message to the mailbox
-        mbox.lock()
-        if id and comment >= len(mbox):
-            ui.warn(
-                    'No such comment number in mailbox, commenting on the '
-                    'issue itself\n')
-
-        if not id:
-            outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (
-                issue_id, socket.gethostname()))
-
-        else:
-            root = keys[0]
-            outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (
-                issue_id, self.random_id(), socket.gethostname()))
-            outer.add_header('References', mbox[
-                (comment < len(mbox) and keys[comment]) or root][
-                'Message-Id'])
-            outer.add_header('In-Reply-To', mbox[
-                (comment < len(mbox) and keys[comment]) or root][
-                'Message-Id'])
-        new_bug_path = issue_fn + '/new/' + mbox.add(outer)
-        commands.add(ui, repo, new_bug_path)
-
-        # Fix properties in the root message
-        if properties:
-            root = Artemis.find_root_key(mbox)
-            msg = mbox[root]
-            for property, value in properties:
-                if property in msg:
-                    msg.replace_header(property, value)
-                else:
-                    msg.add_header(property, value)
-            mbox[root] = msg
-
-        mbox.close()
-
-        if opts['commit']:
-            commands.commit(ui, repo, issue_fn)
-
-        # If adding issue, add the new mailbox to the repository
-        if not id:
-            ui.status('Added new issue %s\n' % issue_id)
-        else:
-            Artemis.show_mbox(ui, mbox, 0)
--- a/artemis/artemis.py	Wed Mar 15 13:12:28 2017 -0400
+++ b/artemis/artemis.py	Sat Apr 08 13:58:54 2017 -0700
@@ -1,14 +1,10 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
 # Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
-__author__ = "Dmitriy Morozov"
 
 """A very simple and lightweight issue tracker for Mercurial."""
 
 from mercurial import hg, util, commands
 from mercurial.i18n import _
-import sys, os, time, random, mailbox, glob, socket, ConfigParser
+import os, time, random, mailbox, glob, socket, ConfigParser
 import mimetypes
 from email import encoders
 from email.generator import Generator
@@ -18,299 +14,500 @@
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from itertools import izip
-from properties import ArtemisProperties
 
 
-def singleton(cls):
-    instances = {}
+state = { 'new':      ['new'],
+          'resolved': ['fixed', 'resolved'] }
+default_state = 'new'
+default_issues_dir = ".issues"
+filter_prefix = ".filter"
+date_format = '%a, %d %b %Y %H:%M:%S %1%2'
+maildir_dirs = ['new','cur','tmp']
+default_format = '%(id)s (%(len)3d) [%(state)s]: %(Subject)s'
+
+def ilist(ui, repo, **opts):
+    """List issues associated with the project"""
+
+    # Process options
+    show_all = opts['all']
+    properties = []
+    match_date, date_match = False, lambda x: True
+    if opts['date']:
+        match_date, date_match = True, util.matchdate(opts['date'])
+    order = 'new'
+    if opts['order']:
+        order = opts['order']
+
+    # Formats
+    formats = _read_formats(ui)
+
+    # Find issues
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): return
+
+    issues = glob.glob(os.path.join(issues_path, '*'))
+
+    _create_all_missing_dirs(issues_path, issues)
 
-    def getinstance():
-        if cls not in instances:
-            instances[cls] = cls()
+    # Process filter
+    if opts['filter']:
+        filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
+        config = ConfigParser.SafeConfigParser()
+        config.read(filters)
+        if not config.has_section(opts['filter']):
+            ui.write('No filter %s defined\n' % opts['filter'])
+        else:
+            properties += config.items(opts['filter'])
+
+    cmd_properties = _get_properties(opts['property'])
+    list_properties = [p[0] for p in cmd_properties if len(p) == 1]
+    list_properties_dict = {}
+    properties += filter(lambda p: len(p) > 1, cmd_properties)
 
-        return instances[cls]
+    summaries = []
+    for issue in issues:
+        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+        root = _find_root_key(mbox)
+        if not root: continue
+        property_match = True
+        for property,value in properties:
+            if value:
+                property_match = property_match and (mbox[root][property] == value)
+            else:
+                property_match = property_match and (property not in mbox[root])
+
+        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['resolved']]): continue
+        if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
 
-    return getinstance
+        if not list_properties:
+            summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], formats),     # +1 for trailing /
+                              _find_mbox_date(mbox, root, order)))
+        else:
+            for lp in list_properties:
+                if lp in mbox[root]:    list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
+
+    if not list_properties:
+        summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
+        for s,d in summaries:
+            ui.write(s + '\n')
+    else:
+        for lp in list_properties_dict.keys():
+            ui.write("%s:\n" % lp)
+            for value in sorted(list_properties_dict[lp]):
+                ui.write("  %s\n" % value)
 
 
-# @singleton
-class Artemis:
-    """Artemis static and common functions
-    """
-    state = {
-        'new'     : ['new'],
-        'resolved': [
-            'fixed',
-            'resolved'
-        ]
-    }
-    default_state = 'new'
-    default_issues_dir = ".issues"
-    filter_prefix = ".filter"
-    date_format = '%a, %d %b %Y %H:%M:%S %1%2'
-    maildir_dirs = ['new', 'cur', 'tmp']
-    default_format = '%(id)s (%(len)3d) [%(state)s]: %(Subject)s'
+def iadd(ui, repo, id = None, comment = 0, **opts):
+    """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
+
+    comment = int(comment)
+
+    # First, make sure issues have a directory
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): os.mkdir(issues_path)
 
-    # def __init__(self):
-    #     pass
+    if id:
+        issue_fn, issue_id = _find_issue(ui, repo, id)
+        if not issue_fn:
+            ui.warn('No such issue\n')
+            return
+        _create_missing_dirs(issues_path, issue_id)
+        mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
+        keys = _order_keys_date(mbox)
+        root = keys[0]
+
+    user = ui.username()
 
-    @staticmethod
-    def create_all_missing_dirs(issues_path, issues):
-        for issue in issues:
-            Artemis.create_missing_dirs(issues_path, issue)
+    default_issue_text  =         "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
+    if not id:
+        default_issue_text +=     "State: %s\n" % default_state
+        default_issue_text +=     "Subject: brief description\n\n"
+    else:
+        subject = mbox[(comment < len(mbox) and keys[comment]) or root]['Subject']
+        if not subject.startswith('Re: '): subject = 'Re: ' + subject
+        default_issue_text +=     "Subject: %s\n\n" % subject
+    default_issue_text +=         "Detailed description."
+
+    # Get properties, and figure out if we need an explicit comment
+    properties = _get_properties(opts['property'])
+    no_comment = id and properties and opts['no_property_comment']
+    message = opts['message']
 
-    @staticmethod
-    def find_issue(ui, repo, id):
-        issues_dir = ui.config('artemis', 'issues',
-                               default=Artemis.default_issues_dir)
-        issues_path = os.path.join(repo.root, issues_dir)
-        if not os.path.exists(issues_path):
-            return False
-
-        issues = glob.glob(os.path.join(issues_path, id + '*'))
-
-        if len(issues) == 0:
-            return False, 0
+    # Create the text
+    if message:
+        if not id:
+            state_str = 'State: %s\n' % default_state
+        else:
+            state_str = ''
+        issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
+                (user, util.datestr(format=date_format), message, state_str)
+    elif not no_comment:
+        issue = ui.edit(default_issue_text, user)
 
-        elif len(issues) > 1:
-            ui.status("Multiple choices:\n")
-            for issue in issues:
-                ui.status('  ', Artemis.get_issue_id(ui, repo, issue), '\n')
+        if issue.strip() == '':
+            ui.warn('Empty issue, ignoring\n')
+            return
+        if issue.strip() == default_issue_text:
+            ui.warn('Unchanged issue text, ignoring\n')
+            return
+    else:
+        # Write down a comment about updated properties
+        properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
 
-            return False, 0
+        issue =     "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
+                     (user, util.datestr(format = date_format), properties_subject)
 
-        else:
-            return issues[0], Artemis.get_issue_id(ui, repo, issues[0])
+    # Create the message
+    msg = mailbox.MaildirMessage(issue)
+    if opts['attach']:
+        outer = _attach_files(msg, opts['attach'])
+    else:
+        outer = msg
 
-    @staticmethod
-    def get_issues_dir(ui):
-        issues_dir = ui.config('artemis',
-                               'issues',
-                               default=Artemis.default_issues_dir)
-
-        return issues_dir
+    # Pick random filename
+    if not id:
+        issue_fn = issues_path
+        while os.path.exists(issue_fn):
+            issue_id = _random_id()
+            issue_fn = os.path.join(issues_path, issue_id)
+        mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
+        keys = _order_keys_date(mbox)
+    # else: issue_fn already set
 
-    @staticmethod
-    def get_issues_path(ui, repo):
-        """gets the full path of the issues directory. returns nothing
-        if the path does not exist.
+    # Add message to the mailbox
+    mbox.lock()
+    if id and comment >= len(mbox):
+        ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
 
-        :Example:
-            issues_path = Artemis.get_issues_path(ui, repo)
-            if not issues_path:
-                # error
-            else
-                # path exists
+    if not id:
+        outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
+    else:
+        root = keys[0]
+        outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
+        outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
+        outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
+    new_bug_path = issue_fn + '/new/' + mbox.add(outer)
+    commands.add(ui, repo, new_bug_path)
 
-        """
-        issues_dir = Artemis.get_issues_dir(ui)
-        issues_path = os.path.join(repo.root, issues_dir)
-
-        if not os.path.exists(issues_path):
-            return
-
-        return issues_path
+    # Fix properties in the root message
+    if properties:
+        root = _find_root_key(mbox)
+        msg = mbox[root]
+        for property, value in properties:
+            if property in msg:
+                msg.replace_header(property, value)
+            else:
+                msg.add_header(property, value)
+        mbox[root] = msg
 
-    @staticmethod
-    def get_all_issues(ui, repo):
-        # Find issues
-        issues_path = Artemis.get_issues_path(ui, repo)
-        if not issues_path:
-            return []
+    mbox.close()
+
+    if opts['commit']:
+        commands.commit(ui, repo, issue_fn)
 
-        issues = glob.glob(os.path.join(issues_path, '*'))
+    # If adding issue, add the new mailbox to the repository
+    if not id:
+        ui.status('Added new issue %s\n' % issue_id)
+    else:
+        _show_mbox(ui, mbox, 0)
+
+def ishow(ui, repo, id, comment = 0, **opts):
+    """Shows issue ID, or possibly its comment COMMENT"""
 
-        Artemis.create_all_missing_dirs(issues_path, issues)
+    comment = int(comment)
+    issue, id = _find_issue(ui, repo, id)
+    if not issue:
+        return ui.warn('No such issue\n')
 
-        return issues
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
+
+    if opts.get('mutt'):
+        return util.system('mutt -R -f %s' % issue)
 
-    @staticmethod
-    def get_all_mboxes(ui, repo):
-        """gets a list of all available mboxes with an added extra
-        property "issue"
+    mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+
+    if opts['all']:
+        ui.write('='*70 + '\n')
+        i = 0
+        keys = _order_keys_date(mbox)
+        for k in keys:
+            _write_message(ui, mbox[k], i, skip = opts['skip'])
+            ui.write('-'*70 + '\n')
+            i += 1
+        return
 
-        :param ui: mercurial ui object
-        :param repo: mercurial repo object
-        :return: list of all available mboxes
-        """
-        issues = Artemis.get_all_issues(ui, repo)
-        if not issues:
-            return []
+    _show_mbox(ui, mbox, comment, skip = opts['skip'])
 
-        mboxes = []
-        for issue in issues:
-            mbox = mailbox.Maildir(issue,
-                                   factory=mailbox.MaildirMessage)
-
-            root = Artemis.find_root_key(mbox)
-            if not root:  # root is None
-                continue
-
-            # add extra property
-            mbox.issue = issue
-            mboxes.append(mbox)
-
-        return mboxes
+    if opts['extract']:
+        attachment_numbers = map(int, opts['extract'])
+        keys = _order_keys_date(mbox)
+        msg = mbox[keys[comment]]
+        counter = 1
+        for part in msg.walk():
+            ctype = part.get_content_type()
+            maintype, subtype = ctype.split('/', 1)
+            if maintype == 'multipart' or ctype == 'text/plain': continue
+            if counter in attachment_numbers:
+                filename = part.get_filename()
+                if not filename:
+                    ext = mimetypes.guess_extension(part.get_content_type()) or ''
+                    filename = 'attachment-%03d%s' % (counter, ext)
+                else:
+                    filename = os.path.basename(filename)
+                fp = open(filename, 'wb')
+                fp.write(part.get_payload(decode = True))
+                fp.close()
+            counter += 1
 
 
-    @staticmethod
-    def get_properties(property_list):
-        return [p.split('=') for p in property_list]
+def _find_issue(ui, repo, id):
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): return False
+
+    issues = glob.glob(os.path.join(issues_path, id + '*'))
 
-    @staticmethod
-    def write_message(ui, message, index=0, skip=None):
-        if index:
-            ui.write("Comment: %d\n" % index)
+    if len(issues) == 0:
+        return False, 0
+    elif len(issues) > 1:
+        ui.status("Multiple choices:\n")
+        for i in issues: ui.status('  ', i[len(issues_path)+1:], '\n')
+        return False, 0
 
-        if ui.verbose:
-            Artemis.show_text(ui, message.as_string().strip(), skip)
-            return
+    return issues[0], issues[0][len(issues_path)+1:]
+
+def _get_properties(property_list):
+    return [p.split('=') for p in property_list]
 
-        if 'From' in message:
-            ui.write('From: %s\n' % message['From'])
-        if 'Date' in message:
-            ui.write('Date: %s\n' % message['Date'])
-        if 'Subject' in message:
-            ui.write('Subject: %s\n' % message['Subject'])
-        if 'State' in message:
-            ui.write('State: %s\n' % message['State'])
-
+def _write_message(ui, message, index = 0, skip = None):
+    if index: ui.write("Comment: %d\n" % index)
+    if ui.verbose:
+        _show_text(ui, message.as_string().strip(), skip)
+    else:
+        if 'From' in message: ui.write('From: %s\n' % message['From'])
+        if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
+        if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
+        if 'State' in message: ui.write('State: %s\n' % message['State'])
         counter = 1
         for part in message.walk():
             ctype = part.get_content_type()
             maintype, subtype = ctype.split('/', 1)
-
-            if maintype == 'multipart':
-                continue
-
+            if maintype == 'multipart': continue
             if ctype == 'text/plain':
                 ui.write('\n')
-                Artemis.show_text(ui, part.get_payload().strip(),
-                                  skip)
-
+                _show_text(ui, part.get_payload().strip(), skip)
             else:
                 filename = part.get_filename()
-                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (
-                    counter, ctype,
-                    Artemis.humanreadable(len(part.get_payload())),
-                    filename) + '\n')
+                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
                 counter += 1
 
-    @staticmethod
-    def show_text(ui, text, skip=None):
-        for line in text.splitlines():
-            if not skip or not line.startswith(skip):
-                ui.write(line + '\n')
-        ui.write('\n')
+def _show_text(ui, text, skip = None):
+    for line in text.splitlines():
+        if not skip or not line.startswith(skip):
+            ui.write(line + '\n')
+    ui.write('\n')
 
-    @staticmethod
-    def show_mbox(ui, mbox, comment, **opts):
-        # Output the issue (or comment)
-        if comment >= len(mbox):
-            comment = 0
-            ui.warn(
-                'Comment out of range, showing the issue itself\n')
+def _show_mbox(ui, mbox, comment, **opts):
+    # Output the issue (or comment)
+    if comment >= len(mbox):
+        comment = 0
+        ui.warn('Comment out of range, showing the issue itself\n')
+    keys = _order_keys_date(mbox)
+    root = keys[0]
+    msg = mbox[keys[comment]]
+    ui.write('='*70 + '\n')
+    if comment:
+        ui.write('Subject: %s\n' % mbox[root]['Subject'])
+        ui.write('State: %s\n' % mbox[root]['State'])
+        ui.write('-'*70 + '\n')
+    _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
+    ui.write('-'*70 + '\n')
 
-        keys = Artemis.order_keys_date(mbox)
-        root = keys[0]
-        msg = mbox[keys[comment]]
-        ui.write('=' * 70 + '\n')
-
-        if comment:
-            ui.write('Subject: %s\n' % mbox[root]['Subject'])
-            ui.write('State: %s\n' % mbox[root]['State'])
-            ui.write('-' * 70 + '\n')
+    # Read the mailbox into the messages and children dictionaries
+    messages = {}
+    children = {}
+    i = 0
+    for k in keys:
+        m = mbox[k]
+        messages[m['Message-Id']] = (i,m)
+        children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
+        i += 1
+    children[None] = []                # Safeguard against infinte loop on empty Message-Id
 
-        Artemis.write_message(ui, msg, comment,
-                              skip=('skip' in opts) and opts['skip'])
-        ui.write('-' * 70 + '\n')
+    # Iterate over children
+    id = msg['Message-Id']
+    id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
+    if not id_stack: return
+    ui.write('Comments:\n')
+    while id_stack:
+        id,offset = id_stack.pop()
+        id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
+        index, msg = messages[id]
+        ui.write('  '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
+    ui.write('-'*70 + '\n')
+
+def _find_root_key(maildir):
+    for k,m in maildir.iteritems():
+        if 'in-reply-to' not in m:
+            return k
+
+def _order_keys_date(mbox):
+    keys = mbox.keys()
+    root = _find_root_key(mbox)
+    keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
+    return keys
 
-        # Read the mailbox into the messages and children dictionaries
-        messages = {}
-        children = {}
-        i = 0
-        for k in keys:
-            m = mbox[k]
-            messages[m['Message-Id']] = (i, m)
-            children.setdefault(m['In-Reply-To'], []).append(
-                m['Message-Id'])
-            i += 1
+def _find_mbox_date(mbox, root, order):
+    if order == 'latest':
+        keys = _order_keys_date(mbox)
+        msg = mbox[keys[-1]]
+    else:   # new
+        msg = mbox[root]
+    return util.parsedate(msg['date'])
+
+def _random_id():
+    return "%x" % random.randint(2**63, 2**64-1)
 
-        # Safeguard against infinte loop on empty Message-Id
-        children[
-            None] = []
+def _create_missing_dirs(issues_path, issue):
+    for d in maildir_dirs:
+        path = os.path.join(issues_path,issue,d)
+        if not os.path.exists(path): os.mkdir(path)
 
-        # Iterate over children
-        id = msg['Message-Id']
-        id_stack = (id in children and
-                    map(lambda x: (x, 1),
-                        reversed(children[id]))) or []
-        if not id_stack:
-            return
+def _create_all_missing_dirs(issues_path, issues):
+    for i in issues:
+        _create_missing_dirs(issues_path, i)
+
+def _humanreadable(size):
+    if size > 1024*1024:
+        return '%5.1fM' % (float(size) / (1024*1024))
+    elif size > 1024:
+        return '%5.1fK' % (float(size) / 1024)
+    else:
+        return '%dB' % size
 
-        ui.write('Comments:\n')
-        while id_stack:
-            id, offset = id_stack.pop()
-            id_stack += (id in children and
-                         map(lambda x: (x, offset + 1),
-                             reversed(children[id]))
-                         ) or []
-
-            index, msg = messages[id]
-            ui.write('  ' * offset +
-                     '%d: [%s] %s\n' %
-                     (index,
-                      util.shortuser(msg['From']), msg['Subject']))
+def _attach_files(msg, filenames):
+    outer = MIMEMultipart()
+    for k in msg.keys(): outer[k] = msg[k]
+    outer.attach(MIMEText(msg.get_payload()))
 
-        ui.write('-' * 70 + '\n')
-
-    @staticmethod
-    def find_root_key(maildir):
-        for k, m in maildir.iteritems():
-            if 'in-reply-to' not in m:
-                return k
+    for filename in filenames:
+        ctype, encoding = mimetypes.guess_type(filename)
+        if ctype is None or encoding is not None:
+            # No guess could be made, or the file is encoded (compressed), so
+            # use a generic bag-of-bits type.
+            ctype = 'application/octet-stream'
+        maintype, subtype = ctype.split('/', 1)
+        if maintype == 'text':
+            fp = open(filename)
+            # Note: we should handle calculating the charset
+            attachment = MIMEText(fp.read(), _subtype=subtype)
+            fp.close()
+        elif maintype == 'image':
+            fp = open(filename, 'rb')
+            attachment = MIMEImage(fp.read(), _subtype=subtype)
+            fp.close()
+        elif maintype == 'audio':
+            fp = open(filename, 'rb')
+            attachment = MIMEAudio(fp.read(), _subtype=subtype)
+            fp.close()
+        else:
+            fp = open(filename, 'rb')
+            attachment = MIMEBase(maintype, subtype)
+            attachment.set_payload(fp.read())
+            fp.close()
+            # Encode the payload using Base64
+            encoders.encode_base64(attachment)
+        # Set the filename parameter
+        attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(filename))
+        outer.attach(attachment)
+    return outer
 
-    @staticmethod
-    def order_keys_date(mbox):
-        keys = mbox.keys()
-        root = Artemis.find_root_key(mbox)
-        keys.sort(lambda k1, k2: -(k1 == root) or
-                                 cmp(util.parsedate(mbox[k1]['date']),
-                                     util.parsedate(
-                                         mbox[k2]['date'])))
+def _read_formats(ui):
+    formats = []
+    global default_format
 
-        return keys
+    for k,v in ui.configitems('artemis'):
+        if not k.startswith('format'): continue
+        if k == 'format':
+            default_format = v
+            continue
+        formats.append((k.split(':')[1], v))
+
+    return formats
 
-    @staticmethod
-    def create_missing_dirs(issues_path, issue):
-        for dir in Artemis.maildir_dirs:
-            path = os.path.join(issues_path, issue, dir)
-            if not os.path.exists(path):
-                os.mkdir(path)
+def _format_match(props, formats):
+    for k,v in formats:
+        eq = k.split('&')
+        eq = [e.split('*') for e in eq]
+        for e in eq:
+            if props[e[0]] != e[1]:
+                break
+        else:
+            return v
 
-    @staticmethod
-    def humanreadable(size):
-        if size > 1024 * 1024:
-            return '%5.1fM' % (float(size) / (1024 * 1024))
+    return default_format
 
-        elif size > 1024:
-            return '%5.1fK' % (float(size) / 1024)
+def _summary_line(mbox, root, issue, formats):
+    props = PropertiesDictionary(mbox[root])
+    props['id']  = issue
+    props['len'] = len(mbox)-1              # number of replies (-1 for self)
 
-        else:
-            return '%dB' % size
+    return _format_match(props, formats) % props
 
-    @staticmethod
-    def get_issue_id(ui, repo, issue):
-        """get the issue id provided the issue"""
-        # Find issues
-        issues_dir = ui.config('artemis', 'issues',
-                               default=Artemis.default_issues_dir)
-        issues_path = os.path.join(repo.root, issues_dir)
-        if not os.path.exists(issues_path):
-            return ""
+class PropertiesDictionary(dict):
+    def __init__(self, msg):
+        # Borrowed from termcolor
+        for k,v in zip(['bold', 'dark', '', 'underline', 'blink', '', 'reverse', 'concealed'], range(1, 9)) + \
+                   zip(['grey', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'], range(30, 38)):
+            self[k] = '\033[' + str(v) + 'm'
+        self['reset']  = '\033[0m'
+        del self['']
 
-        # +1 for trailing /
-        return issue[len(issues_path) + 1:]
+        for k,v in msg.items():
+            self[k] = v
+
+    def __contains__(self, k):
+        return super(PropertiesDictionary, self).__contains__(k.lower())
+
+    def __getitem__(self, k):
+        if k not in self: return ''
+        return super(PropertiesDictionary, self).__getitem__(k.lower())
+
+    def __setitem__(self, k, v):
+        super(PropertiesDictionary, self).__setitem__(k.lower(), v)
 
 
+cmdtable = {
+    'ilist':    (ilist,
+                 [('a', 'all', False,
+                   'list all issues (by default only those with state new)'),
+                  ('p', 'property', [],
+                   'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
+                  ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
+                  ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
+                  ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
+                 _('hg ilist [OPTIONS]')),
+    'iadd':       (iadd,
+                 [('a', 'attach', [],
+                   'attach file(s) (e.g., -a filename1 -a filename2)'),
+                  ('p', 'property', [],
+                   'update properties (e.g., -p state=fixed)'),
+                  ('n', 'no-property-comment', None,
+                   'do not add a comment about changed properties'),
+                  ('m', 'message', '',
+                   'use <text> as an issue subject'),
+                  ('c', 'commit', False,
+                   'perform a commit after the addition')],
+                 _('hg iadd [OPTIONS] [ID] [COMMENT]')),
+    'ishow':      (ishow,
+                 [('a', 'all', None, 'list all comments'),
+                  ('s', 'skip', '>', 'skip lines starting with a substring'),
+                  ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
+                  ('', 'mutt', False, 'use mutt to show issue')],
+                 _('hg ishow [OPTIONS] ID [COMMENT]')),
+}
+
 # vim: expandtab
--- a/artemis/find.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-                                   ..  ┌  -:  .
-                                  ([]▄▄├]▄▄▄¿┐`,
-    .¡                      ,. D{}▓███████████░█∩^}                       =
-    ,▓¼                      .▐▓▓█████████████▓██▓▓,                     (▓
-     █▌╕.                   ─@╣████████████████████▄─                   ./▀'
-    :▓█▌Ç...             └ '└▓██████████████████████Ö'`            ..  '└`'┌
-     \ g█████▄Ü┌        C g██████████████████████████╗─  ²   ²   t▄▓██ù- `╞
-      ¡ Å╠█▌╠█▓▓▓▓▌N@▌|   ╓██████████████████████████▌  .╣▓▓▓▓▓▓▓]▐╙ÅÅ∩!
-      ` ` ╫½┌▀▀▀▀▀▀░╫▌┘    ▀▀▀▀▀██████████████████▀█▀C  :╝▀▀▀▀▀▀▀.   `:
-          L ─ⁿ'``' ─       ''' ─█████████████████▌─█=       ╞º'`'º──
-                                    ..Å╫█╛.             ~
-                                   ╔▄▄▓██▓▄¿        .
-                            -   ]:├╣██▀▀▀▀▀▒Q¿.¿{  )=
-                            <─Ω╚█░▓███'──h⌂▓██╡██▌Ü─4
-                           `┌ ▐▓█▄▓██╟≤¡yQ▄███▓█▌▐▓M
-                           ^. └▀▀▄▓██▄▒▄▄╬▄████▀ └▀╙`
-                            «-'''█▓▓█▓██▓██▓▓██└``'⌐
-                                 #╛ µ:▓▄;xφ(⌠ █
-                                 ╗µ  ~╙'`   `┌█╣
-                                 ╝Ñ. ''  '   L▀▀
-                                    ~         ,
-                                    ^
-                                    ÷        ─.
-                                    N.   ∩ts  )
-                                 ∩    ²⌠  `   ─
-                                   (@   ~▒▒  .
-                                 ` └▀/ .>▀▀  :
-                                     ⌐   ,⌐
-
-
-
- Artemis
- Version 0.5.1
- Copyright (c) 2016 Frostbane Ac
- Apache-2.0, APL-1.0, 0BSD Licensed. ?
- www.??.com
- ?? inspired script.
-
-"""
-
-from mercurial import hg, util, commands, cmdutil
-import sys, os, time, random, mailbox, glob, socket, ConfigParser, re
-from properties import ArtemisProperties
-from artemis import Artemis
-
-__author__ = 'frostbane'
-__date__ = '2016/05/24'
-
-
-class ArtemisFind:
-    commands = [
-        # dashed options will be resolved to underscores
-        # case-sensitive => case_sensitive
-        ('p', 'property', "subject", 'Issue property to match. '
-                                     '[state, from, subject, date, '
-                                     'priority, resolution, etc..].'
-                                     '(e.g. hg isearch -p from me)'),
-        ('n', 'no-property', None, 'Do not match the property. Use '
-                                   'with --message to search only '
-                                   'the message. The --property will '
-                                   'be ignored.'),
-        ('m', 'message', None, 'Search the message. If no match is '
-                               'found it will then search the '
-                               'property for a match. Use '
-                               '--no-property or use a blank '
-                               'property to ignore the property '
-                               'search.'
-                               '(e.g. hg isearch -mp "" "the bug")'),
-        ('c', 'case-sensitive', None, 'Case sensitive search.'),
-        ('r', 'regex', None, 'Use regular expressions. '
-                             'Exact option will be ignored.'
-                             '(e.g. hg isearch -rmn "todo *(:)?"'),
-        ('e', 'exact', None, 'Use exact comparison. '
-                             'Like comparison is used if exact is'
-                             'uspecified.'),
-    ]
-    usage = 'hg ifind [OPTIONS] QUERY'
-
-    ui = None
-    repo = None
-    opts = []
-
-    def __init__(self):
-        pass
-
-    def __is_hit(self, query, search_string):
-        opts = self.opts
-
-        exact_comp = opts["exact"] and not opts["regex"]
-        regexp_comp = opts["regex"]
-
-        if regexp_comp:
-            re_pattern = re.compile(query)
-            return re_pattern.search(search_string)
-        elif exact_comp:
-            return query == search_string
-        else:
-            return query in search_string
-
-    def __get_payload(self, mbox, key):
-        # todo move to Artemis class since this is static, and can
-        #      be possible reused in other classes
-        payload = ""
-
-        for part in mbox[key].walk():
-            ctype = part.get_content_type()
-            maintype, subtype = ctype.split('/', 1)
-
-            if maintype == 'multipart':
-                continue
-
-            if ctype == 'text/plain':
-                payload = part.get_payload().strip()
-
-        return payload
-
-    def __search_payload(self, mbox, query, case_sens):
-        keys = mbox.keys()
-        for key in keys:
-            payload = self.__get_payload(mbox, key)
-
-            if not payload:
-                continue
-
-            if not case_sens:
-                payload = payload.lower()
-
-            if self.__is_hit(query, payload):
-                return True
-
-        return False
-
-    def __search_property(self, mbox, query, case_sens):
-        query_filter = self.opts["property"]
-
-        root = Artemis.find_root_key(mbox)
-        search_string = mbox[root][query_filter]
-
-        # non existing property
-        if not search_string:
-            return False
-
-        if not case_sens:
-            search_string = search_string.lower()
-
-        if self.__is_hit(query, search_string):
-            return True
-
-        return False
-
-    def __search_issues(self, query):
-        case_sens = self.opts["case_sensitive"]
-        search_payload = self.opts["message"]
-        no_property = self.opts["no_property"]
-
-        if not case_sens:
-            query = query.lower()
-
-        hits = []
-
-        mboxes = Artemis.get_all_mboxes(self.ui, self.repo)
-        for mbox in mboxes:
-            has_hit = False
-
-            if search_payload:
-                has_hit = self.__search_payload(mbox,
-                                                query,
-                                                case_sens)
-
-            if has_hit:
-                hits.append(mbox.issue)
-                # if the message has a hit there is no need to
-                # continue searching for a hit with the property
-                continue
-
-            if no_property:
-                # ignore the property search
-                continue
-
-            if self.__search_property(mbox, query, case_sens):
-                hits.append(mbox.issue)
-
-        return hits
-
-    def __show_results(self, issues):
-        ui = self.ui
-        repo = self.repo
-
-        for issue in issues:
-            mbox = mailbox.Maildir(issue,
-                                   factory=mailbox.MaildirMessage)
-            root = Artemis.find_root_key(mbox)
-            # print mbox.items()
-            # print mbox.keys()
-            # print mbox.values()
-
-            num_replies = str(len(mbox.keys()) - 1)
-            # print mbox[root]["message-id"]
-            ui.write("%s (%s) [%s]: %s\n" %
-                     (
-                         Artemis.get_issue_id(ui, repo, issue),
-                         num_replies.rjust(3, " "),
-                         mbox[root]["state"],
-                         mbox[root]["subject"])
-                     )
-
-    def find(self, ui, repo, query, **opts):
-        """Shows a list of issues matching the specified QUERY"""
-        self.opts = opts
-        self.ui = ui
-        self.repo = repo
-
-        issues = self.__search_issues(query)
-        self.__show_results(issues)
--- a/artemis/list.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from mercurial import hg, util, commands
-import sys, os, time, random, mailbox, glob, socket, ConfigParser
-from properties import ArtemisProperties
-from artemis import Artemis
-
-__author__ = 'frostbane'
-__date__ = '2016/03/04'
-
-
-class ArtemisList:
-    commands = [
-        ('a', 'all', False, 'list all issues (by default only those '
-                            'with state new)'),
-        ('p', 'property', [], 'list issues with specific field '
-                              'values (e.g., -p state=fixed); '
-                              'lists all possible values of a '
-                              'property if no = sign'),
-        ("", "all-properties", None, "list all available properties"),
-        ('o', 'order', 'new', 'order of the issues; '
-                              'choices: "new" (date submitted), '
-                              '"latest" (date of the last message)'),
-        ('d', 'date', '', 'restrict to issues matching the date '
-                          '(e.g., -d ">12/28/2007)"'),
-        ('f', 'filter', '', 'restrict to pre-defined filter '
-                            '(in %s/%s*)'
-                            '' % (Artemis.default_issues_dir,
-                                  Artemis.filter_prefix))
-    ]
-    usage = 'hg ilist [OPTIONS]'
-
-    def __init__(self):
-        pass
-
-    def __get_properties(self, ui, repo):
-        """get the list of all available properties
-        :param ui: mercurial ui object
-        :param repo: mercurial repo object
-        :return: returns a list of all available properties
-        """
-
-        properties = []
-
-        mboxes = Artemis.get_all_mboxes(ui, repo)
-        for mbox in mboxes:
-            root = Artemis.find_root_key(mbox)
-            properties = list(set(properties + mbox[root].keys()))
-
-        return properties
-
-    def __read_formats(self, ui):
-        formats = []
-
-        for key, value in ui.configitems('artemis'):
-            if not key.startswith('format'):
-                continue
-            if key == 'format':
-                Artemis.default_format = value
-                continue
-            formats.append((key.split(':')[1], value))
-
-        return formats
-
-    def __format_match(self, props, formats):
-        for key, value in formats:
-            eq = key.split('&')
-            eq = [e.split('*') for e in eq]
-
-            for e in eq:
-                # todo check if else
-                if props[e[0]] != e[1]:
-                    break
-            else:
-                return value
-
-        return Artemis.default_format
-
-    def __summary_line(self, mbox, root, issue, formats):
-        props = ArtemisProperties(mbox[root])
-        props['id'] = issue
-        # number of replies (-1 for self)
-        props['len'] = len(mbox) - 1
-
-        return self.__format_match(props, formats) % props
-
-    def __find_mbox_date(self, mbox, root, order):
-        if order == 'latest':
-            keys = Artemis.order_keys_date(mbox)
-            msg = mbox[keys[-1]]
-
-        else:  # new
-            msg = mbox[root]
-
-        return util.parsedate(msg['date'])
-
-    def __find_issues(self, ui, repo):
-        issues_path = Artemis.get_issues_path(ui, repo)
-        if not issues_path:
-            return
-
-        issues = Artemis.get_all_issues(ui, repo)
-
-        return issues
-
-    def __proc_filters(self, properties, ui, repo, opts):
-        if opts['filter']:
-            filters = glob.glob(
-                os.path.join(Artemis.get_issues_path(ui, repo),
-                             Artemis.filter_prefix + '*'))
-            config = ConfigParser.SafeConfigParser()
-            config.read(filters)
-            if not config.has_section(opts['filter']):
-                ui.write('No filter %s defined\n' % opts['filter'])
-            else:
-                properties += config.items(opts['filter'])
-
-        return properties
-
-    def __list_summaries(self, issues, properties, ui, repo, opts):
-        # Process options
-        show_all = opts['all']
-        match_date, date_match = False, lambda x: True
-        if opts['date']:
-            match_date, date_match = True, util.matchdate(
-                opts['date'])
-        order = 'new'
-        if opts['order']:
-            order = opts['order']
-
-        # Formats
-        formats = self.__read_formats(ui)
-
-        cmd_properties = Artemis.get_properties(opts['property'])
-        list_properties = [p[0] for p in cmd_properties if
-                           len(p) == 1]
-        list_properties_dict = {}
-        properties += filter(lambda p: len(p) > 1, cmd_properties)
-
-        summaries = []
-        for issue in issues:
-            mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-            root = Artemis.find_root_key(mbox)
-            if not root:
-                continue
-
-            property_match = True
-            for property, value in properties:
-                if value:
-                    property_match = property_match and (
-                        mbox[root][property] == value)
-                else:
-                    property_match = property_match and (
-                        property not in mbox[root])
-
-            has_property = properties or \
-                           mbox[root]['State'].upper() in \
-                           [f.upper() for f in Artemis.state['resolved']]
-
-            if not show_all and (not properties or
-                        not property_match) and has_property:
-                continue
-
-            if match_date and not date_match(
-                    util.parsedate(mbox[root]['date'])[0]):
-                continue
-
-            if not list_properties:
-                mbox_date = self.__find_mbox_date(mbox, root, order)
-                sum_line = self.__summary_line(
-                    mbox,
-                    root,
-                    Artemis.get_issue_id(ui, repo, issue),
-                    formats)
-
-                summaries.append((sum_line, mbox_date))
-
-            else:
-                for lp in list_properties:
-                    if lp in mbox[root]:
-                        list_properties_dict\
-                            .setdefault(lp, set())\
-                            .add(mbox[root][lp])
-
-        if not list_properties:
-            summaries.sort(lambda (s1, d1), (s2, d2): cmp(d2, d1))
-            for s, d in summaries:
-                ui.write(s + '\n')
-
-        else:
-            for lp in list_properties_dict.keys():
-                ui.write("%s:\n" % lp)
-                for value in sorted(list_properties_dict[lp]):
-                    ui.write("  %s\n" % value)
-
-    def list(self, ui, repo, **opts):
-        """List issues associated with the project"""
-
-        # Find issues
-        issues = self.__find_issues(ui, repo)
-        if not issues:
-            return
-
-        properties = []
-        if opts["all_properties"]:
-            properties = self.__get_properties(ui, repo)
-            for property in properties:
-                ui.write("%s\n" % property)
-
-            return
-
-        # Process filter
-        properties = self.__proc_filters(properties, ui, repo, opts)
-
-        self.__list_summaries(issues, properties, ui, repo, opts)
-
-
--- a/artemis/main.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from mercurial import hg, util, commands, cmdutil
-from mercurial.i18n import _
-import sys, os, time, random, mailbox, glob, socket, ConfigParser
-import mimetypes
-from email import encoders
-from email.generator import Generator
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from itertools import izip
-from artemis import Artemis
-from list import ArtemisList
-from add import ArtemisAdd
-from show import ArtemisShow
-from find import ArtemisFind
-
-__author__ = 'frostbane'
-__date__ = '2016/03/02'
-
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-@command('ifind', ArtemisFind.commands, ArtemisFind.usage)
-def find(ui, repo, id=None, **opts):
-    '''find issues'''
-    return ArtemisFind().find(ui, repo, id, **opts)
-
-@command('ishow', ArtemisShow.commands, ArtemisShow.usage)
-def show(ui, repo, id=None, **opts):
-    '''show issue details'''
-    return ArtemisShow().show(ui, repo, id, **opts)
-
-@command('ilist', ArtemisList.commands, ArtemisList.usage)
-def list(ui, repo, id=None, **opts):
-    '''list issues'''
-    return ArtemisList().list(ui, repo, **opts)
-
-@command('iadd', ArtemisAdd.commands, ArtemisAdd.usage)
-def add(ui, repo, id=None, **opts):
-    '''add / edit issues'''
-    return ArtemisAdd().add(ui, repo, id, **opts)
-
-if __name__ == "__main__":
-    pass
--- a/artemis/properties.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-__author__ = 'frostbane'
-__date__ = '2016/03/03'
-
-class ArtemisProperties(dict):
-    def __init__(self, msg):
-        # Borrowed from termcolor
-        for k, v in zip(['bold', 'dark', '', 'underline', 'blink', '',
-                         'reverse', 'concealed'], range(1, 9)) + \
-                zip(['grey', 'red', 'green', 'yellow', 'blue',
-                     'magenta', 'cyan', 'white'], range(30, 38)):
-            self[k] = '\033[' + str(v) + 'm'
-        self['reset'] = '\033[0m'
-        del self['']
-
-        for k, v in msg.items():
-            self[k] = v
-
-    def __contains__(self, k):
-        return super(ArtemisProperties, self).__contains__(
-            k.lower())
-
-    def __getitem__(self, k):
-        if k not in self:
-            return ''
-        return super(ArtemisProperties, self).__getitem__(
-            k.lower())
-
-    def __setitem__(self, k, v):
-        super(ArtemisProperties, self).__setitem__(k.lower(), v)
--- a/artemis/show.py	Wed Mar 15 13:12:28 2017 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from mercurial import hg, util, commands
-from mercurial.i18n import _
-import sys, os, time, random, mailbox, glob, socket, ConfigParser
-import mimetypes
-from email import encoders
-from email.generator import Generator
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from itertools import izip
-from artemis import Artemis
-
-__author__ = 'frostbane'
-__date__ = '2016/03/04'
-
-
-class ArtemisShow:
-    commands = [
-        ('a', 'all', None, 'List all comments.'),
-        ('s', 'skip', '>', 'Skip lines starting with a substring.'),
-        ('x', 'extract', [], 'Extract attachment(s) (provide '
-                             'attachment number as argument). If the '
-                             '"message" option is not specified the '
-                             'attachment of the first message (the '
-                             'issue itself) will be extracted. File '
-                             'will be overwritten if it is already '
-                             'existing. '
-                             '(e.g. -x 1 -x 2 -m 1 -o tmp)'),
-        ('i', 'index', 0, 'Message number to be shown (0 based '
-                          'index, 0 is the issue and 1 onwards will '
-                          'be the rest of the replies). If "extract" '
-                          'option is set then the attachment of the '
-                          'message will be extracted. This option is '
-                          'ignored if the "all" option is specified.'),
-        ('o', 'output', '"./tmp"', 'Extract output directory '
-                                   '(e.g. -o "./files/attachments")'),
-        ('', 'mutt', False, 'Use mutt to show the issue.')
-    ]
-    usage = 'hg ishow [OPTIONS] ID'
-
-    def __init__(self):
-        pass
-
-    def show(self, ui, repo, id, **opts):
-        """Shows issue ID, or possibly its comment COMMENT"""
-
-        issue, id = Artemis.find_issue(ui, repo, id)
-        if not issue:
-            return ui.warn('No such issue\n')
-
-        issues_dir = ui.config('artemis', 'issues',
-                               default=Artemis.default_issues_dir)
-        Artemis.create_missing_dirs(
-                os.path.join(repo.root, issues_dir),
-                issue)
-
-        if opts.get('mutt'):
-            return util.system('mutt -R -f %s' % issue)
-
-        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-
-        # show the first mail
-        ui.write('=' * 70 + '\n')
-        keys = Artemis.order_keys_date(mbox)
-        Artemis.write_message(ui, mbox[keys[0]], 0, skip=opts['skip'])
-
-        if opts['all']:
-            ui.write('=' * 70 + '\n')
-            i = 0
-            for k in keys:
-                if (i > 0):
-                    Artemis.write_message(ui, mbox[k], i,
-                                          skip=opts['skip'])
-                    ui.write('-' * 70 + '\n')
-                i += 1
-            return
-
-        elif int(opts["index"]) > 0 and len(keys) > int(
-                opts["index"]):
-            # todo comments replied to this comment should also be shown
-            reply_num = int(opts["index"])
-            ui.write('-' * 70 + '\n')
-            Artemis.write_message(
-                    ui, mbox[keys[reply_num]], reply_num,
-                    skip=opts['skip'])
-            ui.write('=' * 70 + '\n')
-
-        if opts['extract']:
-            attachment_numbers = map(int, opts['extract'])
-            msg = mbox[keys[opts["index"]]]
-
-            counter = 1
-            for part in msg.walk():
-                ctype = part.get_content_type()
-                maintype, subtype = ctype.split('/', 1)
-
-                if maintype == 'multipart' or ctype == 'text/plain':
-                    continue
-
-                if counter in attachment_numbers:
-
-                    filename = part.get_filename()
-                    if not filename:
-                        ext = mimetypes.guess_extension(
-                                part.get_content_type()) or ''
-                        filename = 'attachment-%03d%s' % (
-                            counter, ext)
-
-                    else:
-                        filename = os.path.basename(filename)
-
-                    dirname = opts["output"]
-                    if not os.path.exists(dirname):
-                        os.makedirs(dirname)
-
-                    pathFileName = os.path.join(dirname, filename)
-
-                    fp = open(pathFileName, 'wb')
-                    fp.write(part.get_payload(decode=True))
-                    fp.close()
-
-                counter += 1