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
54
45
from bzrlib import (
55
46
branch as _mod_branch,
56
47
config as _mod_config,
59
49
# Since we are a built-in plugin we share the bzrlib version
62
53
from bzrlib.commands import (
66
56
from bzrlib.directory_service import directories
67
from bzrlib.errors import (
74
57
from bzrlib.help_topics import topic_registry
75
from bzrlib.option import (
81
class cmd_register_branch(Command):
82
__doc__ = """Register a branch with launchpad.net.
84
This command lists a bzr branch in the directory of branches on
85
launchpad.net. Registration allows the branch to be associated with
86
bugs or specifications.
88
Before using this command you must register the project to which the
89
branch belongs, and create an account for yourself on launchpad.net.
92
public_url: The publicly visible url for the branch to register.
93
This must be an http or https url (which Launchpad can read
94
from to access the branch). Local file urls, SFTP urls, and
95
bzr+ssh urls will not work.
96
If no public_url is provided, bzr will use the configured
97
public_url if there is one for the current branch, and
101
bzr register-branch http://foo.com/bzr/fooproject.mine \\
104
takes_args = ['public_url?']
107
'Launchpad project short name to associate with the branch.',
110
'Launchpad product short name to associate with the branch.',
113
Option('branch-name',
114
'Short name for the branch; '
115
'by default taken from the last component of the url.',
117
Option('branch-title',
118
'One-sentence description of the branch.',
120
Option('branch-description',
121
'Longer description of the purpose or contents of the branch.',
124
"Branch author's email address, if not yourself.",
127
'The bug this branch fixes.',
130
'Prepare the request but don\'t actually send it.')
140
branch_description='',
144
from bzrlib.plugins.launchpad.lp_registration import (
145
BranchRegistrationRequest, BranchBugLinkRequest,
146
DryRunLaunchpadService, LaunchpadService)
147
if public_url is None:
149
b = _mod_branch.Branch.open_containing('.')[0]
150
except NotBranchError:
151
raise BzrCommandError(gettext(
152
'register-branch requires a public '
153
'branch url - see bzr help register-branch.'))
154
public_url = b.get_public_branch()
155
if public_url is None:
156
raise NoPublicBranch(b)
157
if product is not None:
160
'--product is deprecated; please use --project.'))
163
rego = BranchRegistrationRequest(branch_url=public_url,
164
branch_name=branch_name,
165
branch_title=branch_title,
166
branch_description=branch_description,
167
product_name=project,
170
linko = BranchBugLinkRequest(branch_url=public_url,
173
service = LaunchpadService()
174
# This gives back the xmlrpc url that can be used for future
175
# operations on the branch. It's not so useful to print to the
176
# user since they can't do anything with it from a web browser; it
177
# might be nice for the server to tell us about an html url as
180
# Run on service entirely in memory
181
service = DryRunLaunchpadService()
182
service.gather_user_credentials()
185
linko.submit(service)
186
print 'Branch registered.'
188
register_command(cmd_register_branch)
191
class cmd_launchpad_open(Command):
192
__doc__ = """Open a Launchpad branch page in your web browser."""
194
aliases = ['lp-open']
197
'Do not actually open the browser. Just say the URL we would '
200
takes_args = ['location?']
202
def _possible_locations(self, location):
203
"""Yield possible external locations for the branch at 'location'."""
206
branch = _mod_branch.Branch.open_containing(location)[0]
207
except NotBranchError:
209
branch_url = branch.get_public_branch()
210
if branch_url is not None:
212
branch_url = branch.get_push_location()
213
if branch_url is not None:
216
def _get_web_url(self, service, location):
217
from bzrlib.plugins.launchpad.lp_registration import (
219
for branch_url in self._possible_locations(location):
221
return service.get_web_url_from_branch_url(branch_url)
222
except (NotLaunchpadBranch, InvalidURL):
224
raise NotLaunchpadBranch(branch_url)
226
def run(self, location=None, dry_run=False):
227
from bzrlib.plugins.launchpad.lp_registration import (
231
web_url = self._get_web_url(LaunchpadService(), location)
232
trace.note(gettext('Opening %s in web browser') % web_url)
234
import webbrowser # this import should not be lazy
235
# otherwise bzr.exe lacks this module
236
webbrowser.open(web_url)
238
register_command(cmd_launchpad_open)
241
class cmd_launchpad_login(Command):
242
__doc__ = """Show or set the Launchpad user ID.
244
When communicating with Launchpad, some commands need to know your
245
Launchpad user ID. This command can be used to set or show the
246
user ID that Bazaar will use for such communication.
249
Show the Launchpad ID of the current user::
253
Set the Launchpad ID of the current user to 'bob'::
255
bzr launchpad-login bob
257
aliases = ['lp-login']
258
takes_args = ['name?']
262
"Don't check that the user name is valid."),
265
def run(self, name=None, no_check=False, verbose=False):
266
# This is totally separate from any launchpadlib login system.
267
from bzrlib.plugins.launchpad import account
268
check_account = not no_check
271
username = account.get_lp_login()
274
account.check_lp_login(username)
276
self.outf.write(gettext(
277
"Launchpad user ID exists and has SSH keys.\n"))
278
self.outf.write(username + '\n')
280
self.outf.write(gettext('No Launchpad user ID configured.\n'))
285
account.check_lp_login(name)
287
self.outf.write(gettext(
288
"Launchpad user ID exists and has SSH keys.\n"))
289
account.set_lp_login(name)
291
self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
294
register_command(cmd_launchpad_login)
297
# XXX: cmd_launchpad_mirror is untested
298
class cmd_launchpad_mirror(Command):
299
__doc__ = """Ask Launchpad to mirror a branch now."""
301
aliases = ['lp-mirror']
302
takes_args = ['location?']
304
def run(self, location='.'):
305
from bzrlib.plugins.launchpad import lp_api
306
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
307
branch, _ = _mod_branch.Branch.open_containing(location)
308
service = LaunchpadService()
309
launchpad = lp_api.login(service)
310
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
311
create_missing=False)
312
lp_branch.lp.requestMirror()
315
register_command(cmd_launchpad_mirror)
318
class cmd_lp_propose_merge(Command):
319
__doc__ = """Propose merging a branch on Launchpad.
321
This will open your usual editor to provide the initial comment. When it
322
has created the proposal, it will open it in your default web browser.
324
The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
325
is not supplied, the remembered submit branch will be used. If no submit
326
branch is remembered, the development focus will be used.
328
By default, the SUBMIT_BRANCH's review team will be requested to review
329
the merge proposal. This can be overriden by specifying --review (-R).
330
The parameter the launchpad account name of the desired reviewer. This
331
may optionally be followed by '=' and the review type. For example:
333
bzr lp-propose-merge --review jrandom --review review-team=qa
335
This will propose a merge, request "jrandom" to perform a review of
336
unspecified type, and request "review-team" to perform a "qa" review.
339
takes_options = [Option('staging',
340
help='Propose the merge on staging.'),
341
Option('message', short_name='m', type=unicode,
342
help='Commit message.'),
344
help='Mark the proposal as approved immediately.'),
345
Option('fixes', 'The bug this proposal fixes.', str),
346
ListOption('review', short_name='R', type=unicode,
347
help='Requested reviewer and optional type.')]
349
takes_args = ['submit_branch?']
351
aliases = ['lp-submit', 'lp-propose']
353
def run(self, submit_branch=None, review=None, staging=False,
354
message=None, approve=False, fixes=None):
355
from bzrlib.plugins.launchpad import lp_propose
356
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
362
for review in review:
364
reviews.append(review.split('=', 2))
366
reviews.append((review, ''))
367
if submit_branch is None:
368
submit_branch = branch.get_submit_branch()
369
if submit_branch is None:
372
target = _mod_branch.Branch.open(submit_branch)
373
proposer = lp_propose.Proposer(tree, branch, target, message,
374
reviews, staging, approve=approve,
376
proposer.check_proposal()
377
proposer.create_proposal()
380
register_command(cmd_lp_propose_merge)
383
class cmd_lp_find_proposal(Command):
385
__doc__ = """Find the proposal to merge this revision.
387
Finds the merge proposal(s) that discussed landing the specified revision.
388
This works only if the selected branch was the merge proposal target, and
389
if the merged_revno is recorded for the merge proposal. The proposal(s)
390
are opened in a web browser.
392
Any revision involved in the merge may be specified-- the revision in
393
which the merge was performed, or one of the revisions that was merged.
395
So, to find the merge proposal that reviewed line 1 of README::
397
bzr lp-find-proposal -r annotate:README:1
400
takes_options = ['revision']
402
def run(self, revision=None):
403
from bzrlib.plugins.launchpad import lp_api
405
b = _mod_branch.Branch.open_containing('.')[0]
406
pb = ui.ui_factory.nested_progress_bar()
409
revno = self._find_merged_revno(revision, b, pb)
410
merged = self._find_proposals(revno, b, pb)
412
raise BzrCommandError(gettext('No review found.'))
413
trace.note(gettext('%d proposals(s) found.') % len(merged))
415
webbrowser.open(lp_api.canonical_url(mp))
420
def _find_merged_revno(self, revision, b, pb):
423
pb.update(gettext('Finding revision-id'))
424
revision_id = revision[0].as_revision_id(b)
425
# a revno spec is necessarily on the mainline.
426
if self._is_revno_spec(revision[0]):
427
merging_revision = revision_id
429
graph = b.repository.get_graph()
430
pb.update(gettext('Finding merge'))
431
merging_revision = graph.find_lefthand_merger(
432
revision_id, b.last_revision())
433
if merging_revision is None:
434
raise InvalidRevisionSpec(revision[0].user_spec, b)
435
pb.update(gettext('Finding revno'))
436
return b.revision_id_to_revno(merging_revision)
438
def _find_proposals(self, revno, b, pb):
439
launchpad = lp_api.login(lp_registration.LaunchpadService())
440
pb.update(gettext('Finding Launchpad branch'))
441
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
442
create_missing=False)
443
pb.update(gettext('Finding proposals'))
444
return list(lpb.lp.getMergeProposals(status=['Merged'],
445
merged_revnos=[revno]))
449
def _is_revno_spec(spec):
458
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")
461
70
def _register_directory():