38
from __future__ import absolute_import
38
40
# The XMLRPC server address can be overridden by setting the environment
39
41
# variable $BZR_LP_XMLRPC_URL
41
43
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
43
from bzrlib.lazy_import import lazy_import
44
lazy_import(globals(), """
49
from bzrlib.i18n import gettext
52
45
from bzrlib import (
53
46
branch as _mod_branch,
47
config as _mod_config,
56
49
# Since we are a built-in plugin we share the bzrlib version
59
53
from bzrlib.commands import (
63
56
from bzrlib.directory_service import directories
64
from bzrlib.errors import (
71
57
from bzrlib.help_topics import topic_registry
72
from bzrlib.option import (
78
class cmd_register_branch(Command):
79
__doc__ = """Register a branch with launchpad.net.
81
This command lists a bzr branch in the directory of branches on
82
launchpad.net. Registration allows the branch to be associated with
83
bugs or specifications.
85
Before using this command you must register the project to which the
86
branch belongs, and create an account for yourself on launchpad.net.
89
public_url: The publicly visible url for the branch to register.
90
This must be an http or https url (which Launchpad can read
91
from to access the branch). Local file urls, SFTP urls, and
92
bzr+ssh urls will not work.
93
If no public_url is provided, bzr will use the configured
94
public_url if there is one for the current branch, and
98
bzr register-branch http://foo.com/bzr/fooproject.mine \\
101
takes_args = ['public_url?']
104
'Launchpad project short name to associate with the branch.',
107
'Launchpad product short name to associate with the branch.',
110
Option('branch-name',
111
'Short name for the branch; '
112
'by default taken from the last component of the url.',
114
Option('branch-title',
115
'One-sentence description of the branch.',
117
Option('branch-description',
118
'Longer description of the purpose or contents of the branch.',
121
"Branch author's email address, if not yourself.",
124
'The bug this branch fixes.',
127
'Prepare the request but don\'t actually send it.')
137
branch_description='',
141
from bzrlib.plugins.launchpad.lp_registration import (
142
BranchRegistrationRequest, BranchBugLinkRequest,
143
DryRunLaunchpadService, LaunchpadService)
144
if public_url is None:
146
b = _mod_branch.Branch.open_containing('.')[0]
147
except NotBranchError:
148
raise BzrCommandError(gettext(
149
'register-branch requires a public '
150
'branch url - see bzr help register-branch.'))
151
public_url = b.get_public_branch()
152
if public_url is None:
153
raise NoPublicBranch(b)
154
if product is not None:
157
'--product is deprecated; please use --project.'))
160
rego = BranchRegistrationRequest(branch_url=public_url,
161
branch_name=branch_name,
162
branch_title=branch_title,
163
branch_description=branch_description,
164
product_name=project,
167
linko = BranchBugLinkRequest(branch_url=public_url,
170
service = LaunchpadService()
171
# This gives back the xmlrpc url that can be used for future
172
# operations on the branch. It's not so useful to print to the
173
# user since they can't do anything with it from a web browser; it
174
# might be nice for the server to tell us about an html url as
177
# Run on service entirely in memory
178
service = DryRunLaunchpadService()
179
service.gather_user_credentials()
182
linko.submit(service)
183
print 'Branch registered.'
185
register_command(cmd_register_branch)
188
class cmd_launchpad_open(Command):
189
__doc__ = """Open a Launchpad branch page in your web browser."""
191
aliases = ['lp-open']
194
'Do not actually open the browser. Just say the URL we would '
197
takes_args = ['location?']
199
def _possible_locations(self, location):
200
"""Yield possible external locations for the branch at 'location'."""
203
branch = _mod_branch.Branch.open_containing(location)[0]
204
except NotBranchError:
206
branch_url = branch.get_public_branch()
207
if branch_url is not None:
209
branch_url = branch.get_push_location()
210
if branch_url is not None:
213
def _get_web_url(self, service, location):
214
from bzrlib.plugins.launchpad.lp_registration import (
216
for branch_url in self._possible_locations(location):
218
return service.get_web_url_from_branch_url(branch_url)
219
except (NotLaunchpadBranch, InvalidURL):
221
raise NotLaunchpadBranch(branch_url)
223
def run(self, location=None, dry_run=False):
224
from bzrlib.plugins.launchpad.lp_registration import (
228
web_url = self._get_web_url(LaunchpadService(), location)
229
trace.note(gettext('Opening %s in web browser') % web_url)
231
import webbrowser # this import should not be lazy
232
# otherwise bzr.exe lacks this module
233
webbrowser.open(web_url)
235
register_command(cmd_launchpad_open)
238
class cmd_launchpad_login(Command):
239
__doc__ = """Show or set the Launchpad user ID.
241
When communicating with Launchpad, some commands need to know your
242
Launchpad user ID. This command can be used to set or show the
243
user ID that Bazaar will use for such communication.
246
Show the Launchpad ID of the current user::
250
Set the Launchpad ID of the current user to 'bob'::
252
bzr launchpad-login bob
254
aliases = ['lp-login']
255
takes_args = ['name?']
259
"Don't check that the user name is valid."),
262
def run(self, name=None, no_check=False, verbose=False):
263
# This is totally separate from any launchpadlib login system.
264
from bzrlib.plugins.launchpad import account
265
check_account = not no_check
268
username = account.get_lp_login()
271
account.check_lp_login(username)
273
self.outf.write(gettext(
274
"Launchpad user ID exists and has SSH keys.\n"))
275
self.outf.write(username + '\n')
277
self.outf.write(gettext('No Launchpad user ID configured.\n'))
282
account.check_lp_login(name)
284
self.outf.write(gettext(
285
"Launchpad user ID exists and has SSH keys.\n"))
286
account.set_lp_login(name)
288
self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
291
register_command(cmd_launchpad_login)
294
# XXX: cmd_launchpad_mirror is untested
295
class cmd_launchpad_mirror(Command):
296
__doc__ = """Ask Launchpad to mirror a branch now."""
298
aliases = ['lp-mirror']
299
takes_args = ['location?']
301
def run(self, location='.'):
302
from bzrlib.plugins.launchpad import lp_api
303
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
304
branch, _ = _mod_branch.Branch.open_containing(location)
305
service = LaunchpadService()
306
launchpad = lp_api.login(service)
307
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
308
create_missing=False)
309
lp_branch.lp.requestMirror()
312
register_command(cmd_launchpad_mirror)
315
class cmd_lp_propose_merge(Command):
316
__doc__ = """Propose merging a branch on Launchpad.
318
This will open your usual editor to provide the initial comment. When it
319
has created the proposal, it will open it in your default web browser.
321
The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
322
is not supplied, the remembered submit branch will be used. If no submit
323
branch is remembered, the development focus will be used.
325
By default, the SUBMIT_BRANCH's review team will be requested to review
326
the merge proposal. This can be overriden by specifying --review (-R).
327
The parameter the launchpad account name of the desired reviewer. This
328
may optionally be followed by '=' and the review type. For example:
330
bzr lp-propose-merge --review jrandom --review review-team=qa
332
This will propose a merge, request "jrandom" to perform a review of
333
unspecified type, and request "review-team" to perform a "qa" review.
336
takes_options = [Option('staging',
337
help='Propose the merge on staging.'),
338
Option('message', short_name='m', type=unicode,
339
help='Commit message.'),
341
help='Mark the proposal as approved immediately.'),
342
ListOption('review', short_name='R', type=unicode,
343
help='Requested reviewer and optional type.')]
345
takes_args = ['submit_branch?']
347
aliases = ['lp-submit', 'lp-propose']
349
def run(self, submit_branch=None, review=None, staging=False,
350
message=None, approve=False):
351
from bzrlib.plugins.launchpad import lp_propose
352
tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
358
for review in review:
360
reviews.append(review.split('=', 2))
362
reviews.append((review, ''))
363
if submit_branch is None:
364
submit_branch = branch.get_submit_branch()
365
if submit_branch is None:
368
target = _mod_branch.Branch.open(submit_branch)
369
proposer = lp_propose.Proposer(tree, branch, target, message,
370
reviews, staging, approve=approve)
371
proposer.check_proposal()
372
proposer.create_proposal()
375
register_command(cmd_lp_propose_merge)
378
class cmd_lp_find_proposal(Command):
380
__doc__ = """Find the proposal to merge this revision.
382
Finds the merge proposal(s) that discussed landing the specified revision.
383
This works only if the selected branch was the merge proposal target, and
384
if the merged_revno is recorded for the merge proposal. The proposal(s)
385
are opened in a web browser.
387
Any revision involved in the merge may be specified-- the revision in
388
which the merge was performed, or one of the revisions that was merged.
390
So, to find the merge proposal that reviewed line 1 of README::
392
bzr lp-find-proposal -r annotate:README:1
395
takes_options = ['revision']
397
def run(self, revision=None):
398
from bzrlib.plugins.launchpad import lp_api
400
b = _mod_branch.Branch.open_containing('.')[0]
401
pb = ui.ui_factory.nested_progress_bar()
404
revno = self._find_merged_revno(revision, b, pb)
405
merged = self._find_proposals(revno, b, pb)
407
raise BzrCommandError(gettext('No review found.'))
408
trace.note(gettext('%d proposals(s) found.') % len(merged))
410
webbrowser.open(lp_api.canonical_url(mp))
415
def _find_merged_revno(self, revision, b, pb):
418
pb.update(gettext('Finding revision-id'))
419
revision_id = revision[0].as_revision_id(b)
420
# a revno spec is necessarily on the mainline.
421
if self._is_revno_spec(revision[0]):
422
merging_revision = revision_id
424
graph = b.repository.get_graph()
425
pb.update(gettext('Finding merge'))
426
merging_revision = graph.find_lefthand_merger(
427
revision_id, b.last_revision())
428
if merging_revision is None:
429
raise InvalidRevisionSpec(revision[0].user_spec, b)
430
pb.update(gettext('Finding revno'))
431
return b.revision_id_to_revno(merging_revision)
433
def _find_proposals(self, revno, b, pb):
434
launchpad = lp_api.login(lp_registration.LaunchpadService())
435
pb.update(gettext('Finding Launchpad branch'))
436
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
437
create_missing=False)
438
pb.update(gettext('Finding proposals'))
439
return list(lpb.lp.getMergeProposals(status=['Merged'],
440
merged_revnos=[revno]))
444
def _is_revno_spec(spec):
453
register_command(cmd_lp_find_proposal)
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")
456
70
def _register_directory():