Merge lp:~cjwatson/bzr/remove-register-branch into lp:bzr

Proposed by Colin Watson on 2017-05-23
Status: Needs review
Proposed branch: lp:~cjwatson/bzr/remove-register-branch
Merge into: lp:bzr
Diff against target: 641 lines (+33/-431)
5 files modified
bzrlib/plugins/launchpad/__init__.py (+1/-8)
bzrlib/plugins/launchpad/cmds.py (+1/-110)
bzrlib/plugins/launchpad/lp_registration.py (+11/-116)
bzrlib/plugins/launchpad/test_register.py (+11/-197)
doc/en/release-notes/bzr-2.8.txt (+9/-0)
To merge this branch: bzr merge lp:~cjwatson/bzr/remove-register-branch
Reviewer Review Type Date Requested Status
bzr-core 2017-05-23 Pending
Review via email: mp+324472@code.launchpad.net

Commit Message

Remove `bzr register-branch`, since it has not worked for a long time.

Description of the Change

The `bzr register-branch` command has been broken for a long time with no bug reports:

 * It relies on password authentication to Launchpad, which was deprecated in favour of SSO many years ago and disabled entirely in January 2012.
 * Its entire purpose is to create mirrored branches, which are generally deprecated in favour of code imports, and the mirroring of those branches no longer works because the relevant systems haven't had firewall-level access to the outside world for quite some time.

There's really not much point trying to resurrect this command and convert it to create code imports with modern authentication. It should just be removed.

To post a comment you must log in.

Unmerged revisions

6623. By Colin Watson on 2017-05-23

Remove `bzr register-branch`, since it has not worked for a long time.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/plugins/launchpad/__init__.py'
2--- bzrlib/plugins/launchpad/__init__.py 2012-03-10 19:11:06 +0000
3+++ bzrlib/plugins/launchpad/__init__.py 2017-05-23 12:36:28 +0000
4@@ -1,4 +1,4 @@
5-# Copyright (C) 2006-2011 Canonical Ltd
6+# Copyright (C) 2006-2017 Canonical Ltd
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10@@ -30,7 +30,6 @@
11 launchpad-login: Show or set the Launchpad user ID
12 launchpad-open: Open a Launchpad branch page in your web browser
13 lp-propose-merge: Propose merging a branch on Launchpad
14- register-branch: Register a branch with launchpad.net
15 launchpad-mirror: Ask Launchpad to mirror a branch now
16
17 """
18@@ -57,7 +56,6 @@
19 from bzrlib.help_topics import topic_registry
20
21 for klsname, aliases in [
22- ("cmd_register_branch", []),
23 ("cmd_launchpad_open", ["lp-open"]),
24 ("cmd_launchpad_login", ["lp-login"]),
25 ("cmd_launchpad_mirror", ["lp-mirror"]),
26@@ -177,11 +175,6 @@
27 Launchpad bug 12345. When you push that branch to Launchpad it will
28 automatically be linked to the bug report.
29
30- * The register-branch command tells Launchpad about the url of a
31- public branch. Launchpad will then mirror the branch, display
32- its contents and allow it to be attached to bugs and other
33- objects.
34-
35 For more information see http://help.launchpad.net/
36 """
37 topic_registry.register('launchpad',
38
39=== modified file 'bzrlib/plugins/launchpad/cmds.py'
40--- bzrlib/plugins/launchpad/cmds.py 2012-10-22 15:51:31 +0000
41+++ bzrlib/plugins/launchpad/cmds.py 2017-05-23 12:36:28 +0000
42@@ -1,4 +1,4 @@
43-# Copyright (C) 2006-2012 Canonical Ltd
44+# Copyright (C) 2006-2017 Canonical Ltd
45 #
46 # This program is free software; you can redistribute it and/or modify
47 # it under the terms of the GNU General Public License as published by
48@@ -29,7 +29,6 @@
49 from bzrlib.errors import (
50 BzrCommandError,
51 InvalidURL,
52- NoPublicBranch,
53 NotBranchError,
54 )
55 from bzrlib.i18n import gettext
56@@ -39,114 +38,6 @@
57 )
58
59
60-class cmd_register_branch(Command):
61- __doc__ = """Register a branch with launchpad.net.
62-
63- This command lists a bzr branch in the directory of branches on
64- launchpad.net. Registration allows the branch to be associated with
65- bugs or specifications.
66-
67- Before using this command you must register the project to which the
68- branch belongs, and create an account for yourself on launchpad.net.
69-
70- arguments:
71- public_url: The publicly visible url for the branch to register.
72- This must be an http or https url (which Launchpad can read
73- from to access the branch). Local file urls, SFTP urls, and
74- bzr+ssh urls will not work.
75- If no public_url is provided, bzr will use the configured
76- public_url if there is one for the current branch, and
77- otherwise error.
78-
79- example:
80- bzr register-branch http://foo.com/bzr/fooproject.mine \\
81- --project fooproject
82- """
83- takes_args = ['public_url?']
84- takes_options = [
85- Option('project',
86- 'Launchpad project short name to associate with the branch.',
87- unicode),
88- Option('product',
89- 'Launchpad product short name to associate with the branch.',
90- unicode,
91- hidden=True),
92- Option('branch-name',
93- 'Short name for the branch; '
94- 'by default taken from the last component of the url.',
95- unicode),
96- Option('branch-title',
97- 'One-sentence description of the branch.',
98- unicode),
99- Option('branch-description',
100- 'Longer description of the purpose or contents of the branch.',
101- unicode),
102- Option('author',
103- "Branch author's email address, if not yourself.",
104- unicode),
105- Option('link-bug',
106- 'The bug this branch fixes.',
107- int),
108- Option('dry-run',
109- 'Prepare the request but don\'t actually send it.')
110- ]
111-
112-
113- def run(self,
114- public_url=None,
115- project='',
116- product=None,
117- branch_name='',
118- branch_title='',
119- branch_description='',
120- author='',
121- link_bug=None,
122- dry_run=False):
123- from bzrlib.plugins.launchpad.lp_registration import (
124- BranchRegistrationRequest, BranchBugLinkRequest,
125- DryRunLaunchpadService, LaunchpadService)
126- if public_url is None:
127- try:
128- b = _mod_branch.Branch.open_containing('.')[0]
129- except NotBranchError:
130- raise BzrCommandError(gettext(
131- 'register-branch requires a public '
132- 'branch url - see bzr help register-branch.'))
133- public_url = b.get_public_branch()
134- if public_url is None:
135- raise NoPublicBranch(b)
136- if product is not None:
137- project = product
138- trace.note(gettext(
139- '--product is deprecated; please use --project.'))
140-
141-
142- rego = BranchRegistrationRequest(branch_url=public_url,
143- branch_name=branch_name,
144- branch_title=branch_title,
145- branch_description=branch_description,
146- product_name=project,
147- author_email=author,
148- )
149- linko = BranchBugLinkRequest(branch_url=public_url,
150- bug_id=link_bug)
151- if not dry_run:
152- service = LaunchpadService()
153- # This gives back the xmlrpc url that can be used for future
154- # operations on the branch. It's not so useful to print to the
155- # user since they can't do anything with it from a web browser; it
156- # might be nice for the server to tell us about an html url as
157- # well.
158- else:
159- # Run on service entirely in memory
160- service = DryRunLaunchpadService()
161- service.gather_user_credentials()
162- rego.submit(service)
163- if link_bug:
164- linko.submit(service)
165- self.outf.write('Branch registered.\n')
166-
167-
168 class cmd_launchpad_open(Command):
169 __doc__ = """Open a Launchpad branch page in your web browser."""
170
171
172=== modified file 'bzrlib/plugins/launchpad/lp_registration.py'
173--- bzrlib/plugins/launchpad/lp_registration.py 2012-01-20 13:07:10 +0000
174+++ bzrlib/plugins/launchpad/lp_registration.py 2017-05-23 12:36:28 +0000
175@@ -1,4 +1,4 @@
176-# Copyright (C) 2006-2011 Canonical Ltd
177+# Copyright (C) 2006-2017 Canonical Ltd
178 #
179 # This program is free software; you can redistribute it and/or modify
180 # it under the terms of the GNU General Public License as published by
181@@ -144,51 +144,13 @@
182 raise errors.InvalidURL(path=url)
183 return cls(lp_instance=lp_instance, **kwargs)
184
185- def get_proxy(self, authenticated):
186+ def get_proxy(self):
187 """Return the proxy for XMLRPC requests."""
188- if authenticated:
189- # auth info must be in url
190- # TODO: if there's no registrant email perhaps we should
191- # just connect anonymously?
192- scheme, hostinfo, path = urlsplit(self.service_url)[:3]
193- if '@' in hostinfo:
194- raise AssertionError(hostinfo)
195- if self.registrant_email is None:
196- raise AssertionError()
197- if self.registrant_password is None:
198- raise AssertionError()
199- # TODO: perhaps fully quote the password to make it very slightly
200- # obscured
201- # TODO: can we perhaps add extra Authorization headers
202- # directly to the request, rather than putting this into
203- # the url? perhaps a bit more secure against accidentally
204- # revealing it. std66 s3.2.1 discourages putting the
205- # password in the url.
206- hostinfo = '%s:%s@%s' % (urlutils.quote(self.registrant_email),
207- urlutils.quote(self.registrant_password),
208- hostinfo)
209- url = urlunsplit((scheme, hostinfo, path, '', ''))
210- else:
211- url = self.service_url
212+ url = self.service_url
213 return xmlrpclib.ServerProxy(url, transport=self.transport)
214
215- def gather_user_credentials(self):
216- """Get the password from the user."""
217- the_config = config.GlobalConfig()
218- self.registrant_email = the_config.user_email()
219- if self.registrant_password is None:
220- auth = config.AuthenticationConfig()
221- scheme, hostinfo = urlsplit(self.service_url)[:2]
222- prompt = 'launchpad.net password for %s: ' % \
223- self.registrant_email
224- # We will reuse http[s] credentials if we can, prompt user
225- # otherwise
226- self.registrant_password = auth.get_password(scheme, hostinfo,
227- self.registrant_email,
228- prompt=prompt)
229-
230- def send_request(self, method_name, method_params, authenticated):
231- proxy = self.get_proxy(authenticated)
232+ def send_request(self, method_name, method_params):
233+ proxy = self.get_proxy()
234 method = getattr(proxy, method_name)
235 try:
236 result = method(*method_params)
237@@ -255,7 +217,6 @@
238
239 # Set this to the XMLRPC method name.
240 _methodname = None
241- _authenticated = True
242
243 def _request_params(self):
244 """Return the arguments to pass to the method"""
245@@ -264,88 +225,22 @@
246 def submit(self, service):
247 """Submit request to Launchpad XMLRPC server.
248
249- :param service: LaunchpadService indicating where to send
250- the request and the authentication credentials.
251+ :param service: LaunchpadService indicating where to send the request.
252 """
253- return service.send_request(self._methodname, self._request_params(),
254- self._authenticated)
255+ return service.send_request(self._methodname, self._request_params())
256
257
258 class DryRunLaunchpadService(LaunchpadService):
259- """Service that just absorbs requests without sending to server.
260-
261- The dummy service does not need authentication.
262- """
263-
264- def send_request(self, method_name, method_params, authenticated):
265- pass
266-
267- def gather_user_credentials(self):
268- pass
269-
270-
271-class BranchRegistrationRequest(BaseRequest):
272- """Request to tell Launchpad about a bzr branch."""
273-
274- _methodname = 'register_branch'
275-
276- def __init__(self, branch_url,
277- branch_name='',
278- branch_title='',
279- branch_description='',
280- author_email='',
281- product_name='',
282- ):
283- if not branch_url:
284- raise errors.InvalidURL(branch_url, "You need to specify a non-empty branch URL.")
285- self.branch_url = branch_url
286- if branch_name:
287- self.branch_name = branch_name
288- else:
289- self.branch_name = self._find_default_branch_name(self.branch_url)
290- self.branch_title = branch_title
291- self.branch_description = branch_description
292- self.author_email = author_email
293- self.product_name = product_name
294-
295- def _request_params(self):
296- """Return xmlrpc request parameters"""
297- # This must match the parameter tuple expected by Launchpad for this
298- # method
299- return (self.branch_url,
300- self.branch_name,
301- self.branch_title,
302- self.branch_description,
303- self.author_email,
304- self.product_name,
305- )
306-
307- def _find_default_branch_name(self, branch_url):
308- i = branch_url.rfind('/')
309- return branch_url[i+1:]
310-
311-
312-class BranchBugLinkRequest(BaseRequest):
313- """Request to link a bzr branch in Launchpad to a bug."""
314-
315- _methodname = 'link_branch_to_bug'
316-
317- def __init__(self, branch_url, bug_id):
318- self.bug_id = bug_id
319- self.branch_url = branch_url
320-
321- def _request_params(self):
322- """Return xmlrpc request parameters"""
323- # This must match the parameter tuple expected by Launchpad for this
324- # method
325- return (self.branch_url, self.bug_id, '')
326+ """Service that just absorbs requests without sending to server."""
327+
328+ def send_request(self, method_name, method_params):
329+ pass
330
331
332 class ResolveLaunchpadPathRequest(BaseRequest):
333 """Request to resolve the path component of an lp: URL."""
334
335 _methodname = 'resolve_lp_path'
336- _authenticated = False
337
338 def __init__(self, path):
339 if not path:
340
341=== modified file 'bzrlib/plugins/launchpad/test_register.py'
342--- bzrlib/plugins/launchpad/test_register.py 2016-02-01 18:06:32 +0000
343+++ bzrlib/plugins/launchpad/test_register.py 2017-05-23 12:36:28 +0000
344@@ -1,4 +1,4 @@
345-# Copyright (C) 2006-2012, 2016 Canonical Ltd
346+# Copyright (C) 2006-2012, 2016-2017 Canonical Ltd
347 #
348 # This program is free software; you can redistribute it and/or modify
349 # it under the terms of the GNU General Public License as published by
350@@ -16,21 +16,13 @@
351
352 import base64
353 from StringIO import StringIO
354-import urlparse
355 import xmlrpclib
356
357-from bzrlib import (
358- config,
359- tests,
360- ui,
361- )
362 from bzrlib.tests import TestCaseWithTransport
363
364 # local import
365 from bzrlib.plugins.launchpad.lp_registration import (
366 BaseRequest,
367- BranchBugLinkRequest,
368- BranchRegistrationRequest,
369 ResolveLaunchpadPathRequest,
370 LaunchpadService,
371 )
372@@ -41,11 +33,6 @@
373 # the results passed in. Not sure how to get the transport object back out to
374 # validate that its OK - may not be necessary.
375
376-# TODO: Add test for (and implement) other command-line options to set
377-# project, author_email, description.
378-
379-# TODO: project_id is not properly handled -- must be passed in rpc or path.
380-
381 class InstrumentedXMLRPCConnection(object):
382 """Stands in place of an http connection for the purposes of testing"""
383
384@@ -102,25 +89,15 @@
385 # Python 2.5's xmlrpclib looks for this.
386 _use_datetime = False
387
388- def __init__(self, testcase, expect_auth):
389+ def __init__(self, testcase):
390 self.testcase = testcase
391- self.expect_auth = expect_auth
392 self._connection = (None, None)
393
394 def make_connection(self, host):
395 host, http_headers, x509 = self.get_host_info(host)
396 test = self.testcase
397 self.connected_host = host
398- if self.expect_auth:
399- auth_hdrs = [v for k,v in http_headers if k == 'Authorization']
400- if len(auth_hdrs) != 1:
401- raise AssertionError("multiple auth headers: %r"
402- % (auth_hdrs,))
403- authinfo = auth_hdrs[0]
404- expected_auth = 'testuser@launchpad.net:testpassword'
405- test.assertEqual(authinfo,
406- 'Basic ' + base64.encodestring(expected_auth).strip())
407- elif http_headers:
408+ if http_headers:
409 raise AssertionError()
410 return InstrumentedXMLRPCConnection(test)
411
412@@ -146,80 +123,22 @@
413
414 class MockLaunchpadService(LaunchpadService):
415
416- def send_request(self, method_name, method_params, authenticated):
417+ def send_request(self, method_name, method_params):
418 """Stash away the method details rather than sending them to a real server"""
419 self.called_method_name = method_name
420 self.called_method_params = method_params
421- self.called_authenticated = authenticated
422-
423-
424-class TestBranchRegistration(TestCaseWithTransport):
425+
426+
427+class TestResolveLaunchpadPathRequest(TestCaseWithTransport):
428
429 def setUp(self):
430- super(TestBranchRegistration, self).setUp()
431+ super(TestResolveLaunchpadPathRequest, self).setUp()
432 # make sure we have a reproducible standard environment
433 self.overrideEnv('BZR_LP_XMLRPC_URL', None)
434
435- def test_register_help(self):
436- """register-branch accepts --help"""
437- out, err = self.run_bzr(['register-branch', '--help'])
438- self.assertContainsRe(out, r'Register a branch')
439-
440- def test_register_no_url_no_branch(self):
441- """register-branch command requires parameters"""
442- self.make_repository('.')
443- self.run_bzr_error(
444- ['register-branch requires a public branch url - '
445- 'see bzr help register-branch'],
446- 'register-branch')
447-
448- def test_register_no_url_in_published_branch_no_error(self):
449- b = self.make_branch('.')
450- b.set_public_branch('http://test-server.com/bzr/branch')
451- out, err = self.run_bzr(['register-branch', '--dry-run'])
452- self.assertEqual('Branch registered.\n', out)
453- self.assertEqual('', err)
454-
455- def test_register_no_url_in_unpublished_branch_errors(self):
456- b = self.make_branch('.')
457- out, err = self.run_bzr_error(['no public branch'],
458- ['register-branch', '--dry-run'])
459- self.assertEqual('', out)
460-
461- def test_register_dry_run(self):
462- out, err = self.run_bzr(['register-branch',
463- 'http://test-server.com/bzr/branch',
464- '--dry-run'])
465- self.assertEqual(out, 'Branch registered.\n')
466-
467 def test_onto_transport(self):
468- """How the request is sent by transmitting across a mock Transport"""
469- # use a real transport, but intercept at the http/xml layer
470- transport = InstrumentedXMLRPCTransport(self, expect_auth=True)
471- service = LaunchpadService(transport)
472- service.registrant_email = 'testuser@launchpad.net'
473- service.registrant_password = 'testpassword'
474- rego = BranchRegistrationRequest('http://test-server.com/bzr/branch',
475- 'branch-id',
476- 'my test branch',
477- 'description',
478- 'author@launchpad.net',
479- 'product')
480- rego.submit(service)
481- self.assertEqual(transport.connected_host, 'xmlrpc.launchpad.net')
482- self.assertEqual(len(transport.sent_params), 6)
483- self.assertEqual(transport.sent_params,
484- ('http://test-server.com/bzr/branch', # branch_url
485- 'branch-id', # branch_name
486- 'my test branch', # branch_title
487- 'description',
488- 'author@launchpad.net',
489- 'product'))
490- self.assertTrue(transport.got_request)
491-
492- def test_onto_transport_unauthenticated(self):
493- """An unauthenticated request is transmitted across a mock Transport"""
494- transport = InstrumentedXMLRPCTransport(self, expect_auth=False)
495+ """A request is transmitted across a mock Transport"""
496+ transport = InstrumentedXMLRPCTransport(self)
497 service = LaunchpadService(transport)
498 resolve = ResolveLaunchpadPathRequest('bzr')
499 resolve.submit(service)
500@@ -243,59 +162,12 @@
501 self.assertEqual(service.called_method_name, 'dummy_request')
502 self.assertEqual(service.called_method_params, (42,))
503
504- def test_mock_server_registration(self):
505- """Send registration to mock server"""
506- test_case = self
507- class MockRegistrationService(MockLaunchpadService):
508- def send_request(self, method_name, method_params, authenticated):
509- test_case.assertEqual(method_name, "register_branch")
510- test_case.assertEqual(list(method_params),
511- ['url', 'name', 'title', 'description', 'email', 'name'])
512- test_case.assertEqual(authenticated, True)
513- return 'result'
514- service = MockRegistrationService()
515- rego = BranchRegistrationRequest('url', 'name', 'title',
516- 'description', 'email', 'name')
517- result = rego.submit(service)
518- self.assertEqual(result, 'result')
519-
520- def test_mock_server_registration_with_defaults(self):
521- """Send registration to mock server"""
522- test_case = self
523- class MockRegistrationService(MockLaunchpadService):
524- def send_request(self, method_name, method_params, authenticated):
525- test_case.assertEqual(method_name, "register_branch")
526- test_case.assertEqual(list(method_params),
527- ['http://server/branch', 'branch', '', '', '', ''])
528- test_case.assertEqual(authenticated, True)
529- return 'result'
530- service = MockRegistrationService()
531- rego = BranchRegistrationRequest('http://server/branch')
532- result = rego.submit(service)
533- self.assertEqual(result, 'result')
534-
535- def test_mock_bug_branch_link(self):
536- """Send bug-branch link to mock server"""
537- test_case = self
538- class MockService(MockLaunchpadService):
539- def send_request(self, method_name, method_params, authenticated):
540- test_case.assertEqual(method_name, "link_branch_to_bug")
541- test_case.assertEqual(list(method_params),
542- ['http://server/branch', 1234, ''])
543- test_case.assertEqual(authenticated, True)
544- return 'http://launchpad.net/bug/1234'
545- service = MockService()
546- rego = BranchBugLinkRequest('http://server/branch', 1234)
547- result = rego.submit(service)
548- self.assertEqual(result, 'http://launchpad.net/bug/1234')
549-
550 def test_mock_resolve_lp_url(self):
551 test_case = self
552 class MockService(MockLaunchpadService):
553- def send_request(self, method_name, method_params, authenticated):
554+ def send_request(self, method_name, method_params):
555 test_case.assertEqual(method_name, "resolve_lp_path")
556 test_case.assertEqual(list(method_params), ['bzr'])
557- test_case.assertEqual(authenticated, False)
558 return dict(urls=[
559 'bzr+ssh://bazaar.launchpad.net~bzr/bzr/trunk',
560 'sftp://bazaar.launchpad.net~bzr/bzr/trunk',
561@@ -310,61 +182,3 @@
562 'sftp://bazaar.launchpad.net~bzr/bzr/trunk',
563 'bzr+http://bazaar.launchpad.net~bzr/bzr/trunk',
564 'http://bazaar.launchpad.net~bzr/bzr/trunk'])
565-
566-
567-class TestGatherUserCredentials(tests.TestCaseInTempDir):
568-
569- def setUp(self):
570- super(TestGatherUserCredentials, self).setUp()
571- # make sure we have a reproducible standard environment
572- self.overrideEnv('BZR_LP_XMLRPC_URL', None)
573-
574- def test_gather_user_credentials_has_password(self):
575- service = LaunchpadService()
576- service.registrant_password = 'mypassword'
577- # This should be a basic no-op, since we already have the password
578- service.gather_user_credentials()
579- self.assertEqual('mypassword', service.registrant_password)
580-
581- def test_gather_user_credentials_from_auth_conf(self):
582- auth_path = config.authentication_config_filename()
583- service = LaunchpadService()
584- g_conf = config.GlobalStack()
585- g_conf.set('email', 'Test User <test@user.com>')
586- g_conf.store.save()
587- # FIXME: auth_path base dir exists only because bazaar.conf has just
588- # been saved, brittle... -- vila 20120731
589- f = open(auth_path, 'wb')
590- try:
591- scheme, hostinfo = urlparse.urlsplit(service.service_url)[:2]
592- f.write('[section]\n'
593- 'scheme=%s\n'
594- 'host=%s\n'
595- 'user=test@user.com\n'
596- 'password=testpass\n'
597- % (scheme, hostinfo))
598- finally:
599- f.close()
600- self.assertIs(None, service.registrant_password)
601- service.gather_user_credentials()
602- self.assertEqual('test@user.com', service.registrant_email)
603- self.assertEqual('testpass', service.registrant_password)
604-
605- def test_gather_user_credentials_prompts(self):
606- service = LaunchpadService()
607- self.assertIs(None, service.registrant_password)
608- g_conf = config.GlobalStack()
609- g_conf.set('email', 'Test User <test@user.com>')
610- g_conf.store.save()
611- stdout = tests.StringIOWrapper()
612- stderr = tests.StringIOWrapper()
613- ui.ui_factory = tests.TestUIFactory(stdin='userpass\n',
614- stdout=stdout, stderr=stderr)
615- self.assertIs(None, service.registrant_password)
616- service.gather_user_credentials()
617- self.assertEqual('test@user.com', service.registrant_email)
618- self.assertEqual('userpass', service.registrant_password)
619- self.assertEqual('', stdout.getvalue())
620- self.assertContainsRe(stderr.getvalue(),
621- 'launchpad.net password for test@user\\.com')
622-
623
624=== modified file 'doc/en/release-notes/bzr-2.8.txt'
625--- doc/en/release-notes/bzr-2.8.txt 2017-03-17 10:39:02 +0000
626+++ doc/en/release-notes/bzr-2.8.txt 2017-05-23 12:36:28 +0000
627@@ -15,6 +15,15 @@
628
629 .. These may require users to change the way they use Bazaar.
630
631+ * The ``bzr register-branch`` command from the Launchpad plugin has been
632+ removed, because it has not worked for at least five years: it relies on
633+ password authentication rather than SSO, the relevant systems no longer
634+ have firewall-level access to the outside world, and in general the
635+ Mirrored branch type is deprecated. Either just push the branch to
636+ Launchpad or use code imports instead
637+ (https://help.launchpad.net/VcsImports).
638+ (Colin Watson, #254567, #483689)
639+
640 New Features
641 ************
642