##// END OF EJS Templates
bugzilla: use absolute_import
Gregory Szorc -
r28091:2f038424 default
parent child Browse files
Show More
@@ -1,914 +1,925 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''hooks for integrating with the Bugzilla bug tracker
9 '''hooks for integrating with the Bugzilla bug tracker
10
10
11 This hook extension adds comments on bugs in Bugzilla when changesets
11 This hook extension adds comments on bugs in Bugzilla when changesets
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 the Mercurial template mechanism.
13 the Mercurial template mechanism.
14
14
15 The bug references can optionally include an update for Bugzilla of the
15 The bug references can optionally include an update for Bugzilla of the
16 hours spent working on the bug. Bugs can also be marked fixed.
16 hours spent working on the bug. Bugs can also be marked fixed.
17
17
18 Three basic modes of access to Bugzilla are provided:
18 Three basic modes of access to Bugzilla are provided:
19
19
20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21
21
22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24
24
25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 using MySQL are supported. Requires Python MySQLdb.
26 using MySQL are supported. Requires Python MySQLdb.
27
27
28 Writing directly to the database is susceptible to schema changes, and
28 Writing directly to the database is susceptible to schema changes, and
29 relies on a Bugzilla contrib script to send out bug change
29 relies on a Bugzilla contrib script to send out bug change
30 notification emails. This script runs as the user running Mercurial,
30 notification emails. This script runs as the user running Mercurial,
31 must be run on the host with the Bugzilla install, and requires
31 must be run on the host with the Bugzilla install, and requires
32 permission to read Bugzilla configuration details and the necessary
32 permission to read Bugzilla configuration details and the necessary
33 MySQL user and password to have full access rights to the Bugzilla
33 MySQL user and password to have full access rights to the Bugzilla
34 database. For these reasons this access mode is now considered
34 database. For these reasons this access mode is now considered
35 deprecated, and will not be updated for new Bugzilla versions going
35 deprecated, and will not be updated for new Bugzilla versions going
36 forward. Only adding comments is supported in this access mode.
36 forward. Only adding comments is supported in this access mode.
37
37
38 Access via XMLRPC needs a Bugzilla username and password to be specified
38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 in the configuration. Comments are added under that username. Since the
39 in the configuration. Comments are added under that username. Since the
40 configuration must be readable by all Mercurial users, it is recommended
40 configuration must be readable by all Mercurial users, it is recommended
41 that the rights of that user are restricted in Bugzilla to the minimum
41 that the rights of that user are restricted in Bugzilla to the minimum
42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43
43
44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 email to the Bugzilla email interface to submit comments to bugs.
45 email to the Bugzilla email interface to submit comments to bugs.
46 The From: address in the email is set to the email address of the Mercurial
46 The From: address in the email is set to the email address of the Mercurial
47 user, so the comment appears to come from the Mercurial user. In the event
47 user, so the comment appears to come from the Mercurial user. In the event
48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
49 user, the email associated with the Bugzilla username used to log into
49 user, the email associated with the Bugzilla username used to log into
50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 works on all supported Bugzilla versions.
51 works on all supported Bugzilla versions.
52
52
53 Configuration items common to all access modes:
53 Configuration items common to all access modes:
54
54
55 bugzilla.version
55 bugzilla.version
56 The access type to use. Values recognized are:
56 The access type to use. Values recognized are:
57
57
58 :``xmlrpc``: Bugzilla XMLRPC interface.
58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 including 3.0.
62 including 3.0.
63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 including 2.18.
64 including 2.18.
65
65
66 bugzilla.regexp
66 bugzilla.regexp
67 Regular expression to match bug IDs for update in changeset commit message.
67 Regular expression to match bug IDs for update in changeset commit message.
68 It must contain one "()" named group ``<ids>`` containing the bug
68 It must contain one "()" named group ``<ids>`` containing the bug
69 IDs separated by non-digit characters. It may also contain
69 IDs separated by non-digit characters. It may also contain
70 a named group ``<hours>`` with a floating-point number giving the
70 a named group ``<hours>`` with a floating-point number giving the
71 hours worked on the bug. If no named groups are present, the first
71 hours worked on the bug. If no named groups are present, the first
72 "()" group is assumed to contain the bug IDs, and work time is not
72 "()" group is assumed to contain the bug IDs, and work time is not
73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 variations thereof, followed by an hours number prefixed by ``h`` or
75 variations thereof, followed by an hours number prefixed by ``h`` or
76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77
77
78 bugzilla.fixregexp
78 bugzilla.fixregexp
79 Regular expression to match bug IDs for marking fixed in changeset
79 Regular expression to match bug IDs for marking fixed in changeset
80 commit message. This must contain a "()" named group ``<ids>` containing
80 commit message. This must contain a "()" named group ``<ids>` containing
81 the bug IDs separated by non-digit characters. It may also contain
81 the bug IDs separated by non-digit characters. It may also contain
82 a named group ``<hours>`` with a floating-point number giving the
82 a named group ``<hours>`` with a floating-point number giving the
83 hours worked on the bug. If no named groups are present, the first
83 hours worked on the bug. If no named groups are present, the first
84 "()" group is assumed to contain the bug IDs, and work time is not
84 "()" group is assumed to contain the bug IDs, and work time is not
85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 variations thereof, followed by an hours number prefixed by ``h`` or
87 variations thereof, followed by an hours number prefixed by ``h`` or
88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89
89
90 bugzilla.fixstatus
90 bugzilla.fixstatus
91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92
92
93 bugzilla.fixresolution
93 bugzilla.fixresolution
94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95
95
96 bugzilla.style
96 bugzilla.style
97 The style file to use when formatting comments.
97 The style file to use when formatting comments.
98
98
99 bugzilla.template
99 bugzilla.template
100 Template to use when formatting comments. Overrides style if
100 Template to use when formatting comments. Overrides style if
101 specified. In addition to the usual Mercurial keywords, the
101 specified. In addition to the usual Mercurial keywords, the
102 extension specifies:
102 extension specifies:
103
103
104 :``{bug}``: The Bugzilla bug ID.
104 :``{bug}``: The Bugzilla bug ID.
105 :``{root}``: The full pathname of the Mercurial repository.
105 :``{root}``: The full pathname of the Mercurial repository.
106 :``{webroot}``: Stripped pathname of the Mercurial repository.
106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108
108
109 Default ``changeset {node|short} in repo {root} refers to bug
109 Default ``changeset {node|short} in repo {root} refers to bug
110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111
111
112 bugzilla.strip
112 bugzilla.strip
113 The number of path separator characters to strip from the front of
113 The number of path separator characters to strip from the front of
114 the Mercurial repository path (``{root}`` in templates) to produce
114 the Mercurial repository path (``{root}`` in templates) to produce
115 ``{webroot}``. For example, a repository with ``{root}``
115 ``{webroot}``. For example, a repository with ``{root}``
116 ``/var/local/my-project`` with a strip of 2 gives a value for
116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 ``{webroot}`` of ``my-project``. Default 0.
117 ``{webroot}`` of ``my-project``. Default 0.
118
118
119 web.baseurl
119 web.baseurl
120 Base URL for browsing Mercurial repositories. Referenced from
120 Base URL for browsing Mercurial repositories. Referenced from
121 templates as ``{hgweb}``.
121 templates as ``{hgweb}``.
122
122
123 Configuration items common to XMLRPC+email and MySQL access modes:
123 Configuration items common to XMLRPC+email and MySQL access modes:
124
124
125 bugzilla.usermap
125 bugzilla.usermap
126 Path of file containing Mercurial committer email to Bugzilla user email
126 Path of file containing Mercurial committer email to Bugzilla user email
127 mappings. If specified, the file should contain one mapping per
127 mappings. If specified, the file should contain one mapping per
128 line::
128 line::
129
129
130 committer = Bugzilla user
130 committer = Bugzilla user
131
131
132 See also the ``[usermap]`` section.
132 See also the ``[usermap]`` section.
133
133
134 The ``[usermap]`` section is used to specify mappings of Mercurial
134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 Contains entries of the form ``committer = Bugzilla user``.
136 Contains entries of the form ``committer = Bugzilla user``.
137
137
138 XMLRPC access mode configuration:
138 XMLRPC access mode configuration:
139
139
140 bugzilla.bzurl
140 bugzilla.bzurl
141 The base URL for the Bugzilla installation.
141 The base URL for the Bugzilla installation.
142 Default ``http://localhost/bugzilla``.
142 Default ``http://localhost/bugzilla``.
143
143
144 bugzilla.user
144 bugzilla.user
145 The username to use to log into Bugzilla via XMLRPC. Default
145 The username to use to log into Bugzilla via XMLRPC. Default
146 ``bugs``.
146 ``bugs``.
147
147
148 bugzilla.password
148 bugzilla.password
149 The password for Bugzilla login.
149 The password for Bugzilla login.
150
150
151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 and also:
152 and also:
153
153
154 bugzilla.bzemail
154 bugzilla.bzemail
155 The Bugzilla email address.
155 The Bugzilla email address.
156
156
157 In addition, the Mercurial email settings must be configured. See the
157 In addition, the Mercurial email settings must be configured. See the
158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159
159
160 MySQL access mode configuration:
160 MySQL access mode configuration:
161
161
162 bugzilla.host
162 bugzilla.host
163 Hostname of the MySQL server holding the Bugzilla database.
163 Hostname of the MySQL server holding the Bugzilla database.
164 Default ``localhost``.
164 Default ``localhost``.
165
165
166 bugzilla.db
166 bugzilla.db
167 Name of the Bugzilla database in MySQL. Default ``bugs``.
167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168
168
169 bugzilla.user
169 bugzilla.user
170 Username to use to access MySQL server. Default ``bugs``.
170 Username to use to access MySQL server. Default ``bugs``.
171
171
172 bugzilla.password
172 bugzilla.password
173 Password to use to access MySQL server.
173 Password to use to access MySQL server.
174
174
175 bugzilla.timeout
175 bugzilla.timeout
176 Database connection timeout (seconds). Default 5.
176 Database connection timeout (seconds). Default 5.
177
177
178 bugzilla.bzuser
178 bugzilla.bzuser
179 Fallback Bugzilla user name to record comments with, if changeset
179 Fallback Bugzilla user name to record comments with, if changeset
180 committer cannot be found as a Bugzilla user.
180 committer cannot be found as a Bugzilla user.
181
181
182 bugzilla.bzdir
182 bugzilla.bzdir
183 Bugzilla install directory. Used by default notify. Default
183 Bugzilla install directory. Used by default notify. Default
184 ``/var/www/html/bugzilla``.
184 ``/var/www/html/bugzilla``.
185
185
186 bugzilla.notify
186 bugzilla.notify
187 The command to run to get Bugzilla to send bug change notification
187 The command to run to get Bugzilla to send bug change notification
188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 id) and ``user`` (committer bugzilla email). Default depends on
189 id) and ``user`` (committer bugzilla email). Default depends on
190 version; from 2.18 it is "cd %(bzdir)s && perl -T
190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 contrib/sendbugmail.pl %(id)s %(user)s".
191 contrib/sendbugmail.pl %(id)s %(user)s".
192
192
193 Activating the extension::
193 Activating the extension::
194
194
195 [extensions]
195 [extensions]
196 bugzilla =
196 bugzilla =
197
197
198 [hooks]
198 [hooks]
199 # run bugzilla hook on every change pulled or pushed in here
199 # run bugzilla hook on every change pulled or pushed in here
200 incoming.bugzilla = python:hgext.bugzilla.hook
200 incoming.bugzilla = python:hgext.bugzilla.hook
201
201
202 Example configurations:
202 Example configurations:
203
203
204 XMLRPC example configuration. This uses the Bugzilla at
204 XMLRPC example configuration. This uses the Bugzilla at
205 ``http://my-project.org/bugzilla``, logging in as user
205 ``http://my-project.org/bugzilla``, logging in as user
206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 with a web interface at ``http://my-project.org/hg``. ::
208 with a web interface at ``http://my-project.org/hg``. ::
209
209
210 [bugzilla]
210 [bugzilla]
211 bzurl=http://my-project.org/bugzilla
211 bzurl=http://my-project.org/bugzilla
212 user=bugmail@my-project.org
212 user=bugmail@my-project.org
213 password=plugh
213 password=plugh
214 version=xmlrpc
214 version=xmlrpc
215 template=Changeset {node|short} in {root|basename}.
215 template=Changeset {node|short} in {root|basename}.
216 {hgweb}/{webroot}/rev/{node|short}\\n
216 {hgweb}/{webroot}/rev/{node|short}\\n
217 {desc}\\n
217 {desc}\\n
218 strip=5
218 strip=5
219
219
220 [web]
220 [web]
221 baseurl=http://my-project.org/hg
221 baseurl=http://my-project.org/hg
222
222
223 XMLRPC+email example configuration. This uses the Bugzilla at
223 XMLRPC+email example configuration. This uses the Bugzilla at
224 ``http://my-project.org/bugzilla``, logging in as user
224 ``http://my-project.org/bugzilla``, logging in as user
225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 with a web interface at ``http://my-project.org/hg``. Bug comments
227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 are sent to the Bugzilla email address
228 are sent to the Bugzilla email address
229 ``bugzilla@my-project.org``. ::
229 ``bugzilla@my-project.org``. ::
230
230
231 [bugzilla]
231 [bugzilla]
232 bzurl=http://my-project.org/bugzilla
232 bzurl=http://my-project.org/bugzilla
233 user=bugmail@my-project.org
233 user=bugmail@my-project.org
234 password=plugh
234 password=plugh
235 version=xmlrpc+email
235 version=xmlrpc+email
236 bzemail=bugzilla@my-project.org
236 bzemail=bugzilla@my-project.org
237 template=Changeset {node|short} in {root|basename}.
237 template=Changeset {node|short} in {root|basename}.
238 {hgweb}/{webroot}/rev/{node|short}\\n
238 {hgweb}/{webroot}/rev/{node|short}\\n
239 {desc}\\n
239 {desc}\\n
240 strip=5
240 strip=5
241
241
242 [web]
242 [web]
243 baseurl=http://my-project.org/hg
243 baseurl=http://my-project.org/hg
244
244
245 [usermap]
245 [usermap]
246 user@emaildomain.com=user.name@bugzilladomain.com
246 user@emaildomain.com=user.name@bugzilladomain.com
247
247
248 MySQL example configuration. This has a local Bugzilla 3.2 installation
248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 the Bugzilla database name is ``bugs`` and MySQL is
250 the Bugzilla database name is ``bugs`` and MySQL is
251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 with a web interface at ``http://my-project.org/hg``. ::
253 with a web interface at ``http://my-project.org/hg``. ::
254
254
255 [bugzilla]
255 [bugzilla]
256 host=localhost
256 host=localhost
257 password=XYZZY
257 password=XYZZY
258 version=3.0
258 version=3.0
259 bzuser=unknown@domain.com
259 bzuser=unknown@domain.com
260 bzdir=/opt/bugzilla-3.2
260 bzdir=/opt/bugzilla-3.2
261 template=Changeset {node|short} in {root|basename}.
261 template=Changeset {node|short} in {root|basename}.
262 {hgweb}/{webroot}/rev/{node|short}\\n
262 {hgweb}/{webroot}/rev/{node|short}\\n
263 {desc}\\n
263 {desc}\\n
264 strip=5
264 strip=5
265
265
266 [web]
266 [web]
267 baseurl=http://my-project.org/hg
267 baseurl=http://my-project.org/hg
268
268
269 [usermap]
269 [usermap]
270 user@emaildomain.com=user.name@bugzilladomain.com
270 user@emaildomain.com=user.name@bugzilladomain.com
271
271
272 All the above add a comment to the Bugzilla bug record of the form::
272 All the above add a comment to the Bugzilla bug record of the form::
273
273
274 Changeset 3b16791d6642 in repository-name.
274 Changeset 3b16791d6642 in repository-name.
275 http://my-project.org/hg/repository-name/rev/3b16791d6642
275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276
276
277 Changeset commit comment. Bug 1234.
277 Changeset commit comment. Bug 1234.
278 '''
278 '''
279
279
280 from __future__ import absolute_import
281
282 import re
283 import time
284 import urlparse
285 import xmlrpclib
286
280 from mercurial.i18n import _
287 from mercurial.i18n import _
281 from mercurial.node import short
288 from mercurial.node import short
282 from mercurial import cmdutil, mail, util, error
289 from mercurial import (
283 import re, time, urlparse, xmlrpclib
290 cmdutil,
291 error,
292 mail,
293 util,
294 )
284
295
285 # Note for extension authors: ONLY specify testedwith = 'internal' for
296 # Note for extension authors: ONLY specify testedwith = 'internal' for
286 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
297 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
287 # be specifying the version(s) of Mercurial they are tested with, or
298 # be specifying the version(s) of Mercurial they are tested with, or
288 # leave the attribute unspecified.
299 # leave the attribute unspecified.
289 testedwith = 'internal'
300 testedwith = 'internal'
290
301
291 class bzaccess(object):
302 class bzaccess(object):
292 '''Base class for access to Bugzilla.'''
303 '''Base class for access to Bugzilla.'''
293
304
294 def __init__(self, ui):
305 def __init__(self, ui):
295 self.ui = ui
306 self.ui = ui
296 usermap = self.ui.config('bugzilla', 'usermap')
307 usermap = self.ui.config('bugzilla', 'usermap')
297 if usermap:
308 if usermap:
298 self.ui.readconfig(usermap, sections=['usermap'])
309 self.ui.readconfig(usermap, sections=['usermap'])
299
310
300 def map_committer(self, user):
311 def map_committer(self, user):
301 '''map name of committer to Bugzilla user name.'''
312 '''map name of committer to Bugzilla user name.'''
302 for committer, bzuser in self.ui.configitems('usermap'):
313 for committer, bzuser in self.ui.configitems('usermap'):
303 if committer.lower() == user.lower():
314 if committer.lower() == user.lower():
304 return bzuser
315 return bzuser
305 return user
316 return user
306
317
307 # Methods to be implemented by access classes.
318 # Methods to be implemented by access classes.
308 #
319 #
309 # 'bugs' is a dict keyed on bug id, where values are a dict holding
320 # 'bugs' is a dict keyed on bug id, where values are a dict holding
310 # updates to bug state. Recognized dict keys are:
321 # updates to bug state. Recognized dict keys are:
311 #
322 #
312 # 'hours': Value, float containing work hours to be updated.
323 # 'hours': Value, float containing work hours to be updated.
313 # 'fix': If key present, bug is to be marked fixed. Value ignored.
324 # 'fix': If key present, bug is to be marked fixed. Value ignored.
314
325
315 def filter_real_bug_ids(self, bugs):
326 def filter_real_bug_ids(self, bugs):
316 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
327 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
317 pass
328 pass
318
329
319 def filter_cset_known_bug_ids(self, node, bugs):
330 def filter_cset_known_bug_ids(self, node, bugs):
320 '''remove bug IDs where node occurs in comment text from bugs.'''
331 '''remove bug IDs where node occurs in comment text from bugs.'''
321 pass
332 pass
322
333
323 def updatebug(self, bugid, newstate, text, committer):
334 def updatebug(self, bugid, newstate, text, committer):
324 '''update the specified bug. Add comment text and set new states.
335 '''update the specified bug. Add comment text and set new states.
325
336
326 If possible add the comment as being from the committer of
337 If possible add the comment as being from the committer of
327 the changeset. Otherwise use the default Bugzilla user.
338 the changeset. Otherwise use the default Bugzilla user.
328 '''
339 '''
329 pass
340 pass
330
341
331 def notify(self, bugs, committer):
342 def notify(self, bugs, committer):
332 '''Force sending of Bugzilla notification emails.
343 '''Force sending of Bugzilla notification emails.
333
344
334 Only required if the access method does not trigger notification
345 Only required if the access method does not trigger notification
335 emails automatically.
346 emails automatically.
336 '''
347 '''
337 pass
348 pass
338
349
339 # Bugzilla via direct access to MySQL database.
350 # Bugzilla via direct access to MySQL database.
340 class bzmysql(bzaccess):
351 class bzmysql(bzaccess):
341 '''Support for direct MySQL access to Bugzilla.
352 '''Support for direct MySQL access to Bugzilla.
342
353
343 The earliest Bugzilla version this is tested with is version 2.16.
354 The earliest Bugzilla version this is tested with is version 2.16.
344
355
345 If your Bugzilla is version 3.4 or above, you are strongly
356 If your Bugzilla is version 3.4 or above, you are strongly
346 recommended to use the XMLRPC access method instead.
357 recommended to use the XMLRPC access method instead.
347 '''
358 '''
348
359
349 @staticmethod
360 @staticmethod
350 def sql_buglist(ids):
361 def sql_buglist(ids):
351 '''return SQL-friendly list of bug ids'''
362 '''return SQL-friendly list of bug ids'''
352 return '(' + ','.join(map(str, ids)) + ')'
363 return '(' + ','.join(map(str, ids)) + ')'
353
364
354 _MySQLdb = None
365 _MySQLdb = None
355
366
356 def __init__(self, ui):
367 def __init__(self, ui):
357 try:
368 try:
358 import MySQLdb as mysql
369 import MySQLdb as mysql
359 bzmysql._MySQLdb = mysql
370 bzmysql._MySQLdb = mysql
360 except ImportError as err:
371 except ImportError as err:
361 raise error.Abort(_('python mysql support not available: %s') % err)
372 raise error.Abort(_('python mysql support not available: %s') % err)
362
373
363 bzaccess.__init__(self, ui)
374 bzaccess.__init__(self, ui)
364
375
365 host = self.ui.config('bugzilla', 'host', 'localhost')
376 host = self.ui.config('bugzilla', 'host', 'localhost')
366 user = self.ui.config('bugzilla', 'user', 'bugs')
377 user = self.ui.config('bugzilla', 'user', 'bugs')
367 passwd = self.ui.config('bugzilla', 'password')
378 passwd = self.ui.config('bugzilla', 'password')
368 db = self.ui.config('bugzilla', 'db', 'bugs')
379 db = self.ui.config('bugzilla', 'db', 'bugs')
369 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
380 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
370 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
381 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
371 (host, db, user, '*' * len(passwd)))
382 (host, db, user, '*' * len(passwd)))
372 self.conn = bzmysql._MySQLdb.connect(host=host,
383 self.conn = bzmysql._MySQLdb.connect(host=host,
373 user=user, passwd=passwd,
384 user=user, passwd=passwd,
374 db=db,
385 db=db,
375 connect_timeout=timeout)
386 connect_timeout=timeout)
376 self.cursor = self.conn.cursor()
387 self.cursor = self.conn.cursor()
377 self.longdesc_id = self.get_longdesc_id()
388 self.longdesc_id = self.get_longdesc_id()
378 self.user_ids = {}
389 self.user_ids = {}
379 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
390 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
380
391
381 def run(self, *args, **kwargs):
392 def run(self, *args, **kwargs):
382 '''run a query.'''
393 '''run a query.'''
383 self.ui.note(_('query: %s %s\n') % (args, kwargs))
394 self.ui.note(_('query: %s %s\n') % (args, kwargs))
384 try:
395 try:
385 self.cursor.execute(*args, **kwargs)
396 self.cursor.execute(*args, **kwargs)
386 except bzmysql._MySQLdb.MySQLError:
397 except bzmysql._MySQLdb.MySQLError:
387 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
398 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
388 raise
399 raise
389
400
390 def get_longdesc_id(self):
401 def get_longdesc_id(self):
391 '''get identity of longdesc field'''
402 '''get identity of longdesc field'''
392 self.run('select fieldid from fielddefs where name = "longdesc"')
403 self.run('select fieldid from fielddefs where name = "longdesc"')
393 ids = self.cursor.fetchall()
404 ids = self.cursor.fetchall()
394 if len(ids) != 1:
405 if len(ids) != 1:
395 raise error.Abort(_('unknown database schema'))
406 raise error.Abort(_('unknown database schema'))
396 return ids[0][0]
407 return ids[0][0]
397
408
398 def filter_real_bug_ids(self, bugs):
409 def filter_real_bug_ids(self, bugs):
399 '''filter not-existing bugs from set.'''
410 '''filter not-existing bugs from set.'''
400 self.run('select bug_id from bugs where bug_id in %s' %
411 self.run('select bug_id from bugs where bug_id in %s' %
401 bzmysql.sql_buglist(bugs.keys()))
412 bzmysql.sql_buglist(bugs.keys()))
402 existing = [id for (id,) in self.cursor.fetchall()]
413 existing = [id for (id,) in self.cursor.fetchall()]
403 for id in bugs.keys():
414 for id in bugs.keys():
404 if id not in existing:
415 if id not in existing:
405 self.ui.status(_('bug %d does not exist\n') % id)
416 self.ui.status(_('bug %d does not exist\n') % id)
406 del bugs[id]
417 del bugs[id]
407
418
408 def filter_cset_known_bug_ids(self, node, bugs):
419 def filter_cset_known_bug_ids(self, node, bugs):
409 '''filter bug ids that already refer to this changeset from set.'''
420 '''filter bug ids that already refer to this changeset from set.'''
410 self.run('''select bug_id from longdescs where
421 self.run('''select bug_id from longdescs where
411 bug_id in %s and thetext like "%%%s%%"''' %
422 bug_id in %s and thetext like "%%%s%%"''' %
412 (bzmysql.sql_buglist(bugs.keys()), short(node)))
423 (bzmysql.sql_buglist(bugs.keys()), short(node)))
413 for (id,) in self.cursor.fetchall():
424 for (id,) in self.cursor.fetchall():
414 self.ui.status(_('bug %d already knows about changeset %s\n') %
425 self.ui.status(_('bug %d already knows about changeset %s\n') %
415 (id, short(node)))
426 (id, short(node)))
416 del bugs[id]
427 del bugs[id]
417
428
418 def notify(self, bugs, committer):
429 def notify(self, bugs, committer):
419 '''tell bugzilla to send mail.'''
430 '''tell bugzilla to send mail.'''
420 self.ui.status(_('telling bugzilla to send mail:\n'))
431 self.ui.status(_('telling bugzilla to send mail:\n'))
421 (user, userid) = self.get_bugzilla_user(committer)
432 (user, userid) = self.get_bugzilla_user(committer)
422 for id in bugs.keys():
433 for id in bugs.keys():
423 self.ui.status(_(' bug %s\n') % id)
434 self.ui.status(_(' bug %s\n') % id)
424 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
435 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
425 bzdir = self.ui.config('bugzilla', 'bzdir',
436 bzdir = self.ui.config('bugzilla', 'bzdir',
426 '/var/www/html/bugzilla')
437 '/var/www/html/bugzilla')
427 try:
438 try:
428 # Backwards-compatible with old notify string, which
439 # Backwards-compatible with old notify string, which
429 # took one string. This will throw with a new format
440 # took one string. This will throw with a new format
430 # string.
441 # string.
431 cmd = cmdfmt % id
442 cmd = cmdfmt % id
432 except TypeError:
443 except TypeError:
433 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
444 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
434 self.ui.note(_('running notify command %s\n') % cmd)
445 self.ui.note(_('running notify command %s\n') % cmd)
435 fp = util.popen('(%s) 2>&1' % cmd)
446 fp = util.popen('(%s) 2>&1' % cmd)
436 out = fp.read()
447 out = fp.read()
437 ret = fp.close()
448 ret = fp.close()
438 if ret:
449 if ret:
439 self.ui.warn(out)
450 self.ui.warn(out)
440 raise error.Abort(_('bugzilla notify command %s') %
451 raise error.Abort(_('bugzilla notify command %s') %
441 util.explainexit(ret)[0])
452 util.explainexit(ret)[0])
442 self.ui.status(_('done\n'))
453 self.ui.status(_('done\n'))
443
454
444 def get_user_id(self, user):
455 def get_user_id(self, user):
445 '''look up numeric bugzilla user id.'''
456 '''look up numeric bugzilla user id.'''
446 try:
457 try:
447 return self.user_ids[user]
458 return self.user_ids[user]
448 except KeyError:
459 except KeyError:
449 try:
460 try:
450 userid = int(user)
461 userid = int(user)
451 except ValueError:
462 except ValueError:
452 self.ui.note(_('looking up user %s\n') % user)
463 self.ui.note(_('looking up user %s\n') % user)
453 self.run('''select userid from profiles
464 self.run('''select userid from profiles
454 where login_name like %s''', user)
465 where login_name like %s''', user)
455 all = self.cursor.fetchall()
466 all = self.cursor.fetchall()
456 if len(all) != 1:
467 if len(all) != 1:
457 raise KeyError(user)
468 raise KeyError(user)
458 userid = int(all[0][0])
469 userid = int(all[0][0])
459 self.user_ids[user] = userid
470 self.user_ids[user] = userid
460 return userid
471 return userid
461
472
462 def get_bugzilla_user(self, committer):
473 def get_bugzilla_user(self, committer):
463 '''See if committer is a registered bugzilla user. Return
474 '''See if committer is a registered bugzilla user. Return
464 bugzilla username and userid if so. If not, return default
475 bugzilla username and userid if so. If not, return default
465 bugzilla username and userid.'''
476 bugzilla username and userid.'''
466 user = self.map_committer(committer)
477 user = self.map_committer(committer)
467 try:
478 try:
468 userid = self.get_user_id(user)
479 userid = self.get_user_id(user)
469 except KeyError:
480 except KeyError:
470 try:
481 try:
471 defaultuser = self.ui.config('bugzilla', 'bzuser')
482 defaultuser = self.ui.config('bugzilla', 'bzuser')
472 if not defaultuser:
483 if not defaultuser:
473 raise error.Abort(_('cannot find bugzilla user id for %s') %
484 raise error.Abort(_('cannot find bugzilla user id for %s') %
474 user)
485 user)
475 userid = self.get_user_id(defaultuser)
486 userid = self.get_user_id(defaultuser)
476 user = defaultuser
487 user = defaultuser
477 except KeyError:
488 except KeyError:
478 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
489 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
479 % (user, defaultuser))
490 % (user, defaultuser))
480 return (user, userid)
491 return (user, userid)
481
492
482 def updatebug(self, bugid, newstate, text, committer):
493 def updatebug(self, bugid, newstate, text, committer):
483 '''update bug state with comment text.
494 '''update bug state with comment text.
484
495
485 Try adding comment as committer of changeset, otherwise as
496 Try adding comment as committer of changeset, otherwise as
486 default bugzilla user.'''
497 default bugzilla user.'''
487 if len(newstate) > 0:
498 if len(newstate) > 0:
488 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
499 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
489
500
490 (user, userid) = self.get_bugzilla_user(committer)
501 (user, userid) = self.get_bugzilla_user(committer)
491 now = time.strftime('%Y-%m-%d %H:%M:%S')
502 now = time.strftime('%Y-%m-%d %H:%M:%S')
492 self.run('''insert into longdescs
503 self.run('''insert into longdescs
493 (bug_id, who, bug_when, thetext)
504 (bug_id, who, bug_when, thetext)
494 values (%s, %s, %s, %s)''',
505 values (%s, %s, %s, %s)''',
495 (bugid, userid, now, text))
506 (bugid, userid, now, text))
496 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
507 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
497 values (%s, %s, %s, %s)''',
508 values (%s, %s, %s, %s)''',
498 (bugid, userid, now, self.longdesc_id))
509 (bugid, userid, now, self.longdesc_id))
499 self.conn.commit()
510 self.conn.commit()
500
511
501 class bzmysql_2_18(bzmysql):
512 class bzmysql_2_18(bzmysql):
502 '''support for bugzilla 2.18 series.'''
513 '''support for bugzilla 2.18 series.'''
503
514
504 def __init__(self, ui):
515 def __init__(self, ui):
505 bzmysql.__init__(self, ui)
516 bzmysql.__init__(self, ui)
506 self.default_notify = \
517 self.default_notify = \
507 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
518 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
508
519
509 class bzmysql_3_0(bzmysql_2_18):
520 class bzmysql_3_0(bzmysql_2_18):
510 '''support for bugzilla 3.0 series.'''
521 '''support for bugzilla 3.0 series.'''
511
522
512 def __init__(self, ui):
523 def __init__(self, ui):
513 bzmysql_2_18.__init__(self, ui)
524 bzmysql_2_18.__init__(self, ui)
514
525
515 def get_longdesc_id(self):
526 def get_longdesc_id(self):
516 '''get identity of longdesc field'''
527 '''get identity of longdesc field'''
517 self.run('select id from fielddefs where name = "longdesc"')
528 self.run('select id from fielddefs where name = "longdesc"')
518 ids = self.cursor.fetchall()
529 ids = self.cursor.fetchall()
519 if len(ids) != 1:
530 if len(ids) != 1:
520 raise error.Abort(_('unknown database schema'))
531 raise error.Abort(_('unknown database schema'))
521 return ids[0][0]
532 return ids[0][0]
522
533
523 # Bugzilla via XMLRPC interface.
534 # Bugzilla via XMLRPC interface.
524
535
525 class cookietransportrequest(object):
536 class cookietransportrequest(object):
526 """A Transport request method that retains cookies over its lifetime.
537 """A Transport request method that retains cookies over its lifetime.
527
538
528 The regular xmlrpclib transports ignore cookies. Which causes
539 The regular xmlrpclib transports ignore cookies. Which causes
529 a bit of a problem when you need a cookie-based login, as with
540 a bit of a problem when you need a cookie-based login, as with
530 the Bugzilla XMLRPC interface prior to 4.4.3.
541 the Bugzilla XMLRPC interface prior to 4.4.3.
531
542
532 So this is a helper for defining a Transport which looks for
543 So this is a helper for defining a Transport which looks for
533 cookies being set in responses and saves them to add to all future
544 cookies being set in responses and saves them to add to all future
534 requests.
545 requests.
535 """
546 """
536
547
537 # Inspiration drawn from
548 # Inspiration drawn from
538 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
549 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
539 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
550 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
540
551
541 cookies = []
552 cookies = []
542 def send_cookies(self, connection):
553 def send_cookies(self, connection):
543 if self.cookies:
554 if self.cookies:
544 for cookie in self.cookies:
555 for cookie in self.cookies:
545 connection.putheader("Cookie", cookie)
556 connection.putheader("Cookie", cookie)
546
557
547 def request(self, host, handler, request_body, verbose=0):
558 def request(self, host, handler, request_body, verbose=0):
548 self.verbose = verbose
559 self.verbose = verbose
549 self.accept_gzip_encoding = False
560 self.accept_gzip_encoding = False
550
561
551 # issue XML-RPC request
562 # issue XML-RPC request
552 h = self.make_connection(host)
563 h = self.make_connection(host)
553 if verbose:
564 if verbose:
554 h.set_debuglevel(1)
565 h.set_debuglevel(1)
555
566
556 self.send_request(h, handler, request_body)
567 self.send_request(h, handler, request_body)
557 self.send_host(h, host)
568 self.send_host(h, host)
558 self.send_cookies(h)
569 self.send_cookies(h)
559 self.send_user_agent(h)
570 self.send_user_agent(h)
560 self.send_content(h, request_body)
571 self.send_content(h, request_body)
561
572
562 # Deal with differences between Python 2.4-2.6 and 2.7.
573 # Deal with differences between Python 2.4-2.6 and 2.7.
563 # In the former h is a HTTP(S). In the latter it's a
574 # In the former h is a HTTP(S). In the latter it's a
564 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
575 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
565 # HTTP(S) has an underlying HTTP(S)Connection, so extract
576 # HTTP(S) has an underlying HTTP(S)Connection, so extract
566 # that and use it.
577 # that and use it.
567 try:
578 try:
568 response = h.getresponse()
579 response = h.getresponse()
569 except AttributeError:
580 except AttributeError:
570 response = h._conn.getresponse()
581 response = h._conn.getresponse()
571
582
572 # Add any cookie definitions to our list.
583 # Add any cookie definitions to our list.
573 for header in response.msg.getallmatchingheaders("Set-Cookie"):
584 for header in response.msg.getallmatchingheaders("Set-Cookie"):
574 val = header.split(": ", 1)[1]
585 val = header.split(": ", 1)[1]
575 cookie = val.split(";", 1)[0]
586 cookie = val.split(";", 1)[0]
576 self.cookies.append(cookie)
587 self.cookies.append(cookie)
577
588
578 if response.status != 200:
589 if response.status != 200:
579 raise xmlrpclib.ProtocolError(host + handler, response.status,
590 raise xmlrpclib.ProtocolError(host + handler, response.status,
580 response.reason, response.msg.headers)
591 response.reason, response.msg.headers)
581
592
582 payload = response.read()
593 payload = response.read()
583 parser, unmarshaller = self.getparser()
594 parser, unmarshaller = self.getparser()
584 parser.feed(payload)
595 parser.feed(payload)
585 parser.close()
596 parser.close()
586
597
587 return unmarshaller.close()
598 return unmarshaller.close()
588
599
589 # The explicit calls to the underlying xmlrpclib __init__() methods are
600 # The explicit calls to the underlying xmlrpclib __init__() methods are
590 # necessary. The xmlrpclib.Transport classes are old-style classes, and
601 # necessary. The xmlrpclib.Transport classes are old-style classes, and
591 # it turns out their __init__() doesn't get called when doing multiple
602 # it turns out their __init__() doesn't get called when doing multiple
592 # inheritance with a new-style class.
603 # inheritance with a new-style class.
593 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
604 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
594 def __init__(self, use_datetime=0):
605 def __init__(self, use_datetime=0):
595 if util.safehasattr(xmlrpclib.Transport, "__init__"):
606 if util.safehasattr(xmlrpclib.Transport, "__init__"):
596 xmlrpclib.Transport.__init__(self, use_datetime)
607 xmlrpclib.Transport.__init__(self, use_datetime)
597
608
598 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
609 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
599 def __init__(self, use_datetime=0):
610 def __init__(self, use_datetime=0):
600 if util.safehasattr(xmlrpclib.Transport, "__init__"):
611 if util.safehasattr(xmlrpclib.Transport, "__init__"):
601 xmlrpclib.SafeTransport.__init__(self, use_datetime)
612 xmlrpclib.SafeTransport.__init__(self, use_datetime)
602
613
603 class bzxmlrpc(bzaccess):
614 class bzxmlrpc(bzaccess):
604 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
615 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
605
616
606 Requires a minimum Bugzilla version 3.4.
617 Requires a minimum Bugzilla version 3.4.
607 """
618 """
608
619
609 def __init__(self, ui):
620 def __init__(self, ui):
610 bzaccess.__init__(self, ui)
621 bzaccess.__init__(self, ui)
611
622
612 bzweb = self.ui.config('bugzilla', 'bzurl',
623 bzweb = self.ui.config('bugzilla', 'bzurl',
613 'http://localhost/bugzilla/')
624 'http://localhost/bugzilla/')
614 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
625 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
615
626
616 user = self.ui.config('bugzilla', 'user', 'bugs')
627 user = self.ui.config('bugzilla', 'user', 'bugs')
617 passwd = self.ui.config('bugzilla', 'password')
628 passwd = self.ui.config('bugzilla', 'password')
618
629
619 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
630 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
620 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
631 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
621 'FIXED')
632 'FIXED')
622
633
623 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
634 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
624 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
635 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
625 self.bzvermajor = int(ver[0])
636 self.bzvermajor = int(ver[0])
626 self.bzverminor = int(ver[1])
637 self.bzverminor = int(ver[1])
627 login = self.bzproxy.User.login({'login': user, 'password': passwd,
638 login = self.bzproxy.User.login({'login': user, 'password': passwd,
628 'restrict_login': True})
639 'restrict_login': True})
629 self.bztoken = login.get('token', '')
640 self.bztoken = login.get('token', '')
630
641
631 def transport(self, uri):
642 def transport(self, uri):
632 if urlparse.urlparse(uri, "http")[0] == "https":
643 if urlparse.urlparse(uri, "http")[0] == "https":
633 return cookiesafetransport()
644 return cookiesafetransport()
634 else:
645 else:
635 return cookietransport()
646 return cookietransport()
636
647
637 def get_bug_comments(self, id):
648 def get_bug_comments(self, id):
638 """Return a string with all comment text for a bug."""
649 """Return a string with all comment text for a bug."""
639 c = self.bzproxy.Bug.comments({'ids': [id],
650 c = self.bzproxy.Bug.comments({'ids': [id],
640 'include_fields': ['text'],
651 'include_fields': ['text'],
641 'token': self.bztoken})
652 'token': self.bztoken})
642 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
653 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
643
654
644 def filter_real_bug_ids(self, bugs):
655 def filter_real_bug_ids(self, bugs):
645 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
656 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
646 'include_fields': [],
657 'include_fields': [],
647 'permissive': True,
658 'permissive': True,
648 'token': self.bztoken,
659 'token': self.bztoken,
649 })
660 })
650 for badbug in probe['faults']:
661 for badbug in probe['faults']:
651 id = badbug['id']
662 id = badbug['id']
652 self.ui.status(_('bug %d does not exist\n') % id)
663 self.ui.status(_('bug %d does not exist\n') % id)
653 del bugs[id]
664 del bugs[id]
654
665
655 def filter_cset_known_bug_ids(self, node, bugs):
666 def filter_cset_known_bug_ids(self, node, bugs):
656 for id in sorted(bugs.keys()):
667 for id in sorted(bugs.keys()):
657 if self.get_bug_comments(id).find(short(node)) != -1:
668 if self.get_bug_comments(id).find(short(node)) != -1:
658 self.ui.status(_('bug %d already knows about changeset %s\n') %
669 self.ui.status(_('bug %d already knows about changeset %s\n') %
659 (id, short(node)))
670 (id, short(node)))
660 del bugs[id]
671 del bugs[id]
661
672
662 def updatebug(self, bugid, newstate, text, committer):
673 def updatebug(self, bugid, newstate, text, committer):
663 args = {}
674 args = {}
664 if 'hours' in newstate:
675 if 'hours' in newstate:
665 args['work_time'] = newstate['hours']
676 args['work_time'] = newstate['hours']
666
677
667 if self.bzvermajor >= 4:
678 if self.bzvermajor >= 4:
668 args['ids'] = [bugid]
679 args['ids'] = [bugid]
669 args['comment'] = {'body' : text}
680 args['comment'] = {'body' : text}
670 if 'fix' in newstate:
681 if 'fix' in newstate:
671 args['status'] = self.fixstatus
682 args['status'] = self.fixstatus
672 args['resolution'] = self.fixresolution
683 args['resolution'] = self.fixresolution
673 args['token'] = self.bztoken
684 args['token'] = self.bztoken
674 self.bzproxy.Bug.update(args)
685 self.bzproxy.Bug.update(args)
675 else:
686 else:
676 if 'fix' in newstate:
687 if 'fix' in newstate:
677 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
688 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
678 "to mark bugs fixed\n"))
689 "to mark bugs fixed\n"))
679 args['id'] = bugid
690 args['id'] = bugid
680 args['comment'] = text
691 args['comment'] = text
681 self.bzproxy.Bug.add_comment(args)
692 self.bzproxy.Bug.add_comment(args)
682
693
683 class bzxmlrpcemail(bzxmlrpc):
694 class bzxmlrpcemail(bzxmlrpc):
684 """Read data from Bugzilla via XMLRPC, send updates via email.
695 """Read data from Bugzilla via XMLRPC, send updates via email.
685
696
686 Advantages of sending updates via email:
697 Advantages of sending updates via email:
687 1. Comments can be added as any user, not just logged in user.
698 1. Comments can be added as any user, not just logged in user.
688 2. Bug statuses or other fields not accessible via XMLRPC can
699 2. Bug statuses or other fields not accessible via XMLRPC can
689 potentially be updated.
700 potentially be updated.
690
701
691 There is no XMLRPC function to change bug status before Bugzilla
702 There is no XMLRPC function to change bug status before Bugzilla
692 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
703 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
693 But bugs can be marked fixed via email from 3.4 onwards.
704 But bugs can be marked fixed via email from 3.4 onwards.
694 """
705 """
695
706
696 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
707 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
697 # in-email fields are specified as '@<fieldname> = <value>'. In
708 # in-email fields are specified as '@<fieldname> = <value>'. In
698 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
709 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
699 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
710 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
700 # compatibility, but rather than rely on this use the new format for
711 # compatibility, but rather than rely on this use the new format for
701 # 4.0 onwards.
712 # 4.0 onwards.
702
713
703 def __init__(self, ui):
714 def __init__(self, ui):
704 bzxmlrpc.__init__(self, ui)
715 bzxmlrpc.__init__(self, ui)
705
716
706 self.bzemail = self.ui.config('bugzilla', 'bzemail')
717 self.bzemail = self.ui.config('bugzilla', 'bzemail')
707 if not self.bzemail:
718 if not self.bzemail:
708 raise error.Abort(_("configuration 'bzemail' missing"))
719 raise error.Abort(_("configuration 'bzemail' missing"))
709 mail.validateconfig(self.ui)
720 mail.validateconfig(self.ui)
710
721
711 def makecommandline(self, fieldname, value):
722 def makecommandline(self, fieldname, value):
712 if self.bzvermajor >= 4:
723 if self.bzvermajor >= 4:
713 return "@%s %s" % (fieldname, str(value))
724 return "@%s %s" % (fieldname, str(value))
714 else:
725 else:
715 if fieldname == "id":
726 if fieldname == "id":
716 fieldname = "bug_id"
727 fieldname = "bug_id"
717 return "@%s = %s" % (fieldname, str(value))
728 return "@%s = %s" % (fieldname, str(value))
718
729
719 def send_bug_modify_email(self, bugid, commands, comment, committer):
730 def send_bug_modify_email(self, bugid, commands, comment, committer):
720 '''send modification message to Bugzilla bug via email.
731 '''send modification message to Bugzilla bug via email.
721
732
722 The message format is documented in the Bugzilla email_in.pl
733 The message format is documented in the Bugzilla email_in.pl
723 specification. commands is a list of command lines, comment is the
734 specification. commands is a list of command lines, comment is the
724 comment text.
735 comment text.
725
736
726 To stop users from crafting commit comments with
737 To stop users from crafting commit comments with
727 Bugzilla commands, specify the bug ID via the message body, rather
738 Bugzilla commands, specify the bug ID via the message body, rather
728 than the subject line, and leave a blank line after it.
739 than the subject line, and leave a blank line after it.
729 '''
740 '''
730 user = self.map_committer(committer)
741 user = self.map_committer(committer)
731 matches = self.bzproxy.User.get({'match': [user],
742 matches = self.bzproxy.User.get({'match': [user],
732 'token': self.bztoken})
743 'token': self.bztoken})
733 if not matches['users']:
744 if not matches['users']:
734 user = self.ui.config('bugzilla', 'user', 'bugs')
745 user = self.ui.config('bugzilla', 'user', 'bugs')
735 matches = self.bzproxy.User.get({'match': [user],
746 matches = self.bzproxy.User.get({'match': [user],
736 'token': self.bztoken})
747 'token': self.bztoken})
737 if not matches['users']:
748 if not matches['users']:
738 raise error.Abort(_("default bugzilla user %s email not found")
749 raise error.Abort(_("default bugzilla user %s email not found")
739 % user)
750 % user)
740 user = matches['users'][0]['email']
751 user = matches['users'][0]['email']
741 commands.append(self.makecommandline("id", bugid))
752 commands.append(self.makecommandline("id", bugid))
742
753
743 text = "\n".join(commands) + "\n\n" + comment
754 text = "\n".join(commands) + "\n\n" + comment
744
755
745 _charsets = mail._charsets(self.ui)
756 _charsets = mail._charsets(self.ui)
746 user = mail.addressencode(self.ui, user, _charsets)
757 user = mail.addressencode(self.ui, user, _charsets)
747 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
758 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
748 msg = mail.mimeencode(self.ui, text, _charsets)
759 msg = mail.mimeencode(self.ui, text, _charsets)
749 msg['From'] = user
760 msg['From'] = user
750 msg['To'] = bzemail
761 msg['To'] = bzemail
751 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
762 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
752 sendmail = mail.connect(self.ui)
763 sendmail = mail.connect(self.ui)
753 sendmail(user, bzemail, msg.as_string())
764 sendmail(user, bzemail, msg.as_string())
754
765
755 def updatebug(self, bugid, newstate, text, committer):
766 def updatebug(self, bugid, newstate, text, committer):
756 cmds = []
767 cmds = []
757 if 'hours' in newstate:
768 if 'hours' in newstate:
758 cmds.append(self.makecommandline("work_time", newstate['hours']))
769 cmds.append(self.makecommandline("work_time", newstate['hours']))
759 if 'fix' in newstate:
770 if 'fix' in newstate:
760 cmds.append(self.makecommandline("bug_status", self.fixstatus))
771 cmds.append(self.makecommandline("bug_status", self.fixstatus))
761 cmds.append(self.makecommandline("resolution", self.fixresolution))
772 cmds.append(self.makecommandline("resolution", self.fixresolution))
762 self.send_bug_modify_email(bugid, cmds, text, committer)
773 self.send_bug_modify_email(bugid, cmds, text, committer)
763
774
764 class bugzilla(object):
775 class bugzilla(object):
765 # supported versions of bugzilla. different versions have
776 # supported versions of bugzilla. different versions have
766 # different schemas.
777 # different schemas.
767 _versions = {
778 _versions = {
768 '2.16': bzmysql,
779 '2.16': bzmysql,
769 '2.18': bzmysql_2_18,
780 '2.18': bzmysql_2_18,
770 '3.0': bzmysql_3_0,
781 '3.0': bzmysql_3_0,
771 'xmlrpc': bzxmlrpc,
782 'xmlrpc': bzxmlrpc,
772 'xmlrpc+email': bzxmlrpcemail
783 'xmlrpc+email': bzxmlrpcemail
773 }
784 }
774
785
775 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
786 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
776 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
787 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
777 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
788 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
778
789
779 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
790 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
780 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
791 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
781 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
792 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
782 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
793 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
783
794
784 def __init__(self, ui, repo):
795 def __init__(self, ui, repo):
785 self.ui = ui
796 self.ui = ui
786 self.repo = repo
797 self.repo = repo
787
798
788 bzversion = self.ui.config('bugzilla', 'version')
799 bzversion = self.ui.config('bugzilla', 'version')
789 try:
800 try:
790 bzclass = bugzilla._versions[bzversion]
801 bzclass = bugzilla._versions[bzversion]
791 except KeyError:
802 except KeyError:
792 raise error.Abort(_('bugzilla version %s not supported') %
803 raise error.Abort(_('bugzilla version %s not supported') %
793 bzversion)
804 bzversion)
794 self.bzdriver = bzclass(self.ui)
805 self.bzdriver = bzclass(self.ui)
795
806
796 self.bug_re = re.compile(
807 self.bug_re = re.compile(
797 self.ui.config('bugzilla', 'regexp',
808 self.ui.config('bugzilla', 'regexp',
798 bugzilla._default_bug_re), re.IGNORECASE)
809 bugzilla._default_bug_re), re.IGNORECASE)
799 self.fix_re = re.compile(
810 self.fix_re = re.compile(
800 self.ui.config('bugzilla', 'fixregexp',
811 self.ui.config('bugzilla', 'fixregexp',
801 bugzilla._default_fix_re), re.IGNORECASE)
812 bugzilla._default_fix_re), re.IGNORECASE)
802 self.split_re = re.compile(r'\D+')
813 self.split_re = re.compile(r'\D+')
803
814
804 def find_bugs(self, ctx):
815 def find_bugs(self, ctx):
805 '''return bugs dictionary created from commit comment.
816 '''return bugs dictionary created from commit comment.
806
817
807 Extract bug info from changeset comments. Filter out any that are
818 Extract bug info from changeset comments. Filter out any that are
808 not known to Bugzilla, and any that already have a reference to
819 not known to Bugzilla, and any that already have a reference to
809 the given changeset in their comments.
820 the given changeset in their comments.
810 '''
821 '''
811 start = 0
822 start = 0
812 hours = 0.0
823 hours = 0.0
813 bugs = {}
824 bugs = {}
814 bugmatch = self.bug_re.search(ctx.description(), start)
825 bugmatch = self.bug_re.search(ctx.description(), start)
815 fixmatch = self.fix_re.search(ctx.description(), start)
826 fixmatch = self.fix_re.search(ctx.description(), start)
816 while True:
827 while True:
817 bugattribs = {}
828 bugattribs = {}
818 if not bugmatch and not fixmatch:
829 if not bugmatch and not fixmatch:
819 break
830 break
820 if not bugmatch:
831 if not bugmatch:
821 m = fixmatch
832 m = fixmatch
822 elif not fixmatch:
833 elif not fixmatch:
823 m = bugmatch
834 m = bugmatch
824 else:
835 else:
825 if bugmatch.start() < fixmatch.start():
836 if bugmatch.start() < fixmatch.start():
826 m = bugmatch
837 m = bugmatch
827 else:
838 else:
828 m = fixmatch
839 m = fixmatch
829 start = m.end()
840 start = m.end()
830 if m is bugmatch:
841 if m is bugmatch:
831 bugmatch = self.bug_re.search(ctx.description(), start)
842 bugmatch = self.bug_re.search(ctx.description(), start)
832 if 'fix' in bugattribs:
843 if 'fix' in bugattribs:
833 del bugattribs['fix']
844 del bugattribs['fix']
834 else:
845 else:
835 fixmatch = self.fix_re.search(ctx.description(), start)
846 fixmatch = self.fix_re.search(ctx.description(), start)
836 bugattribs['fix'] = None
847 bugattribs['fix'] = None
837
848
838 try:
849 try:
839 ids = m.group('ids')
850 ids = m.group('ids')
840 except IndexError:
851 except IndexError:
841 ids = m.group(1)
852 ids = m.group(1)
842 try:
853 try:
843 hours = float(m.group('hours'))
854 hours = float(m.group('hours'))
844 bugattribs['hours'] = hours
855 bugattribs['hours'] = hours
845 except IndexError:
856 except IndexError:
846 pass
857 pass
847 except TypeError:
858 except TypeError:
848 pass
859 pass
849 except ValueError:
860 except ValueError:
850 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
861 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
851
862
852 for id in self.split_re.split(ids):
863 for id in self.split_re.split(ids):
853 if not id:
864 if not id:
854 continue
865 continue
855 bugs[int(id)] = bugattribs
866 bugs[int(id)] = bugattribs
856 if bugs:
867 if bugs:
857 self.bzdriver.filter_real_bug_ids(bugs)
868 self.bzdriver.filter_real_bug_ids(bugs)
858 if bugs:
869 if bugs:
859 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
870 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
860 return bugs
871 return bugs
861
872
862 def update(self, bugid, newstate, ctx):
873 def update(self, bugid, newstate, ctx):
863 '''update bugzilla bug with reference to changeset.'''
874 '''update bugzilla bug with reference to changeset.'''
864
875
865 def webroot(root):
876 def webroot(root):
866 '''strip leading prefix of repo root and turn into
877 '''strip leading prefix of repo root and turn into
867 url-safe path.'''
878 url-safe path.'''
868 count = int(self.ui.config('bugzilla', 'strip', 0))
879 count = int(self.ui.config('bugzilla', 'strip', 0))
869 root = util.pconvert(root)
880 root = util.pconvert(root)
870 while count > 0:
881 while count > 0:
871 c = root.find('/')
882 c = root.find('/')
872 if c == -1:
883 if c == -1:
873 break
884 break
874 root = root[c + 1:]
885 root = root[c + 1:]
875 count -= 1
886 count -= 1
876 return root
887 return root
877
888
878 mapfile = self.ui.config('bugzilla', 'style')
889 mapfile = self.ui.config('bugzilla', 'style')
879 tmpl = self.ui.config('bugzilla', 'template')
890 tmpl = self.ui.config('bugzilla', 'template')
880 if not mapfile and not tmpl:
891 if not mapfile and not tmpl:
881 tmpl = _('changeset {node|short} in repo {root} refers '
892 tmpl = _('changeset {node|short} in repo {root} refers '
882 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
893 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
883 t = cmdutil.changeset_templater(self.ui, self.repo,
894 t = cmdutil.changeset_templater(self.ui, self.repo,
884 False, None, tmpl, mapfile, False)
895 False, None, tmpl, mapfile, False)
885 self.ui.pushbuffer()
896 self.ui.pushbuffer()
886 t.show(ctx, changes=ctx.changeset(),
897 t.show(ctx, changes=ctx.changeset(),
887 bug=str(bugid),
898 bug=str(bugid),
888 hgweb=self.ui.config('web', 'baseurl'),
899 hgweb=self.ui.config('web', 'baseurl'),
889 root=self.repo.root,
900 root=self.repo.root,
890 webroot=webroot(self.repo.root))
901 webroot=webroot(self.repo.root))
891 data = self.ui.popbuffer()
902 data = self.ui.popbuffer()
892 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
903 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
893
904
894 def notify(self, bugs, committer):
905 def notify(self, bugs, committer):
895 '''ensure Bugzilla users are notified of bug change.'''
906 '''ensure Bugzilla users are notified of bug change.'''
896 self.bzdriver.notify(bugs, committer)
907 self.bzdriver.notify(bugs, committer)
897
908
898 def hook(ui, repo, hooktype, node=None, **kwargs):
909 def hook(ui, repo, hooktype, node=None, **kwargs):
899 '''add comment to bugzilla for each changeset that refers to a
910 '''add comment to bugzilla for each changeset that refers to a
900 bugzilla bug id. only add a comment once per bug, so same change
911 bugzilla bug id. only add a comment once per bug, so same change
901 seen multiple times does not fill bug with duplicate data.'''
912 seen multiple times does not fill bug with duplicate data.'''
902 if node is None:
913 if node is None:
903 raise error.Abort(_('hook type %s does not pass a changeset id') %
914 raise error.Abort(_('hook type %s does not pass a changeset id') %
904 hooktype)
915 hooktype)
905 try:
916 try:
906 bz = bugzilla(ui, repo)
917 bz = bugzilla(ui, repo)
907 ctx = repo[node]
918 ctx = repo[node]
908 bugs = bz.find_bugs(ctx)
919 bugs = bz.find_bugs(ctx)
909 if bugs:
920 if bugs:
910 for bug in bugs:
921 for bug in bugs:
911 bz.update(bug, bugs[bug], ctx)
922 bz.update(bug, bugs[bug], ctx)
912 bz.notify(bugs, util.email(ctx.user()))
923 bz.notify(bugs, util.email(ctx.user()))
913 except Exception as e:
924 except Exception as e:
914 raise error.Abort(_('Bugzilla error: %s') % e)
925 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,179 +1,178 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ cd "$TESTDIR"/..
3 $ cd "$TESTDIR"/..
4
4
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
5 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 contrib/casesmash.py not using absolute_import
6 contrib/casesmash.py not using absolute_import
7 contrib/check-code.py not using absolute_import
7 contrib/check-code.py not using absolute_import
8 contrib/check-code.py requires print_function
8 contrib/check-code.py requires print_function
9 contrib/check-config.py not using absolute_import
9 contrib/check-config.py not using absolute_import
10 contrib/check-config.py requires print_function
10 contrib/check-config.py requires print_function
11 contrib/debugcmdserver.py not using absolute_import
11 contrib/debugcmdserver.py not using absolute_import
12 contrib/debugcmdserver.py requires print_function
12 contrib/debugcmdserver.py requires print_function
13 contrib/debugshell.py not using absolute_import
13 contrib/debugshell.py not using absolute_import
14 contrib/fixpax.py not using absolute_import
14 contrib/fixpax.py not using absolute_import
15 contrib/fixpax.py requires print_function
15 contrib/fixpax.py requires print_function
16 contrib/hgclient.py not using absolute_import
16 contrib/hgclient.py not using absolute_import
17 contrib/hgclient.py requires print_function
17 contrib/hgclient.py requires print_function
18 contrib/hgfixes/fix_bytes.py not using absolute_import
18 contrib/hgfixes/fix_bytes.py not using absolute_import
19 contrib/hgfixes/fix_bytesmod.py not using absolute_import
19 contrib/hgfixes/fix_bytesmod.py not using absolute_import
20 contrib/hgfixes/fix_leftover_imports.py not using absolute_import
20 contrib/hgfixes/fix_leftover_imports.py not using absolute_import
21 contrib/import-checker.py not using absolute_import
21 contrib/import-checker.py not using absolute_import
22 contrib/import-checker.py requires print_function
22 contrib/import-checker.py requires print_function
23 contrib/memory.py not using absolute_import
23 contrib/memory.py not using absolute_import
24 contrib/perf.py not using absolute_import
24 contrib/perf.py not using absolute_import
25 contrib/python-hook-examples.py not using absolute_import
25 contrib/python-hook-examples.py not using absolute_import
26 contrib/revsetbenchmarks.py not using absolute_import
26 contrib/revsetbenchmarks.py not using absolute_import
27 contrib/revsetbenchmarks.py requires print_function
27 contrib/revsetbenchmarks.py requires print_function
28 contrib/showstack.py not using absolute_import
28 contrib/showstack.py not using absolute_import
29 contrib/synthrepo.py not using absolute_import
29 contrib/synthrepo.py not using absolute_import
30 contrib/win32/hgwebdir_wsgi.py not using absolute_import
30 contrib/win32/hgwebdir_wsgi.py not using absolute_import
31 doc/check-seclevel.py not using absolute_import
31 doc/check-seclevel.py not using absolute_import
32 doc/gendoc.py not using absolute_import
32 doc/gendoc.py not using absolute_import
33 doc/hgmanpage.py not using absolute_import
33 doc/hgmanpage.py not using absolute_import
34 hgext/__init__.py not using absolute_import
34 hgext/__init__.py not using absolute_import
35 hgext/bugzilla.py not using absolute_import
36 hgext/censor.py not using absolute_import
35 hgext/censor.py not using absolute_import
37 hgext/children.py not using absolute_import
36 hgext/children.py not using absolute_import
38 hgext/churn.py not using absolute_import
37 hgext/churn.py not using absolute_import
39 hgext/clonebundles.py not using absolute_import
38 hgext/clonebundles.py not using absolute_import
40 hgext/color.py not using absolute_import
39 hgext/color.py not using absolute_import
41 hgext/convert/__init__.py not using absolute_import
40 hgext/convert/__init__.py not using absolute_import
42 hgext/convert/bzr.py not using absolute_import
41 hgext/convert/bzr.py not using absolute_import
43 hgext/convert/common.py not using absolute_import
42 hgext/convert/common.py not using absolute_import
44 hgext/convert/convcmd.py not using absolute_import
43 hgext/convert/convcmd.py not using absolute_import
45 hgext/convert/cvs.py not using absolute_import
44 hgext/convert/cvs.py not using absolute_import
46 hgext/convert/cvsps.py not using absolute_import
45 hgext/convert/cvsps.py not using absolute_import
47 hgext/convert/darcs.py not using absolute_import
46 hgext/convert/darcs.py not using absolute_import
48 hgext/convert/filemap.py not using absolute_import
47 hgext/convert/filemap.py not using absolute_import
49 hgext/convert/git.py not using absolute_import
48 hgext/convert/git.py not using absolute_import
50 hgext/convert/gnuarch.py not using absolute_import
49 hgext/convert/gnuarch.py not using absolute_import
51 hgext/convert/hg.py not using absolute_import
50 hgext/convert/hg.py not using absolute_import
52 hgext/convert/monotone.py not using absolute_import
51 hgext/convert/monotone.py not using absolute_import
53 hgext/convert/p4.py not using absolute_import
52 hgext/convert/p4.py not using absolute_import
54 hgext/convert/subversion.py not using absolute_import
53 hgext/convert/subversion.py not using absolute_import
55 hgext/convert/transport.py not using absolute_import
54 hgext/convert/transport.py not using absolute_import
56 hgext/eol.py not using absolute_import
55 hgext/eol.py not using absolute_import
57 hgext/extdiff.py not using absolute_import
56 hgext/extdiff.py not using absolute_import
58 hgext/factotum.py not using absolute_import
57 hgext/factotum.py not using absolute_import
59 hgext/fetch.py not using absolute_import
58 hgext/fetch.py not using absolute_import
60 hgext/gpg.py not using absolute_import
59 hgext/gpg.py not using absolute_import
61 hgext/graphlog.py not using absolute_import
60 hgext/graphlog.py not using absolute_import
62 hgext/hgcia.py not using absolute_import
61 hgext/hgcia.py not using absolute_import
63 hgext/hgk.py not using absolute_import
62 hgext/hgk.py not using absolute_import
64 hgext/highlight/__init__.py not using absolute_import
63 hgext/highlight/__init__.py not using absolute_import
65 hgext/highlight/highlight.py not using absolute_import
64 hgext/highlight/highlight.py not using absolute_import
66 hgext/histedit.py not using absolute_import
65 hgext/histedit.py not using absolute_import
67 hgext/keyword.py not using absolute_import
66 hgext/keyword.py not using absolute_import
68 hgext/largefiles/__init__.py not using absolute_import
67 hgext/largefiles/__init__.py not using absolute_import
69 hgext/largefiles/basestore.py not using absolute_import
68 hgext/largefiles/basestore.py not using absolute_import
70 hgext/largefiles/lfcommands.py not using absolute_import
69 hgext/largefiles/lfcommands.py not using absolute_import
71 hgext/largefiles/lfutil.py not using absolute_import
70 hgext/largefiles/lfutil.py not using absolute_import
72 hgext/largefiles/localstore.py not using absolute_import
71 hgext/largefiles/localstore.py not using absolute_import
73 hgext/largefiles/overrides.py not using absolute_import
72 hgext/largefiles/overrides.py not using absolute_import
74 hgext/largefiles/proto.py not using absolute_import
73 hgext/largefiles/proto.py not using absolute_import
75 hgext/largefiles/remotestore.py not using absolute_import
74 hgext/largefiles/remotestore.py not using absolute_import
76 hgext/largefiles/reposetup.py not using absolute_import
75 hgext/largefiles/reposetup.py not using absolute_import
77 hgext/largefiles/uisetup.py not using absolute_import
76 hgext/largefiles/uisetup.py not using absolute_import
78 hgext/largefiles/wirestore.py not using absolute_import
77 hgext/largefiles/wirestore.py not using absolute_import
79 hgext/mq.py not using absolute_import
78 hgext/mq.py not using absolute_import
80 hgext/notify.py not using absolute_import
79 hgext/notify.py not using absolute_import
81 hgext/pager.py not using absolute_import
80 hgext/pager.py not using absolute_import
82 hgext/patchbomb.py not using absolute_import
81 hgext/patchbomb.py not using absolute_import
83 hgext/purge.py not using absolute_import
82 hgext/purge.py not using absolute_import
84 hgext/rebase.py not using absolute_import
83 hgext/rebase.py not using absolute_import
85 hgext/record.py not using absolute_import
84 hgext/record.py not using absolute_import
86 hgext/relink.py not using absolute_import
85 hgext/relink.py not using absolute_import
87 hgext/schemes.py not using absolute_import
86 hgext/schemes.py not using absolute_import
88 hgext/share.py not using absolute_import
87 hgext/share.py not using absolute_import
89 hgext/shelve.py not using absolute_import
88 hgext/shelve.py not using absolute_import
90 hgext/strip.py not using absolute_import
89 hgext/strip.py not using absolute_import
91 hgext/transplant.py not using absolute_import
90 hgext/transplant.py not using absolute_import
92 hgext/win32mbcs.py not using absolute_import
91 hgext/win32mbcs.py not using absolute_import
93 hgext/win32text.py not using absolute_import
92 hgext/win32text.py not using absolute_import
94 hgext/zeroconf/Zeroconf.py not using absolute_import
93 hgext/zeroconf/Zeroconf.py not using absolute_import
95 hgext/zeroconf/Zeroconf.py requires print_function
94 hgext/zeroconf/Zeroconf.py requires print_function
96 hgext/zeroconf/__init__.py not using absolute_import
95 hgext/zeroconf/__init__.py not using absolute_import
97 i18n/check-translation.py not using absolute_import
96 i18n/check-translation.py not using absolute_import
98 i18n/polib.py not using absolute_import
97 i18n/polib.py not using absolute_import
99 mercurial/cmdutil.py not using absolute_import
98 mercurial/cmdutil.py not using absolute_import
100 mercurial/commands.py not using absolute_import
99 mercurial/commands.py not using absolute_import
101 setup.py not using absolute_import
100 setup.py not using absolute_import
102 tests/filterpyflakes.py requires print_function
101 tests/filterpyflakes.py requires print_function
103 tests/generate-working-copy-states.py requires print_function
102 tests/generate-working-copy-states.py requires print_function
104 tests/get-with-headers.py requires print_function
103 tests/get-with-headers.py requires print_function
105 tests/heredoctest.py requires print_function
104 tests/heredoctest.py requires print_function
106 tests/hypothesishelpers.py not using absolute_import
105 tests/hypothesishelpers.py not using absolute_import
107 tests/hypothesishelpers.py requires print_function
106 tests/hypothesishelpers.py requires print_function
108 tests/killdaemons.py not using absolute_import
107 tests/killdaemons.py not using absolute_import
109 tests/md5sum.py not using absolute_import
108 tests/md5sum.py not using absolute_import
110 tests/mockblackbox.py not using absolute_import
109 tests/mockblackbox.py not using absolute_import
111 tests/printenv.py not using absolute_import
110 tests/printenv.py not using absolute_import
112 tests/readlink.py not using absolute_import
111 tests/readlink.py not using absolute_import
113 tests/readlink.py requires print_function
112 tests/readlink.py requires print_function
114 tests/revlog-formatv0.py not using absolute_import
113 tests/revlog-formatv0.py not using absolute_import
115 tests/run-tests.py not using absolute_import
114 tests/run-tests.py not using absolute_import
116 tests/seq.py not using absolute_import
115 tests/seq.py not using absolute_import
117 tests/seq.py requires print_function
116 tests/seq.py requires print_function
118 tests/silenttestrunner.py not using absolute_import
117 tests/silenttestrunner.py not using absolute_import
119 tests/silenttestrunner.py requires print_function
118 tests/silenttestrunner.py requires print_function
120 tests/sitecustomize.py not using absolute_import
119 tests/sitecustomize.py not using absolute_import
121 tests/svn-safe-append.py not using absolute_import
120 tests/svn-safe-append.py not using absolute_import
122 tests/svnxml.py not using absolute_import
121 tests/svnxml.py not using absolute_import
123 tests/test-ancestor.py requires print_function
122 tests/test-ancestor.py requires print_function
124 tests/test-atomictempfile.py not using absolute_import
123 tests/test-atomictempfile.py not using absolute_import
125 tests/test-batching.py not using absolute_import
124 tests/test-batching.py not using absolute_import
126 tests/test-batching.py requires print_function
125 tests/test-batching.py requires print_function
127 tests/test-bdiff.py not using absolute_import
126 tests/test-bdiff.py not using absolute_import
128 tests/test-bdiff.py requires print_function
127 tests/test-bdiff.py requires print_function
129 tests/test-context.py not using absolute_import
128 tests/test-context.py not using absolute_import
130 tests/test-context.py requires print_function
129 tests/test-context.py requires print_function
131 tests/test-demandimport.py not using absolute_import
130 tests/test-demandimport.py not using absolute_import
132 tests/test-demandimport.py requires print_function
131 tests/test-demandimport.py requires print_function
133 tests/test-dispatch.py not using absolute_import
132 tests/test-dispatch.py not using absolute_import
134 tests/test-dispatch.py requires print_function
133 tests/test-dispatch.py requires print_function
135 tests/test-doctest.py not using absolute_import
134 tests/test-doctest.py not using absolute_import
136 tests/test-duplicateoptions.py not using absolute_import
135 tests/test-duplicateoptions.py not using absolute_import
137 tests/test-duplicateoptions.py requires print_function
136 tests/test-duplicateoptions.py requires print_function
138 tests/test-filecache.py not using absolute_import
137 tests/test-filecache.py not using absolute_import
139 tests/test-filecache.py requires print_function
138 tests/test-filecache.py requires print_function
140 tests/test-filelog.py not using absolute_import
139 tests/test-filelog.py not using absolute_import
141 tests/test-filelog.py requires print_function
140 tests/test-filelog.py requires print_function
142 tests/test-hg-parseurl.py not using absolute_import
141 tests/test-hg-parseurl.py not using absolute_import
143 tests/test-hg-parseurl.py requires print_function
142 tests/test-hg-parseurl.py requires print_function
144 tests/test-hgweb-auth.py not using absolute_import
143 tests/test-hgweb-auth.py not using absolute_import
145 tests/test-hgweb-auth.py requires print_function
144 tests/test-hgweb-auth.py requires print_function
146 tests/test-hgwebdir-paths.py not using absolute_import
145 tests/test-hgwebdir-paths.py not using absolute_import
147 tests/test-hybridencode.py not using absolute_import
146 tests/test-hybridencode.py not using absolute_import
148 tests/test-hybridencode.py requires print_function
147 tests/test-hybridencode.py requires print_function
149 tests/test-lrucachedict.py not using absolute_import
148 tests/test-lrucachedict.py not using absolute_import
150 tests/test-lrucachedict.py requires print_function
149 tests/test-lrucachedict.py requires print_function
151 tests/test-manifest.py not using absolute_import
150 tests/test-manifest.py not using absolute_import
152 tests/test-minirst.py not using absolute_import
151 tests/test-minirst.py not using absolute_import
153 tests/test-minirst.py requires print_function
152 tests/test-minirst.py requires print_function
154 tests/test-parseindex2.py not using absolute_import
153 tests/test-parseindex2.py not using absolute_import
155 tests/test-parseindex2.py requires print_function
154 tests/test-parseindex2.py requires print_function
156 tests/test-pathencode.py not using absolute_import
155 tests/test-pathencode.py not using absolute_import
157 tests/test-pathencode.py requires print_function
156 tests/test-pathencode.py requires print_function
158 tests/test-propertycache.py not using absolute_import
157 tests/test-propertycache.py not using absolute_import
159 tests/test-propertycache.py requires print_function
158 tests/test-propertycache.py requires print_function
160 tests/test-revlog-ancestry.py not using absolute_import
159 tests/test-revlog-ancestry.py not using absolute_import
161 tests/test-revlog-ancestry.py requires print_function
160 tests/test-revlog-ancestry.py requires print_function
162 tests/test-run-tests.py not using absolute_import
161 tests/test-run-tests.py not using absolute_import
163 tests/test-simplemerge.py not using absolute_import
162 tests/test-simplemerge.py not using absolute_import
164 tests/test-status-inprocess.py not using absolute_import
163 tests/test-status-inprocess.py not using absolute_import
165 tests/test-status-inprocess.py requires print_function
164 tests/test-status-inprocess.py requires print_function
166 tests/test-symlink-os-yes-fs-no.py not using absolute_import
165 tests/test-symlink-os-yes-fs-no.py not using absolute_import
167 tests/test-trusted.py not using absolute_import
166 tests/test-trusted.py not using absolute_import
168 tests/test-trusted.py requires print_function
167 tests/test-trusted.py requires print_function
169 tests/test-ui-color.py not using absolute_import
168 tests/test-ui-color.py not using absolute_import
170 tests/test-ui-color.py requires print_function
169 tests/test-ui-color.py requires print_function
171 tests/test-ui-config.py not using absolute_import
170 tests/test-ui-config.py not using absolute_import
172 tests/test-ui-config.py requires print_function
171 tests/test-ui-config.py requires print_function
173 tests/test-ui-verbosity.py not using absolute_import
172 tests/test-ui-verbosity.py not using absolute_import
174 tests/test-ui-verbosity.py requires print_function
173 tests/test-ui-verbosity.py requires print_function
175 tests/test-url.py not using absolute_import
174 tests/test-url.py not using absolute_import
176 tests/test-url.py requires print_function
175 tests/test-url.py requires print_function
177 tests/test-walkrepo.py requires print_function
176 tests/test-walkrepo.py requires print_function
178 tests/test-wireproto.py requires print_function
177 tests/test-wireproto.py requires print_function
179 tests/tinyproxy.py requires print_function
178 tests/tinyproxy.py requires print_function
General Comments 0
You need to be logged in to leave comments. Login now