# HG changeset patch
# User Jim Hague <jim.hague@acm.org>
# Date 2012-03-01 15:25:37
# Node ID d7b7b453c035709b0b0ed609d17dd596539af6b5
# Parent  4fc9fcd991c17c623d4d3de5f41dc7c01d2e91b3

bugzilla: extract optional hours from commit message and update bug time

If an 'hours' group is present in the bug matching regular expression,
and that group contains a float value, update the bug working time in
Bugzilla with those hours.

The patch adds a key 'hours' to the bug state dictionary, and adds
support for the key to the XMLRPC and XMLRPC/email access methods.
The MySQL access method is not supported; a warning is given.

diff --git a/hgext/bugzilla.py b/hgext/bugzilla.py
--- a/hgext/bugzilla.py
+++ b/hgext/bugzilla.py
@@ -12,7 +12,8 @@ This hook extension adds comments on bug
 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
 the Mercurial template mechanism.
 
-The hook does not change bug status.
+The bug references can optionally include an update for Bugzilla of the
+hours spent working on the bug.
 
 Three basic modes of access to Bugzilla are provided:
 
@@ -32,7 +33,7 @@ permission to read Bugzilla configuratio
 MySQL user and password to have full access rights to the Bugzilla
 database. For these reasons this access mode is now considered
 deprecated, and will not be updated for new Bugzilla versions going
-forward.
+forward. Only adding comments is supported in this access mode.
 
 Access via XMLRPC needs a Bugzilla username and password to be specified
 in the configuration. Comments are added under that username. Since the
@@ -63,10 +64,15 @@ bugzilla.version
 
 bugzilla.regexp
   Regular expression to match bug IDs in changeset commit message.
-  Must contain one "()" group. The default expression matches ``Bug
-  1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``,
-  ``Bug 1234 and 5678`` and variations thereof. Matching is case
-  insensitive.
+  It must contain one "()" named group ``<ids>`` containing the bug
+  IDs separated by non-digit characters. It may also contain
+  a named group ``<hours>`` with a floating-point number giving the
+  hours worked on the bug. If no named groups are present, the first
+  "()" group is assumed to contain the bug IDs, and work time is not
+  updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
+  ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
+  variations thereof, followed by an hours number prefixed by ``h`` or
+  ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
 
 bugzilla.style
   The style file to use when formatting comments.
@@ -276,8 +282,10 @@ class bzaccess(object):
     # Methods to be implemented by access classes.
     #
     # 'bugs' is a dict keyed on bug id, where values are a dict holding
-    # updates to bug state. Currently no states are recognised, but this
-    # will change soon.
+    # updates to bug state. Recognised dict keys are:
+    #
+    # 'hours': Value, float containing work hours to be updated.
+
     def filter_real_bug_ids(self, bugs):
         '''remove bug IDs that do not exist in Bugzilla from bugs.'''
         pass
@@ -449,6 +457,9 @@ class bzmysql(bzaccess):
 
         Try adding comment as committer of changeset, otherwise as
         default bugzilla user.'''
+        if len(newstate) > 0:
+            self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
+
         (user, userid) = self.get_bugzilla_user(committer)
         now = time.strftime('%Y-%m-%d %H:%M:%S')
         self.run('''insert into longdescs
@@ -608,6 +619,8 @@ class bzxmlrpc(bzaccess):
 
     def updatebug(self, bugid, newstate, text, committer):
         args = dict(id=bugid, comment=text)
+        if 'hours' in newstate:
+            args['work_time'] = newstate['hours']
         self.bzproxy.Bug.add_comment(args)
 
 class bzxmlrpcemail(bzxmlrpc):
@@ -615,8 +628,11 @@ class bzxmlrpcemail(bzxmlrpc):
 
     Advantages of sending updates via email:
       1. Comments can be added as any user, not just logged in user.
-      2. Bug statuses and other fields not accessible via XMLRPC can
-        be updated. This is not currently used.
+      2. Bug statuses or other fields not accessible via XMLRPC can
+         potentially be updated.
+
+    Currently all status updates recognised can be done via XMLRPC, so
+    item 1 is the only actual advantage.
     """
 
     def __init__(self, ui):
@@ -661,7 +677,10 @@ class bzxmlrpcemail(bzxmlrpc):
         sendmail(user, bzemail, msg.as_string())
 
     def updatebug(self, bugid, newstate, text, committer):
-        self.send_bug_modify_email(bugid, [], text, committer)
+        cmds = []
+        if 'hours' in newstate:
+            cmds.append(self.makecommandline("work_time", newstate['hours']))
+        self.send_bug_modify_email(bugid, cmds, text, committer)
 
 class bugzilla(object):
     # supported versions of bugzilla. different versions have
@@ -675,7 +694,8 @@ class bugzilla(object):
         }
 
     _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
-                       r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
+                       r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
+                       r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
 
     _bz = None
 
@@ -716,16 +736,32 @@ class bugzilla(object):
                 re.IGNORECASE)
             bugzilla._split_re = re.compile(r'\D+')
         start = 0
+        hours = 0.0
         bugs = {}
         while True:
+            bugattribs = {}
             m = bugzilla._bug_re.search(ctx.description(), start)
             if not m:
                 break
             start = m.end()
-            for id in bugzilla._split_re.split(m.group(1)):
+            try:
+                ids = m.group('ids')
+            except IndexError:
+                ids = m.group(1)
+            try:
+                hours = float(m.group('hours'))
+                bugattribs['hours'] = hours
+            except IndexError:
+                pass
+            except TypeError:
+                pass
+            except ValueError:
+                self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
+
+            for id in bugzilla._split_re.split(ids):
                 if not id:
                     continue
-                bugs[int(id)] = {}
+                bugs[int(id)] = bugattribs
         if bugs:
             self.filter_real_bug_ids(bugs)
         if bugs: