13
13
# You should have received a copy of the GNU General Public License
14
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.net integration plugin for Bazaar.
19
This plugin provides facilities for working with Bazaar branches that are
20
hosted on Launchpad (http://launchpad.net). It provides a directory service
21
for referring to Launchpad branches using the "lp:" prefix. For example,
22
lp:bzr refers to the Bazaar's main development branch and
23
lp:~username/project/branch-name can be used to refer to a specific branch.
25
This plugin provides a bug tracker so that "bzr commit --fixes lp:1234" will
26
record that revision as fixing Launchpad's bug 1234.
28
The plugin also provides the following commands:
30
launchpad-login: Show or set the Launchpad user ID
31
launchpad-open: Open a Launchpad branch page in your web browser
32
lp-propose-merge: Propose merging a branch on Launchpad
33
register-branch: Register a branch with launchpad.net
34
launchpad-mirror: Ask Launchpad to mirror a branch now
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Launchpad.net branch registration plugin for bzr
19
This adds commands that tell launchpad about newly-created branches, etc.
21
To install this file, put the 'bzr_lp' directory, or a symlink to it,
22
in your ~/.bazaar/plugins/ directory.
38
# The XMLRPC server address can be overridden by setting the environment
39
# variable $BZR_LP_XMLRPC_URL
41
# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
43
from bzrlib.lazy_import import lazy_import
44
lazy_import(globals(), """
52
branch as _mod_branch,
55
# Since we are a built-in plugin we share the bzrlib version
58
from bzrlib.commands import (
62
from bzrlib.directory_service import directories
63
from bzrlib.errors import (
70
from bzrlib.help_topics import topic_registry
71
from bzrlib.option import (
25
# see http://bazaar-vcs.org/Specs/BranchRegistrationTool
27
from bzrlib.commands import Command, Option, register_command
77
31
class cmd_register_branch(Command):
78
__doc__ = """Register a branch with launchpad.net.
32
"""Register a branch with launchpad.net.
80
34
This command lists a bzr branch in the directory of branches on
81
launchpad.net. Registration allows the branch to be associated with
35
launchpad.net. Registration allows the bug to be associated with
82
36
bugs or specifications.
84
38
Before using this command you must register the project to which the
85
39
branch belongs, and create an account for yourself on launchpad.net.
88
public_url: The publicly visible url for the branch to register.
89
This must be an http or https url (which Launchpad can read
90
from to access the branch). Local file urls, SFTP urls, and
91
bzr+ssh urls will not work.
92
If no public_url is provided, bzr will use the configured
93
public_url if there is one for the current branch, and
42
branch_url: The publicly visible url for the branch.
43
This must be an http or https url, not a local file
97
bzr register-branch http://foo.com/bzr/fooproject.mine \\
47
bzr register-branch http://foo.com/bzr/fooproject.mine \
98
48
--project fooproject
100
takes_args = ['public_url?']
103
'Launchpad project short name to associate with the branch.',
106
'Launchpad product short name to associate with the branch.',
109
Option('branch-name',
110
'Short name for the branch; '
111
'by default taken from the last component of the url.',
113
Option('branch-title',
114
'One-sentence description of the branch.',
116
Option('branch-description',
117
'Longer description of the purpose or contents of the branch.',
120
"Branch author's email address, if not yourself.",
123
'The bug this branch fixes.',
126
'Prepare the request but don\'t actually send it.')
136
branch_description='',
140
from bzrlib.plugins.launchpad.lp_registration import (
141
BranchRegistrationRequest, BranchBugLinkRequest,
142
DryRunLaunchpadService, LaunchpadService)
143
if public_url is None:
145
b = _mod_branch.Branch.open_containing('.')[0]
146
except NotBranchError:
147
raise BzrCommandError('register-branch requires a public '
148
'branch url - see bzr help register-branch.')
149
public_url = b.get_public_branch()
150
if public_url is None:
151
raise NoPublicBranch(b)
152
if product is not None:
154
trace.note('--product is deprecated; please use --project.')
157
rego = BranchRegistrationRequest(branch_url=public_url,
158
branch_name=branch_name,
159
branch_title=branch_title,
160
branch_description=branch_description,
161
product_name=project,
164
linko = BranchBugLinkRequest(branch_url=public_url,
167
service = LaunchpadService()
168
# This gives back the xmlrpc url that can be used for future
169
# operations on the branch. It's not so useful to print to the
170
# user since they can't do anything with it from a web browser; it
171
# might be nice for the server to tell us about an html url as
174
# Run on service entirely in memory
175
service = DryRunLaunchpadService()
176
service.gather_user_credentials()
179
linko.submit(service)
180
print 'Branch registered.'
50
takes_args = ['branch_url']
52
def run(self, branch_url):
53
from lp_registration import register_interactive
54
register_interactive(branch_url)
182
56
register_command(cmd_register_branch)
185
class cmd_launchpad_open(Command):
186
__doc__ = """Open a Launchpad branch page in your web browser."""
188
aliases = ['lp-open']
191
'Do not actually open the browser. Just say the URL we would '
194
takes_args = ['location?']
196
def _possible_locations(self, location):
197
"""Yield possible external locations for the branch at 'location'."""
200
branch = _mod_branch.Branch.open_containing(location)[0]
201
except NotBranchError:
203
branch_url = branch.get_public_branch()
204
if branch_url is not None:
206
branch_url = branch.get_push_location()
207
if branch_url is not None:
210
def _get_web_url(self, service, location):
211
from bzrlib.plugins.launchpad.lp_registration import (
213
for branch_url in self._possible_locations(location):
215
return service.get_web_url_from_branch_url(branch_url)
216
except (NotLaunchpadBranch, InvalidURL):
218
raise NotLaunchpadBranch(branch_url)
220
def run(self, location=None, dry_run=False):
221
from bzrlib.plugins.launchpad.lp_registration import (
225
web_url = self._get_web_url(LaunchpadService(), location)
226
trace.note('Opening %s in web browser' % web_url)
228
import webbrowser # this import should not be lazy
229
# otherwise bzr.exe lacks this module
230
webbrowser.open(web_url)
232
register_command(cmd_launchpad_open)
235
class cmd_launchpad_login(Command):
236
__doc__ = """Show or set the Launchpad user ID.
238
When communicating with Launchpad, some commands need to know your
239
Launchpad user ID. This command can be used to set or show the
240
user ID that Bazaar will use for such communication.
243
Show the Launchpad ID of the current user::
247
Set the Launchpad ID of the current user to 'bob'::
249
bzr launchpad-login bob
251
aliases = ['lp-login']
252
takes_args = ['name?']
256
"Don't check that the user name is valid."),
259
def run(self, name=None, no_check=False, verbose=False):
260
# This is totally separate from any launchpadlib login system.
261
from bzrlib.plugins.launchpad import account
262
check_account = not no_check
265
username = account.get_lp_login()
268
account.check_lp_login(username)
271
"Launchpad user ID exists and has SSH keys.\n")
272
self.outf.write(username + '\n')
274
self.outf.write('No Launchpad user ID configured.\n')
279
account.check_lp_login(name)
282
"Launchpad user ID exists and has SSH keys.\n")
283
account.set_lp_login(name)
285
self.outf.write("Launchpad user ID set to '%s'.\n" % (name,))
287
register_command(cmd_launchpad_login)
290
# XXX: cmd_launchpad_mirror is untested
291
class cmd_launchpad_mirror(Command):
292
__doc__ = """Ask Launchpad to mirror a branch now."""
294
aliases = ['lp-mirror']
295
takes_args = ['location?']
297
def run(self, location='.'):
298
from bzrlib.plugins.launchpad import lp_api
299
from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
300
branch, _ = _mod_branch.Branch.open_containing(location)
301
service = LaunchpadService()
302
launchpad = lp_api.login(service)
303
lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
304
create_missing=False)
305
lp_branch.lp.requestMirror()
308
register_command(cmd_launchpad_mirror)
311
class cmd_lp_propose_merge(Command):
312
__doc__ = """Propose merging a branch on Launchpad.
314
This will open your usual editor to provide the initial comment. When it
315
has created the proposal, it will open it in your default web browser.
317
The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
318
is not supplied, the remembered submit branch will be used. If no submit
319
branch is remembered, the development focus will be used.
321
By default, the SUBMIT_BRANCH's review team will be requested to review
322
the merge proposal. This can be overriden by specifying --review (-R).
323
The parameter the launchpad account name of the desired reviewer. This
324
may optionally be followed by '=' and the review type. For example:
326
bzr lp-propose-merge --review jrandom --review review-team=qa
328
This will propose a merge, request "jrandom" to perform a review of
329
unspecified type, and request "review-team" to perform a "qa" review.
332
takes_options = [Option('staging',
333
help='Propose the merge on staging.'),
334
Option('message', short_name='m', type=unicode,
335
help='Commit message.'),
337
help='Mark the proposal as approved immediately.'),
338
ListOption('review', short_name='R', type=unicode,
339
help='Requested reviewer and optional type.')]
341
takes_args = ['submit_branch?']
343
aliases = ['lp-submit', 'lp-propose']
345
def run(self, submit_branch=None, review=None, staging=False,
346
message=None, approve=False):
347
from bzrlib.plugins.launchpad import lp_propose
348
tree, branch, relpath = bzrdir.BzrDir.open_containing_tree_or_branch(
354
for review in review:
356
reviews.append(review.split('=', 2))
358
reviews.append((review, ''))
359
if submit_branch is None:
360
submit_branch = branch.get_submit_branch()
361
if submit_branch is None:
364
target = _mod_branch.Branch.open(submit_branch)
365
proposer = lp_propose.Proposer(tree, branch, target, message,
366
reviews, staging, approve=approve)
367
proposer.check_proposal()
368
proposer.create_proposal()
371
register_command(cmd_lp_propose_merge)
374
class cmd_lp_find_proposal(Command):
376
__doc__ = """Find the proposal to merge this revision.
378
Finds the merge proposal(s) that discussed landing the specified revision.
379
This works only if the selected branch was the merge proposal target, and
380
if the merged_revno is recorded for the merge proposal. The proposal(s)
381
are opened in a web browser.
383
Any revision involved in the merge may be specified-- the revision in
384
which the merge was performed, or one of the revisions that was merged.
386
So, to find the merge proposal that reviewed line 1 of README::
388
bzr lp-find-proposal -r annotate:README:1
391
takes_options = ['revision']
393
def run(self, revision=None):
394
from bzrlib.plugins.launchpad import lp_api
396
b = _mod_branch.Branch.open_containing('.')[0]
397
pb = ui.ui_factory.nested_progress_bar()
400
revno = self._find_merged_revno(revision, b, pb)
401
merged = self._find_proposals(revno, b, pb)
403
raise BzrCommandError('No review found.')
404
trace.note('%d proposals(s) found.' % len(merged))
406
webbrowser.open(lp_api.canonical_url(mp))
411
def _find_merged_revno(self, revision, b, pb):
414
pb.update('Finding revision-id')
415
revision_id = revision[0].as_revision_id(b)
416
# a revno spec is necessarily on the mainline.
417
if self._is_revno_spec(revision[0]):
418
merging_revision = revision_id
420
graph = b.repository.get_graph()
421
pb.update('Finding merge')
422
merging_revision = graph.find_lefthand_merger(
423
revision_id, b.last_revision())
424
if merging_revision is None:
425
raise InvalidRevisionSpec(revision[0].user_spec, b)
426
pb.update('Finding revno')
427
return b.revision_id_to_revno(merging_revision)
429
def _find_proposals(self, revno, b, pb):
430
launchpad = lp_api.login(lp_registration.LaunchpadService())
431
pb.update('Finding Launchpad branch')
432
lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
433
create_missing=False)
434
pb.update('Finding proposals')
435
return list(lpb.lp.getMergeProposals(status=['Merged'],
436
merged_revnos=[revno]))
440
def _is_revno_spec(spec):
449
register_command(cmd_lp_find_proposal)
452
def _register_directory():
453
directories.register_lazy('lp:', 'bzrlib.plugins.launchpad.lp_directory',
454
'LaunchpadDirectory',
455
'Launchpad-based directory service',)
456
directories.register_lazy(
457
'debianlp:', 'bzrlib.plugins.launchpad.lp_directory',
458
'LaunchpadDirectory',
459
'debianlp: shortcut')
460
directories.register_lazy(
461
'ubuntu:', 'bzrlib.plugins.launchpad.lp_directory',
462
'LaunchpadDirectory',
465
_register_directory()
467
# This is kept in __init__ so that we don't load lp_api_lite unless the branch
468
# actually matches. That way we can avoid importing extra dependencies like
470
_package_branch = lazy_regex.lazy_compile(
471
r'bazaar.launchpad.net.*?/'
472
r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
473
r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
476
def _get_package_branch_info(url):
477
"""Determine the packaging information for this URL.
479
:return: If this isn't a packaging branch, return None. If it is, return
480
(archive, series, project)
484
m = _package_branch.search(url)
487
archive, series, project, user = m.group('archive', 'series',
489
if series is not None:
490
# series is optional, so the regex includes the extra '/', we don't
491
# want to send that on (it causes Internal Server Errors.)
492
series = series.strip('/')
494
user = user.strip('~/')
495
if user != 'ubuntu-branches':
497
return archive, series, project
500
def _check_is_up_to_date(the_branch):
501
info = _get_package_branch_info(the_branch.base)
504
c = the_branch.get_config()
505
verbosity = c.get_user_option('launchpad.packaging_verbosity')
506
if verbosity is not None:
507
verbosity = verbosity.lower()
508
if verbosity == 'off':
509
trace.mutter('not checking %s because verbosity is turned off'
510
% (the_branch.base,))
512
archive, series, project = info
513
from bzrlib.plugins.launchpad import lp_api_lite
514
latest_pub = lp_api_lite.LatestPublication(archive, series, project)
515
lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
518
def _register_hooks():
519
_mod_branch.Branch.hooks.install_named_hook('open',
520
_check_is_up_to_date, 'package-branch-up-to-date')
525
def load_tests(basic_tests, module, loader):
536
basic_tests.addTest(loader.loadTestsFromModuleNames(
537
["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
541
_launchpad_help = """Integration with Launchpad.net
543
Launchpad.net provides free Bazaar branch hosting with integrated bug and
544
specification tracking.
546
The bzr client (through the plugin called 'launchpad') has special
547
features to communicate with Launchpad:
549
* The launchpad-login command tells Bazaar your Launchpad user name. This
550
is then used by the 'lp:' transport to download your branches using
553
* The 'lp:' transport uses Launchpad as a directory service: for example
554
'lp:bzr' and 'lp:python' refer to the main branches of the relevant
555
projects and may be branched, logged, etc. You can also use the 'lp:'
556
transport to refer to specific branches, e.g. lp:~bzr/bzr/trunk.
558
* The 'lp:' bug tracker alias can expand launchpad bug numbers to their
559
URLs for use with 'bzr commit --fixes', e.g. 'bzr commit --fixes lp:12345'
560
will record a revision property that marks that revision as fixing
561
Launchpad bug 12345. When you push that branch to Launchpad it will
562
automatically be linked to the bug report.
564
* The register-branch command tells Launchpad about the url of a
565
public branch. Launchpad will then mirror the branch, display
566
its contents and allow it to be attached to bugs and other
569
For more information see http://help.launchpad.net/
571
topic_registry.register('launchpad',
573
'Using Bazaar with Launchpad.net')
59
"""Called by bzrlib to fetch tests for this plugin"""
60
from unittest import TestSuite, TestLoader
62
return TestLoader().loadTestsFromModule(test_register)