~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to __init__.py

  • Committer: Martin Pool
  • Date: 2006-03-22 19:21:20 UTC
  • mto: (1668.1.8 bzr-0.8.mbp)
  • mto: This revision was merged to the branch mainline in revision 1710.
  • Revision ID: mbp@sourcefrog.net-20060322192120-133f1e99d4c79477
Update xmlrpc api

Prompt for user password when registering

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd
 
1
# Copyright (C) 2006 by 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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
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
 
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Launchpad.net branch registration plugin for bzr
 
18
 
 
19
This adds commands that tell launchpad about newly-created branches, etc.
 
20
 
 
21
To install this file, put the 'bzr_lp' directory, or a symlink to it,
 
22
in your ~/.bazaar/plugins/ directory.
36
23
"""
37
24
 
38
 
# The XMLRPC server address can be overridden by setting the environment
39
 
# variable $BZR_LP_XMLRPC_URL
40
 
 
41
 
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
42
 
 
43
 
from bzrlib.lazy_import import lazy_import
44
 
lazy_import(globals(), """
45
 
from bzrlib import (
46
 
    ui,
47
 
    trace,
48
 
    )
49
 
""")
50
 
 
51
 
from bzrlib import (
52
 
    branch as _mod_branch,
53
 
    bzrdir,
54
 
    lazy_regex,
55
 
    # Since we are a built-in plugin we share the bzrlib version
56
 
    version_info,
57
 
    )
58
 
from bzrlib.commands import (
59
 
    Command,
60
 
    register_command,
61
 
    )
62
 
from bzrlib.directory_service import directories
63
 
from bzrlib.errors import (
64
 
    BzrCommandError,
65
 
    InvalidRevisionSpec,
66
 
    InvalidURL,
67
 
    NoPublicBranch,
68
 
    NotBranchError,
69
 
    )
70
 
from bzrlib.help_topics import topic_registry
71
 
from bzrlib.option import (
72
 
        Option,
73
 
        ListOption,
74
 
)
 
25
# see http://bazaar-vcs.org/Specs/BranchRegistrationTool
 
26
 
 
27
from bzrlib.commands import Command, Option, register_command
 
28
 
75
29
 
76
30
 
77
31
class cmd_register_branch(Command):
78
 
    __doc__ = """Register a branch with launchpad.net.
 
32
    """Register a branch with launchpad.net.
79
33
 
80
34
    This command lists a bzr branch in the directory of branches on
81
 
    launchpad.net.  Registration allows the branch to be associated with
 
35
    launchpad.net.  Registration allows the bug to be associated with
82
36
    bugs or specifications.
83
 
 
 
37
    
84
38
    Before using this command you must register the project to which the
85
39
    branch belongs, and create an account for yourself on launchpad.net.
86
40
 
87
41
    arguments:
88
 
        public_url: The publicly visible url for the branch to register.
89
 
                    This must be an http or https url (which Launchpad can read
90
 
                    from to access the branch). Local file urls, SFTP urls, and
91
 
                    bzr+ssh urls will not work.
92
 
                    If no public_url is provided, bzr will use the configured
93
 
                    public_url if there is one for the current branch, and
94
 
                    otherwise error.
 
42
        branch_url: The publicly visible url for the branch.
 
43
                    This must be an http or https url, not a local file
 
44
                    path.
95
45
 
96
46
    example:
97
 
        bzr register-branch http://foo.com/bzr/fooproject.mine \\
 
47
        bzr register-branch http://foo.com/bzr/fooproject.mine \
98
48
                --project fooproject
99
49
    """
100
 
    takes_args = ['public_url?']
101
 
    takes_options = [
102
 
         Option('project',
103
 
                'Launchpad project short name to associate with the branch.',
104
 
                unicode),
105
 
         Option('product',
106
 
                'Launchpad product short name to associate with the branch.',
107
 
                unicode,
108
 
                hidden=True),
109
 
         Option('branch-name',
110
 
                'Short name for the branch; '
111
 
                'by default taken from the last component of the url.',
112
 
                unicode),
113
 
         Option('branch-title',
114
 
                'One-sentence description of the branch.',
115
 
                unicode),
116
 
         Option('branch-description',
117
 
                'Longer description of the purpose or contents of the branch.',
118
 
                unicode),
119
 
         Option('author',
120
 
                "Branch author's email address, if not yourself.",
121
 
                unicode),
122
 
         Option('link-bug',
123
 
                'The bug this branch fixes.',
124
 
                int),
125
 
         Option('dry-run',
126
 
                'Prepare the request but don\'t actually send it.')
127
 
        ]
128
 
 
129
 
 
130
 
    def run(self,
131
 
            public_url=None,
132
 
            project='',
133
 
            product=None,
134
 
            branch_name='',
135
 
            branch_title='',
136
 
            branch_description='',
137
 
            author='',
138
 
            link_bug=None,
139
 
            dry_run=False):
140
 
        from bzrlib.plugins.launchpad.lp_registration import (
141
 
            BranchRegistrationRequest, BranchBugLinkRequest,
142
 
            DryRunLaunchpadService, LaunchpadService)
143
 
        if public_url is None:
144
 
            try:
145
 
                b = _mod_branch.Branch.open_containing('.')[0]
146
 
            except NotBranchError:
147
 
                raise BzrCommandError('register-branch requires a public '
148
 
                    'branch url - see bzr help register-branch.')
149
 
            public_url = b.get_public_branch()
150
 
            if public_url is None:
151
 
                raise NoPublicBranch(b)
152
 
        if product is not None:
153
 
            project = product
154
 
            trace.note('--product is deprecated; please use --project.')
155
 
 
156
 
 
157
 
        rego = BranchRegistrationRequest(branch_url=public_url,
158
 
                                         branch_name=branch_name,
159
 
                                         branch_title=branch_title,
160
 
                                         branch_description=branch_description,
161
 
                                         product_name=project,
162
 
                                         author_email=author,
163
 
                                         )
164
 
        linko = BranchBugLinkRequest(branch_url=public_url,
165
 
                                     bug_id=link_bug)
166
 
        if not dry_run:
167
 
            service = LaunchpadService()
168
 
            # This gives back the xmlrpc url that can be used for future
169
 
            # operations on the branch.  It's not so useful to print to the
170
 
            # user since they can't do anything with it from a web browser; it
171
 
            # might be nice for the server to tell us about an html url as
172
 
            # well.
173
 
        else:
174
 
            # Run on service entirely in memory
175
 
            service = DryRunLaunchpadService()
176
 
        service.gather_user_credentials()
177
 
        rego.submit(service)
178
 
        if link_bug:
179
 
            linko.submit(service)
180
 
        print 'Branch registered.'
 
50
    takes_args = ['branch_url']
 
51
 
 
52
    def run(self, branch_url):
 
53
        from lp_registration import register_interactive
 
54
        register_interactive(branch_url)
181
55
 
182
56
register_command(cmd_register_branch)
183
57
 
184
 
 
185
 
class cmd_launchpad_open(Command):
186
 
    __doc__ = """Open a Launchpad branch page in your web browser."""
187
 
 
188
 
    aliases = ['lp-open']
189
 
    takes_options = [
190
 
        Option('dry-run',
191
 
               'Do not actually open the browser. Just say the URL we would '
192
 
               'use.'),
193
 
        ]
194
 
    takes_args = ['location?']
195
 
 
196
 
    def _possible_locations(self, location):
197
 
        """Yield possible external locations for the branch at 'location'."""
198
 
        yield location
199
 
        try:
200
 
            branch = _mod_branch.Branch.open_containing(location)[0]
201
 
        except NotBranchError:
202
 
            return
203
 
        branch_url = branch.get_public_branch()
204
 
        if branch_url is not None:
205
 
            yield branch_url
206
 
        branch_url = branch.get_push_location()
207
 
        if branch_url is not None:
208
 
            yield branch_url
209
 
 
210
 
    def _get_web_url(self, service, location):
211
 
        from bzrlib.plugins.launchpad.lp_registration import (
212
 
            NotLaunchpadBranch)
213
 
        for branch_url in self._possible_locations(location):
214
 
            try:
215
 
                return service.get_web_url_from_branch_url(branch_url)
216
 
            except (NotLaunchpadBranch, InvalidURL):
217
 
                pass
218
 
        raise NotLaunchpadBranch(branch_url)
219
 
 
220
 
    def run(self, location=None, dry_run=False):
221
 
        from bzrlib.plugins.launchpad.lp_registration import (
222
 
            LaunchpadService)
223
 
        if location is None:
224
 
            location = u'.'
225
 
        web_url = self._get_web_url(LaunchpadService(), location)
226
 
        trace.note('Opening %s in web browser' % web_url)
227
 
        if not dry_run:
228
 
            import webbrowser   # this import should not be lazy
229
 
                                # otherwise bzr.exe lacks this module
230
 
            webbrowser.open(web_url)
231
 
 
232
 
register_command(cmd_launchpad_open)
233
 
 
234
 
 
235
 
class cmd_launchpad_login(Command):
236
 
    __doc__ = """Show or set the Launchpad user ID.
237
 
 
238
 
    When communicating with Launchpad, some commands need to know your
239
 
    Launchpad user ID.  This command can be used to set or show the
240
 
    user ID that Bazaar will use for such communication.
241
 
 
242
 
    :Examples:
243
 
      Show the Launchpad ID of the current user::
244
 
 
245
 
          bzr launchpad-login
246
 
 
247
 
      Set the Launchpad ID of the current user to 'bob'::
248
 
 
249
 
          bzr launchpad-login bob
250
 
    """
251
 
    aliases = ['lp-login']
252
 
    takes_args = ['name?']
253
 
    takes_options = [
254
 
        'verbose',
255
 
        Option('no-check',
256
 
               "Don't check that the user name is valid."),
257
 
        ]
258
 
 
259
 
    def run(self, name=None, no_check=False, verbose=False):
260
 
        # This is totally separate from any launchpadlib login system.
261
 
        from bzrlib.plugins.launchpad import account
262
 
        check_account = not no_check
263
 
 
264
 
        if name is None:
265
 
            username = account.get_lp_login()
266
 
            if username:
267
 
                if check_account:
268
 
                    account.check_lp_login(username)
269
 
                    if verbose:
270
 
                        self.outf.write(
271
 
                            "Launchpad user ID exists and has SSH keys.\n")
272
 
                self.outf.write(username + '\n')
273
 
            else:
274
 
                self.outf.write('No Launchpad user ID configured.\n')
275
 
                return 1
276
 
        else:
277
 
            name = name.lower()
278
 
            if check_account:
279
 
                account.check_lp_login(name)
280
 
                if verbose:
281
 
                    self.outf.write(
282
 
                        "Launchpad user ID exists and has SSH keys.\n")
283
 
            account.set_lp_login(name)
284
 
            if verbose:
285
 
                self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
286
 
 
287
 
register_command(cmd_launchpad_login)
288
 
 
289
 
 
290
 
# XXX: cmd_launchpad_mirror is untested
291
 
class cmd_launchpad_mirror(Command):
292
 
    __doc__ = """Ask Launchpad to mirror a branch now."""
293
 
 
294
 
    aliases = ['lp-mirror']
295
 
    takes_args = ['location?']
296
 
 
297
 
    def run(self, location='.'):
298
 
        from bzrlib.plugins.launchpad import lp_api
299
 
        from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
300
 
        branch, _ = _mod_branch.Branch.open_containing(location)
301
 
        service = LaunchpadService()
302
 
        launchpad = lp_api.login(service)
303
 
        lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
304
 
                create_missing=False)
305
 
        lp_branch.lp.requestMirror()
306
 
 
307
 
 
308
 
register_command(cmd_launchpad_mirror)
309
 
 
310
 
 
311
 
class cmd_lp_propose_merge(Command):
312
 
    __doc__ = """Propose merging a branch on Launchpad.
313
 
 
314
 
    This will open your usual editor to provide the initial comment.  When it
315
 
    has created the proposal, it will open it in your default web browser.
316
 
 
317
 
    The branch will be proposed to merge into SUBMIT_BRANCH.  If SUBMIT_BRANCH
318
 
    is not supplied, the remembered submit branch will be used.  If no submit
319
 
    branch is remembered, the development focus will be used.
320
 
 
321
 
    By default, the SUBMIT_BRANCH's review team will be requested to review
322
 
    the merge proposal.  This can be overriden by specifying --review (-R).
323
 
    The parameter the launchpad account name of the desired reviewer.  This
324
 
    may optionally be followed by '=' and the review type.  For example:
325
 
 
326
 
      bzr lp-propose-merge --review jrandom --review review-team=qa
327
 
 
328
 
    This will propose a merge,  request "jrandom" to perform a review of
329
 
    unspecified type, and request "review-team" to perform a "qa" review.
330
 
    """
331
 
 
332
 
    takes_options = [Option('staging',
333
 
                            help='Propose the merge on staging.'),
334
 
                     Option('message', short_name='m', type=unicode,
335
 
                            help='Commit message.'),
336
 
                     Option('approve',
337
 
                            help='Mark the proposal as approved immediately.'),
338
 
                     ListOption('review', short_name='R', type=unicode,
339
 
                            help='Requested reviewer and optional type.')]
340
 
 
341
 
    takes_args = ['submit_branch?']
342
 
 
343
 
    aliases = ['lp-submit', 'lp-propose']
344
 
 
345
 
    def run(self, submit_branch=None, review=None, staging=False,
346
 
            message=None, approve=False):
347
 
        from bzrlib.plugins.launchpad import lp_propose
348
 
        tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
349
 
            '.')
350
 
        if review is None:
351
 
            reviews = None
352
 
        else:
353
 
            reviews = []
354
 
            for review in review:
355
 
                if '=' in review:
356
 
                    reviews.append(review.split('=', 2))
357
 
                else:
358
 
                    reviews.append((review, ''))
359
 
            if submit_branch is None:
360
 
                submit_branch = branch.get_submit_branch()
361
 
        if submit_branch is None:
362
 
            target = None
363
 
        else:
364
 
            target = _mod_branch.Branch.open(submit_branch)
365
 
        proposer = lp_propose.Proposer(tree, branch, target, message,
366
 
                                       reviews, staging, approve=approve)
367
 
        proposer.check_proposal()
368
 
        proposer.create_proposal()
369
 
 
370
 
 
371
 
register_command(cmd_lp_propose_merge)
372
 
 
373
 
 
374
 
class cmd_lp_find_proposal(Command):
375
 
 
376
 
    __doc__ = """Find the proposal to merge this revision.
377
 
 
378
 
    Finds the merge proposal(s) that discussed landing the specified revision.
379
 
    This works only if the selected branch was the merge proposal target, and
380
 
    if the merged_revno is recorded for the merge proposal.  The proposal(s)
381
 
    are opened in a web browser.
382
 
 
383
 
    Any revision involved in the merge may be specified-- the revision in
384
 
    which the merge was performed, or one of the revisions that was merged.
385
 
 
386
 
    So, to find the merge proposal that reviewed line 1 of README::
387
 
 
388
 
      bzr lp-find-proposal -r annotate:README:1
389
 
    """
390
 
 
391
 
    takes_options = ['revision']
392
 
 
393
 
    def run(self, revision=None):
394
 
        from bzrlib.plugins.launchpad import lp_api
395
 
        import webbrowser
396
 
        b = _mod_branch.Branch.open_containing('.')[0]
397
 
        pb = ui.ui_factory.nested_progress_bar()
398
 
        b.lock_read()
399
 
        try:
400
 
            revno = self._find_merged_revno(revision, b, pb)
401
 
            merged = self._find_proposals(revno, b, pb)
402
 
            if len(merged) == 0:
403
 
                raise BzrCommandError('No review found.')
404
 
            trace.note('%d proposals(s) found.' % len(merged))
405
 
            for mp in merged:
406
 
                webbrowser.open(lp_api.canonical_url(mp))
407
 
        finally:
408
 
            b.unlock()
409
 
            pb.finished()
410
 
 
411
 
    def _find_merged_revno(self, revision, b, pb):
412
 
        if revision is None:
413
 
            return b.revno()
414
 
        pb.update('Finding revision-id')
415
 
        revision_id = revision[0].as_revision_id(b)
416
 
        # a revno spec is necessarily on the mainline.
417
 
        if self._is_revno_spec(revision[0]):
418
 
            merging_revision = revision_id
419
 
        else:
420
 
            graph = b.repository.get_graph()
421
 
            pb.update('Finding merge')
422
 
            merging_revision = graph.find_lefthand_merger(
423
 
                revision_id, b.last_revision())
424
 
            if merging_revision is None:
425
 
                raise InvalidRevisionSpec(revision[0].user_spec, b)
426
 
        pb.update('Finding revno')
427
 
        return b.revision_id_to_revno(merging_revision)
428
 
 
429
 
    def _find_proposals(self, revno, b, pb):
430
 
        launchpad = lp_api.login(lp_registration.LaunchpadService())
431
 
        pb.update('Finding Launchpad branch')
432
 
        lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
433
 
                                              create_missing=False)
434
 
        pb.update('Finding proposals')
435
 
        return list(lpb.lp.getMergeProposals(status=['Merged'],
436
 
                                             merged_revnos=[revno]))
437
 
 
438
 
 
439
 
    @staticmethod
440
 
    def _is_revno_spec(spec):
441
 
        try:
442
 
            int(spec.user_spec)
443
 
        except ValueError:
444
 
            return False
445
 
        else:
446
 
            return True
447
 
 
448
 
 
449
 
register_command(cmd_lp_find_proposal)
450
 
 
451
 
 
452
 
def _register_directory():
453
 
    directories.register_lazy('lp:', 'bzrlib.plugins.launchpad.lp_directory',
454
 
                              'LaunchpadDirectory',
455
 
                              'Launchpad-based directory service',)
456
 
    directories.register_lazy(
457
 
        'debianlp:', 'bzrlib.plugins.launchpad.lp_directory',
458
 
        'LaunchpadDirectory',
459
 
        'debianlp: shortcut')
460
 
    directories.register_lazy(
461
 
        'ubuntu:', 'bzrlib.plugins.launchpad.lp_directory',
462
 
        'LaunchpadDirectory',
463
 
        'ubuntu: shortcut')
464
 
 
465
 
_register_directory()
466
 
 
467
 
# This is kept in __init__ so that we don't load lp_api_lite unless the branch
468
 
# actually matches. That way we can avoid importing extra dependencies like
469
 
# json.
470
 
_package_branch = lazy_regex.lazy_compile(
471
 
    r'bazaar.launchpad.net.*?/'
472
 
    r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
473
 
    r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
474
 
    )
475
 
 
476
 
def _get_package_branch_info(url):
477
 
    """Determine the packaging information for this URL.
478
 
 
479
 
    :return: If this isn't a packaging branch, return None. If it is, return
480
 
        (archive, series, project)
481
 
    """
482
 
    if url is None:
483
 
        return None
484
 
    m = _package_branch.search(url)
485
 
    if m is None:
486
 
        return None
487
 
    archive, series, project, user = m.group('archive', 'series',
488
 
                                             'project', 'user')
489
 
    if series is not None:
490
 
        # series is optional, so the regex includes the extra '/', we don't
491
 
        # want to send that on (it causes Internal Server Errors.)
492
 
        series = series.strip('/')
493
 
    if user is not None:
494
 
        user = user.strip('~/')
495
 
        if user != 'ubuntu-branches':
496
 
            return None
497
 
    return archive, series, project
498
 
 
499
 
 
500
 
def _check_is_up_to_date(the_branch):
501
 
    info = _get_package_branch_info(the_branch.base)
502
 
    if info is None:
503
 
        return
504
 
    c = the_branch.get_config()
505
 
    verbosity = c.get_user_option('launchpad.packaging_verbosity')
506
 
    if verbosity is not None:
507
 
        verbosity = verbosity.lower()
508
 
    if verbosity == 'off':
509
 
        trace.mutter('not checking %s because verbosity is turned off'
510
 
                     % (the_branch.base,))
511
 
        return
512
 
    archive, series, project = info
513
 
    from bzrlib.plugins.launchpad import lp_api_lite
514
 
    latest_pub = lp_api_lite.LatestPublication(archive, series, project)
515
 
    lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
516
 
 
517
 
 
518
 
def _register_hooks():
519
 
    _mod_branch.Branch.hooks.install_named_hook('open',
520
 
        _check_is_up_to_date, 'package-branch-up-to-date')
521
 
 
522
 
 
523
 
_register_hooks()
524
 
 
525
 
def load_tests(basic_tests, module, loader):
526
 
    testmod_names = [
527
 
        'test_account',
528
 
        'test_register',
529
 
        'test_lp_api',
530
 
        'test_lp_api_lite',
531
 
        'test_lp_directory',
532
 
        'test_lp_login',
533
 
        'test_lp_open',
534
 
        'test_lp_service',
535
 
        ]
536
 
    basic_tests.addTest(loader.loadTestsFromModuleNames(
537
 
            ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
538
 
    return basic_tests
539
 
 
540
 
 
541
 
_launchpad_help = """Integration with Launchpad.net
542
 
 
543
 
Launchpad.net provides free Bazaar branch hosting with integrated bug and
544
 
specification tracking.
545
 
 
546
 
The bzr client (through the plugin called 'launchpad') has special
547
 
features to communicate with Launchpad:
548
 
 
549
 
    * The launchpad-login command tells Bazaar your Launchpad user name. This
550
 
      is then used by the 'lp:' transport to download your branches using
551
 
      bzr+ssh://.
552
 
 
553
 
    * The 'lp:' transport uses Launchpad as a directory service: for example
554
 
      'lp:bzr' and 'lp:python' refer to the main branches of the relevant
555
 
      projects and may be branched, logged, etc. You can also use the 'lp:'
556
 
      transport to refer to specific branches, e.g. lp:~bzr/bzr/trunk.
557
 
 
558
 
    * The 'lp:' bug tracker alias can expand launchpad bug numbers to their
559
 
      URLs for use with 'bzr commit --fixes', e.g. 'bzr commit --fixes lp:12345'
560
 
      will record a revision property that marks that revision as fixing
561
 
      Launchpad bug 12345. When you push that branch to Launchpad it will
562
 
      automatically be linked to the bug report.
563
 
 
564
 
    * The register-branch command tells Launchpad about the url of a
565
 
      public branch.  Launchpad will then mirror the branch, display
566
 
      its contents and allow it to be attached to bugs and other
567
 
      objects.
568
 
 
569
 
For more information see http://help.launchpad.net/
570
 
"""
571
 
topic_registry.register('launchpad',
572
 
    _launchpad_help,
573
 
    'Using Bazaar with Launchpad.net')
 
58
def test_suite():
 
59
    """Called by bzrlib to fetch tests for this plugin"""
 
60
    from unittest import TestSuite, TestLoader
 
61
    import test_register
 
62
    return TestLoader().loadTestsFromModule(test_register)