artemis.py
changeset 28 8b6b282d3ebd
parent 27 6ab60ee8b151
child 29 0b3edabb7da2
equal deleted inserted replaced
27:6ab60ee8b151 28:8b6b282d3ebd
     1 # Author: Dmitriy Morozov <hg@foxcub.org>, 2007
     1 # Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
     2         
     2         
     3 """A very simple and lightweight issue tracker for Mercurial."""
     3 """A very simple and lightweight issue tracker for Mercurial."""
     4 
     4 
     5 from mercurial import hg, util
     5 from mercurial import hg, util
     6 from mercurial.i18n import _
     6 from mercurial.i18n import _
    13 from email.mime.image import MIMEImage
    13 from email.mime.image import MIMEImage
    14 from email.mime.multipart import MIMEMultipart
    14 from email.mime.multipart import MIMEMultipart
    15 from email.mime.text import MIMEText
    15 from email.mime.text import MIMEText
    16 
    16 
    17 
    17 
    18 state = {'new': 'new', 'fixed': 'fixed'}
    18 state = {'new': 'new', 'fixed': ['fixed', 'resolved']}
    19 state['default'] = state['new']
    19 state['default'] = state['new']
    20 issues_dir = ".issues"
    20 issues_dir = ".issues"
    21 filter_prefix = ".filter"
    21 filter_prefix = ".filter"
    22 date_format = '%a, %d %b %Y %H:%M:%S'
    22 date_format = '%a, %d %b %Y %H:%M:%S'
    23 maildir_dirs = ['new','cur','tmp']
    23 maildir_dirs = ['new','cur','tmp']
    57         mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
    57         mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
    58         root = _find_root_key(mbox)
    58         root = _find_root_key(mbox)
    59         property_match = True
    59         property_match = True
    60         for property,value in properties:
    60         for property,value in properties:
    61             property_match = property_match and (mbox[root][property] == value)
    61             property_match = property_match and (mbox[root][property] == value)
    62         if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() == state['fixed'].upper()): continue
    62         if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['fixed']]): continue
    63 
    63 
    64 
    64 
    65         if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
    65         if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
    66         ui.write("%s (%3d) [%s]: %s\n" % (issue[len(issues_path)+1:], # +1 for trailing /
    66         ui.write("%s (%3d) [%s]: %s\n" % (issue[len(issues_path)+1:], # +1 for trailing /
    67                                           len(mbox)-1,                # number of replies (-1 for self)
    67                                           len(mbox)-1,                # number of replies (-1 for self)
    68                                           mbox[root]['State'],
    68                                           _status_msg(mbox[root]),
    69                                           mbox[root]['Subject']))
    69                                           mbox[root]['Subject']))
    70 
    70 
    71 
    71 
    72 def iadd(ui, repo, id = None, comment = 0, **opts):
    72 def iadd(ui, repo, id = None, comment = 0, **opts):
    73     """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
    73     """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
    91     if not id:
    91     if not id:
    92         default_issue_text +=     "State: %s\n" % state['default']
    92         default_issue_text +=     "State: %s\n" % state['default']
    93     default_issue_text +=         "Subject: brief description\n\n"
    93     default_issue_text +=         "Subject: brief description\n\n"
    94     default_issue_text +=         "Detailed description."
    94     default_issue_text +=         "Detailed description."
    95 
    95 
    96     issue = ui.edit(default_issue_text, user)
    96     # Get properties, and figure out if we need an explicit comment
    97     if issue.strip() == '':
    97     properties = _get_properties(opts['property'])
    98         ui.warn('Empty issue, ignoring\n')
    98     no_comment = id and properties and opts['no_property_comment']
    99         return
    99 
   100     if issue.strip() == default_issue_text:
   100     # Create the text
   101         ui.warn('Unchanged issue text, ignoring\n')
   101     if not no_comment:
   102         return
   102         issue = ui.edit(default_issue_text, user)
       
   103 
       
   104         if issue.strip() == '':
       
   105             ui.warn('Empty issue, ignoring\n')
       
   106             return
       
   107         if issue.strip() == default_issue_text:
       
   108             ui.warn('Unchanged issue text, ignoring\n')
       
   109             return
       
   110     else:
       
   111         # Write down a comment about updated properties
       
   112         properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])            
       
   113     
       
   114         issue =     "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
       
   115                      (user, util.datestr(format = date_format), properties_subject)
   103 
   116 
   104     # Create the message
   117     # Create the message
   105     msg = mailbox.MaildirMessage(issue)
   118     msg = mailbox.MaildirMessage(issue)
   106     if opts['attach']:
   119     if opts['attach']:
   107         outer = _attach_files(msg, opts['attach'])
   120         outer = _attach_files(msg, opts['attach'])
   115             issue_id = _random_id()
   128             issue_id = _random_id()
   116             issue_fn = os.path.join(issues_path, issue_id)
   129             issue_fn = os.path.join(issues_path, issue_id)
   117     # else: issue_fn already set
   130     # else: issue_fn already set
   118 
   131 
   119     # Add message to the mailbox
   132     # Add message to the mailbox
   120     mbox = mailbox.Maildir(issue_fn)
   133     mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
   121     keys = _order_keys_date(mbox)
   134     keys = _order_keys_date(mbox)
   122     mbox.lock()
   135     mbox.lock()
   123     if id and comment >= len(mbox):
   136     if id and comment >= len(mbox):
   124         ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
   137         ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
   125 
   138 
   126     if not id:
   139     if not id:
   127         outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
   140         outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
       
   141         root = 0
   128     else:
   142     else:
   129         root = keys[0]
   143         root = keys[0]
   130         outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
   144         outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
   131         outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
   145         outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
   132         outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
   146         outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
   133     repo.add([issue_fn[(len(repo.root)+1):] + '/new/'  + mbox.add(outer)])   # +1 for the trailing /
   147     repo.add([issue_fn[(len(repo.root)+1):] + '/new/'  + mbox.add(outer)])   # +1 for the trailing /
       
   148 
       
   149     # Fix properties in the root message
       
   150     msg = mbox[root]
       
   151     if properties:
       
   152         for property, value in properties:
       
   153             if property in msg:
       
   154                 msg.replace_header(property, value)
       
   155             else:
       
   156                 msg.add_header(property, value)
       
   157     mbox[root] = msg
       
   158 
   134     mbox.close()
   159     mbox.close()
   135 
   160 
   136     # If adding issue, add the new mailbox to the repository
   161     # If adding issue, add the new mailbox to the repository
   137     if not id:
   162     if not id:
   138         ui.status('Added new issue %s\n' % issue_id)
   163         ui.status('Added new issue %s\n' % issue_id)
   139 
   164     else:
       
   165         _show_mbox(ui, mbox, 0)
   140 
   166 
   141 def ishow(ui, repo, id, comment = 0, **opts):
   167 def ishow(ui, repo, id, comment = 0, **opts):
   142     """Shows issue ID, or possibly its comment COMMENT"""
   168     """Shows issue ID, or possibly its comment COMMENT"""
   143 
   169 
   144     comment = int(comment)
   170     comment = int(comment)
   179                 fp.write(part.get_payload(decode = True))
   205                 fp.write(part.get_payload(decode = True))
   180                 fp.close()
   206                 fp.close()
   181             counter += 1
   207             counter += 1
   182 
   208 
   183 
   209 
   184 def iupdate(ui, repo, id, **opts):
       
   185     """Update properties of issue ID"""
       
   186 
       
   187     issue, id = _find_issue(ui, repo, id)
       
   188     if not issue: return
       
   189     
       
   190     _create_missing_dirs(os.path.join(repo.root, issues_dir), id)
       
   191 
       
   192     properties = _get_properties(opts['property'])
       
   193 
       
   194     # Read the issue
       
   195     mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
       
   196     root = _find_root_key(mbox)
       
   197     msg = mbox[root]
       
   198 
       
   199     # Fix the properties
       
   200     properties_text = ''
       
   201     for property, value in properties:
       
   202         if property in msg:
       
   203             msg.replace_header(property, value)
       
   204         else:
       
   205             msg.add_header(property, value)
       
   206         properties_text += '%s=%s\n' % (property, value)
       
   207     mbox.lock()
       
   208     mbox[root] = msg
       
   209 
       
   210     # Write down a comment about updated properties
       
   211     if properties and not opts['no_property_comment']:
       
   212         user = ui.username()
       
   213         properties_text  =     "From: %s\nDate: %s\nSubject: properties changes (%s)\n\n%s" % \
       
   214                             (user, util.datestr(format = date_format),
       
   215                              _pretty_list(list(set([property for property, value in properties]))),
       
   216                              properties_text)
       
   217         msg = mailbox.mboxMessage(properties_text)
       
   218         msg.add_header('Message-Id', "<%s-%s-artemis@%s>" % (id, _random_id(), socket.gethostname()))
       
   219         msg.add_header('References', mbox[root]['Message-Id'])
       
   220         msg.add_header('In-Reply-To', mbox[root]['Message-Id'])
       
   221         #msg.set_from('artemis', True)
       
   222         repo.add([issue[(len(repo.root)+1):] + '/new/'  + mbox.add(msg)])   # +1 for the trailing /
       
   223     mbox.close()
       
   224 
       
   225     # Show updated message
       
   226     _show_mbox(ui, mbox, 0)
       
   227 
       
   228 
       
   229 def _find_issue(ui, repo, id):
   210 def _find_issue(ui, repo, id):
   230     issues_path = os.path.join(repo.root, issues_dir)
   211     issues_path = os.path.join(repo.root, issues_dir)
   231     if not os.path.exists(issues_path): return False
   212     if not os.path.exists(issues_path): return False
   232 
   213 
   233     issues = glob.glob(os.path.join(issues_path, id + '*'))
   214     issues = glob.glob(os.path.join(issues_path, id + '*'))
   312 def _order_keys_date(mbox):
   293 def _order_keys_date(mbox):
   313     keys = mbox.keys()
   294     keys = mbox.keys()
   314     root = _find_root_key(mbox)
   295     root = _find_root_key(mbox)
   315     keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
   296     keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
   316     return keys
   297     return keys
   317 
       
   318 def _pretty_list(lst):
       
   319     s = ''
       
   320     for i in lst:
       
   321         s += i + ', '
       
   322     return s[:-2]
       
   323 
   298 
   324 def _random_id():
   299 def _random_id():
   325     return "%x" % random.randint(2**63, 2**64-1)
   300     return "%x" % random.randint(2**63, 2**64-1)
   326 
   301 
   327 def _create_missing_dirs(issues_path, issue):
   302 def _create_missing_dirs(issues_path, issue):
   376         # Set the filename parameter
   351         # Set the filename parameter
   377         attachment.add_header('Content-Disposition', 'attachment', filename=filename)
   352         attachment.add_header('Content-Disposition', 'attachment', filename=filename)
   378         outer.attach(attachment)
   353         outer.attach(attachment)
   379     return outer
   354     return outer
   380 
   355 
       
   356 def _status_msg(msg):
       
   357     if msg['State'] == 'resolved':
       
   358         return 'resolved=' + msg['resolution']
       
   359     else:
       
   360         return msg['State']
       
   361 
   381 cmdtable = {
   362 cmdtable = {
   382     'ilist':    (ilist,
   363     'ilist':    (ilist,
   383                  [('a', 'all', False,
   364                  [('a', 'all', False,
   384                    'list all issues (by default only those with state new)'),
   365                    'list all issues (by default only those with state new)'),
   385                   ('p', 'property', [],
   366                   ('p', 'property', [],
   387                   ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
   368                   ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
   388                   ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (issues_dir, filter_prefix))],
   369                   ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (issues_dir, filter_prefix))],
   389                  _('hg ilist [OPTIONS]')),
   370                  _('hg ilist [OPTIONS]')),
   390     'iadd':       (iadd, 
   371     'iadd':       (iadd, 
   391                  [('a', 'attach', [],
   372                  [('a', 'attach', [],
   392                    'attach file(s) (e.g., -a filename1 -a filename2)')], 
   373                    'attach file(s) (e.g., -a filename1 -a filename2)'),
   393                  _('hg iadd [ID] [COMMENT]')),
   374                   ('p', 'property', [],
       
   375                    'update properties (e.g., -p state=fixed)'),
       
   376                   ('n', 'no-property-comment', None,
       
   377                    'do not add a comment about changed properties')], 
       
   378                  _('hg iadd [OPTIONS] [ID] [COMMENT]')),
   394     'ishow':      (ishow,
   379     'ishow':      (ishow,
   395                  [('a', 'all', None, 'list all comments'),
   380                  [('a', 'all', None, 'list all comments'),
   396                   ('x', 'extract', [], 'extract attachments')],
   381                   ('x', 'extract', [], 'extract attachments')],
   397                  _('hg ishow [OPTIONS] ID [COMMENT]')),
   382                  _('hg ishow [OPTIONS] ID [COMMENT]')),
   398     'iupdate':    (iupdate,
       
   399                  [('p', 'property', [],
       
   400                    'update properties (e.g., -p state=fixed)'),
       
   401                   ('n', 'no-property-comment', None,
       
   402                    'do not add a comment about changed properties')],
       
   403                  _('hg iupdate [OPTIONS] ID'))
       
   404 }
   383 }
   405 
   384 
   406 # vim: expandtab
   385 # vim: expandtab