Merge lp:~jelmer/launchpad/bzr-code-imports into lp:launchpad
- bzr-code-imports
- Merge into devel
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 |
Related bugs: |
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.
Commit message
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.
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal | # |
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.
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:
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.
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.
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.
>
> 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
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.
> >
> > 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.
lp.codehosting.
(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
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?
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.
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?
Gavin Panella (allenap) : | # |
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
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 |
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.")