refactor into separate classes
add --index and --output option to ishow
add --index option to iadd
--- a/.gitignore Tue Mar 01 09:05:26 2016 +0900
+++ b/.gitignore Fri Mar 04 08:15:26 2016 +0900
@@ -2,4 +2,7 @@
artemis.egg-info/
build/
dist/
-
+.idea/*
+*.pyo
+*.pyc
+pycharm-debug.egg
--- a/.hgignore Tue Mar 01 09:05:26 2016 +0900
+++ b/.hgignore Fri Mar 04 08:15:26 2016 +0900
@@ -3,3 +3,9 @@
artemis.egg-info/
build/
dist/
+.idea/*
+*.pyo
+*.pyc
+pycharm-debug.egg
+syntax: glob
+artemis.iml
--- a/artemis/__init__.py Tue Mar 01 09:05:26 2016 +0900
+++ b/artemis/__init__.py Fri Mar 04 08:15:26 2016 +0900
@@ -1,3 +1,3 @@
-from artemis import *
+from main import *
-__version__ = '0.5.0'
+__version__ = '0.5.1'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/add.py Fri Mar 04 08:15:26 2016 +0900
@@ -0,0 +1,224 @@
+#!/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 Tue Mar 01 09:05:26 2016 +0900
+++ b/artemis/artemis.py Fri Mar 04 08:15:26 2016 +0900
@@ -1,10 +1,14 @@
+#!/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 os, time, random, mailbox, glob, socket, ConfigParser
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
import mimetypes
from email import encoders
from email.generator import Generator
@@ -14,500 +18,299 @@
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from itertools import izip
+from properties import ArtemisProperties
-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 singleton(cls):
+ instances = {}
- # 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)
+ def getinstance():
+ if cls not in instances:
+ instances[cls] = 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 instances[cls]
- 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)
+ return getinstance
-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)
+# @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'
- 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()
+ # def __init__(self):
+ # pass
- 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 create_all_missing_dirs(issues_path, issues):
+ for issue in issues:
+ Artemis.create_missing_dirs(issues_path, issue)
- # 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)
+ @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
- 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])
+ elif len(issues) > 1:
+ ui.status("Multiple choices:\n")
+ for issue in issues:
+ ui.status(' ', Artemis.get_issue_id(ui, repo, issue), '\n')
- issue = "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
- (user, util.datestr(format = date_format), properties_subject)
+ return False, 0
- # Create the message
- msg = mailbox.MaildirMessage(issue)
- if opts['attach']:
- outer = _attach_files(msg, opts['attach'])
- else:
- outer = msg
+ else:
+ return issues[0], Artemis.get_issue_id(ui, repo, issues[0])
- # 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_dir(ui):
+ issues_dir = ui.config('artemis',
+ 'issues',
+ default=Artemis.default_issues_dir)
+
+ return issues_dir
- # 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')
+ @staticmethod
+ def get_issues_path(ui, repo):
+ """gets the full path of the issues directory. returns nothing
+ if the path does not exist.
- 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)
+ :Example:
+ issues_path = Artemis.get_issues_path(ui, repo)
+ if not issues_path:
+ # error
+ else
+ # path exists
- # 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
+ """
+ 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
- mbox.close()
-
- if opts['commit']:
- commands.commit(ui, repo, issue_fn)
+ @staticmethod
+ def get_all_issues(ui, repo):
+ # Find issues
+ issues_path = Artemis.get_issues_path(ui, repo)
+ if not issues_path:
+ return []
- # 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"""
+ issues = glob.glob(os.path.join(issues_path, '*'))
- comment = int(comment)
- issue, id = _find_issue(ui, repo, id)
- if not issue:
- return ui.warn('No such issue\n')
+ Artemis.create_all_missing_dirs(issues_path, 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)
+ return issues
- 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
+ @staticmethod
+ def get_all_mboxes(ui, repo):
+ """gets a list of all available mboxes with an added extra
+ property "issue"
- _show_mbox(ui, mbox, comment, skip = opts['skip'])
+ :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 []
- 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
+ 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
-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 get_properties(property_list):
+ return [p.split('=') for p in property_list]
- 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
+ @staticmethod
+ def write_message(ui, message, index=0, skip=None):
+ if index:
+ ui.write("Comment: %d\n" % index)
- return issues[0], issues[0][len(issues_path)+1:]
-
-def _get_properties(property_list):
- return [p.split('=') for p in property_list]
+ if ui.verbose:
+ Artemis.show_text(ui, message.as_string().strip(), skip)
+ return
-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'])
+ 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')
- _show_text(ui, part.get_payload().strip(), skip)
+ Artemis.show_text(ui, part.get_payload().strip(),
+ skip)
+
else:
filename = part.get_filename()
- ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
+ ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (
+ counter, ctype,
+ Artemis.humanreadable(len(part.get_payload())),
+ filename) + '\n')
counter += 1
-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_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_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')
+ @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')
- # 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
+ 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')
- # 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
+ Artemis.write_message(ui, msg, comment,
+ skip=('skip' in opts) and opts['skip'])
+ ui.write('-' * 70 + '\n')
-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)
+ # 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 _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)
+ # Safeguard against infinte loop on empty Message-Id
+ children[
+ None] = []
-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
+ # 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 _attach_files(msg, filenames):
- outer = MIMEMultipart()
- for k in msg.keys(): outer[k] = msg[k]
- outer.attach(MIMEText(msg.get_payload()))
+ 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']))
- 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
+ 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
-def _read_formats(ui):
- formats = []
- global default_format
+ @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'])))
- 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
+ return keys
-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 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)
- return default_format
+ @staticmethod
+ def humanreadable(size):
+ if size > 1024 * 1024:
+ return '%5.1fM' % (float(size) / (1024 * 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)
+ elif size > 1024:
+ return '%5.1fK' % (float(size) / 1024)
- return _format_match(props, formats) % props
+ else:
+ return '%dB' % size
-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['']
+ @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 ""
- 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)
+ # +1 for trailing /
+ return issue[len(issues_path) + 1:]
-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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/list.py Fri Mar 04 08:15:26 2016 +0900
@@ -0,0 +1,219 @@
+#!/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)
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/main.py Fri Mar 04 08:15:26 2016 +0900
@@ -0,0 +1,38 @@
+#!/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
+from list import ArtemisList
+from add import ArtemisAdd
+from show import ArtemisShow
+
+__author__ = 'frostbane'
+__date__ = '2016/03/02'
+
+
+cmdtable = {
+ 'ilist' : (ArtemisList().list,
+ ArtemisList.commands,
+ _(ArtemisList.usage)),
+ 'iadd' : (ArtemisAdd().add,
+ ArtemisAdd.commands,
+ _(ArtemisAdd.usage)),
+ 'ishow' : (ArtemisShow().show,
+ ArtemisShow.commands,
+ _(ArtemisShow.usage)),
+}
+
+if __name__ == "__main__":
+ pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/properties.py Fri Mar 04 08:15:26 2016 +0900
@@ -0,0 +1,32 @@
+#!/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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/show.py Fri Mar 04 08:15:26 2016 +0900
@@ -0,0 +1,127 @@
+#!/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