~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
35
35
 
36
36
"""
37
37
 
 
38
from __future__ import absolute_import
 
39
 
38
40
# The XMLRPC server address can be overridden by setting the environment
39
41
# variable $BZR_LP_XMLRPC_URL
40
42
 
41
43
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
42
44
 
43
 
# Since we are a built-in plugin we share the bzrlib version
44
 
from bzrlib import version_info
45
 
 
46
 
from bzrlib.lazy_import import lazy_import
47
 
lazy_import(globals(), """
48
45
from bzrlib import (
49
46
    branch as _mod_branch,
50
 
    ui,
 
47
    config as _mod_config,
 
48
    lazy_regex,
 
49
    # Since we are a built-in plugin we share the bzrlib version
51
50
    trace,
 
51
    version_info,
52
52
    )
53
 
""")
54
 
 
55
 
from bzrlib import bzrdir
56
53
from bzrlib.commands import (
57
 
    Command,
58
 
    register_command,
 
54
    plugin_cmds,
59
55
    )
60
56
from bzrlib.directory_service import directories
61
 
from bzrlib.errors import (
62
 
    BzrCommandError,
63
 
    InvalidRevisionSpec,
64
 
    InvalidURL,
65
 
    NoPublicBranch,
66
 
    NotBranchError,
67
 
    )
68
57
from bzrlib.help_topics import topic_registry
69
 
from bzrlib.option import (
70
 
        Option,
71
 
        ListOption,
72
 
)
73
 
 
74
 
 
75
 
class cmd_register_branch(Command):
76
 
    __doc__ = """Register a branch with launchpad.net.
77
 
 
78
 
    This command lists a bzr branch in the directory of branches on
79
 
    launchpad.net.  Registration allows the branch to be associated with
80
 
    bugs or specifications.
81
 
 
82
 
    Before using this command you must register the project to which the
83
 
    branch belongs, and create an account for yourself on launchpad.net.
84
 
 
85
 
    arguments:
86
 
        public_url: The publicly visible url for the branch to register.
87
 
                    This must be an http or https url (which Launchpad can read
88
 
                    from to access the branch). Local file urls, SFTP urls, and
89
 
                    bzr+ssh urls will not work.
90
 
                    If no public_url is provided, bzr will use the configured
91
 
                    public_url if there is one for the current branch, and
92
 
                    otherwise error.
93
 
 
94
 
    example:
95
 
        bzr register-branch http://foo.com/bzr/fooproject.mine \\
96
 
                --project fooproject
97
 
    """
98
 
    takes_args = ['public_url?']
99
 
    takes_options = [
100
 
         Option('project',
101
 
                'Launchpad project short name to associate with the branch.',
102
 
                unicode),
103
 
         Option('product',
104
 
                'Launchpad product short name to associate with the branch.', 
105
 
                unicode,
106
 
                hidden=True),
107
 
         Option('branch-name',
108
 
                'Short name for the branch; '
109
 
                'by default taken from the last component of the url.',
110
 
                unicode),
111
 
         Option('branch-title',
112
 
                'One-sentence description of the branch.',
113
 
                unicode),
114
 
         Option('branch-description',
115
 
                'Longer description of the purpose or contents of the branch.',
116
 
                unicode),
117
 
         Option('author',
118
 
                "Branch author's email address, if not yourself.",
119
 
                unicode),
120
 
         Option('link-bug',
121
 
                'The bug this branch fixes.',
122
 
                int),
123
 
         Option('dry-run',
124
 
                'Prepare the request but don\'t actually send it.')
125
 
        ]
126
 
 
127
 
 
128
 
    def run(self,
129
 
            public_url=None,
130
 
            project='',
131
 
            product=None,
132
 
            branch_name='',
133
 
            branch_title='',
134
 
            branch_description='',
135
 
            author='',
136
 
            link_bug=None,
137
 
            dry_run=False):
138
 
        from bzrlib.plugins.launchpad.lp_registration import (
139
 
            BranchRegistrationRequest, BranchBugLinkRequest,
140
 
            DryRunLaunchpadService, LaunchpadService)
141
 
        if public_url is None:
142
 
            try:
143
 
                b = _mod_branch.Branch.open_containing('.')[0]
144
 
            except NotBranchError:
145
 
                raise BzrCommandError('register-branch requires a public '
146
 
                    'branch url - see bzr help register-branch.')
147
 
            public_url = b.get_public_branch()
148
 
            if public_url is None:
149
 
                raise NoPublicBranch(b)
150
 
        if product is not None:
151
 
            project = product
152
 
            trace.note('--product is deprecated; please use --project.')
153
 
 
154
 
 
155
 
        rego = BranchRegistrationRequest(branch_url=public_url,
156
 
                                         branch_name=branch_name,
157
 
                                         branch_title=branch_title,
158
 
                                         branch_description=branch_description,
159
 
                                         product_name=project,
160
 
                                         author_email=author,
161
 
                                         )
162
 
        linko = BranchBugLinkRequest(branch_url=public_url,
163
 
                                     bug_id=link_bug)
164
 
        if not dry_run:
165
 
            service = LaunchpadService()
166
 
            # This gives back the xmlrpc url that can be used for future
167
 
            # operations on the branch.  It's not so useful to print to the
168
 
            # user since they can't do anything with it from a web browser; it
169
 
            # might be nice for the server to tell us about an html url as
170
 
            # well.
171
 
        else:
172
 
            # Run on service entirely in memory
173
 
            service = DryRunLaunchpadService()
174
 
        service.gather_user_credentials()
175
 
        rego.submit(service)
176
 
        if link_bug:
177
 
            linko.submit(service)
178
 
        print 'Branch registered.'
179
 
 
180
 
register_command(cmd_register_branch)
181
 
 
182
 
 
183
 
class cmd_launchpad_open(Command):
184
 
    __doc__ = """Open a Launchpad branch page in your web browser."""
185
 
 
186
 
    aliases = ['lp-open']
187
 
    takes_options = [
188
 
        Option('dry-run',
189
 
               'Do not actually open the browser. Just say the URL we would '
190
 
               'use.'),
191
 
        ]
192
 
    takes_args = ['location?']
193
 
 
194
 
    def _possible_locations(self, location):
195
 
        """Yield possible external locations for the branch at 'location'."""
196
 
        yield location
197
 
        try:
198
 
            branch = _mod_branch.Branch.open_containing(location)[0]
199
 
        except NotBranchError:
200
 
            return
201
 
        branch_url = branch.get_public_branch()
202
 
        if branch_url is not None:
203
 
            yield branch_url
204
 
        branch_url = branch.get_push_location()
205
 
        if branch_url is not None:
206
 
            yield branch_url
207
 
 
208
 
    def _get_web_url(self, service, location):
209
 
        from bzrlib.plugins.launchpad.lp_registration import (
210
 
            NotLaunchpadBranch)
211
 
        for branch_url in self._possible_locations(location):
212
 
            try:
213
 
                return service.get_web_url_from_branch_url(branch_url)
214
 
            except (NotLaunchpadBranch, InvalidURL):
215
 
                pass
216
 
        raise NotLaunchpadBranch(branch_url)
217
 
 
218
 
    def run(self, location=None, dry_run=False):
219
 
        from bzrlib.plugins.launchpad.lp_registration import (
220
 
            LaunchpadService)
221
 
        if location is None:
222
 
            location = u'.'
223
 
        web_url = self._get_web_url(LaunchpadService(), location)
224
 
        trace.note('Opening %s in web browser' % web_url)
225
 
        if not dry_run:
226
 
            import webbrowser   # this import should not be lazy
227
 
                                # otherwise bzr.exe lacks this module
228
 
            webbrowser.open(web_url)
229
 
 
230
 
register_command(cmd_launchpad_open)
231
 
 
232
 
 
233
 
class cmd_launchpad_login(Command):
234
 
    __doc__ = """Show or set the Launchpad user ID.
235
 
 
236
 
    When communicating with Launchpad, some commands need to know your
237
 
    Launchpad user ID.  This command can be used to set or show the
238
 
    user ID that Bazaar will use for such communication.
239
 
 
240
 
    :Examples:
241
 
      Show the Launchpad ID of the current user::
242
 
 
243
 
          bzr launchpad-login
244
 
 
245
 
      Set the Launchpad ID of the current user to 'bob'::
246
 
 
247
 
          bzr launchpad-login bob
248
 
    """
249
 
    aliases = ['lp-login']
250
 
    takes_args = ['name?']
251
 
    takes_options = [
252
 
        'verbose',
253
 
        Option('no-check',
254
 
               "Don't check that the user name is valid."),
255
 
        ]
256
 
 
257
 
    def run(self, name=None, no_check=False, verbose=False):
258
 
        # This is totally separate from any launchpadlib login system.
259
 
        from bzrlib.plugins.launchpad import account
260
 
        check_account = not no_check
261
 
 
262
 
        if name is None:
263
 
            username = account.get_lp_login()
264
 
            if username:
265
 
                if check_account:
266
 
                    account.check_lp_login(username)
267
 
                    if verbose:
268
 
                        self.outf.write(
269
 
                            "Launchpad user ID exists and has SSH keys.\n")
270
 
                self.outf.write(username + '\n')
271
 
            else:
272
 
                self.outf.write('No Launchpad user ID configured.\n')
273
 
                return 1
274
 
        else:
275
 
            name = name.lower()
276
 
            if check_account:
277
 
                account.check_lp_login(name)
278
 
                if verbose:
279
 
                    self.outf.write(
280
 
                        "Launchpad user ID exists and has SSH keys.\n")
281
 
            account.set_lp_login(name)
282
 
            if verbose:
283
 
                self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
284
 
 
285
 
register_command(cmd_launchpad_login)
286
 
 
287
 
 
288
 
# XXX: cmd_launchpad_mirror is untested
289
 
class cmd_launchpad_mirror(Command):
290
 
    __doc__ = """Ask Launchpad to mirror a branch now."""
291
 
 
292
 
    aliases = ['lp-mirror']
293
 
    takes_args = ['location?']
294
 
 
295
 
    def run(self, location='.'):
296
 
        from bzrlib.plugins.launchpad import lp_api
297
 
        from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
298
 
        branch, _ = _mod_branch.Branch.open_containing(location)
299
 
        service = LaunchpadService()
300
 
        launchpad = lp_api.login(service)
301
 
        lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
302
 
                create_missing=False)
303
 
        lp_branch.lp.requestMirror()
304
 
 
305
 
 
306
 
register_command(cmd_launchpad_mirror)
307
 
 
308
 
 
309
 
class cmd_lp_propose_merge(Command):
310
 
    __doc__ = """Propose merging a branch on Launchpad.
311
 
 
312
 
    This will open your usual editor to provide the initial comment.  When it
313
 
    has created the proposal, it will open it in your default web browser.
314
 
 
315
 
    The branch will be proposed to merge into SUBMIT_BRANCH.  If SUBMIT_BRANCH
316
 
    is not supplied, the remembered submit branch will be used.  If no submit
317
 
    branch is remembered, the development focus will be used.
318
 
 
319
 
    By default, the SUBMIT_BRANCH's review team will be requested to review
320
 
    the merge proposal.  This can be overriden by specifying --review (-R).
321
 
    The parameter the launchpad account name of the desired reviewer.  This
322
 
    may optionally be followed by '=' and the review type.  For example:
323
 
 
324
 
      bzr lp-propose-merge --review jrandom --review review-team=qa
325
 
 
326
 
    This will propose a merge,  request "jrandom" to perform a review of
327
 
    unspecified type, and request "review-team" to perform a "qa" review.
328
 
    """
329
 
 
330
 
    takes_options = [Option('staging',
331
 
                            help='Propose the merge on staging.'),
332
 
                     Option('message', short_name='m', type=unicode,
333
 
                            help='Commit message.'),
334
 
                     Option('approve',
335
 
                            help='Mark the proposal as approved immediately.'),
336
 
                     ListOption('review', short_name='R', type=unicode,
337
 
                            help='Requested reviewer and optional type.')]
338
 
 
339
 
    takes_args = ['submit_branch?']
340
 
 
341
 
    aliases = ['lp-submit', 'lp-propose']
342
 
 
343
 
    def run(self, submit_branch=None, review=None, staging=False,
344
 
            message=None, approve=False):
345
 
        from bzrlib.plugins.launchpad import lp_propose
346
 
        tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
347
 
            '.')
348
 
        if review is None:
349
 
            reviews = None
350
 
        else:
351
 
            reviews = []
352
 
            for review in review:
353
 
                if '=' in review:
354
 
                    reviews.append(review.split('=', 2))
355
 
                else:
356
 
                    reviews.append((review, ''))
357
 
            if submit_branch is None:
358
 
                submit_branch = branch.get_submit_branch()
359
 
        if submit_branch is None:
360
 
            target = None
361
 
        else:
362
 
            target = _mod_branch.Branch.open(submit_branch)
363
 
        proposer = lp_propose.Proposer(tree, branch, target, message,
364
 
                                       reviews, staging, approve=approve)
365
 
        proposer.check_proposal()
366
 
        proposer.create_proposal()
367
 
 
368
 
 
369
 
register_command(cmd_lp_propose_merge)
370
 
 
371
 
 
372
 
class cmd_lp_find_proposal(Command):
373
 
 
374
 
    __doc__ = """Find the proposal to merge this revision.
375
 
 
376
 
    Finds the merge proposal(s) that discussed landing the specified revision.
377
 
    This works only if the selected branch was the merge proposal target, and
378
 
    if the merged_revno is recorded for the merge proposal.  The proposal(s)
379
 
    are opened in a web browser.
380
 
 
381
 
    Any revision involved in the merge may be specified-- the revision in
382
 
    which the merge was performed, or one of the revisions that was merged.
383
 
 
384
 
    So, to find the merge proposal that reviewed line 1 of README::
385
 
 
386
 
      bzr lp-find-proposal -r annotate:README:1
387
 
    """
388
 
 
389
 
    takes_options = ['revision']
390
 
 
391
 
    def run(self, revision=None):
392
 
        from bzrlib.plugins.launchpad import lp_api
393
 
        import webbrowser
394
 
        b = _mod_branch.Branch.open_containing('.')[0]
395
 
        pb = ui.ui_factory.nested_progress_bar()
396
 
        b.lock_read()
397
 
        try:
398
 
            revno = self._find_merged_revno(revision, b, pb)
399
 
            merged = self._find_proposals(revno, b, pb)
400
 
            if len(merged) == 0:
401
 
                raise BzrCommandError('No review found.')
402
 
            trace.note('%d proposals(s) found.' % len(merged))
403
 
            for mp in merged:
404
 
                webbrowser.open(lp_api.canonical_url(mp))
405
 
        finally:
406
 
            b.unlock()
407
 
            pb.finished()
408
 
 
409
 
    def _find_merged_revno(self, revision, b, pb):
410
 
        if revision is None:
411
 
            return b.revno()
412
 
        pb.update('Finding revision-id')
413
 
        revision_id = revision[0].as_revision_id(b)
414
 
        # a revno spec is necessarily on the mainline.
415
 
        if self._is_revno_spec(revision[0]):
416
 
            merging_revision = revision_id
417
 
        else:
418
 
            graph = b.repository.get_graph()
419
 
            pb.update('Finding merge')
420
 
            merging_revision = graph.find_lefthand_merger(
421
 
                revision_id, b.last_revision())
422
 
            if merging_revision is None:
423
 
                raise InvalidRevisionSpec(revision[0].user_spec, b)
424
 
        pb.update('Finding revno')
425
 
        return b.revision_id_to_revno(merging_revision)
426
 
 
427
 
    def _find_proposals(self, revno, b, pb):
428
 
        launchpad = lp_api.login(lp_registration.LaunchpadService())
429
 
        pb.update('Finding Launchpad branch')
430
 
        lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
431
 
                                              create_missing=False)
432
 
        pb.update('Finding proposals')
433
 
        return list(lpb.lp.getMergeProposals(status=['Merged'],
434
 
                                             merged_revnos=[revno]))
435
 
 
436
 
 
437
 
    @staticmethod
438
 
    def _is_revno_spec(spec):
439
 
        try:
440
 
            int(spec.user_spec)
441
 
        except ValueError:
442
 
            return False
443
 
        else:
444
 
            return True
445
 
 
446
 
 
447
 
register_command(cmd_lp_find_proposal)
 
58
 
 
59
for klsname, aliases in [
 
60
    ("cmd_register_branch", []),
 
61
    ("cmd_launchpad_open", ["lp-open"]),
 
62
    ("cmd_launchpad_login", ["lp-login"]),
 
63
    ("cmd_launchpad_mirror", ["lp-mirror"]),
 
64
    ("cmd_lp_propose_merge", ["lp-submit", "lp-propose"]),
 
65
    ("cmd_lp_find_proposal", [])]:
 
66
    plugin_cmds.register_lazy(klsname, aliases,
 
67
        "bzrlib.plugins.launchpad.cmds")
448
68
 
449
69
 
450
70
def _register_directory():
462
82
 
463
83
_register_directory()
464
84
 
 
85
# This is kept in __init__ so that we don't load lp_api_lite unless the branch
 
86
# actually matches. That way we can avoid importing extra dependencies like
 
87
# json.
 
88
_package_branch = lazy_regex.lazy_compile(
 
89
    r'bazaar.launchpad.net.*?/'
 
90
    r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
 
91
    r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
 
92
    )
 
93
 
 
94
def _get_package_branch_info(url):
 
95
    """Determine the packaging information for this URL.
 
96
 
 
97
    :return: If this isn't a packaging branch, return None. If it is, return
 
98
        (archive, series, project)
 
99
    """
 
100
    if url is None:
 
101
        return None
 
102
    m = _package_branch.search(url)
 
103
    if m is None:
 
104
        return None
 
105
    archive, series, project, user = m.group('archive', 'series',
 
106
                                             'project', 'user')
 
107
    if series is not None:
 
108
        # series is optional, so the regex includes the extra '/', we don't
 
109
        # want to send that on (it causes Internal Server Errors.)
 
110
        series = series.strip('/')
 
111
    if user is not None:
 
112
        user = user.strip('~/')
 
113
        if user != 'ubuntu-branches':
 
114
            return None
 
115
    return archive, series, project
 
116
 
 
117
 
 
118
def _check_is_up_to_date(the_branch):
 
119
    info = _get_package_branch_info(the_branch.base)
 
120
    if info is None:
 
121
        return
 
122
    c = the_branch.get_config_stack()
 
123
    verbosity = c.get('launchpad.packaging_verbosity')
 
124
    if not verbosity:
 
125
        trace.mutter('not checking %s because verbosity is turned off'
 
126
                     % (the_branch.base,))
 
127
        return
 
128
    archive, series, project = info
 
129
    from bzrlib.plugins.launchpad import lp_api_lite
 
130
    latest_pub = lp_api_lite.LatestPublication(archive, series, project)
 
131
    lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
 
132
 
 
133
 
 
134
def _register_hooks():
 
135
    _mod_branch.Branch.hooks.install_named_hook('open',
 
136
        _check_is_up_to_date, 'package-branch-up-to-date')
 
137
 
 
138
 
 
139
_register_hooks()
465
140
 
466
141
def load_tests(basic_tests, module, loader):
467
142
    testmod_names = [
468
143
        'test_account',
469
144
        'test_register',
470
145
        'test_lp_api',
 
146
        'test_lp_api_lite',
471
147
        'test_lp_directory',
472
148
        'test_lp_login',
473
149
        'test_lp_open',
511
187
topic_registry.register('launchpad',
512
188
    _launchpad_help,
513
189
    'Using Bazaar with Launchpad.net')
 
190
 
 
191
_mod_config.option_registry.register(
 
192
    _mod_config.Option('launchpad.packaging_verbosity', default=True,
 
193
          from_unicode=_mod_config.bool_from_store,
 
194
          help="""\
 
195
Whether to warn if a UDD package import branch is accessed that is out of date.
 
196
 
 
197
Setting this option to 'off' will disable verbosity.
 
198
"""))
 
199
_mod_config.option_registry.register(
 
200
    _mod_config.Option('launchpad_username', default=None,
 
201
        help="The username to login with when conneting to Launchpad."))