--- a/artemis.py Thu Dec 27 13:14:58 2007 -0500
+++ b/artemis.py Sat Dec 29 02:50:00 2007 -0500
@@ -1,24 +1,60 @@
-#!/usr/bin/env python
+# Author: Dmitriy Morozov <hg@foxcub.org>, 2007
+
+"""A very simple and lightweight issue tracker for Mercurial."""
+
+from mercurial import hg, util
+from mercurial.i18n import _
+import os, time, random, mailbox, glob, socket
-from mercurial import hg
-from mercurial.i18n import _
-import os, time, random
+
+new_state = "new"
+default_state = new_state
+issues_dir = ".issues"
+filter_filename = ".filters"
+date_format = '%a, %d %b %Y %H:%M:%S %Z'
+
def list(ui, repo, **opts):
"""List issues associated with the project"""
+ # Process options
+ show_all = False or opts['all']
+ properties = _get_properties(opts['property']) or [['state', new_state]]
+ date_match = lambda x: True
+ if opts['date']:
+ date_match = util.matchdate(opts['date'])
+
+ # Find issues
+ 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, '*'))
+
+ for issue in issues:
+ mbox = mailbox.mbox(issue)
+ property_match = True
+ for property,value in properties:
+ property_match = property_match and (mbox[0][property] == value)
+ if not show_all and not property_match: continue
+ if not date_match(util.parsedate(mbox[0]['date'], [date_format])[0]): continue
+ print "%s [%s]: %s (%d)" % (issue[len(issues_path)+1:], # +1 for trailing /
+ mbox[0]['State'],
+ mbox[0]['Subject'],
+ len(mbox)-1) # number of replies (-1 for self)
+
+
def add(ui, repo):
"""Adds a new issue"""
# First, make sure issues have a directory
- issues_path = os.path.join(repo.root, '.issues')
+ issues_path = os.path.join(repo.root, issues_dir)
if not os.path.exists(issues_path): os.mkdir(issues_path)
user = ui.username()
- default_issue_text = "From: %s\nDate: %s\n" % (user,
- time.strftime('%a, %d %b %Y %H:%M:%S %Z'))
- default_issue_text += "Status: new\nSubject: brief description\n\n"
+ default_issue_text = "From: %s\nDate: %s\n" % (user,
+ time.strftime(date_format))
+ default_issue_text += "State: %s\nSubject: brief description\n\n" % default_state
default_issue_text += "Detailed description."
issue = ui.edit(default_issue_text, user)
@@ -29,29 +65,104 @@
ui.warn('Unchanged issue text, ignoring\n')
return
+ # Create the message
+ msg = mailbox.mboxMessage(issue)
+ msg.set_from('artemis', True)
+
# Pick random filename
issue_fn = issues_path
while os.path.exists(issue_fn):
- issue_fn = os.path.join(issues_path, "%x" % random.randint(2**32, 2**64-1))
+ issue_id = "%x" % random.randint(2**63, 2**64-1)
+ issue_fn = os.path.join(issues_path, issue_id)
+ msg.add_header('Message-Id', "%s-0-artemis@%s" % (issue_id, socket.gethostname()))
- # FIXME: replace with creating a mailbox
- f = file(issue_fn, "w")
- f.write(issue)
- f.close()
+ # Add message to the mailbox
+ mbox = mailbox.mbox(issue_fn)
+ mbox.add(msg)
+ mbox.close()
+ # Add the new mailbox to the repository
repo.add([issue_fn[(len(repo.root)+1):]]) # +1 for the trailing /
-def show(ui, repo, id):
- """Shows issue ID"""
+
+def show(ui, repo, id, comment = None):
+ """Shows issue ID, or possibly its comment COMMENT"""
+
+ issue = _find_issue(ui, repo, id)
+ if not issue: return
+
+ # Read the issue
+ mbox = mailbox.mbox(issue)
+ msg = mbox[0]
+ ui.write(msg.as_string())
+
+ # Walk the mailbox, and output comments
+
+
+
+def update(ui, repo, id, **opts):
+ """Update properties of issue ID, or add a comment to it or its comment COMMENT"""
+
+ issue = _find_issue(ui, repo, id)
+ if not issue: return
+
+ properties = _get_properties(opts['property'])
+
+ # Read the issue
+ mbox = mailbox.mbox(issue)
+ msg = mbox[0]
+
+ # Fix the properties
+ for property, value in properties:
+ msg.replace_header(property, value)
+ mbox[0] = msg
+ mbox.flush()
+
+ # Deal with comments
+
+ # Show updated message
+ ui.write(mbox[0].as_string())
+
+
+def _find_issue(ui, repo, id):
+ 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
+ elif len(issues) > 1:
+ ui.status("Multiple choices:\n")
+ for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n')
+ return False
+
+ return issues[0]
+
+def _get_properties(property_list):
+ return [p.split('=') for p in property_list]
+
+
cmdtable = {
- 'issues-list': (list,
- [('s', 'status', None, 'restrict status')],
- _('hg issues-list')),
- 'issues-add': (add,
- [],
- _('hg issues-add')),
- 'issues-show': (show,
- [],
- _('hg issues-show ID'))
+ 'ilist': (list,
+ [('a', 'all', None,
+ 'list all issues (by default only those with state new)'),
+ ('p', 'property', [],
+ 'list issues with specific field values (e.g., -p state=fixed)'),
+ ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
+ ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s)' % (issues_dir, filter_filename))],
+ _('hg ilist [OPTIONS]')),
+ 'iadd': (add,
+ [],
+ _('hg iadd')),
+ 'ishow': (show,
+ [('v', 'verbose', None, 'list the comments')],
+ _('hg ishow ID [COMMENT]')),
+ 'iupdate': (update,
+ [('p', 'property', [],
+ 'update properties (e.g., -p state=fixed)'),
+ ('c', 'comment', 0,
+ 'add a comment to issue or its comment COMMENT')],
+ _('hg iupdate [OPTIONS] ID [COMMENT]'))
}