43
43
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
45
from bzrlib.lazy_import import lazy_import
46
lazy_import(globals(), """
51
from bzrlib.i18n import gettext
45
54
from bzrlib import (
46
55
branch as _mod_branch,
47
config as _mod_config,
49
58
# Since we are a built-in plugin we share the bzrlib version
53
61
from bzrlib.commands import (
56
65
from bzrlib.directory_service import directories
66
from bzrlib.errors import (
57
73
from bzrlib.help_topics import topic_registry
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")
74
from bzrlib.option import (
80
class cmd_register_branch(Command):
81
__doc__ = """Register a branch with launchpad.net.
83
This command lists a bzr branch in the directory of branches on
84
launchpad.net. Registration allows the branch to be associated with
85
bugs or specifications.
87
Before using this command you must register the project to which the
88
branch belongs, and create an account for yourself on launchpad.net.
91
public_url: The publicly visible url for the branch to register.
92
This must be an http or https url (which Launchpad can read
93
from to access the branch). Local file urls, SFTP urls, and
94
bzr+ssh urls will not work.
95
If no public_url is provided, bzr will use the configured
96
public_url if there is one for the current branch, and
100
bzr register-branch http://foo.com/bzr/fooproject.mine \\
103
takes_args = ['public_url?']
106
'Launchpad project short name to associate with the branch.',
109
'Launchpad product short name to associate with the branch.',
112
Option('branch-name',
113
'Short name for the branch; '
114
'by default taken from the last component of the url.',
116
Option('branch-title',
117
'One-sentence description of the branch.',
119
Option('branch-description',
120
'Longer description of the purpose or contents of the branch.',
123
"Branch author's email address, if not yourself.",
126
'The bug this branch fixes.',
129
'Prepare the request but don\'t actually send it.')
139
branch_description='',
143
from bzrlib.plugins.launchpad.lp_registration import (
144
BranchRegistrationRequest, BranchBugLinkRequest,
145
DryRunLaunchpadService, LaunchpadService)
146
if public_url is None:
148
b = _mod_branch.Branch.open_containing('.')[0]
149
except NotBranchError:
150
raise BzrCommandError(gettext(
151
'register-branch requires a public '
152
'branch url - see bzr help register-branch.'))
153
public_url = b.get_public_branch()
154
if public_url is None:
155
raise NoPublicBranch(b)
156
if product is not None:
159
'--product is deprecated; please use --project.'))
162
rego = BranchRegistrationRequest(branch_url=public_url,
163
branch_name=branch_name,
164
branch_title=branch_title,
165
branch_description=branch_description,
166
product_name=project,
169
linko = BranchBugLinkRequest(branch_url=public_url,
172
service = LaunchpadService()
173
# This gives back the xmlrpc url that can be used for future
174
# operations on the branch. It's not so useful to print to the
175
# user since they can't do anything with it from a web browser; it
176
# might be nice for the server to tell us about an html url as
179
# Run on service entirely in memory
180
service = DryRunLaunchpadService()
181
service.gather_user_credentials()
184
linko.submit(service)
185
print 'Branch registered.'
187
register_command(cmd_register_branch)
190
class cmd_launchpad_open(Command):
191
__doc__ = """Open a Launchpad branch page in your web browser."""
193
aliases = ['lp-open']
196
'Do not actually open the browser. Just say the URL we would '
199
takes_args = ['location?']
201
def _possible_locations(self, location):
202
"""Yield possible external locations for the branch at 'location'."""
205
branch = _mod_branch.Branch.open_containing(location)[0]
206
except NotBranchError:
208
branch_url = branch.get_public_branch()
209
if branch_url is not None:
211
branch_url = branch.get_push_location()
212
if branch_url is not None:
215
def _get_web_url(self, service, location):
216
from bzrlib.plugins.launchpad.lp_registration import (
218
for branch_url in self._possible_locations(location):
220
return service.get_web_url_from_branch_url(branch_url)
221
except (NotLaunchpadBranch, InvalidURL):
223
raise NotLaunchpadBranch(branch_url)
225
def run(self, location=None, dry_run=False):
226
from bzrlib.plugins.launchpad.lp_registration import (
230
web_url = self._get_web_url(LaunchpadService(), location)
231
trace.note(gettext('Opening %s in web browser') % web_url)
233
import webbrowser # this import should not be lazy
234
# otherwise bzr.exe lacks this module
235
webbrowser.open(web_url)
237
register_command(cmd_launchpad_open)
240
class cmd_launchpad_login(Command):
241
__doc__ = """Show or set the Launchpad user ID.
243
When communicating with Launchpad, some commands need to know your
244
Launchpad user ID. This command can be used to set or show the
245
user ID that Bazaar will use for such communication.
248
Show the Launchpad ID of the current user::
252
Set the Launchpad ID of the current user to 'bob'::
254
bzr launchpad-login bob
256
aliases = ['lp-login']
257
takes_args = ['name?']
261
"Don't check that the user name is valid."),
264
def run(self, name=None, no_check=False, verbose=False):
265
# This is totally separate from any launchpadlib login system.
266
from bzrlib.plugins.launchpad import account
267
check_account = not no_check
270
username = account.get_lp_login()
273
account.check_lp_login(username)
275
self.outf.write(gettext(
276
"Launchpad user ID exists and has SSH keys.\n"))
277
self.outf.write(username + '\n')
279
self.outf.write(gettext('No Launchpad user ID configured.\n'))
284
account.check_lp_login(name)
286
self.outf.write(gettext(
287
"Launchpad user ID exists and has SSH keys.\n"))
288
account.set_lp_login(name)
290
self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
293
register_command(cmd_launchpad_login)
296
# XXX: cmd_launchpad_mirror is untested
297
class cmd_launchpad_mirror(Command):
298
__doc__ = """Ask Launchpad to mirror a branch now."""
300
aliases = ['lp-mirror']
301
takes_args = ['location?']
303
def run(self, location='.'):
304
from bzrlib.plugins.launchpad import lp_api
305
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
306
branch, _ = _mod_branch.Branch.open_containing(location)
307
service = LaunchpadService()
308
launchpad = lp_api.login(service)
309
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
310
create_missing=False)
311
lp_branch.lp.requestMirror()
314
register_command(cmd_launchpad_mirror)
317
class cmd_lp_propose_merge(Command):
318
__doc__ = """Propose merging a branch on Launchpad.
320
This will open your usual editor to provide the initial comment. When it
321
has created the proposal, it will open it in your default web browser.
323
The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
324
is not supplied, the remembered submit branch will be used. If no submit
325
branch is remembered, the development focus will be used.
327
By default, the SUBMIT_BRANCH's review team will be requested to review
328
the merge proposal. This can be overriden by specifying --review (-R).
329
The parameter the launchpad account name of the desired reviewer. This
330
may optionally be followed by '=' and the review type. For example:
332
bzr lp-propose-merge --review jrandom --review review-team=qa
334
This will propose a merge, request "jrandom" to perform a review of
335
unspecified type, and request "review-team" to perform a "qa" review.
338
takes_options = [Option('staging',
339
help='Propose the merge on staging.'),
340
Option('message', short_name='m', type=unicode,
341
help='Commit message.'),
343
help='Mark the proposal as approved immediately.'),
344
ListOption('review', short_name='R', type=unicode,
345
help='Requested reviewer and optional type.')]
347
takes_args = ['submit_branch?']
349
aliases = ['lp-submit', 'lp-propose']
351
def run(self, submit_branch=None, review=None, staging=False,
352
message=None, approve=False):
353
from bzrlib.plugins.launchpad import lp_propose
354
tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
360
for review in review:
362
reviews.append(review.split('=', 2))
364
reviews.append((review, ''))
365
if submit_branch is None:
366
submit_branch = branch.get_submit_branch()
367
if submit_branch is None:
370
target = _mod_branch.Branch.open(submit_branch)
371
proposer = lp_propose.Proposer(tree, branch, target, message,
372
reviews, staging, approve=approve)
373
proposer.check_proposal()
374
proposer.create_proposal()
377
register_command(cmd_lp_propose_merge)
380
class cmd_lp_find_proposal(Command):
382
__doc__ = """Find the proposal to merge this revision.
384
Finds the merge proposal(s) that discussed landing the specified revision.
385
This works only if the selected branch was the merge proposal target, and
386
if the merged_revno is recorded for the merge proposal. The proposal(s)
387
are opened in a web browser.
389
Any revision involved in the merge may be specified-- the revision in
390
which the merge was performed, or one of the revisions that was merged.
392
So, to find the merge proposal that reviewed line 1 of README::
394
bzr lp-find-proposal -r annotate:README:1
397
takes_options = ['revision']
399
def run(self, revision=None):
400
from bzrlib.plugins.launchpad import lp_api
402
b = _mod_branch.Branch.open_containing('.')[0]
403
pb = ui.ui_factory.nested_progress_bar()
406
revno = self._find_merged_revno(revision, b, pb)
407
merged = self._find_proposals(revno, b, pb)
409
raise BzrCommandError(gettext('No review found.'))
410
trace.note(gettext('%d proposals(s) found.') % len(merged))
412
webbrowser.open(lp_api.canonical_url(mp))
417
def _find_merged_revno(self, revision, b, pb):
420
pb.update(gettext('Finding revision-id'))
421
revision_id = revision[0].as_revision_id(b)
422
# a revno spec is necessarily on the mainline.
423
if self._is_revno_spec(revision[0]):
424
merging_revision = revision_id
426
graph = b.repository.get_graph()
427
pb.update(gettext('Finding merge'))
428
merging_revision = graph.find_lefthand_merger(
429
revision_id, b.last_revision())
430
if merging_revision is None:
431
raise InvalidRevisionSpec(revision[0].user_spec, b)
432
pb.update(gettext('Finding revno'))
433
return b.revision_id_to_revno(merging_revision)
435
def _find_proposals(self, revno, b, pb):
436
launchpad = lp_api.login(lp_registration.LaunchpadService())
437
pb.update(gettext('Finding Launchpad branch'))
438
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
439
create_missing=False)
440
pb.update(gettext('Finding proposals'))
441
return list(lpb.lp.getMergeProposals(status=['Merged'],
442
merged_revnos=[revno]))
446
def _is_revno_spec(spec):
455
register_command(cmd_lp_find_proposal)
70
458
def _register_directory():