--- a/hgext/bugzilla.py Wed Mar 30 09:49:45 2011 +0100
+++ b/hgext/bugzilla.py Wed Mar 30 09:49:45 2011 +0100
@@ -1,6 +1,7 @@
# bugzilla.py - bugzilla integration for mercurial
#
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+# Copyright 2011 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.
@@ -8,56 +9,43 @@
'''hooks for integrating with the Bugzilla bug tracker
This hook extension adds comments on bugs in Bugzilla when changesets
-that refer to bugs by Bugzilla ID are seen. The hook does not change
-bug status.
+that refer to bugs by Bugzilla ID are seen. The comment is formatted using
+the Mercurial template mechanism.
-The hook updates the Bugzilla database directly. Only Bugzilla
-installations using MySQL are supported.
+The hook does not change bug status.
-The hook relies on a Bugzilla script to send bug change notification
-emails. That script changes between Bugzilla versions; the
-'processmail' script used prior to 2.18 is replaced in 2.18 and
-subsequent versions by 'config/sendbugmail.pl'. Note that these will
-be run by Mercurial as the user pushing the change; you will need to
-ensure the Bugzilla install file permissions are set appropriately.
+Two basic modes of access to Bugzilla are provided:
-The extension is configured through three different configuration
-sections. These keys are recognized in the [bugzilla] section:
-
-host
- Hostname of the MySQL server holding the Bugzilla database.
+1. Access via the Bugzilla XMLRPC interface (requires Bugzilla 3.4 or later).
-db
- Name of the Bugzilla database in MySQL. Default 'bugs'.
-
-user
- Username to use to access MySQL server. Default 'bugs'.
+2. Writing directly to the Bugzilla database. Only Bugzilla installations
+ using MySQL are supported. Requires Python MySQLdb.
-password
- Password to use to access MySQL server.
-
-timeout
- Database connection timeout (seconds). Default 5.
-
-version
- Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
- '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
- to 2.18.
+Writing directly to the database is susceptible to schema changes, and
+relies on a Bugzilla contrib script to send out bug change
+notification emails. This script runs as the user running Mercurial,
+must be run on the host with the Bugzilla install, and requires
+permission to read Bugzilla configuration details and the necessary
+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.
-bzuser
- Fallback Bugzilla user name to record comments with, if changeset
- committer cannot be found as a Bugzilla user.
+Access via XMLRPC needs a Bugzilla username and password to be specified
+in the configuration. Comments are added under that username. Since the
+configuration must be readable by all Mercurial users, it is recommended
+that the rights of that user are restricted in Bugzilla to the minimum
+necessary to add comments.
+
+Configuration items common to both access modes:
-bzdir
- Bugzilla install directory. Used by default notify. Default
- '/var/www/html/bugzilla'.
-
-notify
- The command to run to get Bugzilla to send bug change notification
- emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
- and 'user' (committer bugzilla email). Default depends on version;
- from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
- %(id)s %(user)s".
+[bugzilla]
+version
+ This access type to use. Values recognised are:
+ xmlrpc Bugzilla XMLRPC interface.
+ 3.0 MySQL access, Bugzilla 3.0 and later.
+ 2.18 MySQL access, Bugzilla 2.18 and up to but not including 3.0.
+ 2.16 MySQL access, Bugzilla 2.16 and up to but not including 2.18.
regexp
Regular expression to match bug IDs in changeset commit message.
@@ -82,23 +70,72 @@
'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
strip
- The number of slashes to strip from the front of {root} to produce
- {webroot}. Default 0.
+ The number of path separator characters to strip from the front of the
+ Mercurial repository path ('{root}' in templates) to produce '{webroot}'.
+ For example, a repository with '{root}' '/var/local/my-project' with a
+ strip of 2 gives a value for '{webroot}' of 'my-project'. Default 0.
+
+[web]
+baseurl
+ Base URL for browsing Mercurial repositories. Referenced from
+ templates as {hgweb}.
+
+XMLRPC access mode configuration:
+
+[bugzilla]
+bzurl
+ The base URL for the Bugzilla installation.
+ Default 'http://localhost/bugzilla'.
+
+user
+ The username to use to log into Bugzilla via XMLRPC. Default 'bugs'.
+
+password
+ The password for Bugzilla login.
+
+MySQL access mode configuration:
+
+[bugzilla]
+host
+ Hostname of the MySQL server holding the Bugzilla database.
+ Default 'localhost'.
+
+db
+ Name of the Bugzilla database in MySQL. Default 'bugs'.
+
+user
+ Username to use to access MySQL server. Default 'bugs'.
+
+password
+ Password to use to access MySQL server.
+
+timeout
+ Database connection timeout (seconds). Default 5.
+
+bzuser
+ Fallback Bugzilla user name to record comments with, if changeset
+ committer cannot be found as a Bugzilla user.
+
+bzdir
+ Bugzilla install directory. Used by default notify. Default
+ '/var/www/html/bugzilla'.
+
+notify
+ The command to run to get Bugzilla to send bug change notification
+ emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
+ and 'user' (committer bugzilla email). Default depends on version;
+ from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
+ %(id)s %(user)s".
usermap
Path of file containing Mercurial committer ID to Bugzilla user ID
mappings. If specified, the file should contain one mapping per
line, "committer"="Bugzilla user". See also the [usermap] section.
+[usermap]
The [usermap] section is used to specify mappings of Mercurial
-committer ID to Bugzilla user ID. See also [bugzilla].usermap.
-"committer"="Bugzilla user"
-
-Finally, the [web] section supports one entry:
-
-baseurl
- Base URL for browsing Mercurial repositories. Reference from
- templates as {hgweb}.
+committer email to Bugzilla user email. See also [bugzilla].usermap.
+Contains entries of the form "committer"="Bugzilla user".
Activating the extension::
@@ -109,11 +146,27 @@
# run bugzilla hook on every change pulled or pushed in here
incoming.bugzilla = python:hgext.bugzilla.hook
-Example configuration:
+Example configurations:
+
+XMLRPC example configuration. This uses the Bugzilla at
+'http://my-project.org/bugzilla', logging in as user 'bugmail@my-project.org'
+wityh password 'plugh'. It is used with a collection of Mercurial
+repositories in '/var/local/hg/repos/'. ::
-This example configuration is for a collection of Mercurial
-repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
-installation in /opt/bugzilla-3.2. ::
+ [bugzilla]
+ bzurl=http://my-project.org/bugzilla
+ user=bugmail@my-project.org
+ password=plugh
+ version=xmlrpc
+
+ [web]
+ baseurl=http://my-project.org/hg
+
+MySQL example configuration. This is for a collection of Mercurial
+repositories in '/var/local/hg/repos/' used with a local Bugzilla 3.2
+installation in /opt/bugzilla-3.2. The MySQL database is on 'localhost',
+the Bugzilla database name is 'bugs' and MySQL is accessed with MySQL
+username 'bugs' password 'XYZZY'. ::
[bugzilla]
host=localhost
@@ -132,7 +185,7 @@
[usermap]
user@emaildomain.com=user.name@bugzilladomain.com
-Commits add a comment to the Bugzilla bug record of the form::
+Both the above add a comment to the Bugzilla bug record of the form::
Changeset 3b16791d6642 in repository-name.
http://dev.domain.com/hg/repository-name/rev/3b16791d6642
@@ -143,7 +196,7 @@
from mercurial.i18n import _
from mercurial.node import short
from mercurial import cmdutil, templater, util
-import re, time
+import re, time, xmlrpclib
class bzaccess(object):
'''Base class for access to Bugzilla.'''
@@ -187,6 +240,9 @@
'''Support for direct MySQL access to Bugzilla.
The earliest Bugzilla version this is tested with is version 2.16.
+
+ If your Bugzilla is version 3.2 or above, you are strongly
+ recommended to use the XMLRPC access method instead.
'''
@staticmethod
@@ -301,7 +357,7 @@
return userid
def get_bugzilla_user(self, committer):
- '''see if committer is a registered bugzilla user. Return
+ '''See if committer is a registered bugzilla user. Return
bugzilla username and userid if so. If not, return default
bugzilla username and userid.'''
user = self.map_committer(committer)
@@ -356,13 +412,122 @@
raise util.Abort(_('unknown database schema'))
return ids[0][0]
+# Buzgilla via XMLRPC interface.
+
+class CookieSafeTransport(xmlrpclib.SafeTransport):
+ """A SafeTransport that retains cookies over its lifetime.
+
+ 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.
+
+ So this is a SafeTransport which looks for cookies being set
+ in responses and saves them to add to all future requests.
+ It appears a SafeTransport can do both HTTP and HTTPS sessions,
+ which saves us having to do a CookieTransport too.
+ """
+
+ # Inspiration drawn from
+ # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
+ # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
+
+ cookies = []
+ def send_cookies(self, connection):
+ if self.cookies:
+ for cookie in self.cookies:
+ connection.putheader("Cookie", cookie)
+
+ def request(self, host, handler, request_body, verbose=0):
+ self.verbose = verbose
+
+ # issue XML-RPC request
+ h = self.make_connection(host)
+ if verbose:
+ h.set_debuglevel(1)
+
+ self.send_request(h, handler, request_body)
+ self.send_host(h, host)
+ self.send_cookies(h)
+ self.send_user_agent(h)
+ self.send_content(h, request_body)
+
+ # Deal with differences between Python 2.4-2.6 and 2.7.
+ # In the former h is a HTTP(S). In the latter it's a
+ # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
+ # HTTP(S) has an underlying HTTP(S)Connection, so extract
+ # that and use it.
+ try:
+ response = h.getresponse()
+ except AttributeError:
+ response = h._conn.getresponse()
+
+ # Add any cookie definitions to our list.
+ for header in response.msg.getallmatchingheaders("Set-Cookie"):
+ val = header.split(": ", 1)[1]
+ cookie = val.split(";", 1)[0]
+ self.cookies.append(cookie)
+
+ if response.status != 200:
+ raise xmlrpclib.ProtocolError(host + handler, response.status,
+ response.reason, response.msg.headers)
+
+ payload = response.read()
+ parser, unmarshaller = self.getparser()
+ parser.feed(payload)
+ parser.close()
+
+ return unmarshaller.close()
+
+class bzxmlrpc(bzaccess):
+ """Support for access to Bugzilla via the Bugzilla XMLRPC API.
+
+ Requires a minimum Bugzilla version 3.4.
+ """
+
+ def __init__(self, ui):
+ bzaccess.__init__(self, ui)
+
+ bzweb = self.ui.config('bugzilla', 'bzurl',
+ 'http://localhost/bugzilla/')
+ bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
+
+ user = self.ui.config('bugzilla', 'user', 'bugs')
+ passwd = self.ui.config('bugzilla', 'password')
+
+ self.bzproxy = xmlrpclib.ServerProxy(bzweb, CookieSafeTransport())
+ self.bzproxy.User.login(dict(login=user, password=passwd))
+
+ def get_bug_comments(self, id):
+ """Return a string with all comment text for a bug."""
+ c = self.bzproxy.Bug.comments(dict(ids=[id]))
+ return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
+
+ def filter_real_bug_ids(self, ids):
+ res = set()
+ bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True))
+ for bug in bugs['bugs']:
+ res.add(bug['id'])
+ return res
+
+ def filter_cset_known_bug_ids(self, node, ids):
+ for id in sorted(ids):
+ if self.get_bug_comments(id).find(short(node)) != -1:
+ self.ui.status(_('bug %d already knows about changeset %s\n') %
+ (id, short(node)))
+ ids.discard(id)
+ return ids
+
+ def add_comment(self, bugid, text, committer):
+ self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text))
+
class bugzilla(object):
# supported versions of bugzilla. different versions have
# different schemas.
_versions = {
'2.16': bzmysql,
'2.18': bzmysql_2_18,
- '3.0': bzmysql_3_0
+ '3.0': bzmysql_3_0,
+ 'xmlrpc': bzxmlrpc
}
_default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'