1
# Copyright (C) 2006-2012 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Launchpad plugin commands."""
19
from __future__ import absolute_import
22
branch as _mod_branch,
26
from bzrlib.commands import (
29
from bzrlib.errors import (
36
from bzrlib.i18n import gettext
37
from bzrlib.option import (
43
class cmd_register_branch(Command):
44
__doc__ = """Register a branch with launchpad.net.
46
This command lists a bzr branch in the directory of branches on
47
launchpad.net. Registration allows the branch to be associated with
48
bugs or specifications.
50
Before using this command you must register the project to which the
51
branch belongs, and create an account for yourself on launchpad.net.
54
public_url: The publicly visible url for the branch to register.
55
This must be an http or https url (which Launchpad can read
56
from to access the branch). Local file urls, SFTP urls, and
57
bzr+ssh urls will not work.
58
If no public_url is provided, bzr will use the configured
59
public_url if there is one for the current branch, and
63
bzr register-branch http://foo.com/bzr/fooproject.mine \\
66
takes_args = ['public_url?']
69
'Launchpad project short name to associate with the branch.',
72
'Launchpad product short name to associate with the branch.',
76
'Short name for the branch; '
77
'by default taken from the last component of the url.',
79
Option('branch-title',
80
'One-sentence description of the branch.',
82
Option('branch-description',
83
'Longer description of the purpose or contents of the branch.',
86
"Branch author's email address, if not yourself.",
89
'The bug this branch fixes.',
92
'Prepare the request but don\'t actually send it.')
102
branch_description='',
106
from bzrlib.plugins.launchpad.lp_registration import (
107
BranchRegistrationRequest, BranchBugLinkRequest,
108
DryRunLaunchpadService, LaunchpadService)
109
if public_url is None:
111
b = _mod_branch.Branch.open_containing('.')[0]
112
except NotBranchError:
113
raise BzrCommandError(gettext(
114
'register-branch requires a public '
115
'branch url - see bzr help register-branch.'))
116
public_url = b.get_public_branch()
117
if public_url is None:
118
raise NoPublicBranch(b)
119
if product is not None:
122
'--product is deprecated; please use --project.'))
125
rego = BranchRegistrationRequest(branch_url=public_url,
126
branch_name=branch_name,
127
branch_title=branch_title,
128
branch_description=branch_description,
129
product_name=project,
132
linko = BranchBugLinkRequest(branch_url=public_url,
135
service = LaunchpadService()
136
# This gives back the xmlrpc url that can be used for future
137
# operations on the branch. It's not so useful to print to the
138
# user since they can't do anything with it from a web browser; it
139
# might be nice for the server to tell us about an html url as
142
# Run on service entirely in memory
143
service = DryRunLaunchpadService()
144
service.gather_user_credentials()
147
linko.submit(service)
148
self.outf.write('Branch registered.\n')
151
class cmd_launchpad_open(Command):
152
__doc__ = """Open a Launchpad branch page in your web browser."""
154
aliases = ['lp-open']
157
'Do not actually open the browser. Just say the URL we would '
160
takes_args = ['location?']
162
def _possible_locations(self, location):
163
"""Yield possible external locations for the branch at 'location'."""
166
branch = _mod_branch.Branch.open_containing(location)[0]
167
except NotBranchError:
169
branch_url = branch.get_public_branch()
170
if branch_url is not None:
172
branch_url = branch.get_push_location()
173
if branch_url is not None:
176
def _get_web_url(self, service, location):
177
from bzrlib.plugins.launchpad.lp_registration import (
179
for branch_url in self._possible_locations(location):
181
return service.get_web_url_from_branch_url(branch_url)
182
except (NotLaunchpadBranch, InvalidURL):
184
raise NotLaunchpadBranch(branch_url)
186
def run(self, location=None, dry_run=False):
187
from bzrlib.plugins.launchpad.lp_registration import (
191
web_url = self._get_web_url(LaunchpadService(), location)
192
trace.note(gettext('Opening %s in web browser') % web_url)
194
import webbrowser # this import should not be lazy
195
# otherwise bzr.exe lacks this module
196
webbrowser.open(web_url)
199
class cmd_launchpad_login(Command):
200
__doc__ = """Show or set the Launchpad user ID.
202
When communicating with Launchpad, some commands need to know your
203
Launchpad user ID. This command can be used to set or show the
204
user ID that Bazaar will use for such communication.
207
Show the Launchpad ID of the current user::
211
Set the Launchpad ID of the current user to 'bob'::
213
bzr launchpad-login bob
215
aliases = ['lp-login']
216
takes_args = ['name?']
220
"Don't check that the user name is valid."),
223
def run(self, name=None, no_check=False, verbose=False):
224
# This is totally separate from any launchpadlib login system.
225
from bzrlib.plugins.launchpad import account
226
check_account = not no_check
229
username = account.get_lp_login()
232
account.check_lp_login(username)
234
self.outf.write(gettext(
235
"Launchpad user ID exists and has SSH keys.\n"))
236
self.outf.write(username + '\n')
238
self.outf.write(gettext('No Launchpad user ID configured.\n'))
243
account.check_lp_login(name)
245
self.outf.write(gettext(
246
"Launchpad user ID exists and has SSH keys.\n"))
247
account.set_lp_login(name)
249
self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
253
# XXX: cmd_launchpad_mirror is untested
254
class cmd_launchpad_mirror(Command):
255
__doc__ = """Ask Launchpad to mirror a branch now."""
257
aliases = ['lp-mirror']
258
takes_args = ['location?']
260
def run(self, location='.'):
261
from bzrlib.plugins.launchpad import lp_api
262
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
263
branch, _ = _mod_branch.Branch.open_containing(location)
264
service = LaunchpadService()
265
launchpad = lp_api.login(service)
266
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
267
create_missing=False)
268
lp_branch.lp.requestMirror()
271
class cmd_lp_propose_merge(Command):
272
__doc__ = """Propose merging a branch on Launchpad.
274
This will open your usual editor to provide the initial comment. When it
275
has created the proposal, it will open it in your default web browser.
277
The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
278
is not supplied, the remembered submit branch will be used. If no submit
279
branch is remembered, the development focus will be used.
281
By default, the SUBMIT_BRANCH's review team will be requested to review
282
the merge proposal. This can be overriden by specifying --review (-R).
283
The parameter the launchpad account name of the desired reviewer. This
284
may optionally be followed by '=' and the review type. For example:
286
bzr lp-propose-merge --review jrandom --review review-team=qa
288
This will propose a merge, request "jrandom" to perform a review of
289
unspecified type, and request "review-team" to perform a "qa" review.
292
takes_options = [Option('staging',
293
help='Propose the merge on staging.'),
294
Option('message', short_name='m', type=unicode,
295
help='Commit message.'),
297
help='Mark the proposal as approved immediately.'),
298
Option('fixes', 'The bug this proposal fixes.', str),
299
ListOption('review', short_name='R', type=unicode,
300
help='Requested reviewer and optional type.')]
302
takes_args = ['submit_branch?']
304
aliases = ['lp-submit', 'lp-propose']
306
def run(self, submit_branch=None, review=None, staging=False,
307
message=None, approve=False, fixes=None):
308
from bzrlib.plugins.launchpad import lp_propose
309
tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
315
for review in review:
317
reviews.append(review.split('=', 2))
319
reviews.append((review, ''))
320
if submit_branch is None:
321
submit_branch = branch.get_submit_branch()
322
if submit_branch is None:
325
target = _mod_branch.Branch.open(submit_branch)
326
proposer = lp_propose.Proposer(tree, branch, target, message,
327
reviews, staging, approve=approve,
329
proposer.check_proposal()
330
proposer.create_proposal()
333
class cmd_lp_find_proposal(Command):
335
__doc__ = """Find the proposal to merge this revision.
337
Finds the merge proposal(s) that discussed landing the specified revision.
338
This works only if the selected branch was the merge proposal target, and
339
if the merged_revno is recorded for the merge proposal. The proposal(s)
340
are opened in a web browser.
342
Any revision involved in the merge may be specified-- the revision in
343
which the merge was performed, or one of the revisions that was merged.
345
So, to find the merge proposal that reviewed line 1 of README::
347
bzr lp-find-proposal -r annotate:README:1
350
takes_options = ['revision']
352
def run(self, revision=None):
353
from bzrlib import ui
354
from bzrlib.plugins.launchpad import lp_api
356
b = _mod_branch.Branch.open_containing('.')[0]
357
pb = ui.ui_factory.nested_progress_bar()
360
revno = self._find_merged_revno(revision, b, pb)
361
merged = self._find_proposals(revno, b, pb)
363
raise BzrCommandError(gettext('No review found.'))
364
trace.note(gettext('%d proposals(s) found.') % len(merged))
366
webbrowser.open(lp_api.canonical_url(mp))
371
def _find_merged_revno(self, revision, b, pb):
374
pb.update(gettext('Finding revision-id'))
375
revision_id = revision[0].as_revision_id(b)
376
# a revno spec is necessarily on the mainline.
377
if self._is_revno_spec(revision[0]):
378
merging_revision = revision_id
380
graph = b.repository.get_graph()
381
pb.update(gettext('Finding merge'))
382
merging_revision = graph.find_lefthand_merger(
383
revision_id, b.last_revision())
384
if merging_revision is None:
385
raise InvalidRevisionSpec(revision[0].user_spec, b)
386
pb.update(gettext('Finding revno'))
387
return b.revision_id_to_revno(merging_revision)
389
def _find_proposals(self, revno, b, pb):
390
from bzrlib.plugins.launchpad import (lp_api, lp_registration)
391
launchpad = lp_api.login(lp_registration.LaunchpadService())
392
pb.update(gettext('Finding Launchpad branch'))
393
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
394
create_missing=False)
395
pb.update(gettext('Finding proposals'))
396
return list(lpb.lp.getMergeProposals(status=['Merged'],
397
merged_revnos=[revno]))
401
def _is_revno_spec(spec):