~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugins/launchpad/__init__.py

Merge bzr.dev, update to use new hooks.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
 
1
# Copyright (C) 2006-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
16
16
 
17
 
"""Launchpad.net integration plugin for Bazaar."""
 
17
"""Launchpad.net integration plugin for Bazaar.
 
18
 
 
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.
 
24
 
 
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.
 
27
 
 
28
The plugin also provides the following commands:
 
29
 
 
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
 
35
 
 
36
"""
18
37
 
19
38
# The XMLRPC server address can be overridden by setting the environment
20
39
# variable $BZR_LP_XMLRPC_URL
21
40
 
22
 
# see http://bazaar-vcs.org/Specs/BranchRegistrationTool
23
 
 
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
26
42
 
27
43
from bzrlib.lazy_import import lazy_import
28
44
lazy_import(globals(), """
29
45
from bzrlib import (
30
 
    branch as _mod_branch,
 
46
    ui,
31
47
    trace,
32
48
    )
 
49
from bzrlib.i18n import gettext
33
50
""")
34
51
 
35
 
from bzrlib import bzrdir
 
52
from bzrlib import (
 
53
    branch as _mod_branch,
 
54
    bzrdir,
 
55
    lazy_regex,
 
56
    # Since we are a built-in plugin we share the bzrlib version
 
57
    version_info,
 
58
    )
36
59
from bzrlib.commands import (
37
 
        Command,
38
 
        register_command,
39
 
)
 
60
    Command,
 
61
    register_command,
 
62
    )
40
63
from bzrlib.directory_service import directories
41
64
from bzrlib.errors import (
42
65
    BzrCommandError,
43
 
    DependencyNotPresent,
 
66
    InvalidRevisionSpec,
44
67
    InvalidURL,
45
68
    NoPublicBranch,
46
69
    NotBranchError,
81
104
                'Launchpad project short name to associate with the branch.',
82
105
                unicode),
83
106
         Option('product',
84
 
                'Launchpad product short name to associate with the branch.', 
 
107
                'Launchpad product short name to associate with the branch.',
85
108
                unicode,
86
109
                hidden=True),
87
110
         Option('branch-name',
122
145
            try:
123
146
                b = _mod_branch.Branch.open_containing('.')[0]
124
147
            except NotBranchError:
125
 
                raise BzrCommandError('register-branch requires a public '
126
 
                    'branch url - see bzr help register-branch.')
 
148
                raise BzrCommandError(gettext(
 
149
                            'register-branch requires a public '
 
150
                            'branch url - see bzr help register-branch.'))
127
151
            public_url = b.get_public_branch()
128
152
            if public_url is None:
129
153
                raise NoPublicBranch(b)
130
154
        if product is not None:
131
155
            project = product
132
 
            trace.note('--product is deprecated; please use --project.')
 
156
            trace.note(gettext(
 
157
                '--product is deprecated; please use --project.'))
133
158
 
134
159
 
135
160
        rego = BranchRegistrationRequest(branch_url=public_url,
201
226
        if location is None:
202
227
            location = u'.'
203
228
        web_url = self._get_web_url(LaunchpadService(), location)
204
 
        trace.note('Opening %s in web browser' % web_url)
 
229
        trace.note(gettext('Opening %s in web browser') % web_url)
205
230
        if not dry_run:
206
231
            import webbrowser   # this import should not be lazy
207
232
                                # otherwise bzr.exe lacks this module
245
270
                if check_account:
246
271
                    account.check_lp_login(username)
247
272
                    if verbose:
248
 
                        self.outf.write(
249
 
                            "Launchpad user ID exists and has SSH keys.\n")
 
273
                        self.outf.write(gettext(
 
274
                            "Launchpad user ID exists and has SSH keys.\n"))
250
275
                self.outf.write(username + '\n')
251
276
            else:
252
 
                self.outf.write('No Launchpad user ID configured.\n')
 
277
                self.outf.write(gettext('No Launchpad user ID configured.\n'))
253
278
                return 1
254
279
        else:
255
280
            name = name.lower()
256
281
            if check_account:
257
282
                account.check_lp_login(name)
258
283
                if verbose:
259
 
                    self.outf.write(
260
 
                        "Launchpad user ID exists and has SSH keys.\n")
 
284
                    self.outf.write(gettext(
 
285
                        "Launchpad user ID exists and has SSH keys.\n"))
261
286
            account.set_lp_login(name)
262
287
            if verbose:
263
 
                self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
 
288
                self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
 
289
                                                                        (name,))
264
290
 
265
291
register_command(cmd_launchpad_login)
266
292
 
275
301
    def run(self, location='.'):
276
302
        from bzrlib.plugins.launchpad import lp_api
277
303
        from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
278
 
        branch = _mod_branch.Branch.open(location)
 
304
        branch, _ = _mod_branch.Branch.open_containing(location)
279
305
        service = LaunchpadService()
280
306
        launchpad = lp_api.login(service)
281
 
        lp_branch = lp_api.load_branch(launchpad, branch)
282
 
        lp_branch.requestMirror()
 
307
        lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
 
308
                create_missing=False)
 
309
        lp_branch.lp.requestMirror()
283
310
 
284
311
 
285
312
register_command(cmd_launchpad_mirror)
348
375
register_command(cmd_lp_propose_merge)
349
376
 
350
377
 
 
378
class cmd_lp_find_proposal(Command):
 
379
 
 
380
    __doc__ = """Find the proposal to merge this revision.
 
381
 
 
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.
 
386
 
 
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.
 
389
 
 
390
    So, to find the merge proposal that reviewed line 1 of README::
 
391
 
 
392
      bzr lp-find-proposal -r annotate:README:1
 
393
    """
 
394
 
 
395
    takes_options = ['revision']
 
396
 
 
397
    def run(self, revision=None):
 
398
        from bzrlib.plugins.launchpad import lp_api
 
399
        import webbrowser
 
400
        b = _mod_branch.Branch.open_containing('.')[0]
 
401
        pb = ui.ui_factory.nested_progress_bar()
 
402
        b.lock_read()
 
403
        try:
 
404
            revno = self._find_merged_revno(revision, b, pb)
 
405
            merged = self._find_proposals(revno, b, pb)
 
406
            if len(merged) == 0:
 
407
                raise BzrCommandError(gettext('No review found.'))
 
408
            trace.note(gettext('%d proposals(s) found.') % len(merged))
 
409
            for mp in merged:
 
410
                webbrowser.open(lp_api.canonical_url(mp))
 
411
        finally:
 
412
            b.unlock()
 
413
            pb.finished()
 
414
 
 
415
    def _find_merged_revno(self, revision, b, pb):
 
416
        if revision is None:
 
417
            return b.revno()
 
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
 
423
        else:
 
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)
 
432
 
 
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]))
 
441
 
 
442
 
 
443
    @staticmethod
 
444
    def _is_revno_spec(spec):
 
445
        try:
 
446
            int(spec.user_spec)
 
447
        except ValueError:
 
448
            return False
 
449
        else:
 
450
            return True
 
451
 
 
452
 
 
453
register_command(cmd_lp_find_proposal)
 
454
 
 
455
 
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',
 
467
        'ubuntu: shortcut')
 
468
 
355
469
_register_directory()
356
470
 
 
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
 
473
# json.
 
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>/[^/]+)?'
 
478
    )
 
479
 
 
480
def _get_package_branch_info(url):
 
481
    """Determine the packaging information for this URL.
 
482
 
 
483
    :return: If this isn't a packaging branch, return None. If it is, return
 
484
        (archive, series, project)
 
485
    """
 
486
    if url is None:
 
487
        return None
 
488
    m = _package_branch.search(url)
 
489
    if m is None:
 
490
        return None
 
491
    archive, series, project, user = m.group('archive', 'series',
 
492
                                             'project', 'user')
 
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('/')
 
497
    if user is not None:
 
498
        user = user.strip('~/')
 
499
        if user != 'ubuntu-branches':
 
500
            return None
 
501
    return archive, series, project
 
502
 
 
503
 
 
504
def _check_is_up_to_date(the_branch):
 
505
    info = _get_package_branch_info(the_branch.base)
 
506
    if info is None:
 
507
        return
 
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,))
 
515
        return
 
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)
 
520
 
 
521
 
 
522
def _register_hooks():
 
523
    _mod_branch.Branch.hooks.install_named_hook('open',
 
524
        _check_is_up_to_date, 'package-branch-up-to-date')
 
525
 
 
526
 
 
527
_register_hooks()
357
528
 
358
529
def load_tests(basic_tests, module, loader):
359
530
    testmod_names = [
360
531
        'test_account',
361
532
        'test_register',
362
533
        'test_lp_api',
 
534
        'test_lp_api_lite',
363
535
        'test_lp_directory',
364
536
        'test_lp_login',
365
537
        'test_lp_open',