14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Launchpad.net integration plugin for Bazaar."""
17
"""Launchpad.net integration plugin for Bazaar.
19
This plugin provides facilities for working with Bazaar branches that are
20
hosted on Launchpad (http://launchpad.net). It provides a directory service
21
for referring to Launchpad branches using the "lp:" prefix. For example,
22
lp:bzr refers to the Bazaar's main development branch and
23
lp:~username/project/branch-name can be used to refer to a specific branch.
25
This plugin provides a bug tracker so that "bzr commit --fixes lp:1234" will
26
record that revision as fixing Launchpad's bug 1234.
28
The plugin also provides the following commands:
30
launchpad-login: Show or set the Launchpad user ID
31
launchpad-open: Open a Launchpad branch page in your web browser
32
lp-propose-merge: Propose merging a branch on Launchpad
33
register-branch: Register a branch with launchpad.net
34
launchpad-mirror: Ask Launchpad to mirror a branch now
19
38
# The XMLRPC server address can be overridden by setting the environment
20
39
# variable $BZR_LP_XMLRPC_URL
22
# see http://bazaar-vcs.org/Specs/BranchRegistrationTool
24
# Since we are a built-in plugin we share the bzrlib version
25
from bzrlib import version_info
41
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
27
43
from bzrlib.lazy_import import lazy_import
28
44
lazy_import(globals(), """
29
45
from bzrlib import (
30
branch as _mod_branch,
49
from bzrlib.i18n import gettext
35
from bzrlib import bzrdir
53
branch as _mod_branch,
56
# Since we are a built-in plugin we share the bzrlib version
36
59
from bzrlib.commands import (
40
63
from bzrlib.directory_service import directories
41
64
from bzrlib.errors import (
348
375
register_command(cmd_lp_propose_merge)
378
class cmd_lp_find_proposal(Command):
380
__doc__ = """Find the proposal to merge this revision.
382
Finds the merge proposal(s) that discussed landing the specified revision.
383
This works only if the selected branch was the merge proposal target, and
384
if the merged_revno is recorded for the merge proposal. The proposal(s)
385
are opened in a web browser.
387
Any revision involved in the merge may be specified-- the revision in
388
which the merge was performed, or one of the revisions that was merged.
390
So, to find the merge proposal that reviewed line 1 of README::
392
bzr lp-find-proposal -r annotate:README:1
395
takes_options = ['revision']
397
def run(self, revision=None):
398
from bzrlib.plugins.launchpad import lp_api
400
b = _mod_branch.Branch.open_containing('.')[0]
401
pb = ui.ui_factory.nested_progress_bar()
404
revno = self._find_merged_revno(revision, b, pb)
405
merged = self._find_proposals(revno, b, pb)
407
raise BzrCommandError(gettext('No review found.'))
408
trace.note(gettext('%d proposals(s) found.') % len(merged))
410
webbrowser.open(lp_api.canonical_url(mp))
415
def _find_merged_revno(self, revision, b, pb):
418
pb.update(gettext('Finding revision-id'))
419
revision_id = revision[0].as_revision_id(b)
420
# a revno spec is necessarily on the mainline.
421
if self._is_revno_spec(revision[0]):
422
merging_revision = revision_id
424
graph = b.repository.get_graph()
425
pb.update(gettext('Finding merge'))
426
merging_revision = graph.find_lefthand_merger(
427
revision_id, b.last_revision())
428
if merging_revision is None:
429
raise InvalidRevisionSpec(revision[0].user_spec, b)
430
pb.update(gettext('Finding revno'))
431
return b.revision_id_to_revno(merging_revision)
433
def _find_proposals(self, revno, b, pb):
434
launchpad = lp_api.login(lp_registration.LaunchpadService())
435
pb.update(gettext('Finding Launchpad branch'))
436
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
437
create_missing=False)
438
pb.update(gettext('Finding proposals'))
439
return list(lpb.lp.getMergeProposals(status=['Merged'],
440
merged_revnos=[revno]))
444
def _is_revno_spec(spec):
453
register_command(cmd_lp_find_proposal)
351
456
def _register_directory():
352
457
directories.register_lazy('lp:', 'bzrlib.plugins.launchpad.lp_directory',
353
458
'LaunchpadDirectory',
354
459
'Launchpad-based directory service',)
460
directories.register_lazy(
461
'debianlp:', 'bzrlib.plugins.launchpad.lp_directory',
462
'LaunchpadDirectory',
463
'debianlp: shortcut')
464
directories.register_lazy(
465
'ubuntu:', 'bzrlib.plugins.launchpad.lp_directory',
466
'LaunchpadDirectory',
355
469
_register_directory()
471
# This is kept in __init__ so that we don't load lp_api_lite unless the branch
472
# actually matches. That way we can avoid importing extra dependencies like
474
_package_branch = lazy_regex.lazy_compile(
475
r'bazaar.launchpad.net.*?/'
476
r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
477
r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
480
def _get_package_branch_info(url):
481
"""Determine the packaging information for this URL.
483
:return: If this isn't a packaging branch, return None. If it is, return
484
(archive, series, project)
488
m = _package_branch.search(url)
491
archive, series, project, user = m.group('archive', 'series',
493
if series is not None:
494
# series is optional, so the regex includes the extra '/', we don't
495
# want to send that on (it causes Internal Server Errors.)
496
series = series.strip('/')
498
user = user.strip('~/')
499
if user != 'ubuntu-branches':
501
return archive, series, project
504
def _check_is_up_to_date(the_branch):
505
info = _get_package_branch_info(the_branch.base)
508
c = the_branch.get_config()
509
verbosity = c.get_user_option('launchpad.packaging_verbosity')
510
if verbosity is not None:
511
verbosity = verbosity.lower()
512
if verbosity == 'off':
513
trace.mutter('not checking %s because verbosity is turned off'
514
% (the_branch.base,))
516
archive, series, project = info
517
from bzrlib.plugins.launchpad import lp_api_lite
518
latest_pub = lp_api_lite.LatestPublication(archive, series, project)
519
lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
522
def _register_hooks():
523
_mod_branch.Branch.hooks.install_named_hook('open',
524
_check_is_up_to_date, 'package-branch-up-to-date')
358
529
def load_tests(basic_tests, module, loader):
359
530
testmod_names = [
363
535
'test_lp_directory',