1
# Copyright (C) 2007-2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
from .lazy_import import lazy_import
22
lazy_import(globals(), """
23
from breezy import urlutils
27
"""Provides a shorthand for referring to bugs on a variety of bug trackers.
29
'commit --fixes' stores references to bugs as a <bug_url> -> <bug_status>
30
mapping in the properties for that revision.
32
However, it's inconvenient to type out full URLs for bugs on the command line,
33
particularly given that many users will be using only a single bug tracker per
36
Thus, this module provides a registry of types of bug tracker (e.g. Launchpad,
37
Trac). Given an abbreviated name (e.g. 'lp', 'twisted') and a branch with
38
configuration information, these tracker types can return an instance capable
39
of converting bug IDs into URLs.
44
When making a commit, metadata about bugs fixed by that change can be
45
recorded by using the ``--fixes`` option. For each bug marked as fixed, an
46
entry is included in the 'bugs' revision property stating '<url> <status>'.
47
(The only ``status`` value currently supported is ``fixed.``)
49
The ``--fixes`` option allows you to specify a bug tracker and a bug identifier
50
rather than a full URL. This looks like::
52
bzr commit --fixes <tracker>:<id>
56
bzr commit --fixes <id>
58
where "<tracker>" is an identifier for the bug tracker, and "<id>" is the
59
identifier for that bug within the bugtracker, usually the bug number.
60
If "<tracker>" is not specified the ``bugtracker`` set in the branch
61
or global configuration is used.
63
Bazaar knows about a few bug trackers that have many users. If
64
you use one of these bug trackers then there is no setup required to
65
use this feature, you just need to know the tracker identifier to use.
66
These are the bugtrackers that are built in:
68
============================ ============ ============
69
URL Abbreviation Example
70
============================ ============ ============
71
https://bugs.launchpad.net/ lp lp:12345
72
http://bugs.debian.org/ deb deb:12345
73
http://bugzilla.gnome.org/ gnome gnome:12345
74
============================ ============ ============
76
For the bug trackers not listed above configuration is required.
77
Support for generating the URLs for any project using Bugzilla or Trac
78
is built in, along with a template mechanism for other bugtrackers with
79
simple URL schemes. If your bug tracker can't be described by one
80
of the schemes described below then you can write a plugin to support
83
If you use Bugzilla or Trac, then you only need to set a configuration
84
variable which contains the base URL of the bug tracker. These options
85
can go into ``breezy.conf``, ``branch.conf`` or into a branch-specific
86
configuration section in ``locations.conf``. You can set up these values
87
for each of the projects you work on.
89
Note: As you provide a short name for each tracker, you can specify one or
90
more bugs in one or more trackers at commit time if you wish.
95
Use ``bzr commit --fixes lp:2`` to record that this commit fixes bug 2.
97
bugzilla_<tracker>_url
98
----------------------
100
If present, the location of the Bugzilla bug tracker referred to by
101
<tracker>. This option can then be used together with ``bzr commit
102
--fixes`` to mark bugs in that tracker as being fixed by that commit. For
105
bugzilla_squid_url = http://bugs.squid-cache.org
107
would allow ``bzr commit --fixes squid:1234`` to mark Squid's bug 1234 as
113
If present, the location of the Trac instance referred to by
114
<tracker>. This option can then be used together with ``bzr commit
115
--fixes`` to mark bugs in that tracker as being fixed by that commit. For
118
trac_twisted_url = http://www.twistedmatrix.com/trac
120
would allow ``bzr commit --fixes twisted:1234`` to mark Twisted's bug 1234 as
123
bugtracker_<tracker>_url
124
------------------------
126
If present, the location of a generic bug tracker instance referred to by
127
<tracker>. The location must contain an ``{id}`` placeholder,
128
which will be replaced by a specific bug ID. This option can then be used
129
together with ``bzr commit --fixes`` to mark bugs in that tracker as being
130
fixed by that commit. For example::
132
bugtracker_python_url = http://bugs.python.org/issue{id}
134
would allow ``bzr commit --fixes python:1234`` to mark bug 1234 in Python's
135
Roundup bug tracker as fixed, or::
137
bugtracker_cpan_url = http://rt.cpan.org/Public/Bug/Display.html?id={id}
139
would allow ``bzr commit --fixes cpan:1234`` to mark bug 1234 in CPAN's
140
RT bug tracker as fixed, or::
142
bugtracker_hudson_url = http://issues.hudson-ci.org/browse/{id}
144
would allow ``bzr commit --fixes hudson:HUDSON-1234`` to mark bug HUDSON-1234
145
in Hudson's JIRA bug tracker as fixed.
149
class MalformedBugIdentifier(errors.BzrError):
151
_fmt = ('Did not understand bug identifier %(bug_id)s: %(reason)s. '
152
'See "brz help bugs" for more information on this feature.')
154
def __init__(self, bug_id, reason):
159
class InvalidBugTrackerURL(errors.BzrError):
161
_fmt = ("The URL for bug tracker \"%(abbreviation)s\" doesn't "
162
"contain {id}: %(url)s")
164
def __init__(self, abbreviation, url):
165
self.abbreviation = abbreviation
169
class UnknownBugTrackerAbbreviation(errors.BzrError):
171
_fmt = ("Cannot find registered bug tracker called %(abbreviation)s "
174
def __init__(self, abbreviation, branch):
175
self.abbreviation = abbreviation
179
class InvalidLineInBugsProperty(errors.BzrError):
181
_fmt = ("Invalid line in bugs property: '%(line)s'")
183
def __init__(self, line):
187
class InvalidBugUrl(errors.BzrError):
189
_fmt = "Invalid bug URL: %(url)s"
191
def __init__(self, url):
195
class InvalidBugStatus(errors.BzrError):
197
_fmt = ("Invalid bug status: '%(status)s'")
199
def __init__(self, status):
203
def get_bug_url(abbreviated_bugtracker_name, branch, bug_id):
204
"""Return a URL pointing to the canonical web page of the bug identified by
207
tracker = tracker_registry.get_tracker(abbreviated_bugtracker_name, branch)
208
return tracker.get_bug_url(bug_id)
211
class TrackerRegistry(registry.Registry):
212
"""Registry of bug tracker types."""
214
def get_tracker(self, abbreviated_bugtracker_name, branch):
215
"""Return the first registered tracker that understands
216
'abbreviated_bugtracker_name'.
218
If no such tracker is found, raise KeyError.
220
for tracker_name in self.keys():
221
tracker_type = self.get(tracker_name)
222
tracker = tracker_type.get(abbreviated_bugtracker_name, branch)
223
if tracker is not None:
225
raise UnknownBugTrackerAbbreviation(
226
abbreviated_bugtracker_name, branch)
228
def help_topic(self, topic):
232
tracker_registry = TrackerRegistry()
233
"""Registry of bug trackers."""
236
class BugTracker(object):
237
"""Base class for bug trackers."""
239
def check_bug_id(self, bug_id):
240
"""Check that the bug_id is valid.
242
The base implementation assumes that all bug_ids are valid.
245
def get_bug_url(self, bug_id):
246
"""Return the URL for bug_id. Raise an error if bug ID is malformed."""
247
self.check_bug_id(bug_id)
248
return self._get_bug_url(bug_id)
250
def _get_bug_url(self, bug_id):
251
"""Given a validated bug_id, return the bug's web page's URL."""
254
class IntegerBugTracker(BugTracker):
255
"""A bug tracker that only allows integer bug IDs."""
257
def check_bug_id(self, bug_id):
261
raise MalformedBugIdentifier(bug_id, "Must be an integer")
264
class UniqueIntegerBugTracker(IntegerBugTracker):
265
"""A style of bug tracker that exists in one place only, such as Launchpad.
267
If you have one of these trackers then register an instance passing in an
268
abbreviated name for the bug tracker and a base URL. The bug ids are
269
appended directly to the URL.
272
def __init__(self, abbreviated_bugtracker_name, base_url):
273
self.abbreviation = abbreviated_bugtracker_name
274
self.base_url = base_url
276
def get(self, abbreviated_bugtracker_name, branch):
277
"""Returns the tracker if the abbreviation matches, otherwise ``None``.
279
if abbreviated_bugtracker_name != self.abbreviation:
283
def _get_bug_url(self, bug_id):
284
"""Return the URL for bug_id."""
285
return self.base_url + str(bug_id)
288
class ProjectIntegerBugTracker(IntegerBugTracker):
289
"""A bug tracker that exists in one place only with per-project ids.
291
If you have one of these trackers then register an instance passing in an
292
abbreviated name for the bug tracker and a base URL. The bug ids are
293
appended directly to the URL.
296
def __init__(self, abbreviated_bugtracker_name, base_url):
297
self.abbreviation = abbreviated_bugtracker_name
298
self._base_url = base_url
300
def get(self, abbreviated_bugtracker_name, branch):
301
"""Returns the tracker if the abbreviation matches, otherwise ``None``.
303
if abbreviated_bugtracker_name != self.abbreviation:
307
def check_bug_id(self, bug_id):
309
(project, bug_id) = bug_id.rsplit('/', 1)
311
raise MalformedBugIdentifier(bug_id, "Expected format: project/id")
315
raise MalformedBugIdentifier(bug_id, "Bug id must be an integer")
317
def _get_bug_url(self, bug_id):
318
(project, bug_id) = bug_id.rsplit('/', 1)
319
"""Return the URL for bug_id."""
320
if '{id}' not in self._base_url:
321
raise InvalidBugTrackerURL(self._abbreviation, self._base_url)
322
if '{project}' not in self._base_url:
323
raise InvalidBugTrackerURL(self._abbreviation, self._base_url)
324
return self._base_url.replace(
325
'{project}', project).replace('{id}', str(bug_id))
328
tracker_registry.register(
329
'launchpad', UniqueIntegerBugTracker('lp', 'https://launchpad.net/bugs/'))
332
tracker_registry.register(
333
'debian', UniqueIntegerBugTracker('deb', 'http://bugs.debian.org/'))
336
tracker_registry.register(
337
'gnome', UniqueIntegerBugTracker(
338
'gnome', 'http://bugzilla.gnome.org/show_bug.cgi?id='))
341
tracker_registry.register(
342
'github', ProjectIntegerBugTracker(
343
'github', 'https://github.com/{project}/issues/{id}'))
346
class URLParametrizedBugTracker(BugTracker):
347
"""A type of bug tracker that can be found on a variety of different sites,
348
and thus needs to have the base URL configured.
350
Looks for a config setting in the form '<type_name>_<abbreviation>_url'.
351
`type_name` is the name of the type of tracker and `abbreviation`
352
is a short name for the particular instance.
355
def get(self, abbreviation, branch):
356
config = branch.get_config()
357
url = config.get_user_option(
358
"%s_%s_url" % (self.type_name, abbreviation), expand=False)
364
def __init__(self, type_name, bug_area):
365
self.type_name = type_name
366
self._bug_area = bug_area
368
def _get_bug_url(self, bug_id):
369
"""Return a URL for a bug on this Trac instance."""
370
return urlutils.join(self._base_url, self._bug_area) + str(bug_id)
373
class URLParametrizedIntegerBugTracker(IntegerBugTracker,
374
URLParametrizedBugTracker):
375
"""A type of bug tracker that only allows integer bug IDs.
377
This can be found on a variety of different sites, and thus needs to have
378
the base URL configured.
380
Looks for a config setting in the form '<type_name>_<abbreviation>_url'.
381
`type_name` is the name of the type of tracker (e.g. 'bugzilla' or 'trac')
382
and `abbreviation` is a short name for the particular instance (e.g.
383
'squid' or 'apache').
387
tracker_registry.register(
388
'trac', URLParametrizedIntegerBugTracker('trac', 'ticket/'))
390
tracker_registry.register(
392
URLParametrizedIntegerBugTracker('bugzilla', 'show_bug.cgi?id='))
395
class GenericBugTracker(URLParametrizedBugTracker):
396
"""Generic bug tracker specified by an URL template."""
399
super(GenericBugTracker, self).__init__('bugtracker', None)
401
def get(self, abbreviation, branch):
402
self._abbreviation = abbreviation
403
return super(GenericBugTracker, self).get(abbreviation, branch)
405
def _get_bug_url(self, bug_id):
406
"""Given a validated bug_id, return the bug's web page's URL."""
407
if '{id}' not in self._base_url:
408
raise InvalidBugTrackerURL(self._abbreviation, self._base_url)
409
return self._base_url.replace('{id}', str(bug_id))
412
tracker_registry.register('generic', GenericBugTracker())
418
ALLOWED_BUG_STATUSES = {FIXED, RELATED}
421
def encode_fixes_bug_urls(bug_urls):
422
"""Get the revision property value for a commit that fixes bugs.
424
:param bug_urls: An iterable of (escaped URL, tag) tuples. These normally
425
come from `get_bug_url`.
426
:return: A string that will be set as the 'bugs' property of a revision
430
for (url, tag) in bug_urls:
432
raise InvalidBugUrl(url)
433
lines.append('%s %s' % (url, tag))
434
return '\n'.join(lines)
437
def decode_bug_urls(bug_text):
438
"""Decode a bug property text.
440
:param bug_text: Contents of a bugs property
441
:return: iterator over (url, status) tuples
443
for line in bug_text.splitlines():
445
url, status = line.split(None, 2)
447
raise InvalidLineInBugsProperty(line)
448
if status not in ALLOWED_BUG_STATUSES:
449
raise InvalidBugStatus(status)