--- 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