The Mercurial Bugzilla extension

The Mercurial Distributed Version Control System comes as standard with an extension providing some integration with Bugzilla bug tracking.

Bugzilla 4.4.3 and later

The Mercurial extension uses the Bugzilla XML-RPC API to interact with Bugzilla.

In 4.4.3, Bugzilla changes the way it authenticates use of the API. This breaks the Bugzilla extension distributed with Mercurial versions 3.0 and prior.

For Mercurial 3.0, this patch will get things working again. It should be part of Mercurial 3.1 and later.

--- a/hgext/bugzilla.py Mon May 05 16:54:15 2014 +0200
+++ b/hgext/bugzilla.py Wed May 21 22:51:16 2014 +0100
@@ -1,7 +1,7 @@
 # bugzilla.py - bugzilla integration for mercurial
 #
 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
-# Copyright 2011-2 Jim Hague <jim.hague@acm.org>
+# Copyright 2011-4 Jim Hague <jim.hague@acm.org>
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
@@ -232,7 +232,7 @@
     bzurl=http://my-project.org/bugzilla
     user=bugmail@my-project.org
     password=plugh
-    version=xmlrpc
+    version=xmlrpc+email
     bzemail=bugzilla@my-project.org
     template=Changeset {node|short} in {root|basename}.
              {hgweb}/{webroot}/rev/{node|short}\\n
@@ -523,7 +523,7 @@
 
     The regular xmlrpclib transports ignore cookies. Which causes
     a bit of a problem when you need a cookie-based login, as with
-    the Bugzilla XMLRPC interface.
+    the Bugzilla XMLRPC interface prior to 4.4.3.
 
     So this is a helper for defining a Transport which looks for
     cookies being set in responses and saves them to add to all future
@@ -620,7 +620,9 @@
         ver = self.bzproxy.Bugzilla.version()['version'].split('.')
         self.bzvermajor = int(ver[0])
         self.bzverminor = int(ver[1])
-        self.bzproxy.User.login({'login': user, 'password': passwd})
+        login = self.bzproxy.User.login({'login': user, 'password': passwd,
+                                         'restrict_login': True})
+        self.bztoken = login.get('token', '')
 
     def transport(self, uri):
         if urlparse.urlparse(uri, "http")[0] == "https":
@@ -631,13 +633,15 @@
     def get_bug_comments(self, id):
         """Return a string with all comment text for a bug."""
         c = self.bzproxy.Bug.comments({'ids': [id],
-                                       'include_fields': ['text']})
+                                       'include_fields': ['text'],
+                                       'token': self.bztoken})
         return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
 
     def filter_real_bug_ids(self, bugs):
         probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
                                       'include_fields': [],
                                       'permissive': True,
+                                      'token': self.bztoken,
                                       })
         for badbug in probe['faults']:
             id = badbug['id']
@@ -662,6 +666,7 @@
             if 'fix' in newstate:
                 args['status'] = self.fixstatus
                 args['resolution'] = self.fixresolution
+            args['token'] = self.bztoken
             self.bzproxy.Bug.update(args)
         else:
             if 'fix' in newstate:
@@ -719,10 +724,12 @@
         than the subject line, and leave a blank line after it.
         '''
         user = self.map_committer(committer)
-        matches = self.bzproxy.User.get({'match': [user]})
+        matches = self.bzproxy.User.get({'match': [user],
+                                         'token': self.bztoken})
         if not matches['users']:
             user = self.ui.config('bugzilla', 'user', 'bugs')
-            matches = self.bzproxy.User.get({'match': [user]})
+            matches = self.bzproxy.User.get({'match': [user],
+                                             'token': self.bztoken})
             if not matches['users']:
                 raise util.Abort(_("default bugzilla user %s email not found") %
                                  user)

For older versions of Mercurial, this patch is equivalent, but will apply without errors.

--- hgext/bugzilla.py.orig      2014-02-12 21:32:36.000000000 +0100
+++ hgext/bugzilla.py   2014-05-22 20:49:41.249870459 +0200
@@ -620,7 +620,9 @@
         ver = self.bzproxy.Bugzilla.version()['version'].split('.')
         self.bzvermajor = int(ver[0])
         self.bzverminor = int(ver[1])
-        self.bzproxy.User.login(dict(login=user, password=passwd))
+        login = self.bzproxy.User.login(dict(login=user, password=passwd,
+                                             restrict_login=True))
+        self.bztoken = login.get('token', '')
 
     def transport(self, uri):
         if urlparse.urlparse(uri, "http")[0] == "https":
@@ -630,12 +632,14 @@
 
     def get_bug_comments(self, id):
         """Return a string with all comment text for a bug."""
-        c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text']))
+        c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text'],
+                                           token=self.bztoken))
         return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
 
     def filter_real_bug_ids(self, bugs):
         probe = self.bzproxy.Bug.get(dict(ids=sorted(bugs.keys()),
                                           include_fields=[],
+                                          token=self.bztoken,
                                           permissive=True))
         for badbug in probe['faults']:
             id = badbug['id']
@@ -660,6 +664,7 @@
             if 'fix' in newstate:
                 args['status'] = self.fixstatus
                 args['resolution'] = self.fixresolution
+            args['token'] = self.bztoken
             self.bzproxy.Bug.update(args)
         else:
             if 'fix' in newstate:
@@ -717,10 +722,10 @@
         than the subject line, and leave a blank line after it.
         '''
         user = self.map_committer(committer)
-        matches = self.bzproxy.User.get(dict(match=[user]))
+        matches = self.bzproxy.User.get(dict(match=[user], token=self.bztoken))
         if not matches['users']:
             user = self.ui.config('bugzilla', 'user', 'bugs')
-            matches = self.bzproxy.User.get(dict(match=[user]))
+            matches = self.bzproxy.User.get(dict(match=[user], token=self.bztoken))
             if not matches['users']:
                 raise util.Abort(_("default bugzilla user %s email not found") %
                                  user)