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.
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
17
"""Launchpad.net integration plugin for Bazaar."""
38
19
# The XMLRPC server address can be overridden by setting the environment
39
20
# variable $BZR_LP_XMLRPC_URL
41
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
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
43
27
from bzrlib.lazy_import import lazy_import
44
28
lazy_import(globals(), """
45
29
from bzrlib import (
30
branch as _mod_branch,
49
from bzrlib.i18n import gettext
53
branch as _mod_branch,
56
# Since we are a built-in plugin we share the bzrlib version
35
from bzrlib import bzrdir
59
36
from bzrlib.commands import (
63
40
from bzrlib.directory_service import directories
64
41
from bzrlib.errors import (
270
245
if check_account:
271
246
account.check_lp_login(username)
273
self.outf.write(gettext(
274
"Launchpad user ID exists and has SSH keys.\n"))
249
"Launchpad user ID exists and has SSH keys.\n")
275
250
self.outf.write(username + '\n')
277
self.outf.write(gettext('No Launchpad user ID configured.\n'))
252
self.outf.write('No Launchpad user ID configured.\n')
280
255
name = name.lower()
281
256
if check_account:
282
257
account.check_lp_login(name)
284
self.outf.write(gettext(
285
"Launchpad user ID exists and has SSH keys.\n"))
260
"Launchpad user ID exists and has SSH keys.\n")
286
261
account.set_lp_login(name)
288
self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
263
self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
291
265
register_command(cmd_launchpad_login)
294
268
# XXX: cmd_launchpad_mirror is untested
295
269
class cmd_launchpad_mirror(Command):
296
__doc__ = """Ask Launchpad to mirror a branch now."""
270
"""Ask Launchpad to mirror a branch now."""
298
272
aliases = ['lp-mirror']
299
273
takes_args = ['location?']
301
275
def run(self, location='.'):
302
276
from bzrlib.plugins.launchpad import lp_api
303
277
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
304
branch, _ = _mod_branch.Branch.open_containing(location)
278
branch = _mod_branch.Branch.open(location)
305
279
service = LaunchpadService()
306
280
launchpad = lp_api.login(service)
307
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
308
create_missing=False)
309
lp_branch.lp.requestMirror()
281
lp_branch = lp_api.load_branch(launchpad, branch)
282
lp_branch.requestMirror()
312
285
register_command(cmd_launchpad_mirror)
315
288
class cmd_lp_propose_merge(Command):
316
__doc__ = """Propose merging a branch on Launchpad.
289
"""Propose merging a branch on Launchpad.
318
291
This will open your usual editor to provide the initial comment. When it
319
292
has created the proposal, it will open it in your default web browser.
375
346
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)
456
349
def _register_directory():
457
350
directories.register_lazy('lp:', 'bzrlib.plugins.launchpad.lp_directory',
458
351
'LaunchpadDirectory',
459
352
'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',
469
353
_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')
529
356
def load_tests(basic_tests, module, loader):
530
357
testmod_names = [
535
361
'test_lp_directory',