~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Martin Pool
  • Date: 2010-02-25 06:17:27 UTC
  • mfrom: (5055 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5057.
  • Revision ID: mbp@sourcefrog.net-20100225061727-4sd9lt0qmdc6087t
merge news

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd
 
1
# Copyright (C) 2006-2010 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.
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
 
"""
 
17
"""Launchpad.net integration plugin for Bazaar."""
37
18
 
38
19
# The XMLRPC server address can be overridden by setting the environment
39
20
# variable $BZR_LP_XMLRPC_URL
40
21
 
41
 
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
 
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
42
26
 
43
27
from bzrlib.lazy_import import lazy_import
44
28
lazy_import(globals(), """
45
29
from bzrlib import (
46
 
    ui,
 
30
    branch as _mod_branch,
47
31
    trace,
48
32
    )
49
 
from bzrlib.i18n import gettext
50
33
""")
51
34
 
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
 
    )
 
35
from bzrlib import bzrdir
59
36
from bzrlib.commands import (
60
 
    Command,
61
 
    register_command,
62
 
    )
 
37
        Command,
 
38
        register_command,
 
39
)
63
40
from bzrlib.directory_service import directories
64
41
from bzrlib.errors import (
65
42
    BzrCommandError,
66
 
    InvalidRevisionSpec,
 
43
    DependencyNotPresent,
67
44
    InvalidURL,
68
45
    NoPublicBranch,
69
46
    NotBranchError,
76
53
 
77
54
 
78
55
class cmd_register_branch(Command):
79
 
    __doc__ = """Register a branch with launchpad.net.
 
56
    """Register a branch with launchpad.net.
80
57
 
81
58
    This command lists a bzr branch in the directory of branches on
82
59
    launchpad.net.  Registration allows the branch to be associated with
104
81
                'Launchpad project short name to associate with the branch.',
105
82
                unicode),
106
83
         Option('product',
107
 
                'Launchpad product short name to associate with the branch.',
 
84
                'Launchpad product short name to associate with the branch.', 
108
85
                unicode,
109
86
                hidden=True),
110
87
         Option('branch-name',
145
122
            try:
146
123
                b = _mod_branch.Branch.open_containing('.')[0]
147
124
            except NotBranchError:
148
 
                raise BzrCommandError(gettext(
149
 
                            'register-branch requires a public '
150
 
                            'branch url - see bzr help register-branch.'))
 
125
                raise BzrCommandError('register-branch requires a public '
 
126
                    'branch url - see bzr help register-branch.')
151
127
            public_url = b.get_public_branch()
152
128
            if public_url is None:
153
129
                raise NoPublicBranch(b)
154
130
        if product is not None:
155
131
            project = product
156
 
            trace.note(gettext(
157
 
                '--product is deprecated; please use --project.'))
 
132
            trace.note('--product is deprecated; please use --project.')
158
133
 
159
134
 
160
135
        rego = BranchRegistrationRequest(branch_url=public_url,
186
161
 
187
162
 
188
163
class cmd_launchpad_open(Command):
189
 
    __doc__ = """Open a Launchpad branch page in your web browser."""
 
164
    """Open a Launchpad branch page in your web browser."""
190
165
 
191
166
    aliases = ['lp-open']
192
167
    takes_options = [
226
201
        if location is None:
227
202
            location = u'.'
228
203
        web_url = self._get_web_url(LaunchpadService(), location)
229
 
        trace.note(gettext('Opening %s in web browser') % web_url)
 
204
        trace.note('Opening %s in web browser' % web_url)
230
205
        if not dry_run:
231
206
            import webbrowser   # this import should not be lazy
232
207
                                # otherwise bzr.exe lacks this module
236
211
 
237
212
 
238
213
class cmd_launchpad_login(Command):
239
 
    __doc__ = """Show or set the Launchpad user ID.
 
214
    """Show or set the Launchpad user ID.
240
215
 
241
216
    When communicating with Launchpad, some commands need to know your
242
217
    Launchpad user ID.  This command can be used to set or show the
270
245
                if check_account:
271
246
                    account.check_lp_login(username)
272
247
                    if verbose:
273
 
                        self.outf.write(gettext(
274
 
                            "Launchpad user ID exists and has SSH keys.\n"))
 
248
                        self.outf.write(
 
249
                            "Launchpad user ID exists and has SSH keys.\n")
275
250
                self.outf.write(username + '\n')
276
251
            else:
277
 
                self.outf.write(gettext('No Launchpad user ID configured.\n'))
 
252
                self.outf.write('No Launchpad user ID configured.\n')
278
253
                return 1
279
254
        else:
280
255
            name = name.lower()
281
256
            if check_account:
282
257
                account.check_lp_login(name)
283
258
                if verbose:
284
 
                    self.outf.write(gettext(
285
 
                        "Launchpad user ID exists and has SSH keys.\n"))
 
259
                    self.outf.write(
 
260
                        "Launchpad user ID exists and has SSH keys.\n")
286
261
            account.set_lp_login(name)
287
262
            if verbose:
288
 
                self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
289
 
                                                                        (name,))
 
263
                self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
290
264
 
291
265
register_command(cmd_launchpad_login)
292
266
 
293
267
 
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."""
297
271
 
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()
310
283
 
311
284
 
312
285
register_command(cmd_launchpad_mirror)
313
286
 
314
287
 
315
288
class cmd_lp_propose_merge(Command):
316
 
    __doc__ = """Propose merging a branch on Launchpad.
 
289
    """Propose merging a branch on Launchpad.
317
290
 
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.
337
310
                            help='Propose the merge on staging.'),
338
311
                     Option('message', short_name='m', type=unicode,
339
312
                            help='Commit message.'),
340
 
                     Option('approve',
341
 
                            help='Mark the proposal as approved immediately.'),
342
313
                     ListOption('review', short_name='R', type=unicode,
343
314
                            help='Requested reviewer and optional type.')]
344
315
 
347
318
    aliases = ['lp-submit', 'lp-propose']
348
319
 
349
320
    def run(self, submit_branch=None, review=None, staging=False,
350
 
            message=None, approve=False):
 
321
            message=None):
351
322
        from bzrlib.plugins.launchpad import lp_propose
352
323
        tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
353
324
            '.')
367
338
        else:
368
339
            target = _mod_branch.Branch.open(submit_branch)
369
340
        proposer = lp_propose.Proposer(tree, branch, target, message,
370
 
                                       reviews, staging, approve=approve)
 
341
                                       reviews, staging)
371
342
        proposer.check_proposal()
372
343
        proposer.create_proposal()
373
344
 
375
346
register_command(cmd_lp_propose_merge)
376
347
 
377
348
 
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
 
 
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',
467
 
        'ubuntu: shortcut')
468
 
 
469
353
_register_directory()
470
354
 
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()
528
355
 
529
356
def load_tests(basic_tests, module, loader):
530
357
    testmod_names = [
531
358
        'test_account',
532
359
        'test_register',
533
360
        'test_lp_api',
534
 
        'test_lp_api_lite',
535
361
        'test_lp_directory',
536
362
        'test_lp_login',
537
363
        'test_lp_open',