Merge lp:~jelmer/bzr/lp-plugin-lazy into lp:bzr

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Martin Packman
Approved revision: no longer in the source branch.
Merged at revision: 6496
Proposed branch: lp:~jelmer/bzr/lp-plugin-lazy
Merge into: lp:bzr
Diff against target: 851 lines (+422/-403)
2 files modified
bzrlib/plugins/launchpad/__init__.py (+12/-403)
bzrlib/plugins/launchpad/cmds.py (+410/-0)
To merge this branch: bzr merge lp:~jelmer/bzr/lp-plugin-lazy
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve
Review via email: mp+96892@code.launchpad.net

Commit message

Lazily load launchpad plugin commands.

Description of the change

Lazily load launchpad plugin commands.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks fine, though unlikely to have a big impact.

Seems like _register_hooks should be replaced with a install_lazy_named_hook call, which would mean the _mod_branch import could go as well?

     # Since we are a built-in plugin we share the bzrlib version
 + trace,
      version_info,

Addition split the comment from the version_info line it's talking about. Mixing up module and object imports is a bit ick, but that's python.

review: Needs Information
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Am 12/03/12 00:46, schrieb Martin Packman:
> Review: Needs Information
>
> Looks fine, though unlikely to have a big impact.
>
> Seems like _register_hooks should be replaced with a install_lazy_named_hook call, which would mean the _mod_branch import could go as well?
I guess, although branch gets imported anyway by other bits of the code
so I don't think that would be all that useful.
>
> # Since we are a built-in plugin we share the bzrlib version
> + trace,
> version_info,
>
> Addition split the comment from the version_info line it's talking about. Mixing up module and object imports is a bit ick, but that's python.
Whoops, thanks.

Cheers,

jelmer

Revision history for this message
Martin Packman (gz) wrote :

Sort of underlines the number of different lazy registration things we have...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/plugins/launchpad/__init__.py'
2--- bzrlib/plugins/launchpad/__init__.py 2012-03-06 17:17:27 +0000
3+++ bzrlib/plugins/launchpad/__init__.py 2012-03-12 14:38:29 +0000
4@@ -42,420 +42,29 @@
5
6 # see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
7
8-from bzrlib.lazy_import import lazy_import
9-lazy_import(globals(), """
10-from bzrlib import (
11- ui,
12- trace,
13- )
14-from bzrlib.i18n import gettext
15-""")
16-
17 from bzrlib import (
18 branch as _mod_branch,
19 config as _mod_config,
20- controldir,
21 lazy_regex,
22 # Since we are a built-in plugin we share the bzrlib version
23+ trace,
24 version_info,
25 )
26 from bzrlib.commands import (
27- Command,
28- register_command,
29+ plugin_cmds,
30 )
31 from bzrlib.directory_service import directories
32-from bzrlib.errors import (
33- BzrCommandError,
34- InvalidRevisionSpec,
35- InvalidURL,
36- NoPublicBranch,
37- NotBranchError,
38- )
39 from bzrlib.help_topics import topic_registry
40-from bzrlib.option import (
41- Option,
42- ListOption,
43- )
44-
45-
46-class cmd_register_branch(Command):
47- __doc__ = """Register a branch with launchpad.net.
48-
49- This command lists a bzr branch in the directory of branches on
50- launchpad.net. Registration allows the branch to be associated with
51- bugs or specifications.
52-
53- Before using this command you must register the project to which the
54- branch belongs, and create an account for yourself on launchpad.net.
55-
56- arguments:
57- public_url: The publicly visible url for the branch to register.
58- This must be an http or https url (which Launchpad can read
59- from to access the branch). Local file urls, SFTP urls, and
60- bzr+ssh urls will not work.
61- If no public_url is provided, bzr will use the configured
62- public_url if there is one for the current branch, and
63- otherwise error.
64-
65- example:
66- bzr register-branch http://foo.com/bzr/fooproject.mine \\
67- --project fooproject
68- """
69- takes_args = ['public_url?']
70- takes_options = [
71- Option('project',
72- 'Launchpad project short name to associate with the branch.',
73- unicode),
74- Option('product',
75- 'Launchpad product short name to associate with the branch.',
76- unicode,
77- hidden=True),
78- Option('branch-name',
79- 'Short name for the branch; '
80- 'by default taken from the last component of the url.',
81- unicode),
82- Option('branch-title',
83- 'One-sentence description of the branch.',
84- unicode),
85- Option('branch-description',
86- 'Longer description of the purpose or contents of the branch.',
87- unicode),
88- Option('author',
89- "Branch author's email address, if not yourself.",
90- unicode),
91- Option('link-bug',
92- 'The bug this branch fixes.',
93- int),
94- Option('dry-run',
95- 'Prepare the request but don\'t actually send it.')
96- ]
97-
98-
99- def run(self,
100- public_url=None,
101- project='',
102- product=None,
103- branch_name='',
104- branch_title='',
105- branch_description='',
106- author='',
107- link_bug=None,
108- dry_run=False):
109- from bzrlib.plugins.launchpad.lp_registration import (
110- BranchRegistrationRequest, BranchBugLinkRequest,
111- DryRunLaunchpadService, LaunchpadService)
112- if public_url is None:
113- try:
114- b = _mod_branch.Branch.open_containing('.')[0]
115- except NotBranchError:
116- raise BzrCommandError(gettext(
117- 'register-branch requires a public '
118- 'branch url - see bzr help register-branch.'))
119- public_url = b.get_public_branch()
120- if public_url is None:
121- raise NoPublicBranch(b)
122- if product is not None:
123- project = product
124- trace.note(gettext(
125- '--product is deprecated; please use --project.'))
126-
127-
128- rego = BranchRegistrationRequest(branch_url=public_url,
129- branch_name=branch_name,
130- branch_title=branch_title,
131- branch_description=branch_description,
132- product_name=project,
133- author_email=author,
134- )
135- linko = BranchBugLinkRequest(branch_url=public_url,
136- bug_id=link_bug)
137- if not dry_run:
138- service = LaunchpadService()
139- # This gives back the xmlrpc url that can be used for future
140- # operations on the branch. It's not so useful to print to the
141- # user since they can't do anything with it from a web browser; it
142- # might be nice for the server to tell us about an html url as
143- # well.
144- else:
145- # Run on service entirely in memory
146- service = DryRunLaunchpadService()
147- service.gather_user_credentials()
148- rego.submit(service)
149- if link_bug:
150- linko.submit(service)
151- print 'Branch registered.'
152-
153-register_command(cmd_register_branch)
154-
155-
156-class cmd_launchpad_open(Command):
157- __doc__ = """Open a Launchpad branch page in your web browser."""
158-
159- aliases = ['lp-open']
160- takes_options = [
161- Option('dry-run',
162- 'Do not actually open the browser. Just say the URL we would '
163- 'use.'),
164- ]
165- takes_args = ['location?']
166-
167- def _possible_locations(self, location):
168- """Yield possible external locations for the branch at 'location'."""
169- yield location
170- try:
171- branch = _mod_branch.Branch.open_containing(location)[0]
172- except NotBranchError:
173- return
174- branch_url = branch.get_public_branch()
175- if branch_url is not None:
176- yield branch_url
177- branch_url = branch.get_push_location()
178- if branch_url is not None:
179- yield branch_url
180-
181- def _get_web_url(self, service, location):
182- from bzrlib.plugins.launchpad.lp_registration import (
183- NotLaunchpadBranch)
184- for branch_url in self._possible_locations(location):
185- try:
186- return service.get_web_url_from_branch_url(branch_url)
187- except (NotLaunchpadBranch, InvalidURL):
188- pass
189- raise NotLaunchpadBranch(branch_url)
190-
191- def run(self, location=None, dry_run=False):
192- from bzrlib.plugins.launchpad.lp_registration import (
193- LaunchpadService)
194- if location is None:
195- location = u'.'
196- web_url = self._get_web_url(LaunchpadService(), location)
197- trace.note(gettext('Opening %s in web browser') % web_url)
198- if not dry_run:
199- import webbrowser # this import should not be lazy
200- # otherwise bzr.exe lacks this module
201- webbrowser.open(web_url)
202-
203-register_command(cmd_launchpad_open)
204-
205-
206-class cmd_launchpad_login(Command):
207- __doc__ = """Show or set the Launchpad user ID.
208-
209- When communicating with Launchpad, some commands need to know your
210- Launchpad user ID. This command can be used to set or show the
211- user ID that Bazaar will use for such communication.
212-
213- :Examples:
214- Show the Launchpad ID of the current user::
215-
216- bzr launchpad-login
217-
218- Set the Launchpad ID of the current user to 'bob'::
219-
220- bzr launchpad-login bob
221- """
222- aliases = ['lp-login']
223- takes_args = ['name?']
224- takes_options = [
225- 'verbose',
226- Option('no-check',
227- "Don't check that the user name is valid."),
228- ]
229-
230- def run(self, name=None, no_check=False, verbose=False):
231- # This is totally separate from any launchpadlib login system.
232- from bzrlib.plugins.launchpad import account
233- check_account = not no_check
234-
235- if name is None:
236- username = account.get_lp_login()
237- if username:
238- if check_account:
239- account.check_lp_login(username)
240- if verbose:
241- self.outf.write(gettext(
242- "Launchpad user ID exists and has SSH keys.\n"))
243- self.outf.write(username + '\n')
244- else:
245- self.outf.write(gettext('No Launchpad user ID configured.\n'))
246- return 1
247- else:
248- name = name.lower()
249- if check_account:
250- account.check_lp_login(name)
251- if verbose:
252- self.outf.write(gettext(
253- "Launchpad user ID exists and has SSH keys.\n"))
254- account.set_lp_login(name)
255- if verbose:
256- self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
257- (name,))
258-
259-register_command(cmd_launchpad_login)
260-
261-
262-# XXX: cmd_launchpad_mirror is untested
263-class cmd_launchpad_mirror(Command):
264- __doc__ = """Ask Launchpad to mirror a branch now."""
265-
266- aliases = ['lp-mirror']
267- takes_args = ['location?']
268-
269- def run(self, location='.'):
270- from bzrlib.plugins.launchpad import lp_api
271- from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
272- branch, _ = _mod_branch.Branch.open_containing(location)
273- service = LaunchpadService()
274- launchpad = lp_api.login(service)
275- lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
276- create_missing=False)
277- lp_branch.lp.requestMirror()
278-
279-
280-register_command(cmd_launchpad_mirror)
281-
282-
283-class cmd_lp_propose_merge(Command):
284- __doc__ = """Propose merging a branch on Launchpad.
285-
286- This will open your usual editor to provide the initial comment. When it
287- has created the proposal, it will open it in your default web browser.
288-
289- The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
290- is not supplied, the remembered submit branch will be used. If no submit
291- branch is remembered, the development focus will be used.
292-
293- By default, the SUBMIT_BRANCH's review team will be requested to review
294- the merge proposal. This can be overriden by specifying --review (-R).
295- The parameter the launchpad account name of the desired reviewer. This
296- may optionally be followed by '=' and the review type. For example:
297-
298- bzr lp-propose-merge --review jrandom --review review-team=qa
299-
300- This will propose a merge, request "jrandom" to perform a review of
301- unspecified type, and request "review-team" to perform a "qa" review.
302- """
303-
304- takes_options = [Option('staging',
305- help='Propose the merge on staging.'),
306- Option('message', short_name='m', type=unicode,
307- help='Commit message.'),
308- Option('approve',
309- help='Mark the proposal as approved immediately.'),
310- Option('fixes', 'The bug this proposal fixes.', str),
311- ListOption('review', short_name='R', type=unicode,
312- help='Requested reviewer and optional type.')]
313-
314- takes_args = ['submit_branch?']
315-
316- aliases = ['lp-submit', 'lp-propose']
317-
318- def run(self, submit_branch=None, review=None, staging=False,
319- message=None, approve=False, fixes=None):
320- from bzrlib.plugins.launchpad import lp_propose
321- tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
322- '.')
323- if review is None:
324- reviews = None
325- else:
326- reviews = []
327- for review in review:
328- if '=' in review:
329- reviews.append(review.split('=', 2))
330- else:
331- reviews.append((review, ''))
332- if submit_branch is None:
333- submit_branch = branch.get_submit_branch()
334- if submit_branch is None:
335- target = None
336- else:
337- target = _mod_branch.Branch.open(submit_branch)
338- proposer = lp_propose.Proposer(tree, branch, target, message,
339- reviews, staging, approve=approve,
340- fixes=fixes)
341- proposer.check_proposal()
342- proposer.create_proposal()
343-
344-
345-register_command(cmd_lp_propose_merge)
346-
347-
348-class cmd_lp_find_proposal(Command):
349-
350- __doc__ = """Find the proposal to merge this revision.
351-
352- Finds the merge proposal(s) that discussed landing the specified revision.
353- This works only if the selected branch was the merge proposal target, and
354- if the merged_revno is recorded for the merge proposal. The proposal(s)
355- are opened in a web browser.
356-
357- Any revision involved in the merge may be specified-- the revision in
358- which the merge was performed, or one of the revisions that was merged.
359-
360- So, to find the merge proposal that reviewed line 1 of README::
361-
362- bzr lp-find-proposal -r annotate:README:1
363- """
364-
365- takes_options = ['revision']
366-
367- def run(self, revision=None):
368- from bzrlib.plugins.launchpad import lp_api
369- import webbrowser
370- b = _mod_branch.Branch.open_containing('.')[0]
371- pb = ui.ui_factory.nested_progress_bar()
372- b.lock_read()
373- try:
374- revno = self._find_merged_revno(revision, b, pb)
375- merged = self._find_proposals(revno, b, pb)
376- if len(merged) == 0:
377- raise BzrCommandError(gettext('No review found.'))
378- trace.note(gettext('%d proposals(s) found.') % len(merged))
379- for mp in merged:
380- webbrowser.open(lp_api.canonical_url(mp))
381- finally:
382- b.unlock()
383- pb.finished()
384-
385- def _find_merged_revno(self, revision, b, pb):
386- if revision is None:
387- return b.revno()
388- pb.update(gettext('Finding revision-id'))
389- revision_id = revision[0].as_revision_id(b)
390- # a revno spec is necessarily on the mainline.
391- if self._is_revno_spec(revision[0]):
392- merging_revision = revision_id
393- else:
394- graph = b.repository.get_graph()
395- pb.update(gettext('Finding merge'))
396- merging_revision = graph.find_lefthand_merger(
397- revision_id, b.last_revision())
398- if merging_revision is None:
399- raise InvalidRevisionSpec(revision[0].user_spec, b)
400- pb.update(gettext('Finding revno'))
401- return b.revision_id_to_revno(merging_revision)
402-
403- def _find_proposals(self, revno, b, pb):
404- launchpad = lp_api.login(lp_registration.LaunchpadService())
405- pb.update(gettext('Finding Launchpad branch'))
406- lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
407- create_missing=False)
408- pb.update(gettext('Finding proposals'))
409- return list(lpb.lp.getMergeProposals(status=['Merged'],
410- merged_revnos=[revno]))
411-
412-
413- @staticmethod
414- def _is_revno_spec(spec):
415- try:
416- int(spec.user_spec)
417- except ValueError:
418- return False
419- else:
420- return True
421-
422-
423-register_command(cmd_lp_find_proposal)
424+
425+for klsname, aliases in [
426+ ("cmd_register_branch", []),
427+ ("cmd_launchpad_open", ["lp-open"]),
428+ ("cmd_launchpad_login", ["lp-login"]),
429+ ("cmd_launchpad_mirror", ["lp-mirror"]),
430+ ("cmd_lp_propose_merge", ["lp-submit", "lp-propose"]),
431+ ("cmd_lp_find_proposal", [])]:
432+ plugin_cmds.register_lazy(klsname, aliases,
433+ "bzrlib.plugins.launchpad.cmds")
434
435
436 def _register_directory():
437
438=== added file 'bzrlib/plugins/launchpad/cmds.py'
439--- bzrlib/plugins/launchpad/cmds.py 1970-01-01 00:00:00 +0000
440+++ bzrlib/plugins/launchpad/cmds.py 2012-03-12 14:38:29 +0000
441@@ -0,0 +1,410 @@
442+# Copyright (C) 2006-2012 Canonical Ltd
443+#
444+# This program is free software; you can redistribute it and/or modify
445+# it under the terms of the GNU General Public License as published by
446+# the Free Software Foundation; either version 2 of the License, or
447+# (at your option) any later version.
448+#
449+# This program is distributed in the hope that it will be useful,
450+# but WITHOUT ANY WARRANTY; without even the implied warranty of
451+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
452+# GNU General Public License for more details.
453+#
454+# You should have received a copy of the GNU General Public License
455+# along with this program; if not, write to the Free Software
456+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
457+
458+"""Launchpad plugin commands."""
459+
460+from __future__ import absolute_import
461+
462+from bzrlib import (
463+ branch as _mod_branch,
464+ controldir,
465+ trace,
466+ )
467+from bzrlib.commands import (
468+ Command,
469+ )
470+from bzrlib.errors import (
471+ BzrCommandError,
472+ InvalidRevisionSpec,
473+ InvalidURL,
474+ NoPublicBranch,
475+ NotBranchError,
476+ )
477+from bzrlib.i18n import gettext
478+from bzrlib.option import (
479+ Option,
480+ ListOption,
481+ )
482+
483+
484+class cmd_register_branch(Command):
485+ __doc__ = """Register a branch with launchpad.net.
486+
487+ This command lists a bzr branch in the directory of branches on
488+ launchpad.net. Registration allows the branch to be associated with
489+ bugs or specifications.
490+
491+ Before using this command you must register the project to which the
492+ branch belongs, and create an account for yourself on launchpad.net.
493+
494+ arguments:
495+ public_url: The publicly visible url for the branch to register.
496+ This must be an http or https url (which Launchpad can read
497+ from to access the branch). Local file urls, SFTP urls, and
498+ bzr+ssh urls will not work.
499+ If no public_url is provided, bzr will use the configured
500+ public_url if there is one for the current branch, and
501+ otherwise error.
502+
503+ example:
504+ bzr register-branch http://foo.com/bzr/fooproject.mine \\
505+ --project fooproject
506+ """
507+ takes_args = ['public_url?']
508+ takes_options = [
509+ Option('project',
510+ 'Launchpad project short name to associate with the branch.',
511+ unicode),
512+ Option('product',
513+ 'Launchpad product short name to associate with the branch.',
514+ unicode,
515+ hidden=True),
516+ Option('branch-name',
517+ 'Short name for the branch; '
518+ 'by default taken from the last component of the url.',
519+ unicode),
520+ Option('branch-title',
521+ 'One-sentence description of the branch.',
522+ unicode),
523+ Option('branch-description',
524+ 'Longer description of the purpose or contents of the branch.',
525+ unicode),
526+ Option('author',
527+ "Branch author's email address, if not yourself.",
528+ unicode),
529+ Option('link-bug',
530+ 'The bug this branch fixes.',
531+ int),
532+ Option('dry-run',
533+ 'Prepare the request but don\'t actually send it.')
534+ ]
535+
536+
537+ def run(self,
538+ public_url=None,
539+ project='',
540+ product=None,
541+ branch_name='',
542+ branch_title='',
543+ branch_description='',
544+ author='',
545+ link_bug=None,
546+ dry_run=False):
547+ from bzrlib.plugins.launchpad.lp_registration import (
548+ BranchRegistrationRequest, BranchBugLinkRequest,
549+ DryRunLaunchpadService, LaunchpadService)
550+ if public_url is None:
551+ try:
552+ b = _mod_branch.Branch.open_containing('.')[0]
553+ except NotBranchError:
554+ raise BzrCommandError(gettext(
555+ 'register-branch requires a public '
556+ 'branch url - see bzr help register-branch.'))
557+ public_url = b.get_public_branch()
558+ if public_url is None:
559+ raise NoPublicBranch(b)
560+ if product is not None:
561+ project = product
562+ trace.note(gettext(
563+ '--product is deprecated; please use --project.'))
564+
565+
566+ rego = BranchRegistrationRequest(branch_url=public_url,
567+ branch_name=branch_name,
568+ branch_title=branch_title,
569+ branch_description=branch_description,
570+ product_name=project,
571+ author_email=author,
572+ )
573+ linko = BranchBugLinkRequest(branch_url=public_url,
574+ bug_id=link_bug)
575+ if not dry_run:
576+ service = LaunchpadService()
577+ # This gives back the xmlrpc url that can be used for future
578+ # operations on the branch. It's not so useful to print to the
579+ # user since they can't do anything with it from a web browser; it
580+ # might be nice for the server to tell us about an html url as
581+ # well.
582+ else:
583+ # Run on service entirely in memory
584+ service = DryRunLaunchpadService()
585+ service.gather_user_credentials()
586+ rego.submit(service)
587+ if link_bug:
588+ linko.submit(service)
589+ self.outf.write('Branch registered.\n')
590+
591+
592+class cmd_launchpad_open(Command):
593+ __doc__ = """Open a Launchpad branch page in your web browser."""
594+
595+ aliases = ['lp-open']
596+ takes_options = [
597+ Option('dry-run',
598+ 'Do not actually open the browser. Just say the URL we would '
599+ 'use.'),
600+ ]
601+ takes_args = ['location?']
602+
603+ def _possible_locations(self, location):
604+ """Yield possible external locations for the branch at 'location'."""
605+ yield location
606+ try:
607+ branch = _mod_branch.Branch.open_containing(location)[0]
608+ except NotBranchError:
609+ return
610+ branch_url = branch.get_public_branch()
611+ if branch_url is not None:
612+ yield branch_url
613+ branch_url = branch.get_push_location()
614+ if branch_url is not None:
615+ yield branch_url
616+
617+ def _get_web_url(self, service, location):
618+ from bzrlib.plugins.launchpad.lp_registration import (
619+ NotLaunchpadBranch)
620+ for branch_url in self._possible_locations(location):
621+ try:
622+ return service.get_web_url_from_branch_url(branch_url)
623+ except (NotLaunchpadBranch, InvalidURL):
624+ pass
625+ raise NotLaunchpadBranch(branch_url)
626+
627+ def run(self, location=None, dry_run=False):
628+ from bzrlib.plugins.launchpad.lp_registration import (
629+ LaunchpadService)
630+ if location is None:
631+ location = u'.'
632+ web_url = self._get_web_url(LaunchpadService(), location)
633+ trace.note(gettext('Opening %s in web browser') % web_url)
634+ if not dry_run:
635+ import webbrowser # this import should not be lazy
636+ # otherwise bzr.exe lacks this module
637+ webbrowser.open(web_url)
638+
639+
640+class cmd_launchpad_login(Command):
641+ __doc__ = """Show or set the Launchpad user ID.
642+
643+ When communicating with Launchpad, some commands need to know your
644+ Launchpad user ID. This command can be used to set or show the
645+ user ID that Bazaar will use for such communication.
646+
647+ :Examples:
648+ Show the Launchpad ID of the current user::
649+
650+ bzr launchpad-login
651+
652+ Set the Launchpad ID of the current user to 'bob'::
653+
654+ bzr launchpad-login bob
655+ """
656+ aliases = ['lp-login']
657+ takes_args = ['name?']
658+ takes_options = [
659+ 'verbose',
660+ Option('no-check',
661+ "Don't check that the user name is valid."),
662+ ]
663+
664+ def run(self, name=None, no_check=False, verbose=False):
665+ # This is totally separate from any launchpadlib login system.
666+ from bzrlib.plugins.launchpad import account
667+ check_account = not no_check
668+
669+ if name is None:
670+ username = account.get_lp_login()
671+ if username:
672+ if check_account:
673+ account.check_lp_login(username)
674+ if verbose:
675+ self.outf.write(gettext(
676+ "Launchpad user ID exists and has SSH keys.\n"))
677+ self.outf.write(username + '\n')
678+ else:
679+ self.outf.write(gettext('No Launchpad user ID configured.\n'))
680+ return 1
681+ else:
682+ name = name.lower()
683+ if check_account:
684+ account.check_lp_login(name)
685+ if verbose:
686+ self.outf.write(gettext(
687+ "Launchpad user ID exists and has SSH keys.\n"))
688+ account.set_lp_login(name)
689+ if verbose:
690+ self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
691+ (name,))
692+
693+
694+# XXX: cmd_launchpad_mirror is untested
695+class cmd_launchpad_mirror(Command):
696+ __doc__ = """Ask Launchpad to mirror a branch now."""
697+
698+ aliases = ['lp-mirror']
699+ takes_args = ['location?']
700+
701+ def run(self, location='.'):
702+ from bzrlib.plugins.launchpad import lp_api
703+ from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
704+ branch, _ = _mod_branch.Branch.open_containing(location)
705+ service = LaunchpadService()
706+ launchpad = lp_api.login(service)
707+ lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
708+ create_missing=False)
709+ lp_branch.lp.requestMirror()
710+
711+
712+class cmd_lp_propose_merge(Command):
713+ __doc__ = """Propose merging a branch on Launchpad.
714+
715+ This will open your usual editor to provide the initial comment. When it
716+ has created the proposal, it will open it in your default web browser.
717+
718+ The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
719+ is not supplied, the remembered submit branch will be used. If no submit
720+ branch is remembered, the development focus will be used.
721+
722+ By default, the SUBMIT_BRANCH's review team will be requested to review
723+ the merge proposal. This can be overriden by specifying --review (-R).
724+ The parameter the launchpad account name of the desired reviewer. This
725+ may optionally be followed by '=' and the review type. For example:
726+
727+ bzr lp-propose-merge --review jrandom --review review-team=qa
728+
729+ This will propose a merge, request "jrandom" to perform a review of
730+ unspecified type, and request "review-team" to perform a "qa" review.
731+ """
732+
733+ takes_options = [Option('staging',
734+ help='Propose the merge on staging.'),
735+ Option('message', short_name='m', type=unicode,
736+ help='Commit message.'),
737+ Option('approve',
738+ help='Mark the proposal as approved immediately.'),
739+ Option('fixes', 'The bug this proposal fixes.', str),
740+ ListOption('review', short_name='R', type=unicode,
741+ help='Requested reviewer and optional type.')]
742+
743+ takes_args = ['submit_branch?']
744+
745+ aliases = ['lp-submit', 'lp-propose']
746+
747+ def run(self, submit_branch=None, review=None, staging=False,
748+ message=None, approve=False, fixes=None):
749+ from bzrlib.plugins.launchpad import lp_propose
750+ tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
751+ '.')
752+ if review is None:
753+ reviews = None
754+ else:
755+ reviews = []
756+ for review in review:
757+ if '=' in review:
758+ reviews.append(review.split('=', 2))
759+ else:
760+ reviews.append((review, ''))
761+ if submit_branch is None:
762+ submit_branch = branch.get_submit_branch()
763+ if submit_branch is None:
764+ target = None
765+ else:
766+ target = _mod_branch.Branch.open(submit_branch)
767+ proposer = lp_propose.Proposer(tree, branch, target, message,
768+ reviews, staging, approve=approve,
769+ fixes=fixes)
770+ proposer.check_proposal()
771+ proposer.create_proposal()
772+
773+
774+class cmd_lp_find_proposal(Command):
775+
776+ __doc__ = """Find the proposal to merge this revision.
777+
778+ Finds the merge proposal(s) that discussed landing the specified revision.
779+ This works only if the selected branch was the merge proposal target, and
780+ if the merged_revno is recorded for the merge proposal. The proposal(s)
781+ are opened in a web browser.
782+
783+ Any revision involved in the merge may be specified-- the revision in
784+ which the merge was performed, or one of the revisions that was merged.
785+
786+ So, to find the merge proposal that reviewed line 1 of README::
787+
788+ bzr lp-find-proposal -r annotate:README:1
789+ """
790+
791+ takes_options = ['revision']
792+
793+ def run(self, revision=None):
794+ from bzrlib import ui
795+ from bzrlib.plugins.launchpad import lp_api
796+ import webbrowser
797+ b = _mod_branch.Branch.open_containing('.')[0]
798+ pb = ui.ui_factory.nested_progress_bar()
799+ b.lock_read()
800+ try:
801+ revno = self._find_merged_revno(revision, b, pb)
802+ merged = self._find_proposals(revno, b, pb)
803+ if len(merged) == 0:
804+ raise BzrCommandError(gettext('No review found.'))
805+ trace.note(gettext('%d proposals(s) found.') % len(merged))
806+ for mp in merged:
807+ webbrowser.open(lp_api.canonical_url(mp))
808+ finally:
809+ b.unlock()
810+ pb.finished()
811+
812+ def _find_merged_revno(self, revision, b, pb):
813+ if revision is None:
814+ return b.revno()
815+ pb.update(gettext('Finding revision-id'))
816+ revision_id = revision[0].as_revision_id(b)
817+ # a revno spec is necessarily on the mainline.
818+ if self._is_revno_spec(revision[0]):
819+ merging_revision = revision_id
820+ else:
821+ graph = b.repository.get_graph()
822+ pb.update(gettext('Finding merge'))
823+ merging_revision = graph.find_lefthand_merger(
824+ revision_id, b.last_revision())
825+ if merging_revision is None:
826+ raise InvalidRevisionSpec(revision[0].user_spec, b)
827+ pb.update(gettext('Finding revno'))
828+ return b.revision_id_to_revno(merging_revision)
829+
830+ def _find_proposals(self, revno, b, pb):
831+ from bzrlib.plugins.launchpad import (lp_api, lp_registration)
832+ launchpad = lp_api.login(lp_registration.LaunchpadService())
833+ pb.update(gettext('Finding Launchpad branch'))
834+ lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
835+ create_missing=False)
836+ pb.update(gettext('Finding proposals'))
837+ return list(lpb.lp.getMergeProposals(status=['Merged'],
838+ merged_revnos=[revno]))
839+
840+
841+ @staticmethod
842+ def _is_revno_spec(spec):
843+ try:
844+ int(spec.user_spec)
845+ except ValueError:
846+ return False
847+ else:
848+ return True
849+
850+
851+