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
# Since we are a built-in plugin we share the bzrlib version
44
from bzrlib import version_info
46
from bzrlib.lazy_import import lazy_import
47
lazy_import(globals(), """
48
45
from bzrlib import (
49
46
branch as _mod_branch,
47
config as _mod_config,
49
# Since we are a built-in plugin we share the bzrlib version
55
from bzrlib import bzrdir
56
53
from bzrlib.commands import (
60
56
from bzrlib.directory_service import directories
61
from bzrlib.errors import (
68
57
from bzrlib.help_topics import topic_registry
69
from bzrlib.option import (
75
class cmd_register_branch(Command):
76
__doc__ = """Register a branch with launchpad.net.
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.
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.
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
95
bzr register-branch http://foo.com/bzr/fooproject.mine \\
98
takes_args = ['public_url?']
101
'Launchpad project short name to associate with the branch.',
104
'Launchpad product short name to associate with the branch.',
107
Option('branch-name',
108
'Short name for the branch; '
109
'by default taken from the last component of the url.',
111
Option('branch-title',
112
'One-sentence description of the branch.',
114
Option('branch-description',
115
'Longer description of the purpose or contents of the branch.',
118
"Branch author's email address, if not yourself.",
121
'The bug this branch fixes.',
124
'Prepare the request but don\'t actually send it.')
134
branch_description='',
138
from bzrlib.plugins.launchpad.lp_registration import (
139
BranchRegistrationRequest, BranchBugLinkRequest,
140
DryRunLaunchpadService, LaunchpadService)
141
if public_url is None:
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:
152
trace.note('--product is deprecated; please use --project.')
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,
162
linko = BranchBugLinkRequest(branch_url=public_url,
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
172
# Run on service entirely in memory
173
service = DryRunLaunchpadService()
174
service.gather_user_credentials()
177
linko.submit(service)
178
print 'Branch registered.'
180
register_command(cmd_register_branch)
183
class cmd_launchpad_open(Command):
184
__doc__ = """Open a Launchpad branch page in your web browser."""
186
aliases = ['lp-open']
189
'Do not actually open the browser. Just say the URL we would '
192
takes_args = ['location?']
194
def _possible_locations(self, location):
195
"""Yield possible external locations for the branch at 'location'."""
198
branch = _mod_branch.Branch.open_containing(location)[0]
199
except NotBranchError:
201
branch_url = branch.get_public_branch()
202
if branch_url is not None:
204
branch_url = branch.get_push_location()
205
if branch_url is not None:
208
def _get_web_url(self, service, location):
209
from bzrlib.plugins.launchpad.lp_registration import (
211
for branch_url in self._possible_locations(location):
213
return service.get_web_url_from_branch_url(branch_url)
214
except (NotLaunchpadBranch, InvalidURL):
216
raise NotLaunchpadBranch(branch_url)
218
def run(self, location=None, dry_run=False):
219
from bzrlib.plugins.launchpad.lp_registration import (
223
web_url = self._get_web_url(LaunchpadService(), location)
224
trace.note('Opening %s in web browser' % web_url)
226
import webbrowser # this import should not be lazy
227
# otherwise bzr.exe lacks this module
228
webbrowser.open(web_url)
230
register_command(cmd_launchpad_open)
233
class cmd_launchpad_login(Command):
234
__doc__ = """Show or set the Launchpad user ID.
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.
241
Show the Launchpad ID of the current user::
245
Set the Launchpad ID of the current user to 'bob'::
247
bzr launchpad-login bob
249
aliases = ['lp-login']
250
takes_args = ['name?']
254
"Don't check that the user name is valid."),
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
263
username = account.get_lp_login()
266
account.check_lp_login(username)
269
"Launchpad user ID exists and has SSH keys.\n")
270
self.outf.write(username + '\n')
272
self.outf.write('No Launchpad user ID configured.\n')
277
account.check_lp_login(name)
280
"Launchpad user ID exists and has SSH keys.\n")
281
account.set_lp_login(name)
283
self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
285
register_command(cmd_launchpad_login)
288
# XXX: cmd_launchpad_mirror is untested
289
class cmd_launchpad_mirror(Command):
290
__doc__ = """Ask Launchpad to mirror a branch now."""
292
aliases = ['lp-mirror']
293
takes_args = ['location?']
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()
306
register_command(cmd_launchpad_mirror)
309
class cmd_lp_propose_merge(Command):
310
__doc__ = """Propose merging a branch on Launchpad.
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.
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.
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:
324
bzr lp-propose-merge --review jrandom --review review-team=qa
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.
330
takes_options = [Option('staging',
331
help='Propose the merge on staging.'),
332
Option('message', short_name='m', type=unicode,
333
help='Commit message.'),
335
help='Mark the proposal as approved immediately.'),
336
ListOption('review', short_name='R', type=unicode,
337
help='Requested reviewer and optional type.')]
339
takes_args = ['submit_branch?']
341
aliases = ['lp-submit', 'lp-propose']
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(
352
for review in review:
354
reviews.append(review.split('=', 2))
356
reviews.append((review, ''))
357
if submit_branch is None:
358
submit_branch = branch.get_submit_branch()
359
if submit_branch is None:
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()
369
register_command(cmd_lp_propose_merge)
372
class cmd_lp_find_proposal(Command):
374
__doc__ = """Find the proposal to merge this revision.
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.
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.
384
So, to find the merge proposal that reviewed line 1 of README::
386
bzr lp-find-proposal -r annotate:README:1
389
takes_options = ['revision']
391
def run(self, revision=None):
392
from bzrlib.plugins.launchpad import lp_api
394
b = _mod_branch.Branch.open_containing('.')[0]
395
pb = ui.ui_factory.nested_progress_bar()
398
revno = self._find_merged_revno(revision, b, pb)
399
merged = self._find_proposals(revno, b, pb)
401
raise BzrCommandError('No review found.')
402
trace.note('%d proposals(s) found.' % len(merged))
404
webbrowser.open(lp_api.canonical_url(mp))
409
def _find_merged_revno(self, revision, b, pb):
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
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)
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]))
438
def _is_revno_spec(spec):
447
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")
450
70
def _register_directory():