Merge lp:~jelmer/launchpad/bzr-code-imports into lp:launchpad

Proposed by Jelmer Vernooij
Status: Superseded
Proposed branch: lp:~jelmer/launchpad/bzr-code-imports
Merge into: lp:launchpad
Prerequisite: lp:~jelmer/launchpad/bzr-2.4b4
Diff against target: 2835 lines (+1213/-739)
26 files modified
lib/canonical/config/schema-lazr.conf (+5/-0)
lib/lp/code/bzr.py (+10/-6)
lib/lp/code/mail/codeimport.py (+3/-1)
lib/lp/code/model/codeimport.py (+8/-3)
lib/lp/code/model/codeimportevent.py (+2/-1)
lib/lp/code/model/tests/test_branchjob.py (+1/-1)
lib/lp/code/model/tests/test_codeimport.py (+44/-0)
lib/lp/codehosting/__init__.py (+10/-12)
lib/lp/codehosting/codeimport/tests/servers.py (+176/-23)
lib/lp/codehosting/codeimport/tests/test_worker.py (+75/-11)
lib/lp/codehosting/codeimport/tests/test_workermonitor.py (+26/-4)
lib/lp/codehosting/codeimport/worker.py (+97/-6)
lib/lp/codehosting/puller/tests/__init__.py (+6/-62)
lib/lp/codehosting/puller/tests/test_errors.py (+3/-5)
lib/lp/codehosting/puller/tests/test_worker.py (+18/-214)
lib/lp/codehosting/puller/tests/test_worker_formats.py (+7/-3)
lib/lp/codehosting/puller/worker.py (+239/-104)
lib/lp/codehosting/safe_open.py (+230/-0)
lib/lp/codehosting/tests/test_safe_open.py (+223/-0)
lib/lp/codehosting/vfs/__init__.py (+0/-2)
lib/lp/codehosting/vfs/branchfs.py (+0/-257)
lib/lp/registry/browser/productseries.py (+9/-17)
lib/lp/testing/factory.py (+9/-3)
lib/lp/translations/scripts/translations_to_branch.py (+4/-1)
scripts/code-import-worker.py (+7/-2)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~jelmer/launchpad/bzr-code-imports
Reviewer Review Type Date Requested Status
Gavin Panella (community) Abstain
Robert Collins (community) Needs Information
Michael Hudson-Doyle Pending
Review via email: mp+68232@code.launchpad.net

This proposal supersedes a proposal from 2011-07-18.

This proposal has been superseded by a proposal from 2011-08-07.

Description of the change

Add support for importing code from Bazaar branches.

At the moment mirrors of remote Bazaar branches are created with completely different infrastructure as the code imports. This is confusing for users (bug 611837) and duplicates a lot of code. Several features are only available for code imports (bug 362622, bug 193607, bug 193607, bug 371484) and vice versa. Having shared infrastructure would also make it easier to fix several open bugs that affect both code imports and code mirrors (bug 519159, bug 136939)

Code imports are a bit heavier than mirrors at the moment, as they run on a separate machine and require an extra copy of the branch that is imported.

This branch only adds backend support for Bazaar branches, it does not yet add a UI which can add code imports of this kind nor does it migrate any of the existing code mirrors.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal

I don't know the ins and outs of bzrlib or codehosting - for that I
guess that's why you've asked Michael to review - but the rest looks
good.

[1]

+ def test_partial(self):
+ # Skip tests of partial tests, as they are disabled for native imports
+ # at the moment.
+ return

You could use TestCase.skip() here:

    def test_partial(self):
        self.skip("Disabled for native imports at the moment.")

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

> I don't know the ins and outs of bzrlib or codehosting - for that I
> guess that's why you've asked Michael to review - but the rest looks
> good.
Thanks :)

I'm pretty confident about this code change (especially since nothing actually triggers the new code yet), but I'd like to get some feedback from more people (Michael, Jono?, Rob?) on to confirm this is the right direction to go in.

> + def test_partial(self):
> + # Skip tests of partial tests, as they are disabled for native
> imports
> + # at the moment.
> + return
>
> You could use TestCase.skip() here:
>
> def test_partial(self):
> self.skip("Disabled for native imports at the moment.")
I looked for things that raised SkipTest or TestSkipped and couldn't find any. I didn't know about TestCase.skip - thanks, fixed.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : Posted in a previous version of this proposal

Some random comments: it would have been nice to see the things you had to do wrt the bzr upgrade

The fact that you put this line in suggests that the tests aren't very well isolated:

        branch.get_config().set_user_option("create_signatures", "never")

Does something need to inherit from bzrlib's TestCase? It's all a complete tangle though.

Is simply prohibiting branch references the right thing to do? The branch puller goes to some lengths to support them safely -- being able to dump all that code would surely be very nice. Relatedly, and on thinking about it I think this is a bit more serious, I think you might need to be careful about stacking -- it's contrived but you might be able to construct a branch that was stacked on some DC-internal branch and have that be imported so you can grab it.

In terms of overall direction, I'm all for removing duplication. I think the UI will require some effort (e.g. do we want to count bzr mirrors as "imported branches" on the code.launchpad.net frontpage?) but engineering wise, this looks fine, modulo the above comments.

review: Approve
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : Posted in a previous version of this proposal

"Some random comments: it would have been nice to see the things you had to do wrt the bzr upgrade " ... in a separate branch, I meant to say.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

On 06/24/2011 05:35 AM, Michael Hudson-Doyle wrote:
> Review: Approve
> Some random comments: it would have been nice to see the things you had to do wrt the bzr upgrade
This branch has two prerequisite branches, but I can only set one in
Launchpad. Hopefully the diff will get smaller when the update to the
newer bzr lands on lp:launchpad...
>
> The fact that you put this line in suggests that the tests aren't very well isolated:
>
> branch.get_config().set_user_option("create_signatures", "never")
>
> Does something need to inherit from bzrlib's TestCase? It's all a complete tangle though.
Perhaps; bzr's TestCase does a lot though, and I'm kindof worried that
mixing it in will break other things. It should do more than just this
ad-hoc override though. Perhaps reset the global bzr configuration in setUp?

>
> Is simply prohibiting branch references the right thing to do? The branch puller goes to some lengths to support them safely -- being able to dump all that code would surely be very nice. Relatedly, and on thinking about it I think this is a bit more serious, I think you might need to be careful about stacking -- it's contrived but you might be able to construct a branch that was stacked on some DC-internal branch and have that be imported so you can grab it.
Stacking is a very good point, and one that I had not considered -
thanks. I should probably also have another, closer, look at the branch
puller to see what it does and why.

For branch references, the easiest thing to do for the moment seemed to
be to just refuse to mirror them. If code mirrors support branch
references at the moment, we should keep that support.

>
> In terms of overall direction, I'm all for removing duplication. I think the UI will require some effort (e.g. do we want to count bzr mirrors as "imported branches" on the code.launchpad.net frontpage?) but engineering wise, this looks fine, modulo the above comments.
Thanks for having a look at this. I'm only just getting into this code
and am not very familiar with it yet.

Cheers,

Jelmer

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote : Posted in a previous version of this proposal

On Fri, 24 Jun 2011 19:55:45 +0200, Jelmer Vernooij <email address hidden> wrote:
> On 06/24/2011 05:35 AM, Michael Hudson-Doyle wrote:
> > Review: Approve
> > Some random comments: it would have been nice to see the things you had to do wrt the bzr upgrade
> This branch has two prerequisite branches, but I can only set one in
> Launchpad. Hopefully the diff will get smaller when the update to the
> newer bzr lands on lp:launchpad...

Ah! I guess you could have created a branch with both of the
prerequisites merged in, but that's getting pretty tedious...

> >
> > The fact that you put this line in suggests that the tests aren't very well isolated:
> >
> > branch.get_config().set_user_option("create_signatures", "never")
> >
> > Does something need to inherit from bzrlib's TestCase? It's all a complete tangle though.
> Perhaps; bzr's TestCase does a lot though, and I'm kindof worried that
> mixing it in will break other things.

I'd be surprised if it broke other stuff on past experience, but you
might be right.

> It should do more than just this ad-hoc override though. Perhaps reset
> the global bzr configuration in setUp?

I guess what you want is a test fixture that just does the environment
isolation in bzrlib you can reuse here... but yes, doing something more
generic than just clearing create_signatures would be better, I think.

> >
> > Is simply prohibiting branch references the right thing to do? The branch puller goes to some lengths to support them safely -- being able to dump all that code would surely be very nice. Relatedly, and on thinking about it I think this is a bit more serious, I think you might need to be careful about stacking -- it's contrived but you might be able to construct a branch that was stacked on some DC-internal branch and have that be imported so you can grab it.
> Stacking is a very good point, and one that I had not considered -
> thanks. I should probably also have another, closer, look at the branch
> puller to see what it does and why.

For reasons I should probably be ashamed of, there seem to be two
implementations of 'safe opening' in Launchpad; one is around
lp.codehosting.bzrutils.safe_open and the other around
lp.codehosting.puller.worker.BranchMirrorer.

(looking at worker.py makes me realize how much code we can delete once
this branch is done, never mind how much can be deleted when the puller
is gone completely).

> For branch references, the easiest thing to do for the moment seemed to
> be to just refuse to mirror them. If code mirrors support branch
> references at the moment, we should keep that support.

Yeah, they do.

> >
> > In terms of overall direction, I'm all for removing duplication. I think the UI will require some effort (e.g. do we want to count bzr mirrors as "imported branches" on the code.launchpad.net frontpage?) but engineering wise, this looks fine, modulo the above comments.
> Thanks for having a look at this. I'm only just getting into this code
> and am not very familiar with it yet.

You seem to be doing fine :)

Cheers,
mwh

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I don't see anything to do with stacking safety and/or supporting branch references in here yet -- is this really ready for review again?

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

> I don't see anything to do with stacking safety and/or supporting branch
> references in here yet -- is this really ready for review again?
No, it indeed isn't - I was merely trying to generate a proper diff. Sorry.

Revision history for this message
Robert Collins (lifeless) wrote :

Does this permit imports from lp hosted branches? if so, thats likely to permit bypassing of private branch ACLs - can you check that that isn't the please?

review: Needs Information
Revision history for this message
Gavin Panella (allenap) :
review: Abstain
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On Sun, 2011-08-07 at 22:41 +0000, Robert Collins wrote:
> Review: Needs Information
> Does this permit imports from lp hosted branches? if so, thats likely to permit bypassing of private branch ACLs - can you check that that isn't the please?
It does not allow imports from lp hosted branches, importing of branch
references to launchpad branches or even importing of branches stacked
on Launchpad branches.

It also limits imports to a specific (whitelist) set of schemes, which
excludes bzr+ssh://, sftp:// and file://.

This is the same policy as is currently in place for code mirrors at the
moment.

Cheers,

Jelmer

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/config/schema-lazr.conf'
2--- lib/canonical/config/schema-lazr.conf 2011-07-27 20:14:40 +0000
3+++ lib/canonical/config/schema-lazr.conf 2011-08-07 20:42:25 +0000
4@@ -488,6 +488,11 @@
5 # datatype: integer
6 default_interval_cvs: 43200
7
8+# The default value of the update interval of a code import from
9+# Bazaar, in seconds.
10+# datatype: integer
11+default_interval_bzr: 21600
12+
13 # Where the tarballs of foreign branches are uploaded for storage.
14 # datatype: string
15 foreign_tree_store: sftp://hoover@escudero/srv/importd/sources/
16
17=== modified file 'lib/lp/code/bzr.py'
18--- lib/lp/code/bzr.py 2011-06-02 19:27:36 +0000
19+++ lib/lp/code/bzr.py 2011-08-07 20:42:25 +0000
20@@ -20,34 +20,38 @@
21
22 from bzrlib.branch import (
23 BranchReferenceFormat,
24- BzrBranchFormat4,
25 BzrBranchFormat5,
26 BzrBranchFormat6,
27 BzrBranchFormat7,
28 )
29 from bzrlib.bzrdir import (
30- BzrDirFormat4,
31- BzrDirFormat5,
32- BzrDirFormat6,
33 BzrDirMetaFormat1,
34 )
35 from bzrlib.plugins.loom.branch import (
36 BzrBranchLoomFormat1,
37 BzrBranchLoomFormat6,
38 )
39+from bzrlib.plugins.weave_fmt.branch import (
40+ BzrBranchFormat4,
41+ )
42+from bzrlib.plugins.weave_fmt.bzrdir import (
43+ BzrDirFormat4,
44+ BzrDirFormat5,
45+ BzrDirFormat6,
46+ )
47 from bzrlib.repofmt.groupcompress_repo import RepositoryFormat2a
48 from bzrlib.repofmt.knitrepo import (
49 RepositoryFormatKnit1,
50 RepositoryFormatKnit3,
51 RepositoryFormatKnit4,
52 )
53-from bzrlib.repofmt.pack_repo import (
54+from bzrlib.repofmt.knitpack_repo import (
55 RepositoryFormatKnitPack1,
56 RepositoryFormatKnitPack3,
57 RepositoryFormatKnitPack4,
58 RepositoryFormatKnitPack5,
59 )
60-from bzrlib.repofmt.weaverepo import (
61+from bzrlib.plugins.weave_fmt.repository import (
62 RepositoryFormat4,
63 RepositoryFormat5,
64 RepositoryFormat6,
65
66=== modified file 'lib/lp/code/mail/codeimport.py'
67--- lib/lp/code/mail/codeimport.py 2011-05-27 21:12:25 +0000
68+++ lib/lp/code/mail/codeimport.py 2011-08-07 20:42:25 +0000
69@@ -51,6 +51,7 @@
70 RevisionControlSystems.BZR_SVN: 'subversion',
71 RevisionControlSystems.GIT: 'git',
72 RevisionControlSystems.HG: 'mercurial',
73+ RevisionControlSystems.BZR: 'bazaar',
74 }
75 body = get_email_template('new-code-import.txt') % {
76 'person': code_import.registrant.displayname,
77@@ -123,7 +124,8 @@
78 elif code_import.rcs_type in (RevisionControlSystems.SVN,
79 RevisionControlSystems.BZR_SVN,
80 RevisionControlSystems.GIT,
81- RevisionControlSystems.HG):
82+ RevisionControlSystems.HG,
83+ RevisionControlSystems.BZR):
84 if CodeImportEventDataType.OLD_URL in event_data:
85 old_url = event_data[CodeImportEventDataType.OLD_URL]
86 body.append(
87
88=== modified file 'lib/lp/code/model/codeimport.py'
89--- lib/lp/code/model/codeimport.py 2011-04-27 01:42:46 +0000
90+++ lib/lp/code/model/codeimport.py 2011-08-07 20:42:25 +0000
91@@ -116,6 +116,8 @@
92 config.codeimport.default_interval_git,
93 RevisionControlSystems.HG:
94 config.codeimport.default_interval_hg,
95+ RevisionControlSystems.BZR:
96+ config.codeimport.default_interval_bzr,
97 }
98 seconds = default_interval_dict[self.rcs_type]
99 return timedelta(seconds=seconds)
100@@ -133,7 +135,8 @@
101 RevisionControlSystems.SVN,
102 RevisionControlSystems.GIT,
103 RevisionControlSystems.BZR_SVN,
104- RevisionControlSystems.HG):
105+ RevisionControlSystems.HG,
106+ RevisionControlSystems.BZR):
107 return self.url
108 else:
109 raise AssertionError(
110@@ -252,7 +255,8 @@
111 elif rcs_type in (RevisionControlSystems.SVN,
112 RevisionControlSystems.BZR_SVN,
113 RevisionControlSystems.GIT,
114- RevisionControlSystems.HG):
115+ RevisionControlSystems.HG,
116+ RevisionControlSystems.BZR):
117 assert cvs_root is None and cvs_module is None
118 assert url is not None
119 else:
120@@ -262,7 +266,8 @@
121 if review_status is None:
122 # Auto approve git and hg imports.
123 if rcs_type in (
124- RevisionControlSystems.GIT, RevisionControlSystems.HG):
125+ RevisionControlSystems.GIT, RevisionControlSystems.HG,
126+ RevisionControlSystems.BZR):
127 review_status = CodeImportReviewStatus.REVIEWED
128 else:
129 review_status = CodeImportReviewStatus.NEW
130
131=== modified file 'lib/lp/code/model/codeimportevent.py'
132--- lib/lp/code/model/codeimportevent.py 2010-10-17 22:51:50 +0000
133+++ lib/lp/code/model/codeimportevent.py 2011-08-07 20:42:25 +0000
134@@ -269,7 +269,8 @@
135 if code_import.rcs_type in (RevisionControlSystems.SVN,
136 RevisionControlSystems.BZR_SVN,
137 RevisionControlSystems.GIT,
138- RevisionControlSystems.HG):
139+ RevisionControlSystems.HG,
140+ RevisionControlSystems.BZR):
141 yield 'URL', code_import.url
142 elif code_import.rcs_type == RevisionControlSystems.CVS:
143 yield 'CVS_ROOT', code_import.cvs_root
144
145=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
146--- lib/lp/code/model/tests/test_branchjob.py 2011-05-23 11:06:50 +0000
147+++ lib/lp/code/model/tests/test_branchjob.py 2011-08-07 20:42:25 +0000
148@@ -17,7 +17,7 @@
149 BzrBranchFormat7,
150 )
151 from bzrlib.bzrdir import BzrDirMetaFormat1
152-from bzrlib.repofmt.pack_repo import RepositoryFormatKnitPack6
153+from bzrlib.repofmt.knitpack_repo import RepositoryFormatKnitPack6
154 from bzrlib.revision import NULL_REVISION
155 from bzrlib.transport import get_transport
156 import pytz
157
158=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
159--- lib/lp/code/model/tests/test_codeimport.py 2011-05-27 21:12:25 +0000
160+++ lib/lp/code/model/tests/test_codeimport.py 2011-08-07 20:42:25 +0000
161@@ -71,6 +71,20 @@
162 # No job is created for the import.
163 self.assertIs(None, code_import.import_job)
164
165+ def test_new_svn_import_svn_scheme(self):
166+ """A subversion import can use the svn:// scheme."""
167+ code_import = CodeImportSet().new(
168+ registrant=self.factory.makePerson(),
169+ target=IBranchTarget(self.factory.makeProduct()),
170+ branch_name='imported',
171+ rcs_type=RevisionControlSystems.SVN,
172+ url=self.factory.getUniqueURL(scheme="svn"))
173+ self.assertEqual(
174+ CodeImportReviewStatus.NEW,
175+ code_import.review_status)
176+ # No job is created for the import.
177+ self.assertIs(None, code_import.import_job)
178+
179 def test_reviewed_svn_import(self):
180 """A specific review status can be set for a new import."""
181 code_import = CodeImportSet().new(
182@@ -117,6 +131,21 @@
183 # A job is created for the import.
184 self.assertIsNot(None, code_import.import_job)
185
186+ def test_git_import_git_scheme(self):
187+ """A git import can have a git:// style URL."""
188+ code_import = CodeImportSet().new(
189+ registrant=self.factory.makePerson(),
190+ target=IBranchTarget(self.factory.makeProduct()),
191+ branch_name='imported',
192+ rcs_type=RevisionControlSystems.GIT,
193+ url=self.factory.getUniqueURL(scheme="git"),
194+ review_status=None)
195+ self.assertEqual(
196+ CodeImportReviewStatus.REVIEWED,
197+ code_import.review_status)
198+ # A job is created for the import.
199+ self.assertIsNot(None, code_import.import_job)
200+
201 def test_git_import_reviewed(self):
202 """A new git import is always reviewed by default."""
203 code_import = CodeImportSet().new(
204@@ -147,6 +176,21 @@
205 # A job is created for the import.
206 self.assertIsNot(None, code_import.import_job)
207
208+ def test_bzr_import_reviewed(self):
209+ """A new bzr import is always reviewed by default."""
210+ code_import = CodeImportSet().new(
211+ registrant=self.factory.makePerson(),
212+ target=IBranchTarget(self.factory.makeProduct()),
213+ branch_name='mirrored',
214+ rcs_type=RevisionControlSystems.BZR,
215+ url=self.factory.getUniqueURL(),
216+ review_status=None)
217+ self.assertEqual(
218+ CodeImportReviewStatus.REVIEWED,
219+ code_import.review_status)
220+ # A job is created for the import.
221+ self.assertIsNot(None, code_import.import_job)
222+
223 def test_junk_code_import_rejected(self):
224 """You are not allowed to create code imports targetting +junk."""
225 registrant = self.factory.makePerson()
226
227=== modified file 'lib/lp/codehosting/__init__.py'
228--- lib/lp/codehosting/__init__.py 2011-03-30 15:16:35 +0000
229+++ lib/lp/codehosting/__init__.py 2011-08-07 20:42:25 +0000
230@@ -70,15 +70,13 @@
231 __import__("bzrlib.plugins.%s" % plugin_name)
232
233
234-def remove_hook(self, hook):
235- """Remove the hook from the HookPoint"""
236- self._callbacks.remove(hook)
237- for name, value in self._callback_names.iteritems():
238- if value is hook:
239- del self._callback_names[name]
240-
241-
242-# XXX: JonathanLange 2011-03-30 bug=301472: Monkeypatch: Branch.hooks is a
243-# list in bzr 1.13, so it supports remove. It is a HookPoint in bzr 1.14, so
244-# add HookPoint.remove.
245-hooks.HookPoint.remove = remove_hook
246+def load_bundled_plugin(plugin_name):
247+ """Load a plugin bundled with Bazaar."""
248+ from bzrlib.plugin import get_core_plugin_path
249+ from bzrlib import plugins
250+ if get_core_plugin_path() not in plugins.__path__:
251+ plugins.__path__.append(get_core_plugin_path())
252+ __import__("bzrlib.plugins.%s" % plugin_name)
253+
254+
255+load_bundled_plugin("weave_fmt")
256
257=== modified file 'lib/lp/codehosting/codeimport/tests/servers.py'
258--- lib/lp/codehosting/codeimport/tests/servers.py 2011-06-02 10:48:54 +0000
259+++ lib/lp/codehosting/codeimport/tests/servers.py 2011-08-07 20:42:25 +0000
260@@ -4,6 +4,7 @@
261 """Server classes that know how to create various kinds of foreign archive."""
262
263 __all__ = [
264+ 'BzrServer',
265 'CVSServer',
266 'GitServer',
267 'MercurialServer',
268@@ -13,6 +14,7 @@
269 __metaclass__ = type
270
271 from cStringIO import StringIO
272+import errno
273 import os
274 import shutil
275 import signal
276@@ -20,8 +22,16 @@
277 import subprocess
278 import tempfile
279 import time
280+import threading
281
282+from bzrlib.branch import Branch
283+from bzrlib.branchbuilder import BranchBuilder
284+from bzrlib.bzrdir import BzrDir
285 from bzrlib.tests.treeshape import build_tree_contents
286+from bzrlib.tests.test_server import (
287+ ReadonlySmartTCPServer_for_testing,
288+ TestServer,
289+ )
290 from bzrlib.transport import Server
291 from bzrlib.urlutils import (
292 escape,
293@@ -31,6 +41,17 @@
294 import dulwich.index
295 from dulwich.objects import Blob
296 from dulwich.repo import Repo as GitRepo
297+from dulwich.server import (
298+ DictBackend,
299+ TCPGitServer,
300+ )
301+from mercurial.ui import (
302+ ui as hg_ui,
303+ )
304+from mercurial.hgweb import (
305+ hgweb,
306+ server as hgweb_server,
307+ )
308 import subvertpy.ra
309 import svn_oo
310
311@@ -107,8 +128,8 @@
312 for i in range(10):
313 try:
314 ra = self._get_ra(self.get_url())
315- except subvertpy.SubversionException, e:
316- if 'Connection refused' in str(e):
317+ except OSError, e:
318+ if e.errno == errno.ECONNREFUSED:
319 time.sleep(delay)
320 delay *= 1.5
321 continue
322@@ -204,45 +225,177 @@
323 self._repository = self.createRepository(self._repository_path)
324
325
326+class TCPGitServerThread(threading.Thread):
327+ """TCP Git server that runs in a separate thread."""
328+
329+ def __init__(self, backend, address, port=None):
330+ super(TCPGitServerThread, self).__init__()
331+ self.setName("TCP Git server on %s:%s" % (address, port))
332+ self.server = TCPGitServer(backend, address, port)
333+
334+ def run(self):
335+ self.server.serve_forever()
336+
337+ def get_address(self):
338+ return self.server.server_address
339+
340+ def stop(self):
341+ self.server.shutdown()
342+
343+
344 class GitServer(Server):
345
346- def __init__(self, repo_url):
347+ def __init__(self, repository_path, use_server=False):
348 super(GitServer, self).__init__()
349- self.repo_url = repo_url
350+ self.repository_path = repository_path
351+ self._use_server = use_server
352+
353+ def get_url(self):
354+ """Return a URL to the Git repository."""
355+ if self._use_server:
356+ return 'git://%s:%d/' % self._server.get_address()
357+ else:
358+ return local_path_to_url(self.repository_path)
359+
360+ def createRepository(self, path):
361+ GitRepo.init(path)
362+
363+ def start_server(self):
364+ super(GitServer, self).start_server()
365+ self.createRepository(self.repository_path)
366+ if self._use_server:
367+ repo = GitRepo(self.repository_path)
368+ self._server = TCPGitServerThread(
369+ DictBackend({"/": repo}), "localhost", 0)
370+ self._server.start()
371+
372+ def stop_server(self):
373+ super(GitServer, self).stop_server()
374+ if self._use_server:
375+ self._server.stop()
376
377 def makeRepo(self, tree_contents):
378- wd = os.getcwd()
379- try:
380- os.chdir(self.repo_url)
381- repo = GitRepo.init(".")
382- blobs = [
383- (Blob.from_string(contents), filename) for (filename, contents)
384- in tree_contents]
385- repo.object_store.add_objects(blobs)
386- root_id = dulwich.index.commit_tree(repo.object_store, [
387- (filename, b.id, stat.S_IFREG | 0644)
388- for (b, filename) in blobs])
389- repo.do_commit(committer='Joe Foo <joe@foo.com>',
390- message=u'<The commit message>', tree=root_id)
391- finally:
392- os.chdir(wd)
393+ repo = GitRepo(self.repository_path)
394+ blobs = [
395+ (Blob.from_string(contents), filename) for (filename, contents)
396+ in tree_contents]
397+ repo.object_store.add_objects(blobs)
398+ root_id = dulwich.index.commit_tree(repo.object_store, [
399+ (filename, b.id, stat.S_IFREG | 0644)
400+ for (b, filename) in blobs])
401+ repo.do_commit(committer='Joe Foo <joe@foo.com>',
402+ message=u'<The commit message>', tree=root_id)
403+
404+
405+class MercurialServerThread(threading.Thread):
406+
407+ def __init__(self, path, address, port=0):
408+ super(MercurialServerThread, self).__init__()
409+ self.ui = hg_ui()
410+ self.ui.setconfig("web", "address", address)
411+ self.ui.setconfig("web", "port", 0)
412+ self.app = hgweb(path, baseui=self.ui)
413+ self.httpd = hgweb_server.create_server(self.ui, self.app)
414+ # By default the Mercurial server output goes to stdout
415+ self.httpd.errorlog = StringIO()
416+ self.httpd.accesslog = StringIO()
417+
418+ def get_address(self):
419+ return (self.httpd.addr, self.httpd.port)
420+
421+ def run(self):
422+ self.httpd.serve_forever()
423+
424+ def stop(self):
425+ self.httpd.shutdown()
426
427
428 class MercurialServer(Server):
429
430- def __init__(self, repo_url):
431+ def __init__(self, repository_path, use_server=False):
432 super(MercurialServer, self).__init__()
433- self.repo_url = repo_url
434+ self.repository_path = repository_path
435+ self._use_server = use_server
436+
437+ def get_url(self):
438+ if self._use_server:
439+ return "http://%s:%d/" % self._hgserver.get_address()
440+ else:
441+ return local_path_to_url(self.repository_path)
442+
443+ def start_server(self):
444+ super(MercurialServer, self).start_server()
445+ self.createRepository(self.repository_path)
446+ if self._use_server:
447+ self._hgserver = MercurialServerThread(self.repository_path, "localhost")
448+ self._hgserver.start()
449+
450+ def stop_server(self):
451+ super(MercurialServer, self).stop_server()
452+ if self._use_server:
453+ self._hgserver.stop()
454+
455+ def createRepository(self, path):
456+ from mercurial.ui import ui
457+ from mercurial.localrepo import localrepository
458+ localrepository(ui(), self.repository_path, create=1)
459
460 def makeRepo(self, tree_contents):
461 from mercurial.ui import ui
462 from mercurial.localrepo import localrepository
463- repo = localrepository(ui(), self.repo_url, create=1)
464+ repo = localrepository(ui(), self.repository_path)
465 for filename, contents in tree_contents:
466- f = open(os.path.join(self.repo_url, filename), 'w')
467+ f = open(os.path.join(self.repository_path, filename), 'w')
468 try:
469 f.write(contents)
470 finally:
471 f.close()
472 repo[None].add([filename])
473 repo.commit(text='<The commit message>', user='jane Foo <joe@foo.com>')
474+
475+
476+class BzrServer(Server):
477+
478+ def __init__(self, repository_path, use_server=False):
479+ super(BzrServer, self).__init__()
480+ self.repository_path = repository_path
481+ self._use_server = use_server
482+
483+ def createRepository(self, path):
484+ BzrDir.create_branch_convenience(path)
485+
486+ def makeRepo(self, tree_contents):
487+ branch = Branch.open(self.repository_path)
488+ branch.get_config().set_user_option("create_signatures", "never")
489+ builder = BranchBuilder(branch=branch)
490+ actions = [('add', ('', 'tree-root', 'directory', None))]
491+ actions += [('add', (path, path+'-id', 'file', content)) for (path,
492+ content) in tree_contents]
493+ builder.build_snapshot(None, None,
494+ actions, committer='Joe Foo <joe@foo.com>',
495+ message=u'<The commit message>')
496+
497+ def get_url(self):
498+ if self._use_server:
499+ return self._bzrserver.get_url()
500+ else:
501+ return local_path_to_url(self.repository_path)
502+
503+ def start_server(self):
504+ super(BzrServer, self).start_server()
505+ self.createRepository(self.repository_path)
506+ class LocalURLServer(TestServer):
507+ def __init__(self, repository_path):
508+ self.repository_path = repository_path
509+ def start_server(self): pass
510+ def get_url(self):
511+ return local_path_to_url(self.repository_path)
512+ if self._use_server:
513+ self._bzrserver = ReadonlySmartTCPServer_for_testing()
514+ self._bzrserver.start_server(
515+ LocalURLServer(self.repository_path))
516+
517+ def stop_server(self):
518+ super(BzrServer, self).stop_server()
519+ if self._use_server:
520+ self._bzrserver.stop_server()
521
522=== modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
523--- lib/lp/codehosting/codeimport/tests/test_worker.py 2011-07-20 17:20:03 +0000
524+++ lib/lp/codehosting/codeimport/tests/test_worker.py 2011-08-07 20:42:25 +0000
525@@ -17,6 +17,9 @@
526 Branch,
527 BranchReferenceFormat,
528 )
529+from bzrlib.branchbuilder import (
530+ BranchBuilder,
531+ )
532 from bzrlib.bzrdir import (
533 BzrDir,
534 BzrDirFormat,
535@@ -47,6 +50,7 @@
536 extract_tarball,
537 )
538 from lp.codehosting.codeimport.tests.servers import (
539+ BzrServer,
540 CVSServer,
541 GitServer,
542 MercurialServer,
543@@ -54,6 +58,7 @@
544 )
545 from lp.codehosting.codeimport.worker import (
546 BazaarBranchStore,
547+ BzrImportWorker,
548 BzrSvnImportWorker,
549 CodeImportWorkerExitCode,
550 CSCVSImportWorker,
551@@ -959,7 +964,7 @@
552 def makeSourceDetails(self, branch_name, files):
553 """Make a SVN `CodeImportSourceDetails` pointing at a real SVN repo.
554 """
555- svn_server = SubversionServer(self.makeTemporaryDirectory())
556+ svn_server = SubversionServer(self.makeTemporaryDirectory(), True)
557 svn_server.start_server()
558 self.addCleanup(svn_server.stop_server)
559
560@@ -999,10 +1004,10 @@
561 # import should be rejected.
562 args = {'rcstype': self.rcstype}
563 reference_url = self.createBranchReference()
564- if self.rcstype in ('git', 'bzr-svn', 'hg'):
565+ if self.rcstype in ('git', 'bzr-svn', 'hg', 'bzr'):
566 args['url'] = reference_url
567 else:
568- raise AssertionError("unexpected rcs_type %r" % self.rcs_type)
569+ raise AssertionError("unexpected rcs_type %r" % self.rcstype)
570 source_details = self.factory.makeCodeImportSourceDetails(**args)
571 worker = self.makeImportWorker(source_details)
572 self.assertEqual(
573@@ -1011,7 +1016,21 @@
574 def test_invalid(self):
575 # If there is no branch in the target URL, exit with FAILURE_INVALID
576 worker = self.makeImportWorker(self.factory.makeCodeImportSourceDetails(
577- rcstype=self.rcstype, url="file:///path/non/existant"))
578+ rcstype=self.rcstype, url="http://localhost/path/non/existant"))
579+ self.assertEqual(
580+ CodeImportWorkerExitCode.FAILURE_INVALID, worker.run())
581+
582+ def test_bad_url(self):
583+ # Local path URLs are not allowed
584+ worker = self.makeImportWorker(self.factory.makeCodeImportSourceDetails(
585+ rcstype=self.rcstype, url="file:///tmp/path/non/existant"))
586+ self.assertEqual(
587+ CodeImportWorkerExitCode.FAILURE_INVALID, worker.run())
588+
589+ def test_launchpad_url(self):
590+ # Launchpad URLs are not allowed
591+ worker = self.makeImportWorker(self.factory.makeCodeImportSourceDetails(
592+ rcstype=self.rcstype, url="https://code.launchpad.net/linux/"))
593 self.assertEqual(
594 CodeImportWorkerExitCode.FAILURE_INVALID, worker.run())
595
596@@ -1071,7 +1090,7 @@
597
598 def makeForeignCommit(self, source_details):
599 """Change the foreign tree, generating exactly one commit."""
600- repo = GitRepo(source_details.url)
601+ repo = GitRepo(local_path_from_url(source_details.url))
602 repo.do_commit(message=self.factory.getUniqueString(),
603 committer="Joe Random Hacker <joe@example.com>")
604 self.foreign_commit_count += 1
605@@ -1080,7 +1099,7 @@
606 """Make a Git `CodeImportSourceDetails` pointing at a real Git repo.
607 """
608 repository_path = self.makeTemporaryDirectory()
609- git_server = GitServer(repository_path)
610+ git_server = GitServer(repository_path, use_server=True)
611 git_server.start_server()
612 self.addCleanup(git_server.stop_server)
613
614@@ -1088,8 +1107,7 @@
615 self.foreign_commit_count = 1
616
617 return self.factory.makeCodeImportSourceDetails(
618- rcstype='git', url=repository_path)
619-
620+ rcstype='git', url=git_server.get_url())
621
622
623 class TestMercurialImport(WorkerTest, TestActualImportMixin,
624@@ -1124,7 +1142,7 @@
625 """Change the foreign tree, generating exactly one commit."""
626 from mercurial.ui import ui
627 from mercurial.localrepo import localrepository
628- repo = localrepository(ui(), source_details.url)
629+ repo = localrepository(ui(), local_path_from_url(source_details.url))
630 repo.commit(text="hello world!", user="Jane Random Hacker", force=1)
631 self.foreign_commit_count += 1
632
633@@ -1132,7 +1150,7 @@
634 """Make a Mercurial `CodeImportSourceDetails` pointing at a real repo.
635 """
636 repository_path = self.makeTemporaryDirectory()
637- hg_server = MercurialServer(repository_path)
638+ hg_server = MercurialServer(repository_path, use_server=True)
639 hg_server.start_server()
640 self.addCleanup(hg_server.stop_server)
641
642@@ -1140,7 +1158,7 @@
643 self.foreign_commit_count = 1
644
645 return self.factory.makeCodeImportSourceDetails(
646- rcstype='hg', url=repository_path)
647+ rcstype='hg', url=hg_server.get_url())
648
649
650 class TestBzrSvnImport(WorkerTest, SubversionImportHelpers,
651@@ -1160,5 +1178,51 @@
652 self.bazaar_store, logging.getLogger())
653
654
655+class TestBzrImport(WorkerTest, TestActualImportMixin,
656+ PullingImportWorkerTests):
657+
658+ rcstype = 'bzr'
659+
660+ def setUp(self):
661+ super(TestBzrImport, self).setUp()
662+ self.setUpImport()
663+
664+ def makeImportWorker(self, source_details):
665+ """Make a new `ImportWorker`."""
666+ return BzrImportWorker(
667+ source_details, self.get_transport('import_data'),
668+ self.bazaar_store, logging.getLogger())
669+
670+ def makeForeignCommit(self, source_details):
671+ """Change the foreign tree, generating exactly one commit."""
672+ branch = Branch.open(source_details.url)
673+ builder = BranchBuilder(branch=branch)
674+ builder.build_commit(message=self.factory.getUniqueString(),
675+ committer="Joe Random Hacker <joe@example.com>")
676+ self.foreign_commit_count += 1
677+
678+ def makeSourceDetails(self, branch_name, files):
679+ """Make Bzr `CodeImportSourceDetails` pointing at a real Bzr repo.
680+ """
681+ repository_path = self.makeTemporaryDirectory()
682+ bzr_server = BzrServer(repository_path, use_server=True)
683+ bzr_server.start_server()
684+ self.addCleanup(bzr_server.stop_server)
685+
686+ bzr_server.makeRepo(files)
687+ self.foreign_commit_count = 1
688+
689+ return self.factory.makeCodeImportSourceDetails(
690+ rcstype='bzr', url=bzr_server.get_url())
691+
692+ def test_partial(self):
693+ self.skip(
694+ "Partial fetching is not supported for native bzr branches "
695+ "at the moment.")
696+
697+ def test_unsupported_feature(self):
698+ self.skip("All Bazaar features are supported by Bazaar.")
699+
700+
701 def test_suite():
702 return unittest.TestLoader().loadTestsFromName(__name__)
703
704=== modified file 'lib/lp/codehosting/codeimport/tests/test_workermonitor.py'
705--- lib/lp/codehosting/codeimport/tests/test_workermonitor.py 2011-06-16 23:43:04 +0000
706+++ lib/lp/codehosting/codeimport/tests/test_workermonitor.py 2011-08-07 20:42:25 +0000
707@@ -50,6 +50,7 @@
708 from lp.code.model.codeimportjob import CodeImportJob
709 from lp.codehosting import load_optional_plugin
710 from lp.codehosting.codeimport.tests.servers import (
711+ BzrServer,
712 CVSServer,
713 GitServer,
714 MercurialServer,
715@@ -661,26 +662,38 @@
716 def makeGitCodeImport(self):
717 """Make a `CodeImport` that points to a real Git repository."""
718 load_optional_plugin('git')
719- self.git_server = GitServer(self.repo_path)
720+ self.git_server = GitServer(self.repo_path, use_server=True)
721 self.git_server.start_server()
722 self.addCleanup(self.git_server.stop_server)
723
724 self.git_server.makeRepo([('README', 'contents')])
725 self.foreign_commit_count = 1
726
727- return self.factory.makeCodeImport(git_repo_url=self.repo_path)
728+ return self.factory.makeCodeImport(
729+ git_repo_url=self.git_server.get_url())
730
731 def makeHgCodeImport(self):
732 """Make a `CodeImport` that points to a real Mercurial repository."""
733 load_optional_plugin('hg')
734- self.hg_server = MercurialServer(self.repo_path)
735+ self.hg_server = MercurialServer(self.repo_path, use_server=True)
736 self.hg_server.start_server()
737 self.addCleanup(self.hg_server.stop_server)
738
739 self.hg_server.makeRepo([('README', 'contents')])
740 self.foreign_commit_count = 1
741
742- return self.factory.makeCodeImport(hg_repo_url=self.repo_path)
743+ return self.factory.makeCodeImport(
744+ hg_repo_url=self.hg_server.get_url())
745+
746+ def makeBzrCodeImport(self):
747+ """Make a `CodeImport` that points to a real Bazaar branch."""
748+ self.bzr_server = BzrServer(self.repo_path)
749+ self.bzr_server.start_server()
750+ self.addCleanup(self.bzr_server.stop_server)
751+
752+ self.bzr_server.makeRepo([('README', 'contents')])
753+ self.foreign_commit_count = 1
754+ return self.factory.makeCodeImport(bzr_branch_url=self.repo_path)
755
756 def getStartedJobForImport(self, code_import):
757 """Get a started `CodeImportJob` for `code_import`.
758@@ -782,6 +795,15 @@
759 result = self.performImport(job_id)
760 return result.addCallback(self.assertImported, code_import_id)
761
762+ def test_import_bzr(self):
763+ # Create a Bazaar CodeImport and import it.
764+ job = self.getStartedJobForImport(self.makeBzrCodeImport())
765+ code_import_id = job.code_import.id
766+ job_id = job.id
767+ self.layer.txn.commit()
768+ result = self.performImport(job_id)
769+ return result.addCallback(self.assertImported, code_import_id)
770+
771 # XXX 2010-03-24 MichaelHudson, bug=541526: This test fails intermittently
772 # in EC2.
773 def DISABLED_test_import_bzrsvn(self):
774
775=== modified file 'lib/lp/codehosting/codeimport/worker.py'
776--- lib/lp/codehosting/codeimport/worker.py 2011-08-02 11:28:46 +0000
777+++ lib/lp/codehosting/codeimport/worker.py 2011-08-07 20:42:25 +0000
778@@ -6,6 +6,7 @@
779 __metaclass__ = type
780 __all__ = [
781 'BazaarBranchStore',
782+ 'BzrImportWorker',
783 'BzrSvnImportWorker',
784 'CSCVSImportWorker',
785 'CodeImportSourceDetails',
786@@ -21,6 +22,7 @@
787 import os
788 import shutil
789
790+from bzrlib.upgrade import upgrade
791 from bzrlib.branch import (
792 Branch,
793 InterBranch,
794@@ -38,7 +40,6 @@
795 )
796 from bzrlib.transport import get_transport
797 import bzrlib.ui
798-from bzrlib.upgrade import upgrade
799 from bzrlib.urlutils import (
800 join as urljoin,
801 local_path_from_url,
802@@ -49,11 +50,22 @@
803 import SCM
804
805 from canonical.config import config
806+
807+from lazr.uri import (
808+ URI,
809+ )
810+
811 from lp.code.enums import RevisionControlSystems
812+from lp.code.interfaces.branch import get_blacklisted_hostnames
813 from lp.codehosting.codeimport.foreigntree import (
814 CVSWorkingTree,
815 SubversionWorkingTree,
816 )
817+from lp.codehosting.safe_open import (
818+ BadUrl,
819+ BranchOpenPolicy,
820+ SafeBranchOpener,
821+ )
822 from lp.codehosting.codeimport.tarball import (
823 create_tarball,
824 extract_tarball,
825@@ -62,6 +74,52 @@
826 from lp.services.propertycache import cachedproperty
827
828
829+class CodeImportBranchOpenPolicy(BranchOpenPolicy):
830+ """Branch open policy for code imports.
831+
832+ In summary:
833+ - follow references,
834+ - only open non-Launchpad URLs
835+ - only open the allowed schemes
836+ """
837+
838+ allowed_schemes = ['http', 'https', 'svn', 'git', 'ftp']
839+
840+ def shouldFollowReferences(self):
841+ """See `BranchOpenPolicy.shouldFollowReferences`.
842+
843+ We traverse branch references for MIRRORED branches because they
844+ provide a useful redirection mechanism and we want to be consistent
845+ with the bzr command line.
846+ """
847+ return True
848+
849+ def transformFallbackLocation(self, branch, url):
850+ """See `BranchOpenPolicy.transformFallbackLocation`.
851+
852+ For mirrored branches, we stack on whatever the remote branch claims
853+ to stack on, but this URL still needs to be checked.
854+ """
855+ return urljoin(branch.base, url), True
856+
857+ def checkOneURL(self, url):
858+ """See `BranchOpenPolicy.checkOneURL`.
859+
860+ We refuse to mirror from Launchpad or a ssh-like or file URL.
861+ """
862+ uri = URI(url)
863+ launchpad_domain = config.vhost.mainsite.hostname
864+ if uri.underDomain(launchpad_domain):
865+ raise BadUrl(url)
866+ for hostname in get_blacklisted_hostnames():
867+ if uri.underDomain(hostname):
868+ raise BadUrl(url)
869+ if uri.scheme in ['sftp', 'bzr+ssh']:
870+ raise BadUrl(url)
871+ elif uri.scheme not in self.allowed_schemes:
872+ raise BadUrl(url)
873+
874+
875 class CodeImportWorkerExitCode:
876 """Exit codes used by the code import worker script."""
877
878@@ -187,9 +245,9 @@
879
880 :ivar branch_id: The id of the branch associated to this code import, used
881 for locating the existing import and the foreign tree.
882- :ivar rcstype: 'svn' or 'cvs' as appropriate.
883+ :ivar rcstype: 'svn', 'cvs', 'hg', 'git', 'bzr-svn', 'bzr' as appropriate.
884 :ivar url: The branch URL if rcstype in ['svn', 'bzr-svn',
885- 'git'], None otherwise.
886+ 'git', 'hg', 'bzr'], None otherwise.
887 :ivar cvs_root: The $CVSROOT if rcstype == 'cvs', None otherwise.
888 :ivar cvs_module: The CVS module if rcstype == 'cvs', None otherwise.
889 """
890@@ -207,7 +265,7 @@
891 """Convert command line-style arguments to an instance."""
892 branch_id = int(arguments.pop(0))
893 rcstype = arguments.pop(0)
894- if rcstype in ['svn', 'bzr-svn', 'git', 'hg']:
895+ if rcstype in ['svn', 'bzr-svn', 'git', 'hg', 'bzr']:
896 [url] = arguments
897 cvs_root = cvs_module = None
898 elif rcstype == 'cvs':
899@@ -234,6 +292,8 @@
900 return cls(branch_id, 'git', str(code_import.url))
901 elif code_import.rcs_type == RevisionControlSystems.HG:
902 return cls(branch_id, 'hg', str(code_import.url))
903+ elif code_import.rcs_type == RevisionControlSystems.BZR:
904+ return cls(branch_id, 'bzr', str(code_import.url))
905 else:
906 raise AssertionError("Unknown rcstype %r." % code_import.rcs_type)
907
908@@ -241,7 +301,7 @@
909 """Return a list of arguments suitable for passing to a child process.
910 """
911 result = [str(self.branch_id), self.rcstype]
912- if self.rcstype in ['svn', 'bzr-svn', 'git', 'hg']:
913+ if self.rcstype in ['svn', 'bzr-svn', 'git', 'hg', 'bzr']:
914 result.append(self.url)
915 elif self.rcstype == 'cvs':
916 result.append(self.cvs_root)
917@@ -601,11 +661,18 @@
918 def _doImport(self):
919 self._logger.info("Starting job.")
920 saved_factory = bzrlib.ui.ui_factory
921+ opener_policy = CodeImportBranchOpenPolicy()
922+ opener = SafeBranchOpener(opener_policy)
923 bzrlib.ui.ui_factory = LoggingUIFactory(logger=self._logger)
924 try:
925 self._logger.info(
926 "Getting exising bzr branch from central store.")
927 bazaar_branch = self.getBazaarBranch()
928+ try:
929+ opener_policy.checkOneURL(self.source_details.url)
930+ except BadUrl, e:
931+ self._logger.info("Invalid URL: %s" % e)
932+ return CodeImportWorkerExitCode.FAILURE_INVALID
933 transport = get_transport(self.source_details.url)
934 for prober_kls in self.probers:
935 prober = prober_kls()
936@@ -617,7 +684,13 @@
937 else:
938 self._logger.info("No branch found at remote location.")
939 return CodeImportWorkerExitCode.FAILURE_INVALID
940- remote_branch = format.open(transport).open_branch()
941+ remote_dir = format.open(transport)
942+ try:
943+ remote_branch = opener.runWithTransformFallbackLocationHookInstalled(
944+ remote_dir.open_branch)
945+ except BadUrl, e:
946+ self._logger.info("Invalid URL: %s" % e)
947+ return CodeImportWorkerExitCode.FAILURE_INVALID
948 remote_branch_tip = remote_branch.last_revision()
949 inter_branch = InterBranch.get(remote_branch, bazaar_branch)
950 self._logger.info("Importing branch.")
951@@ -820,3 +893,21 @@
952 """See `PullingImportWorker.probers`."""
953 from bzrlib.plugins.svn import SvnRemoteProber
954 return [SvnRemoteProber]
955+
956+
957+class BzrImportWorker(PullingImportWorker):
958+ """An import worker for importing Bazaar branches."""
959+
960+ invalid_branch_exceptions = []
961+ unsupported_feature_exceptions = []
962+
963+ def getRevisionLimit(self):
964+ """See `PullingImportWorker.getRevisionLimit`."""
965+ # For now, just grab the whole branch at once
966+ return None
967+
968+ @property
969+ def probers(self):
970+ """See `PullingImportWorker.probers`."""
971+ from bzrlib.bzrdir import BzrProber, RemoteBzrProber
972+ return [BzrProber, RemoteBzrProber]
973
974=== modified file 'lib/lp/codehosting/puller/tests/__init__.py'
975--- lib/lp/codehosting/puller/tests/__init__.py 2011-06-02 11:16:47 +0000
976+++ lib/lp/codehosting/puller/tests/__init__.py 2011-08-07 20:42:25 +0000
977@@ -25,75 +25,19 @@
978 from canonical.config import config
979 from lp.codehosting.puller.worker import (
980 BranchMirrorer,
981+ BranchMirrorerPolicy,
982 PullerWorker,
983 PullerWorkerProtocol,
984 )
985 from lp.codehosting.tests.helpers import LoomTestMixin
986+from lp.codehosting.safe_open import AcceptAnythingPolicy
987 from lp.codehosting.vfs import branch_id_to_path
988-from lp.codehosting.vfs.branchfs import (
989- BadUrl,
990- BranchPolicy,
991- )
992 from lp.testing import TestCaseWithFactory
993
994
995-class BlacklistPolicy(BranchPolicy):
996- """Branch policy that forbids certain URLs."""
997-
998- def __init__(self, should_follow_references, unsafe_urls=None):
999- if unsafe_urls is None:
1000- unsafe_urls = set()
1001- self._unsafe_urls = unsafe_urls
1002- self._should_follow_references = should_follow_references
1003-
1004- def shouldFollowReferences(self):
1005- return self._should_follow_references
1006-
1007- def checkOneURL(self, url):
1008- if url in self._unsafe_urls:
1009- raise BadUrl(url)
1010-
1011- def transformFallbackLocation(self, branch, url):
1012- """See `BranchPolicy.transformFallbackLocation`.
1013-
1014- This class is not used for testing our smarter stacking features so we
1015- just do the simplest thing: return the URL that would be used anyway
1016- and don't check it.
1017- """
1018- return urlutils.join(branch.base, url), False
1019-
1020-
1021-class AcceptAnythingPolicy(BlacklistPolicy):
1022- """Accept anything, to make testing easier."""
1023-
1024- def __init__(self):
1025- super(AcceptAnythingPolicy, self).__init__(True, set())
1026-
1027-
1028-class WhitelistPolicy(BranchPolicy):
1029- """Branch policy that only allows certain URLs."""
1030-
1031- def __init__(self, should_follow_references, allowed_urls=None,
1032- check=False):
1033- if allowed_urls is None:
1034- allowed_urls = []
1035- self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
1036- self.check = check
1037-
1038- def shouldFollowReferences(self):
1039- return self._should_follow_references
1040-
1041- def checkOneURL(self, url):
1042- if url.rstrip('/') not in self.allowed_urls:
1043- raise BadUrl(url)
1044-
1045- def transformFallbackLocation(self, branch, url):
1046- """See `BranchPolicy.transformFallbackLocation`.
1047-
1048- Here we return the URL that would be used anyway and optionally check
1049- it.
1050- """
1051- return urlutils.join(branch.base, url), self.check
1052+class AcceptAnythingBranchMirrorerPolicy(AcceptAnythingPolicy,
1053+ BranchMirrorerPolicy): pass
1054+
1055
1056
1057 class PullerWorkerMixin:
1058@@ -114,7 +58,7 @@
1059 oops_prefix = ''
1060 if branch_type is None:
1061 if policy is None:
1062- policy = AcceptAnythingPolicy()
1063+ policy = AcceptAnythingBranchMirrorerPolicy()
1064 opener = BranchMirrorer(policy, protocol)
1065 else:
1066 opener = None
1067
1068=== modified file 'lib/lp/codehosting/puller/tests/test_errors.py'
1069--- lib/lp/codehosting/puller/tests/test_errors.py 2010-08-20 20:31:18 +0000
1070+++ lib/lp/codehosting/puller/tests/test_errors.py 2011-08-07 20:42:25 +0000
1071@@ -23,17 +23,15 @@
1072
1073 from lp.code.enums import BranchType
1074 from lp.codehosting.puller.worker import (
1075+ BadUrlLaunchpad,
1076+ BadUrlScheme,
1077+ BadUrlSsh,
1078 BranchLoopError,
1079 BranchMirrorer,
1080 BranchReferenceForbidden,
1081 PullerWorker,
1082 PullerWorkerProtocol,
1083 )
1084-from lp.codehosting.vfs.branchfs import (
1085- BadUrlLaunchpad,
1086- BadUrlScheme,
1087- BadUrlSsh,
1088- )
1089
1090
1091 class StubbedPullerWorkerProtocol(PullerWorkerProtocol):
1092
1093=== modified file 'lib/lp/codehosting/puller/tests/test_worker.py'
1094--- lib/lp/codehosting/puller/tests/test_worker.py 2010-10-15 08:47:20 +0000
1095+++ lib/lp/codehosting/puller/tests/test_worker.py 2011-08-07 20:42:25 +0000
1096@@ -14,18 +14,15 @@
1097 import bzrlib.branch
1098 from bzrlib.branch import (
1099 BranchReferenceFormat,
1100- BzrBranchFormat7,
1101 )
1102 from bzrlib.bzrdir import (
1103 BzrDir,
1104- BzrDirMetaFormat1,
1105 )
1106 from bzrlib.errors import (
1107 IncompatibleRepositories,
1108 NotBranchError,
1109 NotStacked,
1110 )
1111-from bzrlib.repofmt.pack_repo import RepositoryFormatKnitPack1
1112 from bzrlib.revision import NULL_REVISION
1113 from bzrlib.tests import (
1114 TestCaseInTempDir,
1115@@ -35,28 +32,25 @@
1116
1117 from lp.code.enums import BranchType
1118 from lp.codehosting.puller.tests import (
1119- AcceptAnythingPolicy,
1120- BlacklistPolicy,
1121 FixedHttpServer,
1122 PullerWorkerMixin,
1123- WhitelistPolicy,
1124 )
1125 from lp.codehosting.puller.worker import (
1126- BranchLoopError,
1127- BranchMirrorer,
1128- BranchReferenceForbidden,
1129- install_worker_ui_factory,
1130- PullerWorkerProtocol,
1131- WORKER_ACTIVITY_NETWORK,
1132- )
1133-from lp.codehosting.vfs.branchfs import (
1134- BadUrl,
1135 BadUrlLaunchpad,
1136 BadUrlScheme,
1137 BadUrlSsh,
1138- BranchPolicy,
1139+ BranchMirrorerPolicy,
1140 ImportedBranchPolicy,
1141+ install_worker_ui_factory,
1142 MirroredBranchPolicy,
1143+ PullerWorkerProtocol,
1144+ SafeBranchOpener,
1145+ WORKER_ACTIVITY_NETWORK,
1146+ )
1147+from lp.codehosting.safe_open import (
1148+ AcceptAnythingPolicy,
1149+ BadUrl,
1150+ BranchOpenPolicy,
1151 )
1152 from lp.testing import TestCase
1153 from lp.testing.factory import (
1154@@ -81,7 +75,8 @@
1155 return strings
1156
1157
1158-class PrearrangedStackedBranchPolicy(AcceptAnythingPolicy):
1159+class PrearrangedStackedBranchPolicy(BranchMirrorerPolicy,
1160+ AcceptAnythingPolicy):
1161 """A branch policy that returns a pre-configurable stack-on URL."""
1162
1163 def __init__(self, stack_on_url):
1164@@ -280,199 +275,8 @@
1165 self.assertEqual('', stacked_on_url)
1166
1167
1168-class TestBranchMirrorerCheckAndFollowBranchReference(TestCase):
1169- """Unit tests for `BranchMirrorer.checkAndFollowBranchReference`."""
1170-
1171- class StubbedBranchMirrorer(BranchMirrorer):
1172- """BranchMirrorer that provides canned answers.
1173-
1174- We implement the methods we need to to be able to control all the
1175- inputs to the `BranchMirrorer.checkSource` method, which is what is
1176- being tested in this class.
1177- """
1178-
1179- def __init__(self, references, policy):
1180- parent_cls = TestBranchMirrorerCheckAndFollowBranchReference
1181- super(parent_cls.StubbedBranchMirrorer, self).__init__(policy)
1182- self._reference_values = {}
1183- for i in range(len(references) - 1):
1184- self._reference_values[references[i]] = references[i+1]
1185- self.follow_reference_calls = []
1186-
1187- def followReference(self, url):
1188- self.follow_reference_calls.append(url)
1189- return self._reference_values[url]
1190-
1191- def makeBranchMirrorer(self, should_follow_references, references,
1192- unsafe_urls=None):
1193- policy = BlacklistPolicy(should_follow_references, unsafe_urls)
1194- opener = self.StubbedBranchMirrorer(references, policy)
1195- return opener
1196-
1197- def testCheckInitialURL(self):
1198- # checkSource rejects all URLs that are not allowed.
1199- opener = self.makeBranchMirrorer(None, [], set(['a']))
1200- self.assertRaises(BadUrl, opener.checkAndFollowBranchReference, 'a')
1201-
1202- def testNotReference(self):
1203- # When branch references are forbidden, checkAndFollowBranchReference
1204- # does not raise on non-references.
1205- opener = self.makeBranchMirrorer(False, ['a', None])
1206- self.assertEquals('a', opener.checkAndFollowBranchReference('a'))
1207- self.assertEquals(['a'], opener.follow_reference_calls)
1208-
1209- def testBranchReferenceForbidden(self):
1210- # checkAndFollowBranchReference raises BranchReferenceForbidden if
1211- # branch references are forbidden and the source URL points to a
1212- # branch reference.
1213- opener = self.makeBranchMirrorer(False, ['a', 'b'])
1214- self.assertRaises(
1215- BranchReferenceForbidden,
1216- opener.checkAndFollowBranchReference, 'a')
1217- self.assertEquals(['a'], opener.follow_reference_calls)
1218-
1219- def testAllowedReference(self):
1220- # checkAndFollowBranchReference does not raise if following references
1221- # is allowed and the source URL points to a branch reference to a
1222- # permitted location.
1223- opener = self.makeBranchMirrorer(True, ['a', 'b', None])
1224- self.assertEquals('b', opener.checkAndFollowBranchReference('a'))
1225- self.assertEquals(['a', 'b'], opener.follow_reference_calls)
1226-
1227- def testCheckReferencedURLs(self):
1228- # checkAndFollowBranchReference checks if the URL a reference points
1229- # to is safe.
1230- opener = self.makeBranchMirrorer(
1231- True, ['a', 'b', None], unsafe_urls=set('b'))
1232- self.assertRaises(BadUrl, opener.checkAndFollowBranchReference, 'a')
1233- self.assertEquals(['a'], opener.follow_reference_calls)
1234-
1235- def testSelfReferencingBranch(self):
1236- # checkAndFollowBranchReference raises BranchReferenceLoopError if
1237- # following references is allowed and the source url points to a
1238- # self-referencing branch reference.
1239- opener = self.makeBranchMirrorer(True, ['a', 'a'])
1240- self.assertRaises(
1241- BranchLoopError, opener.checkAndFollowBranchReference, 'a')
1242- self.assertEquals(['a'], opener.follow_reference_calls)
1243-
1244- def testBranchReferenceLoop(self):
1245- # checkAndFollowBranchReference raises BranchReferenceLoopError if
1246- # following references is allowed and the source url points to a loop
1247- # of branch references.
1248- references = ['a', 'b', 'a']
1249- opener = self.makeBranchMirrorer(True, references)
1250- self.assertRaises(
1251- BranchLoopError, opener.checkAndFollowBranchReference, 'a')
1252- self.assertEquals(['a', 'b'], opener.follow_reference_calls)
1253-
1254-
1255-class TestBranchMirrorerStacking(TestCaseWithTransport):
1256-
1257- def makeBranchMirrorer(self, allowed_urls):
1258- policy = WhitelistPolicy(True, allowed_urls, True)
1259- return BranchMirrorer(policy)
1260-
1261- def makeBranch(self, path, branch_format, repository_format):
1262- """Make a Bazaar branch at 'path' with the given formats."""
1263- bzrdir_format = BzrDirMetaFormat1()
1264- bzrdir_format.set_branch_format(branch_format)
1265- bzrdir = self.make_bzrdir(path, format=bzrdir_format)
1266- repository_format.initialize(bzrdir)
1267- return bzrdir.create_branch()
1268-
1269- def testAllowedURL(self):
1270- # checkSource does not raise an exception for branches stacked on
1271- # branches with allowed URLs.
1272- stacked_on_branch = self.make_branch('base-branch', format='1.6')
1273- stacked_branch = self.make_branch('stacked-branch', format='1.6')
1274- stacked_branch.set_stacked_on_url(stacked_on_branch.base)
1275- opener = self.makeBranchMirrorer(
1276- [stacked_branch.base, stacked_on_branch.base])
1277- # This doesn't raise an exception.
1278- opener.open(stacked_branch.base)
1279-
1280- def testUnstackableRepository(self):
1281- # checkSource treats branches with UnstackableRepositoryFormats as
1282- # being not stacked.
1283- branch = self.makeBranch(
1284- 'unstacked', BzrBranchFormat7(), RepositoryFormatKnitPack1())
1285- opener = self.makeBranchMirrorer([branch.base])
1286- # This doesn't raise an exception.
1287- opener.open(branch.base)
1288-
1289- def testAllowedRelativeURL(self):
1290- # checkSource passes on absolute urls to checkOneURL, even if the
1291- # value of stacked_on_location in the config is set to a relative URL.
1292- stacked_on_branch = self.make_branch('base-branch', format='1.6')
1293- stacked_branch = self.make_branch('stacked-branch', format='1.6')
1294- stacked_branch.set_stacked_on_url('../base-branch')
1295- opener = self.makeBranchMirrorer(
1296- [stacked_branch.base, stacked_on_branch.base])
1297- # Note that stacked_on_branch.base is not '../base-branch', it's an
1298- # absolute URL.
1299- self.assertNotEqual('../base-branch', stacked_on_branch.base)
1300- # This doesn't raise an exception.
1301- opener.open(stacked_branch.base)
1302-
1303- def testAllowedRelativeNested(self):
1304- # Relative URLs are resolved relative to the stacked branch.
1305- self.get_transport().mkdir('subdir')
1306- a = self.make_branch('subdir/a', format='1.6')
1307- b = self.make_branch('b', format='1.6')
1308- b.set_stacked_on_url('../subdir/a')
1309- c = self.make_branch('subdir/c', format='1.6')
1310- c.set_stacked_on_url('../../b')
1311- opener = self.makeBranchMirrorer([c.base, b.base, a.base])
1312- # This doesn't raise an exception.
1313- opener.open(c.base)
1314-
1315- def testForbiddenURL(self):
1316- # checkSource raises a BadUrl exception if a branch is stacked on a
1317- # branch with a forbidden URL.
1318- stacked_on_branch = self.make_branch('base-branch', format='1.6')
1319- stacked_branch = self.make_branch('stacked-branch', format='1.6')
1320- stacked_branch.set_stacked_on_url(stacked_on_branch.base)
1321- opener = self.makeBranchMirrorer([stacked_branch.base])
1322- self.assertRaises(BadUrl, opener.open, stacked_branch.base)
1323-
1324- def testForbiddenURLNested(self):
1325- # checkSource raises a BadUrl exception if a branch is stacked on a
1326- # branch that is in turn stacked on a branch with a forbidden URL.
1327- a = self.make_branch('a', format='1.6')
1328- b = self.make_branch('b', format='1.6')
1329- b.set_stacked_on_url(a.base)
1330- c = self.make_branch('c', format='1.6')
1331- c.set_stacked_on_url(b.base)
1332- opener = self.makeBranchMirrorer([c.base, b.base])
1333- self.assertRaises(BadUrl, opener.open, c.base)
1334-
1335- def testSelfStackedBranch(self):
1336- # checkSource raises StackingLoopError if a branch is stacked on
1337- # itself. This avoids infinite recursion errors.
1338- a = self.make_branch('a', format='1.6')
1339- # Bazaar 1.17 and up make it harder to create branches like this.
1340- # It's still worth testing that we don't blow up in the face of them,
1341- # so we grovel around a bit to create one anyway.
1342- a.get_config().set_user_option('stacked_on_location', a.base)
1343- opener = self.makeBranchMirrorer([a.base])
1344- self.assertRaises(BranchLoopError, opener.open, a.base)
1345-
1346- def testLoopStackedBranch(self):
1347- # checkSource raises StackingLoopError if a branch is stacked in such
1348- # a way so that it is ultimately stacked on itself. e.g. a stacked on
1349- # b stacked on a.
1350- a = self.make_branch('a', format='1.6')
1351- b = self.make_branch('b', format='1.6')
1352- a.set_stacked_on_url(b.base)
1353- b.set_stacked_on_url(a.base)
1354- opener = self.makeBranchMirrorer([a.base, b.base])
1355- self.assertRaises(BranchLoopError, opener.open, a.base)
1356- self.assertRaises(BranchLoopError, opener.open, b.base)
1357-
1358-
1359-class TestReferenceMirroring(TestCaseWithTransport):
1360- """Feature tests for mirroring of branch references."""
1361+class TestReferenceOpener(TestCaseWithTransport):
1362+ """Feature tests for safe opening of branch references."""
1363
1364 def createBranchReference(self, url):
1365 """Create a pure branch reference that points to the specified URL.
1366@@ -513,19 +317,19 @@
1367 self.assertEqual(opened_branch.base, target_branch.base)
1368
1369 def testFollowReferenceValue(self):
1370- # BranchMirrorer.followReference gives the reference value for
1371+ # SafeBranchOpener.followReference gives the reference value for
1372 # a branch reference.
1373- opener = BranchMirrorer(BranchPolicy())
1374+ opener = SafeBranchOpener(BranchOpenPolicy())
1375 reference_value = 'http://example.com/branch'
1376 reference_url = self.createBranchReference(reference_value)
1377 self.assertEqual(
1378 reference_value, opener.followReference(reference_url))
1379
1380 def testFollowReferenceNone(self):
1381- # BranchMirrorer.followReference gives None for a normal branch.
1382+ # SafeBranchOpener.followReference gives None for a normal branch.
1383 self.make_branch('repo')
1384 branch_url = self.get_url('repo')
1385- opener = BranchMirrorer(BranchPolicy())
1386+ opener = SafeBranchOpener(BranchOpenPolicy())
1387 self.assertIs(None, opener.followReference(branch_url))
1388
1389
1390
1391=== modified file 'lib/lp/codehosting/puller/tests/test_worker_formats.py'
1392--- lib/lp/codehosting/puller/tests/test_worker_formats.py 2010-08-20 20:31:18 +0000
1393+++ lib/lp/codehosting/puller/tests/test_worker_formats.py 2011-08-07 20:42:25 +0000
1394@@ -7,14 +7,18 @@
1395
1396 import unittest
1397
1398+import lp.codehosting # for bzr plugins
1399+
1400 from bzrlib.branch import Branch
1401 from bzrlib.bzrdir import (
1402- BzrDirFormat6,
1403 BzrDirMetaFormat1,
1404 )
1405 from bzrlib.repofmt.knitrepo import RepositoryFormatKnit1
1406-from bzrlib.repofmt.pack_repo import RepositoryFormatKnitPack5
1407-from bzrlib.repofmt.weaverepo import (
1408+from bzrlib.repofmt.knitpack_repo import RepositoryFormatKnitPack5
1409+from bzrlib.plugins.weave_fmt.bzrdir import (
1410+ BzrDirFormat6,
1411+ )
1412+from bzrlib.plugins.weave_fmt.repository import (
1413 RepositoryFormat6,
1414 RepositoryFormat7,
1415 )
1416
1417=== modified file 'lib/lp/codehosting/puller/worker.py'
1418--- lib/lp/codehosting/puller/worker.py 2011-06-03 01:00:53 +0000
1419+++ lib/lp/codehosting/puller/worker.py 2011-08-07 20:42:25 +0000
1420@@ -8,20 +8,32 @@
1421 import sys
1422 import urllib2
1423
1424-from bzrlib import errors
1425+import lp.codehosting # to load bzr plugins
1426+
1427+from bzrlib import (
1428+ errors,
1429+ urlutils,
1430+ )
1431 from bzrlib.branch import (
1432 Branch,
1433+ )
1434+from bzrlib.plugins.weave_fmt.branch import (
1435 BzrBranchFormat4,
1436 )
1437-from bzrlib.bzrdir import BzrDir
1438-from bzrlib.repofmt.weaverepo import (
1439+from bzrlib.plugins.weave_fmt.repository import (
1440 RepositoryFormat4,
1441 RepositoryFormat5,
1442 RepositoryFormat6,
1443 )
1444+
1445 import bzrlib.ui
1446+from bzrlib.plugins.loom.branch import LoomSupport
1447+from bzrlib.transport import get_transport
1448 from bzrlib.ui import SilentUIFactory
1449-from lazr.uri import InvalidURIError
1450+from lazr.uri import (
1451+ InvalidURIError,
1452+ URI,
1453+ )
1454
1455 from canonical.config import config
1456 from canonical.launchpad.webapp import errorlog
1457@@ -32,18 +44,21 @@
1458 from lp.code.enums import BranchType
1459 from lp.codehosting.bzrutils import identical_formats
1460 from lp.codehosting.puller import get_lock_id_for_branch_id
1461-from lp.codehosting.vfs.branchfs import (
1462- BadUrlLaunchpad,
1463- BadUrlScheme,
1464- BadUrlSsh,
1465- make_branch_mirrorer,
1466+from lp.codehosting.safe_open import (
1467+ BadUrl,
1468+ BranchLoopError,
1469+ BranchOpenPolicy,
1470+ BranchReferenceForbidden,
1471+ SafeBranchOpener,
1472 )
1473
1474
1475 __all__ = [
1476+ 'BadUrlLaunchpad',
1477+ 'BadUrlScheme',
1478+ 'BadUrlSsh',
1479 'BranchMirrorer',
1480- 'BranchLoopError',
1481- 'BranchReferenceForbidden',
1482+ 'BranchMirrorerPolicy',
1483 'get_canonical_url_for_branch_name',
1484 'install_worker_ui_factory',
1485 'PullerWorker',
1486@@ -51,19 +66,22 @@
1487 ]
1488
1489
1490-class BranchReferenceForbidden(Exception):
1491- """Trying to mirror a branch reference and the branch type does not allow
1492- references.
1493- """
1494-
1495-
1496-class BranchLoopError(Exception):
1497- """Encountered a branch cycle.
1498-
1499- A URL may point to a branch reference or it may point to a stacked branch.
1500- In either case, it's possible for there to be a cycle in these references,
1501- and this exception is raised when we detect such a cycle.
1502- """
1503+class BadUrlSsh(BadUrl):
1504+ """Tried to access a branch from sftp or bzr+ssh."""
1505+
1506+
1507+class BadUrlLaunchpad(BadUrl):
1508+ """Tried to access a branch from launchpad.net."""
1509+
1510+
1511+class BadUrlScheme(BadUrl):
1512+ """Found a URL with an untrusted scheme."""
1513+
1514+ def __init__(self, scheme, url):
1515+ BadUrl.__init__(self, scheme, url)
1516+ self.scheme = scheme
1517+
1518+
1519
1520
1521 def get_canonical_url_for_branch_name(unique_name):
1522@@ -120,12 +138,50 @@
1523 self.sendEvent('log', fmt % args)
1524
1525
1526+class BranchMirrorerPolicy(BranchOpenPolicy):
1527+ """The policy for what branches to open and how to stack them."""
1528+
1529+ def createDestinationBranch(self, source_branch, destination_url):
1530+ """Create a destination branch for 'source_branch'.
1531+
1532+ Creates a branch at 'destination_url' that is has the same format as
1533+ 'source_branch'. Any content already at 'destination_url' will be
1534+ deleted. Generally the new branch will have no revisions, but they
1535+ will be copied for import branches, because this can be done safely
1536+ and efficiently with a vfs-level copy (see `ImportedBranchPolicy`).
1537+
1538+ :param source_branch: The Bazaar branch that will be mirrored.
1539+ :param destination_url: The place to make the destination branch. This
1540+ URL must point to a writable location.
1541+ :return: The destination branch.
1542+ """
1543+ dest_transport = get_transport(destination_url)
1544+ if dest_transport.has('.'):
1545+ dest_transport.delete_tree('.')
1546+ if isinstance(source_branch, LoomSupport):
1547+ # Looms suck.
1548+ revision_id = None
1549+ else:
1550+ revision_id = 'null:'
1551+ source_branch.bzrdir.clone_on_transport(
1552+ dest_transport, revision_id=revision_id)
1553+ return Branch.open(destination_url)
1554+
1555+ def getStackedOnURLForDestinationBranch(self, source_branch,
1556+ destination_url):
1557+ """Get the stacked on URL for `source_branch`.
1558+
1559+ In particular, the URL it should be stacked on when it is mirrored to
1560+ `destination_url`.
1561+ """
1562+ return None
1563+
1564+
1565 class BranchMirrorer(object):
1566 """A `BranchMirrorer` safely makes mirrors of branches.
1567
1568- A `BranchMirrorer` has a `BranchPolicy` to tell it which URLs are safe to
1569- accesss, whether or not to follow branch references and how to stack
1570- branches when they are mirrored.
1571+ A `BranchMirrorer` has a `BranchOpenPolicy` to tell it which URLs are safe
1572+ to accesss and whether or not to follow branch references.
1573
1574 The mirrorer knows how to follow branch references, create new mirrors,
1575 update existing mirrors, determine stacked-on branches and the like.
1576@@ -136,92 +192,20 @@
1577 def __init__(self, policy, protocol=None, log=None):
1578 """Construct a branch opener with 'policy'.
1579
1580- :param policy: A `BranchPolicy` that tells us what URLs are valid and
1581- similar things.
1582+ :param policy: A `BranchOpenPolicy` that tells us what URLs are valid
1583+ and similar things.
1584 :param log: A callable which can be called with a format string and
1585 arguments to log messages in the scheduler, or None, in which case
1586 log messages are discarded.
1587 """
1588- self._seen_urls = set()
1589 self.policy = policy
1590 self.protocol = protocol
1591+ self.opener = SafeBranchOpener(policy)
1592 if log is not None:
1593 self.log = log
1594 else:
1595 self.log = lambda *args: None
1596
1597- def _runWithTransformFallbackLocationHookInstalled(
1598- self, callable, *args, **kw):
1599- Branch.hooks.install_named_hook(
1600- 'transform_fallback_location', self.transformFallbackLocationHook,
1601- 'BranchMirrorer.transformFallbackLocationHook')
1602- try:
1603- return callable(*args, **kw)
1604- finally:
1605- # XXX 2008-11-24 MichaelHudson, bug=301472: This is the hacky way
1606- # to remove a hook. The linked bug report asks for an API to do
1607- # it.
1608- Branch.hooks['transform_fallback_location'].remove(
1609- self.transformFallbackLocationHook)
1610- # We reset _seen_urls here to avoid multiple calls to open giving
1611- # spurious loop exceptions.
1612- self._seen_urls = set()
1613-
1614- def open(self, url):
1615- """Open the Bazaar branch at url, first checking for safety.
1616-
1617- What safety means is defined by a subclasses `followReference` and
1618- `checkOneURL` methods.
1619- """
1620- url = self.checkAndFollowBranchReference(url)
1621- return self._runWithTransformFallbackLocationHookInstalled(
1622- Branch.open, url)
1623-
1624- def transformFallbackLocationHook(self, branch, url):
1625- """Installed as the 'transform_fallback_location' Branch hook.
1626-
1627- This method calls `transformFallbackLocation` on the policy object and
1628- either returns the url it provides or passes it back to
1629- checkAndFollowBranchReference.
1630- """
1631- new_url, check = self.policy.transformFallbackLocation(branch, url)
1632- if check:
1633- return self.checkAndFollowBranchReference(new_url)
1634- else:
1635- return new_url
1636-
1637- def followReference(self, url):
1638- """Get the branch-reference value at the specified url.
1639-
1640- This exists as a separate method only to be overriden in unit tests.
1641- """
1642- bzrdir = BzrDir.open(url)
1643- return bzrdir.get_branch_reference()
1644-
1645- def checkAndFollowBranchReference(self, url):
1646- """Check URL (and possibly the referenced URL) for safety.
1647-
1648- This method checks that `url` passes the policy's `checkOneURL`
1649- method, and if `url` refers to a branch reference, it checks whether
1650- references are allowed and whether the reference's URL passes muster
1651- also -- recursively, until a real branch is found.
1652-
1653- :raise BranchLoopError: If the branch references form a loop.
1654- :raise BranchReferenceForbidden: If this opener forbids branch
1655- references.
1656- """
1657- while True:
1658- if url in self._seen_urls:
1659- raise BranchLoopError()
1660- self._seen_urls.add(url)
1661- self.policy.checkOneURL(url)
1662- next_url = self.followReference(url)
1663- if next_url is None:
1664- return url
1665- url = next_url
1666- if not self.policy.shouldFollowReferences():
1667- raise BranchReferenceForbidden(url)
1668-
1669 def createDestinationBranch(self, source_branch, destination_url):
1670 """Create a destination branch for 'source_branch'.
1671
1672@@ -234,7 +218,7 @@
1673 URL must point to a writable location.
1674 :return: The destination branch.
1675 """
1676- return self._runWithTransformFallbackLocationHookInstalled(
1677+ return self.opener.runWithTransformFallbackLocationHookInstalled(
1678 self.policy.createDestinationBranch, source_branch,
1679 destination_url)
1680
1681@@ -296,6 +280,9 @@
1682 stacked_on_url = self.updateBranch(source_branch, branch)
1683 return branch, revid_before, stacked_on_url
1684
1685+ def open(self, url):
1686+ return self.opener.open(url)
1687+
1688
1689 class PullerWorker:
1690 """This class represents a single branch that needs mirroring.
1691@@ -305,7 +292,7 @@
1692 """
1693
1694 def _checkerForBranchType(self, branch_type):
1695- """Return a `BranchMirrorer` with an appropriate `BranchPolicy`.
1696+ """Return a `BranchMirrorer` with an appropriate policy.
1697
1698 :param branch_type: A `BranchType`. The policy of the mirrorer will
1699 be based on this.
1700@@ -546,3 +533,151 @@
1701 created by another puller worker process.
1702 """
1703 bzrlib.ui.ui_factory = PullerWorkerUIFactory(puller_worker_protocol)
1704+
1705+
1706+class MirroredBranchPolicy(BranchMirrorerPolicy):
1707+ """Mirroring policy for MIRRORED branches.
1708+
1709+ In summary:
1710+
1711+ - follow references,
1712+ - only open non-Launchpad http: and https: URLs.
1713+ """
1714+
1715+ def __init__(self, stacked_on_url=None):
1716+ self.stacked_on_url = stacked_on_url
1717+
1718+ def getStackedOnURLForDestinationBranch(self, source_branch,
1719+ destination_url):
1720+ """Return the stacked on URL for the destination branch.
1721+
1722+ Mirrored branches are stacked on the default stacked-on branch of
1723+ their product, except when we're mirroring the default stacked-on
1724+ branch itself.
1725+ """
1726+ if self.stacked_on_url is None:
1727+ return None
1728+ stacked_on_url = urlutils.join(destination_url, self.stacked_on_url)
1729+ if destination_url == stacked_on_url:
1730+ return None
1731+ return self.stacked_on_url
1732+
1733+ def shouldFollowReferences(self):
1734+ """See `BranchOpenPolicy.shouldFollowReferences`.
1735+
1736+ We traverse branch references for MIRRORED branches because they
1737+ provide a useful redirection mechanism and we want to be consistent
1738+ with the bzr command line.
1739+ """
1740+ return True
1741+
1742+ def transformFallbackLocation(self, branch, url):
1743+ """See `BranchOpenPolicy.transformFallbackLocation`.
1744+
1745+ For mirrored branches, we stack on whatever the remote branch claims
1746+ to stack on, but this URL still needs to be checked.
1747+ """
1748+ return urlutils.join(branch.base, url), True
1749+
1750+ def checkOneURL(self, url):
1751+ """See `BranchOpenPolicy.checkOneURL`.
1752+
1753+ We refuse to mirror from Launchpad or a ssh-like or file URL.
1754+ """
1755+ # Avoid circular import
1756+ from lp.code.interfaces.branch import get_blacklisted_hostnames
1757+ uri = URI(url)
1758+ launchpad_domain = config.vhost.mainsite.hostname
1759+ if uri.underDomain(launchpad_domain):
1760+ raise BadUrlLaunchpad(url)
1761+ for hostname in get_blacklisted_hostnames():
1762+ if uri.underDomain(hostname):
1763+ raise BadUrl(url)
1764+ if uri.scheme in ['sftp', 'bzr+ssh']:
1765+ raise BadUrlSsh(url)
1766+ elif uri.scheme not in ['http', 'https']:
1767+ raise BadUrlScheme(uri.scheme, url)
1768+
1769+
1770+class ImportedBranchPolicy(BranchMirrorerPolicy):
1771+ """Mirroring policy for IMPORTED branches.
1772+
1773+ In summary:
1774+
1775+ - don't follow references,
1776+ - assert the URLs start with the prefix we expect for imported branches.
1777+ """
1778+
1779+ def createDestinationBranch(self, source_branch, destination_url):
1780+ """See `BranchOpenPolicy.createDestinationBranch`.
1781+
1782+ Because we control the process that creates import branches, a
1783+ vfs-level copy is safe and more efficient than a bzr fetch.
1784+ """
1785+ source_transport = source_branch.bzrdir.root_transport
1786+ dest_transport = get_transport(destination_url)
1787+ while True:
1788+ # We loop until the remote file list before and after the copy is
1789+ # the same to catch the case where the remote side is being
1790+ # mutated as we copy it.
1791+ if dest_transport.has('.'):
1792+ dest_transport.delete_tree('.')
1793+ files_before = set(source_transport.iter_files_recursive())
1794+ source_transport.copy_tree_to_transport(dest_transport)
1795+ files_after = set(source_transport.iter_files_recursive())
1796+ if files_before == files_after:
1797+ break
1798+ return Branch.open_from_transport(dest_transport)
1799+
1800+ def shouldFollowReferences(self):
1801+ """See `BranchOpenerPolicy.shouldFollowReferences`.
1802+
1803+ We do not traverse references for IMPORTED branches because the
1804+ code-import system should never produce branch references.
1805+ """
1806+ return False
1807+
1808+ def transformFallbackLocation(self, branch, url):
1809+ """See `BranchOpenerPolicy.transformFallbackLocation`.
1810+
1811+ Import branches should not be stacked, ever.
1812+ """
1813+ raise AssertionError("Import branch unexpectedly stacked!")
1814+
1815+ def checkOneURL(self, url):
1816+ """See `BranchOpenerPolicy.checkOneURL`.
1817+
1818+ If the URL we are mirroring from does not start how we expect the pull
1819+ URLs of import branches to start, something has gone badly wrong, so
1820+ we raise AssertionError if that's happened.
1821+ """
1822+ if not url.startswith(config.launchpad.bzr_imports_root_url):
1823+ raise AssertionError(
1824+ "Bogus URL for imported branch: %r" % url)
1825+
1826+
1827+def make_branch_mirrorer(branch_type, protocol=None,
1828+ mirror_stacked_on_url=None):
1829+ """Create a `BranchMirrorer` with the appropriate `BranchOpenerPolicy`.
1830+
1831+ :param branch_type: A `BranchType` to select a policy by.
1832+ :param protocol: Optional protocol for the mirrorer to work with.
1833+ If given, its log will also be used.
1834+ :param mirror_stacked_on_url: For mirrored branches, the default URL
1835+ to stack on. Ignored for other branch types.
1836+ :return: A `BranchMirrorer`.
1837+ """
1838+ if branch_type == BranchType.MIRRORED:
1839+ policy = MirroredBranchPolicy(mirror_stacked_on_url)
1840+ elif branch_type == BranchType.IMPORTED:
1841+ policy = ImportedBranchPolicy()
1842+ else:
1843+ raise AssertionError(
1844+ "Unexpected branch type: %r" % branch_type)
1845+
1846+ if protocol is not None:
1847+ log_function = protocol.log
1848+ else:
1849+ log_function = None
1850+
1851+ return BranchMirrorer(policy, protocol, log_function)
1852
1853=== added file 'lib/lp/codehosting/safe_open.py'
1854--- lib/lp/codehosting/safe_open.py 1970-01-01 00:00:00 +0000
1855+++ lib/lp/codehosting/safe_open.py 2011-08-07 20:42:25 +0000
1856@@ -0,0 +1,230 @@
1857+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1858+# GNU Affero General Public License version 3 (see the file LICENSE).
1859+
1860+"""Safe branch opening."""
1861+
1862+__metaclass__ = type
1863+
1864+from bzrlib import urlutils
1865+from bzrlib.branch import Branch
1866+from bzrlib.bzrdir import BzrDir
1867+
1868+__all__ = [
1869+ 'AcceptAnythingPolicy',
1870+ 'BadUrl',
1871+ 'BlacklistPolicy',
1872+ 'BranchLoopError',
1873+ 'BranchOpenPolicy',
1874+ 'BranchReferenceForbidden',
1875+ 'SafeBranchOpener',
1876+ 'WhitelistPolicy',
1877+ ]
1878+
1879+
1880+# TODO JelmerVernooij 2011-08-06: This module is generic enough to be
1881+# in bzrlib, and may be of use to others.
1882+
1883+
1884+class BadUrl(Exception):
1885+ """Tried to access a branch from a bad URL."""
1886+
1887+
1888+class BranchReferenceForbidden(Exception):
1889+ """Trying to mirror a branch reference and the branch type does not allow
1890+ references.
1891+ """
1892+
1893+
1894+class BranchLoopError(Exception):
1895+ """Encountered a branch cycle.
1896+
1897+ A URL may point to a branch reference or it may point to a stacked branch.
1898+ In either case, it's possible for there to be a cycle in these references,
1899+ and this exception is raised when we detect such a cycle.
1900+ """
1901+
1902+
1903+class BranchOpenPolicy:
1904+ """Policy on how to open branches.
1905+
1906+ In particular, a policy determines which branches are safe to open by
1907+ checking their URLs and deciding whether or not to follow branch
1908+ references.
1909+ """
1910+
1911+ def shouldFollowReferences(self):
1912+ """Whether we traverse references when mirroring.
1913+
1914+ Subclasses must override this method.
1915+
1916+ If we encounter a branch reference and this returns false, an error is
1917+ raised.
1918+
1919+ :returns: A boolean to indicate whether to follow a branch reference.
1920+ """
1921+ raise NotImplementedError(self.shouldFollowReferences)
1922+
1923+ def transformFallbackLocation(self, branch, url):
1924+ """Validate, maybe modify, 'url' to be used as a stacked-on location.
1925+
1926+ :param branch: The branch that is being opened.
1927+ :param url: The URL that the branch provides for its stacked-on
1928+ location.
1929+ :return: (new_url, check) where 'new_url' is the URL of the branch to
1930+ actually open and 'check' is true if 'new_url' needs to be
1931+ validated by checkAndFollowBranchReference.
1932+ """
1933+ raise NotImplementedError(self.transformFallbackLocation)
1934+
1935+ def checkOneURL(self, url):
1936+ """Check the safety of the source URL.
1937+
1938+ Subclasses must override this method.
1939+
1940+ :param url: The source URL to check.
1941+ :raise BadUrl: subclasses are expected to raise this or a subclass
1942+ when it finds a URL it deems to be unsafe.
1943+ """
1944+ raise NotImplementedError(self.checkOneURL)
1945+
1946+
1947+class BlacklistPolicy(BranchOpenPolicy):
1948+ """Branch policy that forbids certain URLs."""
1949+
1950+ def __init__(self, should_follow_references, unsafe_urls=None):
1951+ if unsafe_urls is None:
1952+ unsafe_urls = set()
1953+ self._unsafe_urls = unsafe_urls
1954+ self._should_follow_references = should_follow_references
1955+
1956+ def shouldFollowReferences(self):
1957+ return self._should_follow_references
1958+
1959+ def checkOneURL(self, url):
1960+ if url in self._unsafe_urls:
1961+ raise BadUrl(url)
1962+
1963+ def transformFallbackLocation(self, branch, url):
1964+ """See `BranchOpenPolicy.transformFallbackLocation`.
1965+
1966+ This class is not used for testing our smarter stacking features so we
1967+ just do the simplest thing: return the URL that would be used anyway
1968+ and don't check it.
1969+ """
1970+ return urlutils.join(branch.base, url), False
1971+
1972+
1973+class AcceptAnythingPolicy(BlacklistPolicy):
1974+ """Accept anything, to make testing easier."""
1975+
1976+ def __init__(self):
1977+ super(AcceptAnythingPolicy, self).__init__(True, set())
1978+
1979+
1980+class WhitelistPolicy(BranchOpenPolicy):
1981+ """Branch policy that only allows certain URLs."""
1982+
1983+ def __init__(self, should_follow_references, allowed_urls=None,
1984+ check=False):
1985+ if allowed_urls is None:
1986+ allowed_urls = []
1987+ self.allowed_urls = set(url.rstrip('/') for url in allowed_urls)
1988+ self.check = check
1989+
1990+ def shouldFollowReferences(self):
1991+ return self._should_follow_references
1992+
1993+ def checkOneURL(self, url):
1994+ if url.rstrip('/') not in self.allowed_urls:
1995+ raise BadUrl(url)
1996+
1997+ def transformFallbackLocation(self, branch, url):
1998+ """See `BranchOpenPolicy.transformFallbackLocation`.
1999+
2000+ Here we return the URL that would be used anyway and optionally check
2001+ it.
2002+ """
2003+ return urlutils.join(branch.base, url), self.check
2004+
2005+
2006+class SafeBranchOpener(object):
2007+ """Safe branch opener.
2008+
2009+ The policy object is expected to have the following methods:
2010+ * checkOneURL
2011+ * shouldFollowReferences
2012+ * transformFallbackLocation
2013+ """
2014+
2015+ def __init__(self, policy):
2016+ self.policy = policy
2017+ self._seen_urls = set()
2018+
2019+ def checkAndFollowBranchReference(self, url):
2020+ """Check URL (and possibly the referenced URL) for safety.
2021+
2022+ This method checks that `url` passes the policy's `checkOneURL`
2023+ method, and if `url` refers to a branch reference, it checks whether
2024+ references are allowed and whether the reference's URL passes muster
2025+ also -- recursively, until a real branch is found.
2026+
2027+ :raise BranchLoopError: If the branch references form a loop.
2028+ :raise BranchReferenceForbidden: If this opener forbids branch
2029+ references.
2030+ """
2031+ while True:
2032+ if url in self._seen_urls:
2033+ raise BranchLoopError()
2034+ self._seen_urls.add(url)
2035+ self.policy.checkOneURL(url)
2036+ next_url = self.followReference(url)
2037+ if next_url is None:
2038+ return url
2039+ url = next_url
2040+ if not self.policy.shouldFollowReferences():
2041+ raise BranchReferenceForbidden(url)
2042+
2043+ def transformFallbackLocationHook(self, branch, url):
2044+ """Installed as the 'transform_fallback_location' Branch hook.
2045+
2046+ This method calls `transformFallbackLocation` on the policy object and
2047+ either returns the url it provides or passes it back to
2048+ checkAndFollowBranchReference.
2049+ """
2050+ new_url, check = self.policy.transformFallbackLocation(branch, url)
2051+ if check:
2052+ return self.checkAndFollowBranchReference(new_url)
2053+ else:
2054+ return new_url
2055+
2056+ def runWithTransformFallbackLocationHookInstalled(
2057+ self, callable, *args, **kw):
2058+ Branch.hooks.install_named_hook(
2059+ 'transform_fallback_location', self.transformFallbackLocationHook,
2060+ 'SafeBranchOpener.transformFallbackLocationHook')
2061+ try:
2062+ return callable(*args, **kw)
2063+ finally:
2064+ Branch.hooks.uninstall_named_hook('transform_fallback_location',
2065+ 'SafeBranchOpener.transformFallbackLocationHook')
2066+ # We reset _seen_urls here to avoid multiple calls to open giving
2067+ # spurious loop exceptions.
2068+ self._seen_urls = set()
2069+
2070+ def followReference(self, url):
2071+ """Get the branch-reference value at the specified url.
2072+
2073+ This exists as a separate method only to be overriden in unit tests.
2074+ """
2075+ bzrdir = BzrDir.open(url)
2076+ return bzrdir.get_branch_reference()
2077+
2078+ def open(self, url):
2079+ """Open the Bazaar branch at url, first checking for safety.
2080+
2081+ What safety means is defined by a subclasses `followReference` and
2082+ `checkOneURL` methods.
2083+ """
2084+ url = self.checkAndFollowBranchReference(url)
2085+ return self.runWithTransformFallbackLocationHookInstalled(
2086+ Branch.open, url)
2087
2088=== added file 'lib/lp/codehosting/tests/test_safe_open.py'
2089--- lib/lp/codehosting/tests/test_safe_open.py 1970-01-01 00:00:00 +0000
2090+++ lib/lp/codehosting/tests/test_safe_open.py 2011-08-07 20:42:25 +0000
2091@@ -0,0 +1,223 @@
2092+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2093+# GNU Affero General Public License version 3 (see the file LICENSE).
2094+
2095+"""Tests for the safe branch open code."""
2096+
2097+
2098+__metaclass__ = type
2099+
2100+
2101+from lp.codehosting.puller.worker import (
2102+ BranchLoopError,
2103+ BranchReferenceForbidden,
2104+ SafeBranchOpener,
2105+ )
2106+from lp.codehosting.safe_open import (
2107+ BadUrl,
2108+ BlacklistPolicy,
2109+ WhitelistPolicy,
2110+ )
2111+
2112+from lp.testing import TestCase
2113+
2114+from bzrlib.branch import (
2115+ BzrBranchFormat7,
2116+ )
2117+from bzrlib.bzrdir import (
2118+ BzrDirMetaFormat1,
2119+ )
2120+from bzrlib.repofmt.pack_repo import RepositoryFormatKnitPack1
2121+from bzrlib.tests import (
2122+ TestCaseWithTransport,
2123+ )
2124+
2125+
2126+class TestSafeBranchOpenerCheckAndFollowBranchReference(TestCase):
2127+ """Unit tests for `SafeBranchOpener.checkAndFollowBranchReference`."""
2128+
2129+ class StubbedSafeBranchOpener(SafeBranchOpener):
2130+ """SafeBranchOpener that provides canned answers.
2131+
2132+ We implement the methods we need to to be able to control all the
2133+ inputs to the `BranchMirrorer.checkSource` method, which is what is
2134+ being tested in this class.
2135+ """
2136+
2137+ def __init__(self, references, policy):
2138+ parent_cls = TestSafeBranchOpenerCheckAndFollowBranchReference
2139+ super(parent_cls.StubbedSafeBranchOpener, self).__init__(policy)
2140+ self._reference_values = {}
2141+ for i in range(len(references) - 1):
2142+ self._reference_values[references[i]] = references[i+1]
2143+ self.follow_reference_calls = []
2144+
2145+ def followReference(self, url):
2146+ self.follow_reference_calls.append(url)
2147+ return self._reference_values[url]
2148+
2149+ def makeBranchOpener(self, should_follow_references, references,
2150+ unsafe_urls=None):
2151+ policy = BlacklistPolicy(should_follow_references, unsafe_urls)
2152+ opener = self.StubbedSafeBranchOpener(references, policy)
2153+ return opener
2154+
2155+ def testCheckInitialURL(self):
2156+ # checkSource rejects all URLs that are not allowed.
2157+ opener = self.makeBranchOpener(None, [], set(['a']))
2158+ self.assertRaises(BadUrl, opener.checkAndFollowBranchReference, 'a')
2159+
2160+ def testNotReference(self):
2161+ # When branch references are forbidden, checkAndFollowBranchReference
2162+ # does not raise on non-references.
2163+ opener = self.makeBranchOpener(False, ['a', None])
2164+ self.assertEquals('a', opener.checkAndFollowBranchReference('a'))
2165+ self.assertEquals(['a'], opener.follow_reference_calls)
2166+
2167+ def testBranchReferenceForbidden(self):
2168+ # checkAndFollowBranchReference raises BranchReferenceForbidden if
2169+ # branch references are forbidden and the source URL points to a
2170+ # branch reference.
2171+ opener = self.makeBranchOpener(False, ['a', 'b'])
2172+ self.assertRaises(
2173+ BranchReferenceForbidden,
2174+ opener.checkAndFollowBranchReference, 'a')
2175+ self.assertEquals(['a'], opener.follow_reference_calls)
2176+
2177+ def testAllowedReference(self):
2178+ # checkAndFollowBranchReference does not raise if following references
2179+ # is allowed and the source URL points to a branch reference to a
2180+ # permitted location.
2181+ opener = self.makeBranchOpener(True, ['a', 'b', None])
2182+ self.assertEquals('b', opener.checkAndFollowBranchReference('a'))
2183+ self.assertEquals(['a', 'b'], opener.follow_reference_calls)
2184+
2185+ def testCheckReferencedURLs(self):
2186+ # checkAndFollowBranchReference checks if the URL a reference points
2187+ # to is safe.
2188+ opener = self.makeBranchOpener(
2189+ True, ['a', 'b', None], unsafe_urls=set('b'))
2190+ self.assertRaises(BadUrl, opener.checkAndFollowBranchReference, 'a')
2191+ self.assertEquals(['a'], opener.follow_reference_calls)
2192+
2193+ def testSelfReferencingBranch(self):
2194+ # checkAndFollowBranchReference raises BranchReferenceLoopError if
2195+ # following references is allowed and the source url points to a
2196+ # self-referencing branch reference.
2197+ opener = self.makeBranchOpener(True, ['a', 'a'])
2198+ self.assertRaises(
2199+ BranchLoopError, opener.checkAndFollowBranchReference, 'a')
2200+ self.assertEquals(['a'], opener.follow_reference_calls)
2201+
2202+ def testBranchReferenceLoop(self):
2203+ # checkAndFollowBranchReference raises BranchReferenceLoopError if
2204+ # following references is allowed and the source url points to a loop
2205+ # of branch references.
2206+ references = ['a', 'b', 'a']
2207+ opener = self.makeBranchOpener(True, references)
2208+ self.assertRaises(
2209+ BranchLoopError, opener.checkAndFollowBranchReference, 'a')
2210+ self.assertEquals(['a', 'b'], opener.follow_reference_calls)
2211+
2212+
2213+class TestSafeBranchOpenerStacking(TestCaseWithTransport):
2214+
2215+ def makeBranchOpener(self, allowed_urls):
2216+ policy = WhitelistPolicy(True, allowed_urls, True)
2217+ return SafeBranchOpener(policy)
2218+
2219+ def makeBranch(self, path, branch_format, repository_format):
2220+ """Make a Bazaar branch at 'path' with the given formats."""
2221+ bzrdir_format = BzrDirMetaFormat1()
2222+ bzrdir_format.set_branch_format(branch_format)
2223+ bzrdir = self.make_bzrdir(path, format=bzrdir_format)
2224+ repository_format.initialize(bzrdir)
2225+ return bzrdir.create_branch()
2226+
2227+ def testAllowedURL(self):
2228+ # checkSource does not raise an exception for branches stacked on
2229+ # branches with allowed URLs.
2230+ stacked_on_branch = self.make_branch('base-branch', format='1.6')
2231+ stacked_branch = self.make_branch('stacked-branch', format='1.6')
2232+ stacked_branch.set_stacked_on_url(stacked_on_branch.base)
2233+ opener = self.makeBranchOpener(
2234+ [stacked_branch.base, stacked_on_branch.base])
2235+ # This doesn't raise an exception.
2236+ opener.open(stacked_branch.base)
2237+
2238+ def testUnstackableRepository(self):
2239+ # checkSource treats branches with UnstackableRepositoryFormats as
2240+ # being not stacked.
2241+ branch = self.makeBranch(
2242+ 'unstacked', BzrBranchFormat7(), RepositoryFormatKnitPack1())
2243+ opener = self.makeBranchOpener([branch.base])
2244+ # This doesn't raise an exception.
2245+ opener.open(branch.base)
2246+
2247+ def testAllowedRelativeURL(self):
2248+ # checkSource passes on absolute urls to checkOneURL, even if the
2249+ # value of stacked_on_location in the config is set to a relative URL.
2250+ stacked_on_branch = self.make_branch('base-branch', format='1.6')
2251+ stacked_branch = self.make_branch('stacked-branch', format='1.6')
2252+ stacked_branch.set_stacked_on_url('../base-branch')
2253+ opener = self.makeBranchOpener(
2254+ [stacked_branch.base, stacked_on_branch.base])
2255+ # Note that stacked_on_branch.base is not '../base-branch', it's an
2256+ # absolute URL.
2257+ self.assertNotEqual('../base-branch', stacked_on_branch.base)
2258+ # This doesn't raise an exception.
2259+ opener.open(stacked_branch.base)
2260+
2261+ def testAllowedRelativeNested(self):
2262+ # Relative URLs are resolved relative to the stacked branch.
2263+ self.get_transport().mkdir('subdir')
2264+ a = self.make_branch('subdir/a', format='1.6')
2265+ b = self.make_branch('b', format='1.6')
2266+ b.set_stacked_on_url('../subdir/a')
2267+ c = self.make_branch('subdir/c', format='1.6')
2268+ c.set_stacked_on_url('../../b')
2269+ opener = self.makeBranchOpener([c.base, b.base, a.base])
2270+ # This doesn't raise an exception.
2271+ opener.open(c.base)
2272+
2273+ def testForbiddenURL(self):
2274+ # checkSource raises a BadUrl exception if a branch is stacked on a
2275+ # branch with a forbidden URL.
2276+ stacked_on_branch = self.make_branch('base-branch', format='1.6')
2277+ stacked_branch = self.make_branch('stacked-branch', format='1.6')
2278+ stacked_branch.set_stacked_on_url(stacked_on_branch.base)
2279+ opener = self.makeBranchOpener([stacked_branch.base])
2280+ self.assertRaises(BadUrl, opener.open, stacked_branch.base)
2281+
2282+ def testForbiddenURLNested(self):
2283+ # checkSource raises a BadUrl exception if a branch is stacked on a
2284+ # branch that is in turn stacked on a branch with a forbidden URL.
2285+ a = self.make_branch('a', format='1.6')
2286+ b = self.make_branch('b', format='1.6')
2287+ b.set_stacked_on_url(a.base)
2288+ c = self.make_branch('c', format='1.6')
2289+ c.set_stacked_on_url(b.base)
2290+ opener = self.makeBranchOpener([c.base, b.base])
2291+ self.assertRaises(BadUrl, opener.open, c.base)
2292+
2293+ def testSelfStackedBranch(self):
2294+ # checkSource raises StackingLoopError if a branch is stacked on
2295+ # itself. This avoids infinite recursion errors.
2296+ a = self.make_branch('a', format='1.6')
2297+ # Bazaar 1.17 and up make it harder to create branches like this.
2298+ # It's still worth testing that we don't blow up in the face of them,
2299+ # so we grovel around a bit to create one anyway.
2300+ a.get_config().set_user_option('stacked_on_location', a.base)
2301+ opener = self.makeBranchOpener([a.base])
2302+ self.assertRaises(BranchLoopError, opener.open, a.base)
2303+
2304+ def testLoopStackedBranch(self):
2305+ # checkSource raises StackingLoopError if a branch is stacked in such
2306+ # a way so that it is ultimately stacked on itself. e.g. a stacked on
2307+ # b stacked on a.
2308+ a = self.make_branch('a', format='1.6')
2309+ b = self.make_branch('b', format='1.6')
2310+ a.set_stacked_on_url(b.base)
2311+ b.set_stacked_on_url(a.base)
2312+ opener = self.makeBranchOpener([a.base, b.base])
2313+ self.assertRaises(BranchLoopError, opener.open, a.base)
2314+ self.assertRaises(BranchLoopError, opener.open, b.base)
2315
2316=== modified file 'lib/lp/codehosting/vfs/__init__.py'
2317--- lib/lp/codehosting/vfs/__init__.py 2010-09-22 17:16:19 +0000
2318+++ lib/lp/codehosting/vfs/__init__.py 2011-08-07 20:42:25 +0000
2319@@ -11,7 +11,6 @@
2320 'get_ro_server',
2321 'get_rw_server',
2322 'LaunchpadServer',
2323- 'make_branch_mirrorer',
2324 ]
2325
2326 from lp.codehosting.vfs.branchfs import (
2327@@ -21,7 +20,6 @@
2328 get_ro_server,
2329 get_rw_server,
2330 LaunchpadServer,
2331- make_branch_mirrorer,
2332 )
2333 from lp.codehosting.vfs.branchfsclient import (
2334 BranchFileSystemClient,
2335
2336=== modified file 'lib/lp/codehosting/vfs/branchfs.py'
2337--- lib/lp/codehosting/vfs/branchfs.py 2011-05-25 19:57:07 +0000
2338+++ lib/lp/codehosting/vfs/branchfs.py 2011-08-07 20:42:25 +0000
2339@@ -46,17 +46,14 @@
2340 __metaclass__ = type
2341 __all__ = [
2342 'AsyncLaunchpadTransport',
2343- 'BadUrl',
2344 'BadUrlLaunchpad',
2345 'BadUrlScheme',
2346 'BadUrlSsh',
2347 'branch_id_to_path',
2348- 'BranchPolicy',
2349 'DirectDatabaseLaunchpadServer',
2350 'get_lp_server',
2351 'get_ro_server',
2352 'get_rw_server',
2353- 'make_branch_mirrorer',
2354 'LaunchpadInternalServer',
2355 'LaunchpadServer',
2356 ]
2357@@ -65,7 +62,6 @@
2358 import xmlrpclib
2359
2360 from bzrlib import urlutils
2361-from bzrlib.branch import Branch
2362 from bzrlib.bzrdir import (
2363 BzrDir,
2364 BzrDirFormat,
2365@@ -76,7 +72,6 @@
2366 PermissionDenied,
2367 TransportNotPossible,
2368 )
2369-from bzrlib.plugins.loom.branch import LoomSupport
2370 from bzrlib.smart.request import jail_info
2371 from bzrlib.transport import get_transport
2372 from bzrlib.transport.memory import MemoryServer
2373@@ -98,7 +93,6 @@
2374 from canonical.config import config
2375 from canonical.launchpad.webapp import errorlog
2376 from canonical.launchpad.xmlrpc import faults
2377-from lp.code.enums import BranchType
2378 from lp.code.interfaces.branchlookup import IBranchLookup
2379 from lp.code.interfaces.codehosting import (
2380 BRANCH_TRANSPORT,
2381@@ -126,26 +120,6 @@
2382 )
2383
2384
2385-class BadUrl(Exception):
2386- """Tried to access a branch from a bad URL."""
2387-
2388-
2389-class BadUrlSsh(BadUrl):
2390- """Tried to access a branch from sftp or bzr+ssh."""
2391-
2392-
2393-class BadUrlLaunchpad(BadUrl):
2394- """Tried to access a branch from launchpad.net."""
2395-
2396-
2397-class BadUrlScheme(BadUrl):
2398- """Found a URL with an untrusted scheme."""
2399-
2400- def __init__(self, scheme, url):
2401- BadUrl.__init__(self, scheme, url)
2402- self.scheme = scheme
2403-
2404-
2405 # The directories allowed directly beneath a branch directory. These are the
2406 # directories that Bazaar creates as part of regular operation. We support
2407 # only two numbered backups to avoid indefinite space usage.
2408@@ -774,234 +748,3 @@
2409 seen_new_branch_hook)
2410 return lp_server
2411
2412-
2413-class BranchPolicy:
2414- """Policy on how to mirror branches.
2415-
2416- In particular, a policy determines which branches are safe to mirror by
2417- checking their URLs and deciding whether or not to follow branch
2418- references. A policy also determines how the mirrors of branches should be
2419- stacked.
2420- """
2421-
2422- def createDestinationBranch(self, source_branch, destination_url):
2423- """Create a destination branch for 'source_branch'.
2424-
2425- Creates a branch at 'destination_url' that is has the same format as
2426- 'source_branch'. Any content already at 'destination_url' will be
2427- deleted. Generally the new branch will have no revisions, but they
2428- will be copied for import branches, because this can be done safely
2429- and efficiently with a vfs-level copy (see `ImportedBranchPolicy`,
2430- below).
2431-
2432- :param source_branch: The Bazaar branch that will be mirrored.
2433- :param destination_url: The place to make the destination branch. This
2434- URL must point to a writable location.
2435- :return: The destination branch.
2436- """
2437- dest_transport = get_transport(destination_url)
2438- if dest_transport.has('.'):
2439- dest_transport.delete_tree('.')
2440- if isinstance(source_branch, LoomSupport):
2441- # Looms suck.
2442- revision_id = None
2443- else:
2444- revision_id = 'null:'
2445- source_branch.bzrdir.clone_on_transport(
2446- dest_transport, revision_id=revision_id)
2447- return Branch.open(destination_url)
2448-
2449- def getStackedOnURLForDestinationBranch(self, source_branch,
2450- destination_url):
2451- """Get the stacked on URL for `source_branch`.
2452-
2453- In particular, the URL it should be stacked on when it is mirrored to
2454- `destination_url`.
2455- """
2456- return None
2457-
2458- def shouldFollowReferences(self):
2459- """Whether we traverse references when mirroring.
2460-
2461- Subclasses must override this method.
2462-
2463- If we encounter a branch reference and this returns false, an error is
2464- raised.
2465-
2466- :returns: A boolean to indicate whether to follow a branch reference.
2467- """
2468- raise NotImplementedError(self.shouldFollowReferences)
2469-
2470- def transformFallbackLocation(self, branch, url):
2471- """Validate, maybe modify, 'url' to be used as a stacked-on location.
2472-
2473- :param branch: The branch that is being opened.
2474- :param url: The URL that the branch provides for its stacked-on
2475- location.
2476- :return: (new_url, check) where 'new_url' is the URL of the branch to
2477- actually open and 'check' is true if 'new_url' needs to be
2478- validated by checkAndFollowBranchReference.
2479- """
2480- raise NotImplementedError(self.transformFallbackLocation)
2481-
2482- def checkOneURL(self, url):
2483- """Check the safety of the source URL.
2484-
2485- Subclasses must override this method.
2486-
2487- :param url: The source URL to check.
2488- :raise BadUrl: subclasses are expected to raise this or a subclass
2489- when it finds a URL it deems to be unsafe.
2490- """
2491- raise NotImplementedError(self.checkOneURL)
2492-
2493-
2494-class MirroredBranchPolicy(BranchPolicy):
2495- """Mirroring policy for MIRRORED branches.
2496-
2497- In summary:
2498-
2499- - follow references,
2500- - only open non-Launchpad http: and https: URLs.
2501- """
2502-
2503- def __init__(self, stacked_on_url=None):
2504- self.stacked_on_url = stacked_on_url
2505-
2506- def getStackedOnURLForDestinationBranch(self, source_branch,
2507- destination_url):
2508- """See `BranchPolicy.getStackedOnURLForDestinationBranch`.
2509-
2510- Mirrored branches are stacked on the default stacked-on branch of
2511- their product, except when we're mirroring the default stacked-on
2512- branch itself.
2513- """
2514- if self.stacked_on_url is None:
2515- return None
2516- stacked_on_url = urlutils.join(destination_url, self.stacked_on_url)
2517- if destination_url == stacked_on_url:
2518- return None
2519- return self.stacked_on_url
2520-
2521- def shouldFollowReferences(self):
2522- """See `BranchPolicy.shouldFollowReferences`.
2523-
2524- We traverse branch references for MIRRORED branches because they
2525- provide a useful redirection mechanism and we want to be consistent
2526- with the bzr command line.
2527- """
2528- return True
2529-
2530- def transformFallbackLocation(self, branch, url):
2531- """See `BranchPolicy.transformFallbackLocation`.
2532-
2533- For mirrored branches, we stack on whatever the remote branch claims
2534- to stack on, but this URL still needs to be checked.
2535- """
2536- return urlutils.join(branch.base, url), True
2537-
2538- def checkOneURL(self, url):
2539- """See `BranchPolicy.checkOneURL`.
2540-
2541- We refuse to mirror from Launchpad or a ssh-like or file URL.
2542- """
2543- # Avoid circular import
2544- from lp.code.interfaces.branch import get_blacklisted_hostnames
2545- uri = URI(url)
2546- launchpad_domain = config.vhost.mainsite.hostname
2547- if uri.underDomain(launchpad_domain):
2548- raise BadUrlLaunchpad(url)
2549- for hostname in get_blacklisted_hostnames():
2550- if uri.underDomain(hostname):
2551- raise BadUrl(url)
2552- if uri.scheme in ['sftp', 'bzr+ssh']:
2553- raise BadUrlSsh(url)
2554- elif uri.scheme not in ['http', 'https']:
2555- raise BadUrlScheme(uri.scheme, url)
2556-
2557-
2558-class ImportedBranchPolicy(BranchPolicy):
2559- """Mirroring policy for IMPORTED branches.
2560-
2561- In summary:
2562-
2563- - don't follow references,
2564- - assert the URLs start with the prefix we expect for imported branches.
2565- """
2566-
2567- def createDestinationBranch(self, source_branch, destination_url):
2568- """See `BranchPolicy.createDestinationBranch`.
2569-
2570- Because we control the process that creates import branches, a
2571- vfs-level copy is safe and more efficient than a bzr fetch.
2572- """
2573- source_transport = source_branch.bzrdir.root_transport
2574- dest_transport = get_transport(destination_url)
2575- while True:
2576- # We loop until the remote file list before and after the copy is
2577- # the same to catch the case where the remote side is being
2578- # mutated as we copy it.
2579- if dest_transport.has('.'):
2580- dest_transport.delete_tree('.')
2581- files_before = set(source_transport.iter_files_recursive())
2582- source_transport.copy_tree_to_transport(dest_transport)
2583- files_after = set(source_transport.iter_files_recursive())
2584- if files_before == files_after:
2585- break
2586- return Branch.open_from_transport(dest_transport)
2587-
2588- def shouldFollowReferences(self):
2589- """See `BranchPolicy.shouldFollowReferences`.
2590-
2591- We do not traverse references for IMPORTED branches because the
2592- code-import system should never produce branch references.
2593- """
2594- return False
2595-
2596- def transformFallbackLocation(self, branch, url):
2597- """See `BranchPolicy.transformFallbackLocation`.
2598-
2599- Import branches should not be stacked, ever.
2600- """
2601- raise AssertionError("Import branch unexpectedly stacked!")
2602-
2603- def checkOneURL(self, url):
2604- """See `BranchPolicy.checkOneURL`.
2605-
2606- If the URL we are mirroring from does not start how we expect the pull
2607- URLs of import branches to start, something has gone badly wrong, so
2608- we raise AssertionError if that's happened.
2609- """
2610- if not url.startswith(config.launchpad.bzr_imports_root_url):
2611- raise AssertionError(
2612- "Bogus URL for imported branch: %r" % url)
2613-
2614-
2615-def make_branch_mirrorer(branch_type, protocol=None,
2616- mirror_stacked_on_url=None):
2617- """Create a `BranchMirrorer` with the appropriate `BranchPolicy`.
2618-
2619- :param branch_type: A `BranchType` to select a policy by.
2620- :param protocol: Optional protocol for the mirrorer to work with.
2621- If given, its log will also be used.
2622- :param mirror_stacked_on_url: For mirrored branches, the default URL
2623- to stack on. Ignored for other branch types.
2624- :return: A `BranchMirrorer`.
2625- """
2626- # Avoid circular import
2627- from lp.codehosting.puller.worker import BranchMirrorer
2628-
2629- if branch_type == BranchType.MIRRORED:
2630- policy = MirroredBranchPolicy(mirror_stacked_on_url)
2631- elif branch_type == BranchType.IMPORTED:
2632- policy = ImportedBranchPolicy()
2633- else:
2634- raise AssertionError(
2635- "Unexpected branch type: %r" % branch_type)
2636-
2637- if protocol is not None:
2638- log_function = protocol.log
2639- else:
2640- log_function = None
2641-
2642- return BranchMirrorer(policy, protocol, log_function)
2643
2644=== modified file 'lib/lp/registry/browser/productseries.py'
2645--- lib/lp/registry/browser/productseries.py 2011-05-27 21:12:25 +0000
2646+++ lib/lp/registry/browser/productseries.py 2011-08-07 20:42:25 +0000
2647@@ -827,15 +827,6 @@
2648 ))
2649
2650
2651-class RevisionControlSystemsExtended(RevisionControlSystems):
2652- """External RCS plus Bazaar."""
2653- BZR = DBItem(99, """
2654- Bazaar
2655-
2656- External Bazaar branch.
2657- """)
2658-
2659-
2660 class SetBranchForm(Interface):
2661 """The fields presented on the form for setting a branch."""
2662
2663@@ -844,7 +835,7 @@
2664 ['cvs_module'])
2665
2666 rcs_type = Choice(title=_("Type of RCS"),
2667- required=False, vocabulary=RevisionControlSystemsExtended,
2668+ required=False, vocabulary=RevisionControlSystems,
2669 description=_(
2670 "The version control system to import from. "))
2671
2672@@ -908,7 +899,7 @@
2673 @property
2674 def initial_values(self):
2675 return dict(
2676- rcs_type=RevisionControlSystemsExtended.BZR,
2677+ rcs_type=RevisionControlSystems.BZR,
2678 branch_type=LINK_LP_BZR,
2679 branch_location=self.context.branch)
2680
2681@@ -989,7 +980,7 @@
2682 self.setFieldError(
2683 'rcs_type',
2684 'You must specify the type of RCS for the remote host.')
2685- elif rcs_type == RevisionControlSystemsExtended.CVS:
2686+ elif rcs_type == RevisionControlSystems.CVS:
2687 if 'cvs_module' not in data:
2688 self.setFieldError(
2689 'cvs_module',
2690@@ -1022,8 +1013,9 @@
2691 # Extend the allowed schemes for the repository URL based on
2692 # rcs_type.
2693 extra_schemes = {
2694- RevisionControlSystemsExtended.BZR_SVN: ['svn'],
2695- RevisionControlSystemsExtended.GIT: ['git'],
2696+ RevisionControlSystems.BZR_SVN: ['svn'],
2697+ RevisionControlSystems.GIT: ['git'],
2698+ RevisionControlSystems.BZR: ['bzr'],
2699 }
2700 schemes.update(extra_schemes.get(rcs_type, []))
2701 return schemes
2702@@ -1050,7 +1042,7 @@
2703 # The branch location is not required for validation.
2704 self._setRequired(['branch_location'], False)
2705 # The cvs_module is required if it is a CVS import.
2706- if rcs_type == RevisionControlSystemsExtended.CVS:
2707+ if rcs_type == RevisionControlSystems.CVS:
2708 self._setRequired(['cvs_module'], True)
2709 else:
2710 raise AssertionError("Unknown branch type %s" % branch_type)
2711@@ -1110,7 +1102,7 @@
2712 # Either create an externally hosted bzr branch
2713 # (a.k.a. 'mirrored') or create a new code import.
2714 rcs_type = data.get('rcs_type')
2715- if rcs_type == RevisionControlSystemsExtended.BZR:
2716+ if rcs_type == RevisionControlSystems.BZR:
2717 branch = self._createBzrBranch(
2718 BranchType.MIRRORED, branch_name, branch_owner,
2719 data['repo_url'])
2720@@ -1123,7 +1115,7 @@
2721 'the series.')
2722 else:
2723 # We need to create an import request.
2724- if rcs_type == RevisionControlSystemsExtended.CVS:
2725+ if rcs_type == RevisionControlSystems.CVS:
2726 cvs_root = data.get('repo_url')
2727 cvs_module = data.get('cvs_module')
2728 url = None
2729
2730=== modified file 'lib/lp/testing/factory.py'
2731--- lib/lp/testing/factory.py 2011-08-05 06:40:47 +0000
2732+++ lib/lp/testing/factory.py 2011-08-07 20:42:25 +0000
2733@@ -474,7 +474,7 @@
2734 branch_id = self.getUniqueInteger()
2735 if rcstype is None:
2736 rcstype = 'svn'
2737- if rcstype in ['svn', 'bzr-svn', 'hg']:
2738+ if rcstype in ['svn', 'bzr-svn', 'hg', 'bzr']:
2739 assert cvs_root is cvs_module is None
2740 if url is None:
2741 url = self.getUniqueURL()
2742@@ -2117,7 +2117,8 @@
2743
2744 def makeCodeImport(self, svn_branch_url=None, cvs_root=None,
2745 cvs_module=None, target=None, branch_name=None,
2746- git_repo_url=None, hg_repo_url=None, registrant=None,
2747+ git_repo_url=None, hg_repo_url=None,
2748+ bzr_branch_url=None, registrant=None,
2749 rcs_type=None, review_status=None):
2750 """Create and return a new, arbitrary code import.
2751
2752@@ -2126,7 +2127,7 @@
2753 unique URL.
2754 """
2755 if (svn_branch_url is cvs_root is cvs_module is git_repo_url is
2756- hg_repo_url is None):
2757+ hg_repo_url is bzr_branch_url is None):
2758 svn_branch_url = self.getUniqueURL()
2759
2760 if target is None:
2761@@ -2157,6 +2158,11 @@
2762 registrant, target, branch_name,
2763 rcs_type=RevisionControlSystems.HG,
2764 url=hg_repo_url)
2765+ elif bzr_branch_url is not None:
2766+ code_import = code_import_set.new(
2767+ registrant, target, branch_name,
2768+ rcs_type=RevisionControlSystems.BZR,
2769+ url=bzr_branch_url)
2770 else:
2771 assert rcs_type in (None, RevisionControlSystems.CVS)
2772 code_import = code_import_set.new(
2773
2774=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
2775--- lib/lp/translations/scripts/translations_to_branch.py 2011-05-27 13:36:02 +0000
2776+++ lib/lp/translations/scripts/translations_to_branch.py 2011-08-07 20:42:25 +0000
2777@@ -14,6 +14,7 @@
2778 import os.path
2779
2780 from bzrlib.errors import NotBranchError
2781+from bzrlib.revision import NULL_REVISION
2782 import pytz
2783 from storm.expr import (
2784 And,
2785@@ -157,7 +158,9 @@
2786
2787 revno, current_rev = branch.last_revision_info()
2788 repository = branch.repository
2789- for rev_id in repository.iter_reverse_revision_history(current_rev):
2790+ graph = repository.get_graph()
2791+ for rev_id in graph.iter_lefthand_ancestry(
2792+ current_rev, (NULL_REVISION, )):
2793 revision = repository.get_revision(rev_id)
2794 revision_date = self._getRevisionTime(revision)
2795 if self._isTranslationsCommit(revision):
2796
2797=== modified file 'scripts/code-import-worker.py'
2798--- scripts/code-import-worker.py 2011-06-16 23:43:04 +0000
2799+++ scripts/code-import-worker.py 2011-08-07 20:42:25 +0000
2800@@ -26,8 +26,9 @@
2801 from canonical.config import config
2802 from lp.codehosting import load_optional_plugin
2803 from lp.codehosting.codeimport.worker import (
2804- BzrSvnImportWorker, CSCVSImportWorker, CodeImportSourceDetails,
2805- GitImportWorker, HgImportWorker, get_default_bazaar_branch_store)
2806+ BzrImportWorker, BzrSvnImportWorker, CSCVSImportWorker,
2807+ CodeImportSourceDetails, GitImportWorker, HgImportWorker,
2808+ get_default_bazaar_branch_store)
2809 from canonical.launchpad import scripts
2810
2811
2812@@ -65,6 +66,10 @@
2813 elif source_details.rcstype == 'hg':
2814 load_optional_plugin('hg')
2815 import_worker_cls = HgImportWorker
2816+ elif source_details.rcstype == 'bzr':
2817+ load_optional_plugin('loom')
2818+ load_optional_plugin('weave_fmt')
2819+ import_worker_cls = BzrImportWorker
2820 elif source_details.rcstype in ['cvs', 'svn']:
2821 import_worker_cls = CSCVSImportWorker
2822 else:
2823
2824=== modified file 'versions.cfg'
2825--- versions.cfg 2011-08-03 13:05:12 +0000
2826+++ versions.cfg 2011-08-07 20:42:25 +0000
2827@@ -7,7 +7,7 @@
2828 ampoule = 0.2.0
2829 amqplib = 0.6.1
2830 BeautifulSoup = 3.1.0.1
2831-bzr = 2.3.3
2832+bzr = 2.4b4-r6001
2833 chameleon.core = 1.0b35
2834 chameleon.zpt = 1.0b17
2835 ClientForm = 0.2.10