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,
57
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
ListOption('review', short_name='R', type=unicode,
346
help='Requested reviewer and optional type.')]
348
takes_args = ['submit_branch?']
350
aliases = ['lp-submit', 'lp-propose']
352
def run(self, submit_branch=None, review=None, staging=False,
353
message=None, approve=False):
354
from bzrlib.plugins.launchpad import lp_propose
355
tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
361
for review in review:
363
reviews.append(review.split('=', 2))
365
reviews.append((review, ''))
366
if submit_branch is None:
367
submit_branch = branch.get_submit_branch()
368
if submit_branch is None:
371
target = _mod_branch.Branch.open(submit_branch)
372
proposer = lp_propose.Proposer(tree, branch, target, message,
373
reviews, staging, approve=approve)
374
proposer.check_proposal()
375
proposer.create_proposal()
378
register_command(cmd_lp_propose_merge)
381
class cmd_lp_find_proposal(Command):
383
__doc__ = """Find the proposal to merge this revision.
385
Finds the merge proposal(s) that discussed landing the specified revision.
386
This works only if the selected branch was the merge proposal target, and
387
if the merged_revno is recorded for the merge proposal. The proposal(s)
388
are opened in a web browser.
390
Any revision involved in the merge may be specified-- the revision in
391
which the merge was performed, or one of the revisions that was merged.
393
So, to find the merge proposal that reviewed line 1 of README::
395
bzr lp-find-proposal -r annotate:README:1
398
takes_options = ['revision']
400
def run(self, revision=None):
401
from bzrlib.plugins.launchpad import lp_api
403
b = _mod_branch.Branch.open_containing('.')[0]
404
pb = ui.ui_factory.nested_progress_bar()
407
revno = self._find_merged_revno(revision, b, pb)
408
merged = self._find_proposals(revno, b, pb)
410
raise BzrCommandError(gettext('No review found.'))
411
trace.note(gettext('%d proposals(s) found.') % len(merged))
413
webbrowser.open(lp_api.canonical_url(mp))
418
def _find_merged_revno(self, revision, b, pb):
421
pb.update(gettext('Finding revision-id'))
422
revision_id = revision[0].as_revision_id(b)
423
# a revno spec is necessarily on the mainline.
424
if self._is_revno_spec(revision[0]):
425
merging_revision = revision_id
427
graph = b.repository.get_graph()
428
pb.update(gettext('Finding merge'))
429
merging_revision = graph.find_lefthand_merger(
430
revision_id, b.last_revision())
431
if merging_revision is None:
432
raise InvalidRevisionSpec(revision[0].user_spec, b)
433
pb.update(gettext('Finding revno'))
434
return b.revision_id_to_revno(merging_revision)
436
def _find_proposals(self, revno, b, pb):
437
launchpad = lp_api.login(lp_registration.LaunchpadService())
438
pb.update(gettext('Finding Launchpad branch'))
439
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
440
create_missing=False)
441
pb.update(gettext('Finding proposals'))
442
return list(lpb.lp.getMergeProposals(status=['Merged'],
443
merged_revnos=[revno]))
447
def _is_revno_spec(spec):
456
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")
459
70
def _register_directory():