~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2012-04-16 11:08:11 UTC
  • mfrom: (6521 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6522.
  • Revision ID: jelmer@samba.org-20120416110811-0y996ihqy9o2bb1t
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2012 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
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 plugin commands."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from bzrlib import (
 
22
    branch as _mod_branch,
 
23
    controldir,
 
24
    trace,
 
25
    )
 
26
from bzrlib.commands import (
 
27
    Command,
 
28
    )
 
29
from bzrlib.errors import (
 
30
    BzrCommandError,
 
31
    InvalidRevisionSpec,
 
32
    InvalidURL,
 
33
    NoPublicBranch,
 
34
    NotBranchError,
 
35
    )
 
36
from bzrlib.i18n import gettext
 
37
from bzrlib.option import (
 
38
    Option,
 
39
    ListOption,
 
40
    )
 
41
 
 
42
 
 
43
class cmd_register_branch(Command):
 
44
    __doc__ = """Register a branch with launchpad.net.
 
45
 
 
46
    This command lists a bzr branch in the directory of branches on
 
47
    launchpad.net.  Registration allows the branch to be associated with
 
48
    bugs or specifications.
 
49
 
 
50
    Before using this command you must register the project to which the
 
51
    branch belongs, and create an account for yourself on launchpad.net.
 
52
 
 
53
    arguments:
 
54
        public_url: The publicly visible url for the branch to register.
 
55
                    This must be an http or https url (which Launchpad can read
 
56
                    from to access the branch). Local file urls, SFTP urls, and
 
57
                    bzr+ssh urls will not work.
 
58
                    If no public_url is provided, bzr will use the configured
 
59
                    public_url if there is one for the current branch, and
 
60
                    otherwise error.
 
61
 
 
62
    example:
 
63
        bzr register-branch http://foo.com/bzr/fooproject.mine \\
 
64
                --project fooproject
 
65
    """
 
66
    takes_args = ['public_url?']
 
67
    takes_options = [
 
68
         Option('project',
 
69
                'Launchpad project short name to associate with the branch.',
 
70
                unicode),
 
71
         Option('product',
 
72
                'Launchpad product short name to associate with the branch.',
 
73
                unicode,
 
74
                hidden=True),
 
75
         Option('branch-name',
 
76
                'Short name for the branch; '
 
77
                'by default taken from the last component of the url.',
 
78
                unicode),
 
79
         Option('branch-title',
 
80
                'One-sentence description of the branch.',
 
81
                unicode),
 
82
         Option('branch-description',
 
83
                'Longer description of the purpose or contents of the branch.',
 
84
                unicode),
 
85
         Option('author',
 
86
                "Branch author's email address, if not yourself.",
 
87
                unicode),
 
88
         Option('link-bug',
 
89
                'The bug this branch fixes.',
 
90
                int),
 
91
         Option('dry-run',
 
92
                'Prepare the request but don\'t actually send it.')
 
93
        ]
 
94
 
 
95
 
 
96
    def run(self,
 
97
            public_url=None,
 
98
            project='',
 
99
            product=None,
 
100
            branch_name='',
 
101
            branch_title='',
 
102
            branch_description='',
 
103
            author='',
 
104
            link_bug=None,
 
105
            dry_run=False):
 
106
        from bzrlib.plugins.launchpad.lp_registration import (
 
107
            BranchRegistrationRequest, BranchBugLinkRequest,
 
108
            DryRunLaunchpadService, LaunchpadService)
 
109
        if public_url is None:
 
110
            try:
 
111
                b = _mod_branch.Branch.open_containing('.')[0]
 
112
            except NotBranchError:
 
113
                raise BzrCommandError(gettext(
 
114
                            'register-branch requires a public '
 
115
                            'branch url - see bzr help register-branch.'))
 
116
            public_url = b.get_public_branch()
 
117
            if public_url is None:
 
118
                raise NoPublicBranch(b)
 
119
        if product is not None:
 
120
            project = product
 
121
            trace.note(gettext(
 
122
                '--product is deprecated; please use --project.'))
 
123
 
 
124
 
 
125
        rego = BranchRegistrationRequest(branch_url=public_url,
 
126
                                         branch_name=branch_name,
 
127
                                         branch_title=branch_title,
 
128
                                         branch_description=branch_description,
 
129
                                         product_name=project,
 
130
                                         author_email=author,
 
131
                                         )
 
132
        linko = BranchBugLinkRequest(branch_url=public_url,
 
133
                                     bug_id=link_bug)
 
134
        if not dry_run:
 
135
            service = LaunchpadService()
 
136
            # This gives back the xmlrpc url that can be used for future
 
137
            # operations on the branch.  It's not so useful to print to the
 
138
            # user since they can't do anything with it from a web browser; it
 
139
            # might be nice for the server to tell us about an html url as
 
140
            # well.
 
141
        else:
 
142
            # Run on service entirely in memory
 
143
            service = DryRunLaunchpadService()
 
144
        service.gather_user_credentials()
 
145
        rego.submit(service)
 
146
        if link_bug:
 
147
            linko.submit(service)
 
148
        self.outf.write('Branch registered.\n')
 
149
 
 
150
 
 
151
class cmd_launchpad_open(Command):
 
152
    __doc__ = """Open a Launchpad branch page in your web browser."""
 
153
 
 
154
    aliases = ['lp-open']
 
155
    takes_options = [
 
156
        Option('dry-run',
 
157
               'Do not actually open the browser. Just say the URL we would '
 
158
               'use.'),
 
159
        ]
 
160
    takes_args = ['location?']
 
161
 
 
162
    def _possible_locations(self, location):
 
163
        """Yield possible external locations for the branch at 'location'."""
 
164
        yield location
 
165
        try:
 
166
            branch = _mod_branch.Branch.open_containing(location)[0]
 
167
        except NotBranchError:
 
168
            return
 
169
        branch_url = branch.get_public_branch()
 
170
        if branch_url is not None:
 
171
            yield branch_url
 
172
        branch_url = branch.get_push_location()
 
173
        if branch_url is not None:
 
174
            yield branch_url
 
175
 
 
176
    def _get_web_url(self, service, location):
 
177
        from bzrlib.plugins.launchpad.lp_registration import (
 
178
            NotLaunchpadBranch)
 
179
        for branch_url in self._possible_locations(location):
 
180
            try:
 
181
                return service.get_web_url_from_branch_url(branch_url)
 
182
            except (NotLaunchpadBranch, InvalidURL):
 
183
                pass
 
184
        raise NotLaunchpadBranch(branch_url)
 
185
 
 
186
    def run(self, location=None, dry_run=False):
 
187
        from bzrlib.plugins.launchpad.lp_registration import (
 
188
            LaunchpadService)
 
189
        if location is None:
 
190
            location = u'.'
 
191
        web_url = self._get_web_url(LaunchpadService(), location)
 
192
        trace.note(gettext('Opening %s in web browser') % web_url)
 
193
        if not dry_run:
 
194
            import webbrowser   # this import should not be lazy
 
195
                                # otherwise bzr.exe lacks this module
 
196
            webbrowser.open(web_url)
 
197
 
 
198
 
 
199
class cmd_launchpad_login(Command):
 
200
    __doc__ = """Show or set the Launchpad user ID.
 
201
 
 
202
    When communicating with Launchpad, some commands need to know your
 
203
    Launchpad user ID.  This command can be used to set or show the
 
204
    user ID that Bazaar will use for such communication.
 
205
 
 
206
    :Examples:
 
207
      Show the Launchpad ID of the current user::
 
208
 
 
209
          bzr launchpad-login
 
210
 
 
211
      Set the Launchpad ID of the current user to 'bob'::
 
212
 
 
213
          bzr launchpad-login bob
 
214
    """
 
215
    aliases = ['lp-login']
 
216
    takes_args = ['name?']
 
217
    takes_options = [
 
218
        'verbose',
 
219
        Option('no-check',
 
220
               "Don't check that the user name is valid."),
 
221
        ]
 
222
 
 
223
    def run(self, name=None, no_check=False, verbose=False):
 
224
        # This is totally separate from any launchpadlib login system.
 
225
        from bzrlib.plugins.launchpad import account
 
226
        check_account = not no_check
 
227
 
 
228
        if name is None:
 
229
            username = account.get_lp_login()
 
230
            if username:
 
231
                if check_account:
 
232
                    account.check_lp_login(username)
 
233
                    if verbose:
 
234
                        self.outf.write(gettext(
 
235
                            "Launchpad user ID exists and has SSH keys.\n"))
 
236
                self.outf.write(username + '\n')
 
237
            else:
 
238
                self.outf.write(gettext('No Launchpad user ID configured.\n'))
 
239
                return 1
 
240
        else:
 
241
            name = name.lower()
 
242
            if check_account:
 
243
                account.check_lp_login(name)
 
244
                if verbose:
 
245
                    self.outf.write(gettext(
 
246
                        "Launchpad user ID exists and has SSH keys.\n"))
 
247
            account.set_lp_login(name)
 
248
            if verbose:
 
249
                self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
 
250
                                                                        (name,))
 
251
 
 
252
 
 
253
# XXX: cmd_launchpad_mirror is untested
 
254
class cmd_launchpad_mirror(Command):
 
255
    __doc__ = """Ask Launchpad to mirror a branch now."""
 
256
 
 
257
    aliases = ['lp-mirror']
 
258
    takes_args = ['location?']
 
259
 
 
260
    def run(self, location='.'):
 
261
        from bzrlib.plugins.launchpad import lp_api
 
262
        from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
 
263
        branch, _ = _mod_branch.Branch.open_containing(location)
 
264
        service = LaunchpadService()
 
265
        launchpad = lp_api.login(service)
 
266
        lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
 
267
                create_missing=False)
 
268
        lp_branch.lp.requestMirror()
 
269
 
 
270
 
 
271
class cmd_lp_propose_merge(Command):
 
272
    __doc__ = """Propose merging a branch on Launchpad.
 
273
 
 
274
    This will open your usual editor to provide the initial comment.  When it
 
275
    has created the proposal, it will open it in your default web browser.
 
276
 
 
277
    The branch will be proposed to merge into SUBMIT_BRANCH.  If SUBMIT_BRANCH
 
278
    is not supplied, the remembered submit branch will be used.  If no submit
 
279
    branch is remembered, the development focus will be used.
 
280
 
 
281
    By default, the SUBMIT_BRANCH's review team will be requested to review
 
282
    the merge proposal.  This can be overriden by specifying --review (-R).
 
283
    The parameter the launchpad account name of the desired reviewer.  This
 
284
    may optionally be followed by '=' and the review type.  For example:
 
285
 
 
286
      bzr lp-propose-merge --review jrandom --review review-team=qa
 
287
 
 
288
    This will propose a merge,  request "jrandom" to perform a review of
 
289
    unspecified type, and request "review-team" to perform a "qa" review.
 
290
    """
 
291
 
 
292
    takes_options = [Option('staging',
 
293
                            help='Propose the merge on staging.'),
 
294
                     Option('message', short_name='m', type=unicode,
 
295
                            help='Commit message.'),
 
296
                     Option('approve',
 
297
                            help='Mark the proposal as approved immediately.'),
 
298
                     Option('fixes', 'The bug this proposal fixes.', str),
 
299
                     ListOption('review', short_name='R', type=unicode,
 
300
                            help='Requested reviewer and optional type.')]
 
301
 
 
302
    takes_args = ['submit_branch?']
 
303
 
 
304
    aliases = ['lp-submit', 'lp-propose']
 
305
 
 
306
    def run(self, submit_branch=None, review=None, staging=False,
 
307
            message=None, approve=False, fixes=None):
 
308
        from bzrlib.plugins.launchpad import lp_propose
 
309
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
 
310
            '.')
 
311
        if review is None:
 
312
            reviews = None
 
313
        else:
 
314
            reviews = []
 
315
            for review in review:
 
316
                if '=' in review:
 
317
                    reviews.append(review.split('=', 2))
 
318
                else:
 
319
                    reviews.append((review, ''))
 
320
            if submit_branch is None:
 
321
                submit_branch = branch.get_submit_branch()
 
322
        if submit_branch is None:
 
323
            target = None
 
324
        else:
 
325
            target = _mod_branch.Branch.open(submit_branch)
 
326
        proposer = lp_propose.Proposer(tree, branch, target, message,
 
327
                                       reviews, staging, approve=approve,
 
328
                                       fixes=fixes)
 
329
        proposer.check_proposal()
 
330
        proposer.create_proposal()
 
331
 
 
332
 
 
333
class cmd_lp_find_proposal(Command):
 
334
 
 
335
    __doc__ = """Find the proposal to merge this revision.
 
336
 
 
337
    Finds the merge proposal(s) that discussed landing the specified revision.
 
338
    This works only if the selected branch was the merge proposal target, and
 
339
    if the merged_revno is recorded for the merge proposal.  The proposal(s)
 
340
    are opened in a web browser.
 
341
 
 
342
    Any revision involved in the merge may be specified-- the revision in
 
343
    which the merge was performed, or one of the revisions that was merged.
 
344
 
 
345
    So, to find the merge proposal that reviewed line 1 of README::
 
346
 
 
347
      bzr lp-find-proposal -r annotate:README:1
 
348
    """
 
349
 
 
350
    takes_options = ['revision']
 
351
 
 
352
    def run(self, revision=None):
 
353
        from bzrlib import ui
 
354
        from bzrlib.plugins.launchpad import lp_api
 
355
        import webbrowser
 
356
        b = _mod_branch.Branch.open_containing('.')[0]
 
357
        pb = ui.ui_factory.nested_progress_bar()
 
358
        b.lock_read()
 
359
        try:
 
360
            revno = self._find_merged_revno(revision, b, pb)
 
361
            merged = self._find_proposals(revno, b, pb)
 
362
            if len(merged) == 0:
 
363
                raise BzrCommandError(gettext('No review found.'))
 
364
            trace.note(gettext('%d proposals(s) found.') % len(merged))
 
365
            for mp in merged:
 
366
                webbrowser.open(lp_api.canonical_url(mp))
 
367
        finally:
 
368
            b.unlock()
 
369
            pb.finished()
 
370
 
 
371
    def _find_merged_revno(self, revision, b, pb):
 
372
        if revision is None:
 
373
            return b.revno()
 
374
        pb.update(gettext('Finding revision-id'))
 
375
        revision_id = revision[0].as_revision_id(b)
 
376
        # a revno spec is necessarily on the mainline.
 
377
        if self._is_revno_spec(revision[0]):
 
378
            merging_revision = revision_id
 
379
        else:
 
380
            graph = b.repository.get_graph()
 
381
            pb.update(gettext('Finding merge'))
 
382
            merging_revision = graph.find_lefthand_merger(
 
383
                revision_id, b.last_revision())
 
384
            if merging_revision is None:
 
385
                raise InvalidRevisionSpec(revision[0].user_spec, b)
 
386
        pb.update(gettext('Finding revno'))
 
387
        return b.revision_id_to_revno(merging_revision)
 
388
 
 
389
    def _find_proposals(self, revno, b, pb):
 
390
        from bzrlib.plugins.launchpad import (lp_api, lp_registration)
 
391
        launchpad = lp_api.login(lp_registration.LaunchpadService())
 
392
        pb.update(gettext('Finding Launchpad branch'))
 
393
        lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
 
394
                                              create_missing=False)
 
395
        pb.update(gettext('Finding proposals'))
 
396
        return list(lpb.lp.getMergeProposals(status=['Merged'],
 
397
                                             merged_revnos=[revno]))
 
398
 
 
399
 
 
400
    @staticmethod
 
401
    def _is_revno_spec(spec):
 
402
        try:
 
403
            int(spec.user_spec)
 
404
        except ValueError:
 
405
            return False
 
406
        else:
 
407
            return True
 
408
 
 
409
 
 
410