##// END OF EJS Templates
merge with stable
Augie Fackler -
r32652:9929af2b merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,53 +1,54 b''
1 1 Source: mercurial
2 2 Section: vcs
3 3 Priority: optional
4 4 Maintainer: Mercurial Developers <mercurial-devel@mercurial-scm.org>
5 5 Build-Depends:
6 6 debhelper (>= 9),
7 7 dh-python,
8 less,
8 9 netbase,
9 10 python-all,
10 11 python-all-dev,
11 12 python-docutils,
12 13 unzip,
13 14 zip
14 15 Standards-Version: 3.9.4
15 16 X-Python-Version: >= 2.7
16 17
17 18 Package: mercurial
18 19 Depends:
19 20 python,
20 21 ${shlibs:Depends},
21 22 ${misc:Depends},
22 23 ${python:Depends},
23 24 mercurial-common (= ${source:Version})
24 25 Architecture: any
25 26 Description: fast, easy to use, distributed revision control tool.
26 27 Mercurial is a fast, lightweight Source Control Management system designed
27 28 for efficient handling of very large distributed projects.
28 29 .
29 30 Its features include:
30 31 * O(1) delta-compressed file storage and retrieval scheme
31 32 * Complete cross-indexing of files and changesets for efficient exploration
32 33 of project history
33 34 * Robust SHA1-based integrity checking and append-only storage model
34 35 * Decentralized development model with arbitrary merging between trees
35 36 * Easy-to-use command-line interface
36 37 * Integrated stand-alone web interface
37 38 * Small Python codebase
38 39
39 40 Package: mercurial-common
40 41 Architecture: all
41 42 Depends:
42 43 ${misc:Depends},
43 44 ${python:Depends},
44 45 Recommends: mercurial (= ${source:Version}), ca-certificates
45 46 Suggests: wish
46 47 Breaks: mercurial (<< ${source:Version})
47 48 Replaces: mercurial (<< 2.6.3)
48 49 Description: easy-to-use, scalable distributed version control system (common files)
49 50 Mercurial is a fast, lightweight Source Control Management system designed
50 51 for efficient handling of very large distributed projects.
51 52 .
52 53 This package contains the architecture independent components of Mercurial,
53 54 and is generally useless without the mercurial package.
@@ -1,35 +1,36 b''
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3 3
4 4 <?include guids.wxi ?>
5 5 <?include defines.wxi ?>
6 6
7 7 <Fragment>
8 8 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
9 9 <Component Id="distOutput" Guid="$(var.dist.guid)" Win64='$(var.IsX64)'>
10 10 <File Name="python27.dll" KeyPath="yes" />
11 11 </Component>
12 12 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
13 13 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
14 14 <File Name="library.zip" KeyPath="yes" />
15 15 <File Name="mercurial.cext.base85.pyd" />
16 16 <File Name="mercurial.cext.bdiff.pyd" />
17 17 <File Name="mercurial.cext.diffhelpers.pyd" />
18 18 <File Name="mercurial.cext.mpatch.pyd" />
19 19 <File Name="mercurial.cext.osutil.pyd" />
20 20 <File Name="mercurial.cext.parsers.pyd" />
21 <File Name="mercurial.zstd.pyd" />
21 22 <File Name="pyexpat.pyd" />
22 23 <File Name="bz2.pyd" />
23 24 <File Name="select.pyd" />
24 25 <File Name="unicodedata.pyd" />
25 26 <File Name="_ctypes.pyd" />
26 27 <File Name="_elementtree.pyd" />
27 28 <File Name="_hashlib.pyd" />
28 29 <File Name="_socket.pyd" />
29 30 <File Name="_ssl.pyd" />
30 31 </Component>
31 32 </Directory>
32 33 </DirectoryRef>
33 34 </Fragment>
34 35
35 36 </Wix>
@@ -1,34 +1,34 b''
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3 3
4 4 <?include defines.wxi ?>
5 5
6 6 <?define hglocales =
7 da;de;el;fr;it;ja;pt_BR;sv;zh_CN;zh_TW
7 da;de;el;fr;it;ja;pt_BR;ro;ru;sv;zh_CN;zh_TW
8 8 ?>
9 9
10 10 <Fragment>
11 11 <ComponentGroup Id="localeFolder">
12 12 <?foreach LOC in $(var.hglocales) ?>
13 13 <ComponentRef Id="hg.locale.$(var.LOC)"/>
14 14 <?endforeach?>
15 15 </ComponentGroup>
16 16 </Fragment>
17 17
18 18 <Fragment>
19 19 <DirectoryRef Id="INSTALLDIR">
20 20 <Directory Id="localedir" Name="locale" FileSource="$(var.SourceDir)">
21 21 <?foreach LOC in $(var.hglocales) ?>
22 22 <Directory Id="hg.locale.$(var.LOC)" Name="$(var.LOC)">
23 23 <Directory Id="hg.locale.$(var.LOC).LC_MESSAGES" Name="LC_MESSAGES">
24 24 <Component Id="hg.locale.$(var.LOC)" Guid="*" Win64='$(var.IsX64)'>
25 25 <File Id="hg.mo.$(var.LOC)" Name="hg.mo" KeyPath="yes" />
26 26 </Component>
27 27 </Directory>
28 28 </Directory>
29 29 <?endforeach?>
30 30 </Directory>
31 31 </DirectoryRef>
32 32 </Fragment>
33 33
34 34 </Wix>
@@ -1,1074 +1,1074 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 the given username or the user assoicated with the apikey in Bugzilla.
57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 cmdutil,
304 304 error,
305 305 mail,
306 306 url,
307 307 util,
308 308 )
309 309
310 310 xmlrpclib = util.xmlrpclib
311 311
312 312 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
313 313 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
314 314 # be specifying the version(s) of Mercurial they are tested with, or
315 315 # leave the attribute unspecified.
316 316 testedwith = 'ships-with-hg-core'
317 317
318 318 class bzaccess(object):
319 319 '''Base class for access to Bugzilla.'''
320 320
321 321 def __init__(self, ui):
322 322 self.ui = ui
323 323 usermap = self.ui.config('bugzilla', 'usermap')
324 324 if usermap:
325 325 self.ui.readconfig(usermap, sections=['usermap'])
326 326
327 327 def map_committer(self, user):
328 328 '''map name of committer to Bugzilla user name.'''
329 329 for committer, bzuser in self.ui.configitems('usermap'):
330 330 if committer.lower() == user.lower():
331 331 return bzuser
332 332 return user
333 333
334 334 # Methods to be implemented by access classes.
335 335 #
336 336 # 'bugs' is a dict keyed on bug id, where values are a dict holding
337 337 # updates to bug state. Recognized dict keys are:
338 338 #
339 339 # 'hours': Value, float containing work hours to be updated.
340 340 # 'fix': If key present, bug is to be marked fixed. Value ignored.
341 341
342 342 def filter_real_bug_ids(self, bugs):
343 343 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
344 344 pass
345 345
346 346 def filter_cset_known_bug_ids(self, node, bugs):
347 347 '''remove bug IDs where node occurs in comment text from bugs.'''
348 348 pass
349 349
350 350 def updatebug(self, bugid, newstate, text, committer):
351 351 '''update the specified bug. Add comment text and set new states.
352 352
353 353 If possible add the comment as being from the committer of
354 354 the changeset. Otherwise use the default Bugzilla user.
355 355 '''
356 356 pass
357 357
358 358 def notify(self, bugs, committer):
359 359 '''Force sending of Bugzilla notification emails.
360 360
361 361 Only required if the access method does not trigger notification
362 362 emails automatically.
363 363 '''
364 364 pass
365 365
366 366 # Bugzilla via direct access to MySQL database.
367 367 class bzmysql(bzaccess):
368 368 '''Support for direct MySQL access to Bugzilla.
369 369
370 370 The earliest Bugzilla version this is tested with is version 2.16.
371 371
372 372 If your Bugzilla is version 3.4 or above, you are strongly
373 373 recommended to use the XMLRPC access method instead.
374 374 '''
375 375
376 376 @staticmethod
377 377 def sql_buglist(ids):
378 378 '''return SQL-friendly list of bug ids'''
379 379 return '(' + ','.join(map(str, ids)) + ')'
380 380
381 381 _MySQLdb = None
382 382
383 383 def __init__(self, ui):
384 384 try:
385 385 import MySQLdb as mysql
386 386 bzmysql._MySQLdb = mysql
387 387 except ImportError as err:
388 388 raise error.Abort(_('python mysql support not available: %s') % err)
389 389
390 390 bzaccess.__init__(self, ui)
391 391
392 392 host = self.ui.config('bugzilla', 'host', 'localhost')
393 393 user = self.ui.config('bugzilla', 'user', 'bugs')
394 394 passwd = self.ui.config('bugzilla', 'password')
395 395 db = self.ui.config('bugzilla', 'db', 'bugs')
396 396 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
397 397 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
398 398 (host, db, user, '*' * len(passwd)))
399 399 self.conn = bzmysql._MySQLdb.connect(host=host,
400 400 user=user, passwd=passwd,
401 401 db=db,
402 402 connect_timeout=timeout)
403 403 self.cursor = self.conn.cursor()
404 404 self.longdesc_id = self.get_longdesc_id()
405 405 self.user_ids = {}
406 406 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
407 407
408 408 def run(self, *args, **kwargs):
409 409 '''run a query.'''
410 410 self.ui.note(_('query: %s %s\n') % (args, kwargs))
411 411 try:
412 412 self.cursor.execute(*args, **kwargs)
413 413 except bzmysql._MySQLdb.MySQLError:
414 414 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
415 415 raise
416 416
417 417 def get_longdesc_id(self):
418 418 '''get identity of longdesc field'''
419 419 self.run('select fieldid from fielddefs where name = "longdesc"')
420 420 ids = self.cursor.fetchall()
421 421 if len(ids) != 1:
422 422 raise error.Abort(_('unknown database schema'))
423 423 return ids[0][0]
424 424
425 425 def filter_real_bug_ids(self, bugs):
426 426 '''filter not-existing bugs from set.'''
427 427 self.run('select bug_id from bugs where bug_id in %s' %
428 428 bzmysql.sql_buglist(bugs.keys()))
429 429 existing = [id for (id,) in self.cursor.fetchall()]
430 430 for id in bugs.keys():
431 431 if id not in existing:
432 432 self.ui.status(_('bug %d does not exist\n') % id)
433 433 del bugs[id]
434 434
435 435 def filter_cset_known_bug_ids(self, node, bugs):
436 436 '''filter bug ids that already refer to this changeset from set.'''
437 437 self.run('''select bug_id from longdescs where
438 438 bug_id in %s and thetext like "%%%s%%"''' %
439 439 (bzmysql.sql_buglist(bugs.keys()), short(node)))
440 440 for (id,) in self.cursor.fetchall():
441 441 self.ui.status(_('bug %d already knows about changeset %s\n') %
442 442 (id, short(node)))
443 443 del bugs[id]
444 444
445 445 def notify(self, bugs, committer):
446 446 '''tell bugzilla to send mail.'''
447 447 self.ui.status(_('telling bugzilla to send mail:\n'))
448 448 (user, userid) = self.get_bugzilla_user(committer)
449 449 for id in bugs.keys():
450 450 self.ui.status(_(' bug %s\n') % id)
451 451 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
452 452 bzdir = self.ui.config('bugzilla', 'bzdir',
453 453 '/var/www/html/bugzilla')
454 454 try:
455 455 # Backwards-compatible with old notify string, which
456 456 # took one string. This will throw with a new format
457 457 # string.
458 458 cmd = cmdfmt % id
459 459 except TypeError:
460 460 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
461 461 self.ui.note(_('running notify command %s\n') % cmd)
462 462 fp = util.popen('(%s) 2>&1' % cmd)
463 463 out = fp.read()
464 464 ret = fp.close()
465 465 if ret:
466 466 self.ui.warn(out)
467 467 raise error.Abort(_('bugzilla notify command %s') %
468 468 util.explainexit(ret)[0])
469 469 self.ui.status(_('done\n'))
470 470
471 471 def get_user_id(self, user):
472 472 '''look up numeric bugzilla user id.'''
473 473 try:
474 474 return self.user_ids[user]
475 475 except KeyError:
476 476 try:
477 477 userid = int(user)
478 478 except ValueError:
479 479 self.ui.note(_('looking up user %s\n') % user)
480 480 self.run('''select userid from profiles
481 481 where login_name like %s''', user)
482 482 all = self.cursor.fetchall()
483 483 if len(all) != 1:
484 484 raise KeyError(user)
485 485 userid = int(all[0][0])
486 486 self.user_ids[user] = userid
487 487 return userid
488 488
489 489 def get_bugzilla_user(self, committer):
490 490 '''See if committer is a registered bugzilla user. Return
491 491 bugzilla username and userid if so. If not, return default
492 492 bugzilla username and userid.'''
493 493 user = self.map_committer(committer)
494 494 try:
495 495 userid = self.get_user_id(user)
496 496 except KeyError:
497 497 try:
498 498 defaultuser = self.ui.config('bugzilla', 'bzuser')
499 499 if not defaultuser:
500 500 raise error.Abort(_('cannot find bugzilla user id for %s') %
501 501 user)
502 502 userid = self.get_user_id(defaultuser)
503 503 user = defaultuser
504 504 except KeyError:
505 505 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
506 506 % (user, defaultuser))
507 507 return (user, userid)
508 508
509 509 def updatebug(self, bugid, newstate, text, committer):
510 510 '''update bug state with comment text.
511 511
512 512 Try adding comment as committer of changeset, otherwise as
513 513 default bugzilla user.'''
514 514 if len(newstate) > 0:
515 515 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
516 516
517 517 (user, userid) = self.get_bugzilla_user(committer)
518 518 now = time.strftime('%Y-%m-%d %H:%M:%S')
519 519 self.run('''insert into longdescs
520 520 (bug_id, who, bug_when, thetext)
521 521 values (%s, %s, %s, %s)''',
522 522 (bugid, userid, now, text))
523 523 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
524 524 values (%s, %s, %s, %s)''',
525 525 (bugid, userid, now, self.longdesc_id))
526 526 self.conn.commit()
527 527
528 528 class bzmysql_2_18(bzmysql):
529 529 '''support for bugzilla 2.18 series.'''
530 530
531 531 def __init__(self, ui):
532 532 bzmysql.__init__(self, ui)
533 533 self.default_notify = \
534 534 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
535 535
536 536 class bzmysql_3_0(bzmysql_2_18):
537 537 '''support for bugzilla 3.0 series.'''
538 538
539 539 def __init__(self, ui):
540 540 bzmysql_2_18.__init__(self, ui)
541 541
542 542 def get_longdesc_id(self):
543 543 '''get identity of longdesc field'''
544 544 self.run('select id from fielddefs where name = "longdesc"')
545 545 ids = self.cursor.fetchall()
546 546 if len(ids) != 1:
547 547 raise error.Abort(_('unknown database schema'))
548 548 return ids[0][0]
549 549
550 550 # Bugzilla via XMLRPC interface.
551 551
552 552 class cookietransportrequest(object):
553 553 """A Transport request method that retains cookies over its lifetime.
554 554
555 555 The regular xmlrpclib transports ignore cookies. Which causes
556 556 a bit of a problem when you need a cookie-based login, as with
557 557 the Bugzilla XMLRPC interface prior to 4.4.3.
558 558
559 559 So this is a helper for defining a Transport which looks for
560 560 cookies being set in responses and saves them to add to all future
561 561 requests.
562 562 """
563 563
564 564 # Inspiration drawn from
565 565 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
566 566 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
567 567
568 568 cookies = []
569 569 def send_cookies(self, connection):
570 570 if self.cookies:
571 571 for cookie in self.cookies:
572 572 connection.putheader("Cookie", cookie)
573 573
574 574 def request(self, host, handler, request_body, verbose=0):
575 575 self.verbose = verbose
576 576 self.accept_gzip_encoding = False
577 577
578 578 # issue XML-RPC request
579 579 h = self.make_connection(host)
580 580 if verbose:
581 581 h.set_debuglevel(1)
582 582
583 583 self.send_request(h, handler, request_body)
584 584 self.send_host(h, host)
585 585 self.send_cookies(h)
586 586 self.send_user_agent(h)
587 587 self.send_content(h, request_body)
588 588
589 589 # Deal with differences between Python 2.6 and 2.7.
590 590 # In the former h is a HTTP(S). In the latter it's a
591 591 # HTTP(S)Connection. Luckily, the 2.6 implementation of
592 592 # HTTP(S) has an underlying HTTP(S)Connection, so extract
593 593 # that and use it.
594 594 try:
595 595 response = h.getresponse()
596 596 except AttributeError:
597 597 response = h._conn.getresponse()
598 598
599 599 # Add any cookie definitions to our list.
600 600 for header in response.msg.getallmatchingheaders("Set-Cookie"):
601 601 val = header.split(": ", 1)[1]
602 602 cookie = val.split(";", 1)[0]
603 603 self.cookies.append(cookie)
604 604
605 605 if response.status != 200:
606 606 raise xmlrpclib.ProtocolError(host + handler, response.status,
607 607 response.reason, response.msg.headers)
608 608
609 609 payload = response.read()
610 610 parser, unmarshaller = self.getparser()
611 611 parser.feed(payload)
612 612 parser.close()
613 613
614 614 return unmarshaller.close()
615 615
616 616 # The explicit calls to the underlying xmlrpclib __init__() methods are
617 617 # necessary. The xmlrpclib.Transport classes are old-style classes, and
618 618 # it turns out their __init__() doesn't get called when doing multiple
619 619 # inheritance with a new-style class.
620 620 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
621 621 def __init__(self, use_datetime=0):
622 622 if util.safehasattr(xmlrpclib.Transport, "__init__"):
623 623 xmlrpclib.Transport.__init__(self, use_datetime)
624 624
625 625 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
626 626 def __init__(self, use_datetime=0):
627 627 if util.safehasattr(xmlrpclib.Transport, "__init__"):
628 628 xmlrpclib.SafeTransport.__init__(self, use_datetime)
629 629
630 630 class bzxmlrpc(bzaccess):
631 631 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
632 632
633 633 Requires a minimum Bugzilla version 3.4.
634 634 """
635 635
636 636 def __init__(self, ui):
637 637 bzaccess.__init__(self, ui)
638 638
639 639 bzweb = self.ui.config('bugzilla', 'bzurl',
640 640 'http://localhost/bugzilla/')
641 641 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
642 642
643 643 user = self.ui.config('bugzilla', 'user', 'bugs')
644 644 passwd = self.ui.config('bugzilla', 'password')
645 645
646 646 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
647 647 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
648 648 'FIXED')
649 649
650 650 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
651 651 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
652 652 self.bzvermajor = int(ver[0])
653 653 self.bzverminor = int(ver[1])
654 654 login = self.bzproxy.User.login({'login': user, 'password': passwd,
655 655 'restrict_login': True})
656 656 self.bztoken = login.get('token', '')
657 657
658 658 def transport(self, uri):
659 659 if util.urlreq.urlparse(uri, "http")[0] == "https":
660 660 return cookiesafetransport()
661 661 else:
662 662 return cookietransport()
663 663
664 664 def get_bug_comments(self, id):
665 665 """Return a string with all comment text for a bug."""
666 666 c = self.bzproxy.Bug.comments({'ids': [id],
667 667 'include_fields': ['text'],
668 668 'token': self.bztoken})
669 669 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
670 670
671 671 def filter_real_bug_ids(self, bugs):
672 672 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
673 673 'include_fields': [],
674 674 'permissive': True,
675 675 'token': self.bztoken,
676 676 })
677 677 for badbug in probe['faults']:
678 678 id = badbug['id']
679 679 self.ui.status(_('bug %d does not exist\n') % id)
680 680 del bugs[id]
681 681
682 682 def filter_cset_known_bug_ids(self, node, bugs):
683 683 for id in sorted(bugs.keys()):
684 684 if self.get_bug_comments(id).find(short(node)) != -1:
685 685 self.ui.status(_('bug %d already knows about changeset %s\n') %
686 686 (id, short(node)))
687 687 del bugs[id]
688 688
689 689 def updatebug(self, bugid, newstate, text, committer):
690 690 args = {}
691 691 if 'hours' in newstate:
692 692 args['work_time'] = newstate['hours']
693 693
694 694 if self.bzvermajor >= 4:
695 695 args['ids'] = [bugid]
696 696 args['comment'] = {'body' : text}
697 697 if 'fix' in newstate:
698 698 args['status'] = self.fixstatus
699 699 args['resolution'] = self.fixresolution
700 700 args['token'] = self.bztoken
701 701 self.bzproxy.Bug.update(args)
702 702 else:
703 703 if 'fix' in newstate:
704 704 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
705 705 "to mark bugs fixed\n"))
706 706 args['id'] = bugid
707 707 args['comment'] = text
708 708 self.bzproxy.Bug.add_comment(args)
709 709
710 710 class bzxmlrpcemail(bzxmlrpc):
711 711 """Read data from Bugzilla via XMLRPC, send updates via email.
712 712
713 713 Advantages of sending updates via email:
714 714 1. Comments can be added as any user, not just logged in user.
715 715 2. Bug statuses or other fields not accessible via XMLRPC can
716 716 potentially be updated.
717 717
718 718 There is no XMLRPC function to change bug status before Bugzilla
719 719 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
720 720 But bugs can be marked fixed via email from 3.4 onwards.
721 721 """
722 722
723 723 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
724 724 # in-email fields are specified as '@<fieldname> = <value>'. In
725 725 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
726 726 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
727 727 # compatibility, but rather than rely on this use the new format for
728 728 # 4.0 onwards.
729 729
730 730 def __init__(self, ui):
731 731 bzxmlrpc.__init__(self, ui)
732 732
733 733 self.bzemail = self.ui.config('bugzilla', 'bzemail')
734 734 if not self.bzemail:
735 735 raise error.Abort(_("configuration 'bzemail' missing"))
736 736 mail.validateconfig(self.ui)
737 737
738 738 def makecommandline(self, fieldname, value):
739 739 if self.bzvermajor >= 4:
740 740 return "@%s %s" % (fieldname, str(value))
741 741 else:
742 742 if fieldname == "id":
743 743 fieldname = "bug_id"
744 744 return "@%s = %s" % (fieldname, str(value))
745 745
746 746 def send_bug_modify_email(self, bugid, commands, comment, committer):
747 747 '''send modification message to Bugzilla bug via email.
748 748
749 749 The message format is documented in the Bugzilla email_in.pl
750 750 specification. commands is a list of command lines, comment is the
751 751 comment text.
752 752
753 753 To stop users from crafting commit comments with
754 754 Bugzilla commands, specify the bug ID via the message body, rather
755 755 than the subject line, and leave a blank line after it.
756 756 '''
757 757 user = self.map_committer(committer)
758 758 matches = self.bzproxy.User.get({'match': [user],
759 759 'token': self.bztoken})
760 760 if not matches['users']:
761 761 user = self.ui.config('bugzilla', 'user', 'bugs')
762 762 matches = self.bzproxy.User.get({'match': [user],
763 763 'token': self.bztoken})
764 764 if not matches['users']:
765 765 raise error.Abort(_("default bugzilla user %s email not found")
766 766 % user)
767 767 user = matches['users'][0]['email']
768 768 commands.append(self.makecommandline("id", bugid))
769 769
770 770 text = "\n".join(commands) + "\n\n" + comment
771 771
772 772 _charsets = mail._charsets(self.ui)
773 773 user = mail.addressencode(self.ui, user, _charsets)
774 774 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
775 775 msg = mail.mimeencode(self.ui, text, _charsets)
776 776 msg['From'] = user
777 777 msg['To'] = bzemail
778 778 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
779 779 sendmail = mail.connect(self.ui)
780 780 sendmail(user, bzemail, msg.as_string())
781 781
782 782 def updatebug(self, bugid, newstate, text, committer):
783 783 cmds = []
784 784 if 'hours' in newstate:
785 785 cmds.append(self.makecommandline("work_time", newstate['hours']))
786 786 if 'fix' in newstate:
787 787 cmds.append(self.makecommandline("bug_status", self.fixstatus))
788 788 cmds.append(self.makecommandline("resolution", self.fixresolution))
789 789 self.send_bug_modify_email(bugid, cmds, text, committer)
790 790
791 791 class NotFound(LookupError):
792 792 pass
793 793
794 794 class bzrestapi(bzaccess):
795 795 """Read and write bugzilla data using the REST API available since
796 796 Bugzilla 5.0.
797 797 """
798 798 def __init__(self, ui):
799 799 bzaccess.__init__(self, ui)
800 800 bz = self.ui.config('bugzilla', 'bzurl',
801 801 'http://localhost/bugzilla/')
802 802 self.bzroot = '/'.join([bz, 'rest'])
803 803 self.apikey = self.ui.config('bugzilla', 'apikey', '')
804 804 self.user = self.ui.config('bugzilla', 'user', 'bugs')
805 805 self.passwd = self.ui.config('bugzilla', 'password')
806 806 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
807 807 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
808 808 'FIXED')
809 809
810 810 def apiurl(self, targets, include_fields=None):
811 811 url = '/'.join([self.bzroot] + [str(t) for t in targets])
812 812 qv = {}
813 813 if self.apikey:
814 814 qv['api_key'] = self.apikey
815 815 elif self.user and self.passwd:
816 816 qv['login'] = self.user
817 817 qv['password'] = self.passwd
818 818 if include_fields:
819 819 qv['include_fields'] = include_fields
820 820 if qv:
821 821 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
822 822 return url
823 823
824 824 def _fetch(self, burl):
825 825 try:
826 826 resp = url.open(self.ui, burl)
827 827 return json.loads(resp.read())
828 828 except util.urlerr.httperror as inst:
829 829 if inst.code == 401:
830 830 raise error.Abort(_('authorization failed'))
831 831 if inst.code == 404:
832 832 raise NotFound()
833 833 else:
834 834 raise
835 835
836 836 def _submit(self, burl, data, method='POST'):
837 837 data = json.dumps(data)
838 838 if method == 'PUT':
839 839 class putrequest(util.urlreq.request):
840 840 def get_method(self):
841 841 return 'PUT'
842 842 request_type = putrequest
843 843 else:
844 844 request_type = util.urlreq.request
845 845 req = request_type(burl, data,
846 846 {'Content-Type': 'application/json'})
847 847 try:
848 848 resp = url.opener(self.ui).open(req)
849 849 return json.loads(resp.read())
850 850 except util.urlerr.httperror as inst:
851 851 if inst.code == 401:
852 852 raise error.Abort(_('authorization failed'))
853 853 if inst.code == 404:
854 854 raise NotFound()
855 855 else:
856 856 raise
857 857
858 858 def filter_real_bug_ids(self, bugs):
859 859 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
860 860 badbugs = set()
861 861 for bugid in bugs:
862 862 burl = self.apiurl(('bug', bugid), include_fields='status')
863 863 try:
864 864 self._fetch(burl)
865 865 except NotFound:
866 866 badbugs.add(bugid)
867 867 for bugid in badbugs:
868 868 del bugs[bugid]
869 869
870 870 def filter_cset_known_bug_ids(self, node, bugs):
871 871 '''remove bug IDs where node occurs in comment text from bugs.'''
872 872 sn = short(node)
873 873 for bugid in bugs.keys():
874 874 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
875 875 result = self._fetch(burl)
876 876 comments = result['bugs'][str(bugid)]['comments']
877 877 if any(sn in c['text'] for c in comments):
878 878 self.ui.status(_('bug %d already knows about changeset %s\n') %
879 879 (bugid, sn))
880 880 del bugs[bugid]
881 881
882 882 def updatebug(self, bugid, newstate, text, committer):
883 883 '''update the specified bug. Add comment text and set new states.
884 884
885 885 If possible add the comment as being from the committer of
886 886 the changeset. Otherwise use the default Bugzilla user.
887 887 '''
888 888 bugmod = {}
889 889 if 'hours' in newstate:
890 890 bugmod['work_time'] = newstate['hours']
891 891 if 'fix' in newstate:
892 892 bugmod['status'] = self.fixstatus
893 893 bugmod['resolution'] = self.fixresolution
894 894 if bugmod:
895 895 # if we have to change the bugs state do it here
896 896 bugmod['comment'] = {
897 897 'comment': text,
898 898 'is_private': False,
899 899 'is_markdown': False,
900 900 }
901 901 burl = self.apiurl(('bug', bugid))
902 902 self._submit(burl, bugmod, method='PUT')
903 903 self.ui.debug('updated bug %s\n' % bugid)
904 904 else:
905 905 burl = self.apiurl(('bug', bugid, 'comment'))
906 906 self._submit(burl, {
907 907 'comment': text,
908 908 'is_private': False,
909 909 'is_markdown': False,
910 910 })
911 911 self.ui.debug('added comment to bug %s\n' % bugid)
912 912
913 913 def notify(self, bugs, committer):
914 914 '''Force sending of Bugzilla notification emails.
915 915
916 916 Only required if the access method does not trigger notification
917 917 emails automatically.
918 918 '''
919 919 pass
920 920
921 921 class bugzilla(object):
922 922 # supported versions of bugzilla. different versions have
923 923 # different schemas.
924 924 _versions = {
925 925 '2.16': bzmysql,
926 926 '2.18': bzmysql_2_18,
927 927 '3.0': bzmysql_3_0,
928 928 'xmlrpc': bzxmlrpc,
929 929 'xmlrpc+email': bzxmlrpcemail,
930 930 'restapi': bzrestapi,
931 931 }
932 932
933 933 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
934 934 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
935 935 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
936 936
937 937 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
938 938 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
939 939 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
940 940 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
941 941
942 942 def __init__(self, ui, repo):
943 943 self.ui = ui
944 944 self.repo = repo
945 945
946 946 bzversion = self.ui.config('bugzilla', 'version')
947 947 try:
948 948 bzclass = bugzilla._versions[bzversion]
949 949 except KeyError:
950 950 raise error.Abort(_('bugzilla version %s not supported') %
951 951 bzversion)
952 952 self.bzdriver = bzclass(self.ui)
953 953
954 954 self.bug_re = re.compile(
955 955 self.ui.config('bugzilla', 'regexp',
956 956 bugzilla._default_bug_re), re.IGNORECASE)
957 957 self.fix_re = re.compile(
958 958 self.ui.config('bugzilla', 'fixregexp',
959 959 bugzilla._default_fix_re), re.IGNORECASE)
960 960 self.split_re = re.compile(r'\D+')
961 961
962 962 def find_bugs(self, ctx):
963 963 '''return bugs dictionary created from commit comment.
964 964
965 965 Extract bug info from changeset comments. Filter out any that are
966 966 not known to Bugzilla, and any that already have a reference to
967 967 the given changeset in their comments.
968 968 '''
969 969 start = 0
970 970 hours = 0.0
971 971 bugs = {}
972 972 bugmatch = self.bug_re.search(ctx.description(), start)
973 973 fixmatch = self.fix_re.search(ctx.description(), start)
974 974 while True:
975 975 bugattribs = {}
976 976 if not bugmatch and not fixmatch:
977 977 break
978 978 if not bugmatch:
979 979 m = fixmatch
980 980 elif not fixmatch:
981 981 m = bugmatch
982 982 else:
983 983 if bugmatch.start() < fixmatch.start():
984 984 m = bugmatch
985 985 else:
986 986 m = fixmatch
987 987 start = m.end()
988 988 if m is bugmatch:
989 989 bugmatch = self.bug_re.search(ctx.description(), start)
990 990 if 'fix' in bugattribs:
991 991 del bugattribs['fix']
992 992 else:
993 993 fixmatch = self.fix_re.search(ctx.description(), start)
994 994 bugattribs['fix'] = None
995 995
996 996 try:
997 997 ids = m.group('ids')
998 998 except IndexError:
999 999 ids = m.group(1)
1000 1000 try:
1001 1001 hours = float(m.group('hours'))
1002 1002 bugattribs['hours'] = hours
1003 1003 except IndexError:
1004 1004 pass
1005 1005 except TypeError:
1006 1006 pass
1007 1007 except ValueError:
1008 1008 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1009 1009
1010 1010 for id in self.split_re.split(ids):
1011 1011 if not id:
1012 1012 continue
1013 1013 bugs[int(id)] = bugattribs
1014 1014 if bugs:
1015 1015 self.bzdriver.filter_real_bug_ids(bugs)
1016 1016 if bugs:
1017 1017 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1018 1018 return bugs
1019 1019
1020 1020 def update(self, bugid, newstate, ctx):
1021 1021 '''update bugzilla bug with reference to changeset.'''
1022 1022
1023 1023 def webroot(root):
1024 1024 '''strip leading prefix of repo root and turn into
1025 1025 url-safe path.'''
1026 1026 count = int(self.ui.config('bugzilla', 'strip', 0))
1027 1027 root = util.pconvert(root)
1028 1028 while count > 0:
1029 1029 c = root.find('/')
1030 1030 if c == -1:
1031 1031 break
1032 1032 root = root[c + 1:]
1033 1033 count -= 1
1034 1034 return root
1035 1035
1036 1036 mapfile = None
1037 1037 tmpl = self.ui.config('bugzilla', 'template')
1038 1038 if not tmpl:
1039 1039 mapfile = self.ui.config('bugzilla', 'style')
1040 1040 if not mapfile and not tmpl:
1041 1041 tmpl = _('changeset {node|short} in repo {root} refers '
1042 1042 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1043 1043 t = cmdutil.changeset_templater(self.ui, self.repo,
1044 1044 False, None, tmpl, mapfile, False)
1045 1045 self.ui.pushbuffer()
1046 1046 t.show(ctx, changes=ctx.changeset(),
1047 1047 bug=str(bugid),
1048 1048 hgweb=self.ui.config('web', 'baseurl'),
1049 1049 root=self.repo.root,
1050 1050 webroot=webroot(self.repo.root))
1051 1051 data = self.ui.popbuffer()
1052 1052 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1053 1053
1054 1054 def notify(self, bugs, committer):
1055 1055 '''ensure Bugzilla users are notified of bug change.'''
1056 1056 self.bzdriver.notify(bugs, committer)
1057 1057
1058 1058 def hook(ui, repo, hooktype, node=None, **kwargs):
1059 1059 '''add comment to bugzilla for each changeset that refers to a
1060 1060 bugzilla bug id. only add a comment once per bug, so same change
1061 1061 seen multiple times does not fill bug with duplicate data.'''
1062 1062 if node is None:
1063 1063 raise error.Abort(_('hook type %s does not pass a changeset id') %
1064 1064 hooktype)
1065 1065 try:
1066 1066 bz = bugzilla(ui, repo)
1067 1067 ctx = repo[node]
1068 1068 bugs = bz.find_bugs(ctx)
1069 1069 if bugs:
1070 1070 for bug in bugs:
1071 1071 bz.update(bug, bugs[bug], ctx)
1072 1072 bz.notify(bugs, util.email(ctx.user()))
1073 1073 except Exception as e:
1074 1074 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,186 +1,186 b''
1 1 # This software may be used and distributed according to the terms of the
2 2 # GNU General Public License version 2 or any later version.
3 3
4 4 """advertise pre-generated bundles to seed clones
5 5
6 6 "clonebundles" is a server-side extension used to advertise the existence
7 7 of pre-generated, externally hosted bundle files to clients that are
8 8 cloning so that cloning can be faster, more reliable, and require less
9 9 resources on the server.
10 10
11 11 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
12 12 the server, in response to a client's request to clone, dynamically generates
13 13 a bundle containing the entire repository content and sends it to the client.
14 14 There is no caching on the server and the server will have to redundantly
15 15 generate the same outgoing bundle in response to each clone request. For
16 16 servers with large repositories or with high clone volume, the load from
17 17 clones can make scaling the server challenging and costly.
18 18
19 19 This extension provides server operators the ability to offload potentially
20 20 expensive clone load to an external service. Here's how it works.
21 21
22 22 1. A server operator establishes a mechanism for making bundle files available
23 23 on a hosting service where Mercurial clients can fetch them.
24 24 2. A manifest file listing available bundle URLs and some optional metadata
25 25 is added to the Mercurial repository on the server.
26 26 3. A client initiates a clone against a clone bundles aware server.
27 27 4. The client sees the server is advertising clone bundles and fetches the
28 28 manifest listing available bundles.
29 29 5. The client filters and sorts the available bundles based on what it
30 30 supports and prefers.
31 31 6. The client downloads and applies an available bundle from the
32 32 server-specified URL.
33 33 7. The client reconnects to the original server and performs the equivalent
34 34 of :hg:`pull` to retrieve all repository data not in the bundle. (The
35 35 repository could have been updated between when the bundle was created
36 36 and when the client started the clone.)
37 37
38 38 Instead of the server generating full repository bundles for every clone
39 39 request, it generates full bundles once and they are subsequently reused to
40 40 bootstrap new clones. The server may still transfer data at clone time.
41 41 However, this is only data that has been added/changed since the bundle was
42 42 created. For large, established repositories, this can reduce server load for
43 43 clones to less than 1% of original.
44 44
45 45 To work, this extension requires the following of server operators:
46 46
47 47 * Generating bundle files of repository content (typically periodically,
48 48 such as once per day).
49 49 * A file server that clients have network access to and that Python knows
50 50 how to talk to through its normal URL handling facility (typically an
51 51 HTTP server).
52 52 * A process for keeping the bundles manifest in sync with available bundle
53 53 files.
54 54
55 55 Strictly speaking, using a static file hosting server isn't required: a server
56 56 operator could use a dynamic service for retrieving bundle data. However,
57 57 static file hosting services are simple and scalable and should be sufficient
58 58 for most needs.
59 59
60 60 Bundle files can be generated with the :hg:`bundle` command. Typically
61 61 :hg:`bundle --all` is used to produce a bundle of the entire repository.
62 62
63 63 :hg:`debugcreatestreamclonebundle` can be used to produce a special
64 64 *streaming clone bundle*. These are bundle files that are extremely efficient
65 65 to produce and consume (read: fast). However, they are larger than
66 66 traditional bundle formats and require that clients support the exact set
67 67 of repository data store formats in use by the repository that created them.
68 68 Typically, a newer server can serve data that is compatible with older clients.
69 69 However, *streaming clone bundles* don't have this guarantee. **Server
70 70 operators need to be aware that newer versions of Mercurial may produce
71 71 streaming clone bundles incompatible with older Mercurial versions.**
72 72
73 73 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
74 74 file containing the list of available bundle files suitable for seeding
75 75 clones. If this file does not exist, the repository will not advertise the
76 76 existence of clone bundles when clients connect.
77 77
78 The manifest file contains a newline (\n) delimited list of entries.
78 The manifest file contains a newline (\\n) delimited list of entries.
79 79
80 80 Each line in this file defines an available bundle. Lines have the format:
81 81
82 82 <URL> [<key>=<value>[ <key>=<value>]]
83 83
84 84 That is, a URL followed by an optional, space-delimited list of key=value
85 85 pairs describing additional properties of this bundle. Both keys and values
86 86 are URI encoded.
87 87
88 88 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
89 89 All non-uppercase keys can be used by site installations. An example use
90 90 for custom properties is to use the *datacenter* attribute to define which
91 91 data center a file is hosted in. Clients could then prefer a server in the
92 92 data center closest to them.
93 93
94 94 The following reserved keys are currently defined:
95 95
96 96 BUNDLESPEC
97 97 A "bundle specification" string that describes the type of the bundle.
98 98
99 99 These are string values that are accepted by the "--type" argument of
100 100 :hg:`bundle`.
101 101
102 102 The values are parsed in strict mode, which means they must be of the
103 103 "<compression>-<type>" form. See
104 104 mercurial.exchange.parsebundlespec() for more details.
105 105
106 106 :hg:`debugbundle --spec` can be used to print the bundle specification
107 107 string for a bundle file. The output of this command can be used verbatim
108 108 for the value of ``BUNDLESPEC`` (it is already escaped).
109 109
110 110 Clients will automatically filter out specifications that are unknown or
111 111 unsupported so they won't attempt to download something that likely won't
112 112 apply.
113 113
114 114 The actual value doesn't impact client behavior beyond filtering:
115 115 clients will still sniff the bundle type from the header of downloaded
116 116 files.
117 117
118 118 **Use of this key is highly recommended**, as it allows clients to
119 119 easily skip unsupported bundles. If this key is not defined, an old
120 120 client may attempt to apply a bundle that it is incapable of reading.
121 121
122 122 REQUIRESNI
123 123 Whether Server Name Indication (SNI) is required to connect to the URL.
124 124 SNI allows servers to use multiple certificates on the same IP. It is
125 125 somewhat common in CDNs and other hosting providers. Older Python
126 126 versions do not support SNI. Defining this attribute enables clients
127 127 with older Python versions to filter this entry without experiencing
128 128 an opaque SSL failure at connection time.
129 129
130 130 If this is defined, it is important to advertise a non-SNI fallback
131 131 URL or clients running old Python releases may not be able to clone
132 132 with the clonebundles facility.
133 133
134 134 Value should be "true".
135 135
136 136 Manifests can contain multiple entries. Assuming metadata is defined, clients
137 137 will filter entries from the manifest that they don't support. The remaining
138 138 entries are optionally sorted by client preferences
139 139 (``experimental.clonebundleprefers`` config option). The client then attempts
140 140 to fetch the bundle at the first URL in the remaining list.
141 141
142 142 **Errors when downloading a bundle will fail the entire clone operation:
143 143 clients do not automatically fall back to a traditional clone.** The reason
144 144 for this is that if a server is using clone bundles, it is probably doing so
145 145 because the feature is necessary to help it scale. In other words, there
146 146 is an assumption that clone load will be offloaded to another service and
147 147 that the Mercurial server isn't responsible for serving this clone load.
148 148 If that other service experiences issues and clients start mass falling back to
149 149 the original Mercurial server, the added clone load could overwhelm the server
150 150 due to unexpected load and effectively take it offline. Not having clients
151 151 automatically fall back to cloning from the original server mitigates this
152 152 scenario.
153 153
154 154 Because there is no automatic Mercurial server fallback on failure of the
155 155 bundle hosting service, it is important for server operators to view the bundle
156 156 hosting service as an extension of the Mercurial server in terms of
157 157 availability and service level agreements: if the bundle hosting service goes
158 158 down, so does the ability for clients to clone. Note: clients will see a
159 159 message informing them how to bypass the clone bundles facility when a failure
160 160 occurs. So server operators should prepare for some people to follow these
161 161 instructions when a failure occurs, thus driving more load to the original
162 162 Mercurial server when the bundle hosting service fails.
163 163 """
164 164
165 165 from __future__ import absolute_import
166 166
167 167 from mercurial import (
168 168 extensions,
169 169 wireproto,
170 170 )
171 171
172 172 testedwith = 'ships-with-hg-core'
173 173
174 174 def capabilities(orig, repo, proto):
175 175 caps = orig(repo, proto)
176 176
177 177 # Only advertise if a manifest exists. This does add some I/O to requests.
178 178 # But this should be cheaper than a wasted network round trip due to
179 179 # missing file.
180 180 if repo.vfs.exists('clonebundles.manifest'):
181 181 caps.append('clonebundles')
182 182
183 183 return caps
184 184
185 185 def extsetup(ui):
186 186 extensions.wrapfunction(wireproto, '_capabilities', capabilities)
@@ -1,196 +1,196 b''
1 1 # win32mbcs.py -- MBCS filename support for Mercurial
2 2 #
3 3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 4 #
5 5 # Version: 0.3
6 6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10 #
11 11
12 12 '''allow the use of MBCS paths with problematic encodings
13 13
14 14 Some MBCS encodings are not good for some path operations (i.e.
15 15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 17 This extension can be used to fix the issue with those encodings by
18 18 wrapping some functions to convert to Unicode string before path
19 19 operation.
20 20
21 21 This extension is useful for:
22 22
23 23 - Japanese Windows users using shift_jis encoding.
24 24 - Chinese Windows users using big5 encoding.
25 25 - All users who use a repository with one of problematic encodings on
26 26 case-insensitive file system.
27 27
28 28 This extension is not needed for:
29 29
30 30 - Any user who use only ASCII chars in path.
31 31 - Any user who do not use any of problematic encodings.
32 32
33 33 Note that there are some limitations on using this extension:
34 34
35 35 - You should use single encoding in one repository.
36 36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 37 - win32mbcs is not compatible with fixutf8 extension.
38 38
39 39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 40 You can specify the encoding by config option::
41 41
42 42 [win32mbcs]
43 43 encoding = sjis
44 44
45 45 It is useful for the users who want to commit with UTF-8 log message.
46 46 '''
47 47 from __future__ import absolute_import
48 48
49 49 import os
50 50 import sys
51 51
52 52 from mercurial.i18n import _
53 53 from mercurial import (
54 54 encoding,
55 55 error,
56 56 pycompat,
57 57 )
58 58
59 59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
60 60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
61 61 # be specifying the version(s) of Mercurial they are tested with, or
62 62 # leave the attribute unspecified.
63 63 testedwith = 'ships-with-hg-core'
64 64
65 65 _encoding = None # see extsetup
66 66
67 67 def decode(arg):
68 68 if isinstance(arg, str):
69 69 uarg = arg.decode(_encoding)
70 70 if arg == uarg.encode(_encoding):
71 71 return uarg
72 72 raise UnicodeError("Not local encoding")
73 73 elif isinstance(arg, tuple):
74 74 return tuple(map(decode, arg))
75 75 elif isinstance(arg, list):
76 76 return map(decode, arg)
77 77 elif isinstance(arg, dict):
78 78 for k, v in arg.items():
79 79 arg[k] = decode(v)
80 80 return arg
81 81
82 82 def encode(arg):
83 83 if isinstance(arg, unicode):
84 84 return arg.encode(_encoding)
85 85 elif isinstance(arg, tuple):
86 86 return tuple(map(encode, arg))
87 87 elif isinstance(arg, list):
88 88 return map(encode, arg)
89 89 elif isinstance(arg, dict):
90 90 for k, v in arg.items():
91 91 arg[k] = encode(v)
92 92 return arg
93 93
94 94 def appendsep(s):
95 95 # ensure the path ends with os.sep, appending it if necessary.
96 96 try:
97 97 us = decode(s)
98 98 except UnicodeError:
99 99 us = s
100 100 if us and us[-1] not in ':/\\':
101 101 s += pycompat.ossep
102 102 return s
103 103
104 104
105 105 def basewrapper(func, argtype, enc, dec, args, kwds):
106 106 # check check already converted, then call original
107 107 for arg in args:
108 108 if isinstance(arg, argtype):
109 109 return func(*args, **kwds)
110 110
111 111 try:
112 112 # convert string arguments, call func, then convert back the
113 113 # return value.
114 114 return enc(func(*dec(args), **dec(kwds)))
115 115 except UnicodeError:
116 116 raise error.Abort(_("[win32mbcs] filename conversion failed with"
117 117 " %s encoding\n") % (_encoding))
118 118
119 119 def wrapper(func, args, kwds):
120 120 return basewrapper(func, unicode, encode, decode, args, kwds)
121 121
122 122
123 123 def reversewrapper(func, args, kwds):
124 124 return basewrapper(func, str, decode, encode, args, kwds)
125 125
126 126 def wrapperforlistdir(func, args, kwds):
127 127 # Ensure 'path' argument ends with os.sep to avoids
128 128 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
129 129 if args:
130 130 args = list(args)
131 131 args[0] = appendsep(args[0])
132 132 if 'path' in kwds:
133 133 kwds['path'] = appendsep(kwds['path'])
134 134 return func(*args, **kwds)
135 135
136 136 def wrapname(name, wrapper):
137 137 module, name = name.rsplit('.', 1)
138 138 module = sys.modules[module]
139 139 func = getattr(module, name)
140 140 def f(*args, **kwds):
141 141 return wrapper(func, args, kwds)
142 142 f.__name__ = func.__name__
143 143 setattr(module, name, f)
144 144
145 145 # List of functions to be wrapped.
146 146 # NOTE: os.path.dirname() and os.path.basename() are safe because
147 147 # they use result of os.path.split()
148 148 funcs = '''os.path.join os.path.split os.path.splitext
149 149 os.path.normpath os.makedirs mercurial.util.endswithsep
150 150 mercurial.util.splitpath mercurial.util.fscasesensitive
151 151 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
152 152 mercurial.util.checkwinfilename mercurial.util.checkosfilename
153 153 mercurial.util.split'''
154 154
155 155 # These functions are required to be called with local encoded string
156 156 # because they expects argument is local encoded string and cause
157 157 # problem with unicode string.
158 158 rfuncs = '''mercurial.encoding.upper mercurial.encoding.lower
159 mercurial.pycompat.bytestr'''
159 mercurial.util._filenamebytestr'''
160 160
161 161 # List of Windows specific functions to be wrapped.
162 162 winfuncs = '''os.path.splitunc'''
163 163
164 164 # codec and alias names of sjis and big5 to be faked.
165 165 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
166 166 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
167 167 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
168 168 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
169 169
170 170 def extsetup(ui):
171 171 # TODO: decide use of config section for this extension
172 172 if ((not os.path.supports_unicode_filenames) and
173 173 (pycompat.sysplatform != 'cygwin')):
174 174 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
175 175 return
176 176 # determine encoding for filename
177 177 global _encoding
178 178 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
179 179 # fake is only for relevant environment.
180 180 if _encoding.lower() in problematic_encodings.split():
181 181 for f in funcs.split():
182 182 wrapname(f, wrapper)
183 183 if pycompat.osname == 'nt':
184 184 for f in winfuncs.split():
185 185 wrapname(f, wrapper)
186 186 wrapname("mercurial.util.listdir", wrapperforlistdir)
187 187 wrapname("mercurial.windows.listdir", wrapperforlistdir)
188 188 # wrap functions to be called with local byte string arguments
189 189 for f in rfuncs.split():
190 190 wrapname(f, reversewrapper)
191 191 # Check sys.args manually instead of using ui.debug() because
192 192 # command line options is not yet applied when
193 193 # extensions.loadall() is called.
194 194 if '--debug' in sys.argv:
195 195 ui.write(("[win32mbcs] activated with encoding: %s\n")
196 196 % _encoding)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now