Merge lp:~cjwatson/launchpad/git-lookup into lp:launchpad
- git-lookup
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | no longer in the source branch. |
Merged at revision: | 17364 |
Proposed branch: | lp:~cjwatson/launchpad/git-lookup |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/git-defaults |
Diff against target: |
1320 lines (+974/-241) 9 files modified
lib/lp/code/configure.zcml (+18/-0) lib/lp/code/interfaces/gitlookup.py (+137/-0) lib/lp/code/interfaces/gitnamespace.py (+0/-77) lib/lp/code/model/branchlookup.py (+3/-3) lib/lp/code/model/gitlookup.py (+349/-0) lib/lp/code/model/gitnamespace.py (+0/-159) lib/lp/code/model/gitrepository.py (+5/-2) lib/lp/code/model/tests/test_gitlookup.py (+449/-0) lib/lp/code/model/tests/test_gitrepository.py (+13/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-lookup |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+250628@code.launchpad.net |
Commit message
Add support for looking up Git repositories by path.
Description of the change
Add support for looking up Git repositories by path.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Needs Fixing
(code)
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/configure.zcml' | |||
2 | --- lib/lp/code/configure.zcml 2015-02-20 16:28:06 +0000 | |||
3 | +++ lib/lp/code/configure.zcml 2015-02-27 10:23:01 +0000 | |||
4 | @@ -865,6 +865,24 @@ | |||
5 | 865 | <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" /> | 865 | <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" /> |
6 | 866 | <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" /> | 866 | <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" /> |
7 | 867 | 867 | ||
8 | 868 | <class class="lp.code.model.gitlookup.GitLookup"> | ||
9 | 869 | <allow interface="lp.code.interfaces.gitlookup.IGitLookup" /> | ||
10 | 870 | </class> | ||
11 | 871 | <securedutility | ||
12 | 872 | class="lp.code.model.gitlookup.GitLookup" | ||
13 | 873 | provides="lp.code.interfaces.gitlookup.IGitLookup"> | ||
14 | 874 | <allow interface="lp.code.interfaces.gitlookup.IGitLookup" /> | ||
15 | 875 | </securedutility> | ||
16 | 876 | <securedutility | ||
17 | 877 | class="lp.code.model.gitlookup.GitTraverser" | ||
18 | 878 | provides="lp.code.interfaces.gitlookup.IGitTraverser"> | ||
19 | 879 | <allow interface="lp.code.interfaces.gitlookup.IGitTraverser" /> | ||
20 | 880 | </securedutility> | ||
21 | 881 | <adapter factory="lp.code.model.gitlookup.PersonGitTraversable" /> | ||
22 | 882 | <adapter factory="lp.code.model.gitlookup.ProjectGitTraversable" /> | ||
23 | 883 | <adapter factory="lp.code.model.gitlookup.DistributionGitTraversable" /> | ||
24 | 884 | <adapter factory="lp.code.model.gitlookup.DistributionSourcePackageGitTraversable" /> | ||
25 | 885 | |||
26 | 868 | <lp:help-folder folder="help" name="+help-code" /> | 886 | <lp:help-folder folder="help" name="+help-code" /> |
27 | 869 | 887 | ||
28 | 870 | <!-- Diffs --> | 888 | <!-- Diffs --> |
29 | 871 | 889 | ||
30 | === added file 'lib/lp/code/interfaces/gitlookup.py' | |||
31 | --- lib/lp/code/interfaces/gitlookup.py 1970-01-01 00:00:00 +0000 | |||
32 | +++ lib/lp/code/interfaces/gitlookup.py 2015-02-27 10:23:01 +0000 | |||
33 | @@ -0,0 +1,137 @@ | |||
34 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
35 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
36 | 3 | |||
37 | 4 | """Utility for looking up Git repositories by name.""" | ||
38 | 5 | |||
39 | 6 | __metaclass__ = type | ||
40 | 7 | __all__ = [ | ||
41 | 8 | 'IGitLookup', | ||
42 | 9 | 'IGitTraversable', | ||
43 | 10 | 'IGitTraverser', | ||
44 | 11 | ] | ||
45 | 12 | |||
46 | 13 | from zope.interface import Interface | ||
47 | 14 | |||
48 | 15 | |||
49 | 16 | class IGitTraversable(Interface): | ||
50 | 17 | """A thing that can be traversed to find a thing with a Git repository.""" | ||
51 | 18 | |||
52 | 19 | def traverse(owner, name, segments): | ||
53 | 20 | """Return the object beneath this one that matches 'name'. | ||
54 | 21 | |||
55 | 22 | :param owner: The current `IPerson` context, or None. | ||
56 | 23 | :param name: The name of the object being traversed to. | ||
57 | 24 | :param segments: An iterator over remaining path segments. | ||
58 | 25 | :return: A tuple of | ||
59 | 26 | * an `IPerson`, or None; | ||
60 | 27 | * an `IGitTraversable`; | ||
61 | 28 | * an `IGitRepository`, or None; if this is non-None then | ||
62 | 29 | traversing should stop. | ||
63 | 30 | """ | ||
64 | 31 | |||
65 | 32 | |||
66 | 33 | class IGitTraverser(Interface): | ||
67 | 34 | """Utility for traversing to an object that can have a Git repository.""" | ||
68 | 35 | |||
69 | 36 | def traverse(segments): | ||
70 | 37 | """Traverse to the object referred to by a prefix of the 'segments' | ||
71 | 38 | iterable. | ||
72 | 39 | |||
73 | 40 | :raises InvalidNamespace: If the path cannot be parsed as a | ||
74 | 41 | repository namespace. | ||
75 | 42 | :raises InvalidProductName: If the project component of the path is | ||
76 | 43 | not a valid name. | ||
77 | 44 | :raises NoSuchGitRepository: If there is a '+git' segment, but the | ||
78 | 45 | following segment doesn't match an existing Git repository. | ||
79 | 46 | :raises NoSuchPerson: If the first segment of the path begins with a | ||
80 | 47 | '~', but we can't find a person matching the remainder. | ||
81 | 48 | :raises NoSuchProduct: If we can't find a project that matches the | ||
82 | 49 | project component of the path. | ||
83 | 50 | :raises NoSuchSourcePackageName: If the source package referred to | ||
84 | 51 | does not exist. | ||
85 | 52 | |||
86 | 53 | :return: A tuple of:: | ||
87 | 54 | * an `IPerson`, or None; | ||
88 | 55 | * an `IHasGitRepositories`; | ||
89 | 56 | * an `IGitRepository`, or None. | ||
90 | 57 | """ | ||
91 | 58 | |||
92 | 59 | def traverse_path(path): | ||
93 | 60 | """Traverse to the object referred to by 'path'. | ||
94 | 61 | |||
95 | 62 | All segments of 'path' must be consumed. | ||
96 | 63 | |||
97 | 64 | :raises InvalidNamespace: If the path cannot be parsed as a | ||
98 | 65 | repository namespace. | ||
99 | 66 | :raises InvalidProductName: If the project component of the path is | ||
100 | 67 | not a valid name. | ||
101 | 68 | :raises NoSuchGitRepository: If there is a '+git' segment, but the | ||
102 | 69 | following segment doesn't match an existing Git repository. | ||
103 | 70 | :raises NoSuchPerson: If the first segment of the path begins with a | ||
104 | 71 | '~', but we can't find a person matching the remainder. | ||
105 | 72 | :raises NoSuchProduct: If we can't find a project that matches the | ||
106 | 73 | project component of the path. | ||
107 | 74 | :raises NoSuchSourcePackageName: If the source package referred to | ||
108 | 75 | does not exist. | ||
109 | 76 | |||
110 | 77 | :return: A tuple of:: | ||
111 | 78 | * an `IPerson`, or None; | ||
112 | 79 | * an `IHasGitRepositories`; | ||
113 | 80 | * an `IGitRepository`, or None. | ||
114 | 81 | """ | ||
115 | 82 | |||
116 | 83 | |||
117 | 84 | class IGitLookup(Interface): | ||
118 | 85 | """Utility for looking up a Git repository by name.""" | ||
119 | 86 | |||
120 | 87 | def get(repository_id, default=None): | ||
121 | 88 | """Return the repository with the given id. | ||
122 | 89 | |||
123 | 90 | Return the default value if there is no such repository. | ||
124 | 91 | """ | ||
125 | 92 | |||
126 | 93 | def getByUniqueName(unique_name): | ||
127 | 94 | """Find a repository by its unique name. | ||
128 | 95 | |||
129 | 96 | Unique names have one of the following forms: | ||
130 | 97 | ~OWNER/PROJECT/+git/NAME | ||
131 | 98 | ~OWNER/DISTRO/+source/SOURCE/+git/NAME | ||
132 | 99 | ~OWNER/+git/NAME | ||
133 | 100 | |||
134 | 101 | :return: An `IGitRepository`, or None. | ||
135 | 102 | """ | ||
136 | 103 | |||
137 | 104 | def uriToPath(uri): | ||
138 | 105 | """Return the path for the URI, if the URI is on codehosting. | ||
139 | 106 | |||
140 | 107 | This does not ensure that the path is valid. | ||
141 | 108 | |||
142 | 109 | :param uri: An instance of lazr.uri.URI | ||
143 | 110 | :return: The path if possible; None if the URI is not a valid | ||
144 | 111 | codehosting URI. | ||
145 | 112 | """ | ||
146 | 113 | |||
147 | 114 | def getByUrl(url): | ||
148 | 115 | """Find a repository by URL. | ||
149 | 116 | |||
150 | 117 | Either from the URL on git.launchpad.net (various schemes) or the | ||
151 | 118 | lp: URL (which relies on client-side configuration). | ||
152 | 119 | """ | ||
153 | 120 | |||
154 | 121 | def getByPath(path): | ||
155 | 122 | """Find a repository by its path. | ||
156 | 123 | |||
157 | 124 | Any of these forms may be used, with or without a leading slash: | ||
158 | 125 | Unique names: | ||
159 | 126 | ~OWNER/PROJECT/+git/NAME | ||
160 | 127 | ~OWNER/DISTRO/+source/SOURCE/+git/NAME | ||
161 | 128 | ~OWNER/+git/NAME | ||
162 | 129 | Owner-target default aliases: | ||
163 | 130 | ~OWNER/PROJECT | ||
164 | 131 | ~OWNER/DISTRO/+source/SOURCE | ||
165 | 132 | Official aliases: | ||
166 | 133 | PROJECT | ||
167 | 134 | DISTRO/+source/SOURCE | ||
168 | 135 | |||
169 | 136 | :return: An `IGitRepository`, or None. | ||
170 | 137 | """ | ||
171 | 0 | 138 | ||
172 | === modified file 'lib/lp/code/interfaces/gitnamespace.py' | |||
173 | --- lib/lp/code/interfaces/gitnamespace.py 2015-02-26 13:43:51 +0000 | |||
174 | +++ lib/lp/code/interfaces/gitnamespace.py 2015-02-27 10:23:01 +0000 | |||
175 | @@ -148,83 +148,6 @@ | |||
176 | 148 | def get(person, project=None, distribution=None, sourcepackagename=None): | 148 | def get(person, project=None, distribution=None, sourcepackagename=None): |
177 | 149 | """Return the appropriate `IGitNamespace` for the given objects.""" | 149 | """Return the appropriate `IGitNamespace` for the given objects.""" |
178 | 150 | 150 | ||
179 | 151 | def interpret(person, project, distribution, sourcepackagename): | ||
180 | 152 | """Like `get`, but takes names of objects. | ||
181 | 153 | |||
182 | 154 | :raise NoSuchPerson: If the person referred to cannot be found. | ||
183 | 155 | :raise NoSuchProduct: If the project referred to cannot be found. | ||
184 | 156 | :raise NoSuchDistribution: If the distribution referred to cannot be | ||
185 | 157 | found. | ||
186 | 158 | :raise NoSuchSourcePackageName: If the sourcepackagename referred to | ||
187 | 159 | cannot be found. | ||
188 | 160 | :return: An `IGitNamespace`. | ||
189 | 161 | """ | ||
190 | 162 | |||
191 | 163 | def parse(namespace_name): | ||
192 | 164 | """Parse 'namespace_name' into its components. | ||
193 | 165 | |||
194 | 166 | The name of a namespace is actually a path containing many elements, | ||
195 | 167 | each of which maps to a particular kind of object in Launchpad. | ||
196 | 168 | Elements that can appear in a namespace name are: 'person', | ||
197 | 169 | 'project', 'distribution', and 'sourcepackagename'. | ||
198 | 170 | |||
199 | 171 | `parse` returns a dict which maps the names of these elements (e.g. | ||
200 | 172 | 'person', 'project') to the values of these elements (e.g. 'mark', | ||
201 | 173 | 'firefox'). If the given path doesn't include a particular kind of | ||
202 | 174 | element, the dict maps that element name to None. | ||
203 | 175 | |||
204 | 176 | For example:: | ||
205 | 177 | parse('~foo/bar') => { | ||
206 | 178 | 'person': 'foo', 'project': 'bar', 'distribution': None, | ||
207 | 179 | 'sourcepackagename': None, | ||
208 | 180 | } | ||
209 | 181 | |||
210 | 182 | If the given 'namespace_name' cannot be parsed, then we raise an | ||
211 | 183 | `InvalidNamespace` error. | ||
212 | 184 | |||
213 | 185 | :raise InvalidNamespace: If the name is too long, too short, or | ||
214 | 186 | malformed. | ||
215 | 187 | :return: A dict with keys matching each component in | ||
216 | 188 | 'namespace_name'. | ||
217 | 189 | """ | ||
218 | 190 | |||
219 | 191 | def lookup(namespace_name): | ||
220 | 192 | """Return the `IGitNamespace` for 'namespace_name'. | ||
221 | 193 | |||
222 | 194 | :raise InvalidNamespace: if namespace_name cannot be parsed. | ||
223 | 195 | :raise NoSuchPerson: if the person referred to cannot be found. | ||
224 | 196 | :raise NoSuchProduct: if the project referred to cannot be found. | ||
225 | 197 | :raise NoSuchDistribution: if the distribution referred to cannot be | ||
226 | 198 | found. | ||
227 | 199 | :raise NoSuchSourcePackageName: if the sourcepackagename referred to | ||
228 | 200 | cannot be found. | ||
229 | 201 | :return: An `IGitNamespace`. | ||
230 | 202 | """ | ||
231 | 203 | |||
232 | 204 | def traverse(segments): | ||
233 | 205 | """Look up the Git repository at the path given by 'segments'. | ||
234 | 206 | |||
235 | 207 | The iterable 'segments' will be consumed until a repository is | ||
236 | 208 | found. As soon as a repository is found, the repository will be | ||
237 | 209 | returned and the consumption of segments will stop. Thus, there | ||
238 | 210 | will often be unconsumed segments that can be used for further | ||
239 | 211 | traversal. | ||
240 | 212 | |||
241 | 213 | :param segments: An iterable of URL segments, a prefix of which | ||
242 | 214 | identifies a Git repository. The first segment is the username, | ||
243 | 215 | *not* preceded by a '~`. | ||
244 | 216 | :raise InvalidNamespace: if there are not enough segments to define a | ||
245 | 217 | repository. | ||
246 | 218 | :raise NoSuchPerson: if the person referred to cannot be found. | ||
247 | 219 | :raise NoSuchProduct: if the product or distro referred to cannot be | ||
248 | 220 | found. | ||
249 | 221 | :raise NoSuchDistribution: if the distribution referred to cannot be | ||
250 | 222 | found. | ||
251 | 223 | :raise NoSuchSourcePackageName: if the sourcepackagename referred to | ||
252 | 224 | cannot be found. | ||
253 | 225 | :return: `IGitRepository`. | ||
254 | 226 | """ | ||
255 | 227 | |||
256 | 228 | 151 | ||
257 | 229 | def get_git_namespace(target, owner): | 152 | def get_git_namespace(target, owner): |
258 | 230 | if IProduct.providedBy(target): | 153 | if IProduct.providedBy(target): |
259 | 231 | 154 | ||
260 | === modified file 'lib/lp/code/model/branchlookup.py' | |||
261 | --- lib/lp/code/model/branchlookup.py 2015-01-29 13:09:37 +0000 | |||
262 | +++ lib/lp/code/model/branchlookup.py 2015-02-27 10:23:01 +0000 | |||
263 | @@ -72,13 +72,13 @@ | |||
264 | 72 | from lp.services.webapp.authorization import check_permission | 72 | from lp.services.webapp.authorization import check_permission |
265 | 73 | 73 | ||
266 | 74 | 74 | ||
268 | 75 | def adapt(provided, interface): | 75 | def adapt(obj, interface): |
269 | 76 | """Adapt 'obj' to 'interface', using multi-adapters if necessary.""" | 76 | """Adapt 'obj' to 'interface', using multi-adapters if necessary.""" |
271 | 77 | required = interface(provided, None) | 77 | required = interface(obj, None) |
272 | 78 | if required is not None: | 78 | if required is not None: |
273 | 79 | return required | 79 | return required |
274 | 80 | try: | 80 | try: |
276 | 81 | return queryMultiAdapter(provided, interface) | 81 | return queryMultiAdapter(obj, interface) |
277 | 82 | except TypeError: | 82 | except TypeError: |
278 | 83 | return None | 83 | return None |
279 | 84 | 84 | ||
280 | 85 | 85 | ||
281 | === added file 'lib/lp/code/model/gitlookup.py' | |||
282 | --- lib/lp/code/model/gitlookup.py 1970-01-01 00:00:00 +0000 | |||
283 | +++ lib/lp/code/model/gitlookup.py 2015-02-27 10:23:01 +0000 | |||
284 | @@ -0,0 +1,349 @@ | |||
285 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
286 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
287 | 3 | |||
288 | 4 | """Database implementation of the Git repository lookup utility.""" | ||
289 | 5 | |||
290 | 6 | __metaclass__ = type | ||
291 | 7 | # This module doesn't export anything. If you want to look up Git | ||
292 | 8 | # repositories by name, then get the IGitLookup utility. | ||
293 | 9 | __all__ = [] | ||
294 | 10 | |||
295 | 11 | from lazr.uri import ( | ||
296 | 12 | InvalidURIError, | ||
297 | 13 | URI, | ||
298 | 14 | ) | ||
299 | 15 | from zope.component import ( | ||
300 | 16 | adapts, | ||
301 | 17 | getUtility, | ||
302 | 18 | queryMultiAdapter, | ||
303 | 19 | ) | ||
304 | 20 | from zope.interface import implements | ||
305 | 21 | |||
306 | 22 | from lp.app.errors import NameLookupFailed | ||
307 | 23 | from lp.app.validators.name import valid_name | ||
308 | 24 | from lp.code.errors import ( | ||
309 | 25 | InvalidNamespace, | ||
310 | 26 | NoSuchGitRepository, | ||
311 | 27 | ) | ||
312 | 28 | from lp.code.interfaces.gitlookup import ( | ||
313 | 29 | IGitLookup, | ||
314 | 30 | IGitTraversable, | ||
315 | 31 | IGitTraverser, | ||
316 | 32 | ) | ||
317 | 33 | from lp.code.interfaces.gitnamespace import IGitNamespaceSet | ||
318 | 34 | from lp.code.interfaces.gitrepository import IGitRepositorySet | ||
319 | 35 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories | ||
320 | 36 | from lp.code.model.gitrepository import GitRepository | ||
321 | 37 | from lp.registry.errors import NoSuchSourcePackageName | ||
322 | 38 | from lp.registry.interfaces.distribution import IDistribution | ||
323 | 39 | from lp.registry.interfaces.distributionsourcepackage import ( | ||
324 | 40 | IDistributionSourcePackage, | ||
325 | 41 | ) | ||
326 | 42 | from lp.registry.interfaces.person import ( | ||
327 | 43 | IPerson, | ||
328 | 44 | IPersonSet, | ||
329 | 45 | NoSuchPerson, | ||
330 | 46 | ) | ||
331 | 47 | from lp.registry.interfaces.pillar import IPillarNameSet | ||
332 | 48 | from lp.registry.interfaces.product import ( | ||
333 | 49 | InvalidProductName, | ||
334 | 50 | IProduct, | ||
335 | 51 | NoSuchProduct, | ||
336 | 52 | ) | ||
337 | 53 | from lp.services.config import config | ||
338 | 54 | from lp.services.database.interfaces import IStore | ||
339 | 55 | |||
340 | 56 | |||
341 | 57 | def adapt(obj, interface): | ||
342 | 58 | """Adapt 'obj' to 'interface', using multi-adapters if necessary.""" | ||
343 | 59 | required = interface(obj, None) | ||
344 | 60 | if required is not None: | ||
345 | 61 | return required | ||
346 | 62 | try: | ||
347 | 63 | return queryMultiAdapter(obj, interface) | ||
348 | 64 | except TypeError: | ||
349 | 65 | return None | ||
350 | 66 | |||
351 | 67 | |||
352 | 68 | class RootGitTraversable: | ||
353 | 69 | """Root traversable for Git repository objects. | ||
354 | 70 | |||
355 | 71 | Corresponds to '/' in the path. From here, you can traverse to a | ||
356 | 72 | project or a distribution, optionally with a person context as well. | ||
357 | 73 | """ | ||
358 | 74 | |||
359 | 75 | implements(IGitTraversable) | ||
360 | 76 | |||
361 | 77 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
362 | 78 | def traverse(self, owner, name, segments): | ||
363 | 79 | """See `IGitTraversable`. | ||
364 | 80 | |||
365 | 81 | :raises InvalidProductName: If 'name' is not a valid name. | ||
366 | 82 | :raises NoSuchPerson: If 'name' begins with a '~', but the remainder | ||
367 | 83 | doesn't match an existing person. | ||
368 | 84 | :raises NoSuchProduct: If 'name' doesn't match an existing pillar. | ||
369 | 85 | :return: A tuple of (`IPerson`, `IPillar`, None). | ||
370 | 86 | """ | ||
371 | 87 | assert owner is None | ||
372 | 88 | if name.startswith("~"): | ||
373 | 89 | owner_name = name[1:] | ||
374 | 90 | owner = getUtility(IPersonSet).getByName(owner_name) | ||
375 | 91 | if owner is None: | ||
376 | 92 | raise NoSuchPerson(owner_name) | ||
377 | 93 | return owner, owner, None | ||
378 | 94 | else: | ||
379 | 95 | if not valid_name(name): | ||
380 | 96 | raise InvalidProductName(name) | ||
381 | 97 | pillar = getUtility(IPillarNameSet).getByName(name) | ||
382 | 98 | if pillar is None: | ||
383 | 99 | # Actually, the pillar is no such *anything*. | ||
384 | 100 | raise NoSuchProduct(name) | ||
385 | 101 | return owner, pillar, None | ||
386 | 102 | |||
387 | 103 | |||
388 | 104 | class _BaseGitTraversable: | ||
389 | 105 | """Base class for traversable implementations.""" | ||
390 | 106 | |||
391 | 107 | def __init__(self, context): | ||
392 | 108 | self.context = context | ||
393 | 109 | |||
394 | 110 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
395 | 111 | def traverse(self, owner, name, segments): | ||
396 | 112 | """See `IGitTraversable`. | ||
397 | 113 | |||
398 | 114 | :raises InvalidNamespace: If 'name' is not '+git', or there is no | ||
399 | 115 | owner, or there are no further segments. | ||
400 | 116 | :raises NoSuchGitRepository: If the segment after '+git' doesn't | ||
401 | 117 | match an existing Git repository. | ||
402 | 118 | :return: A tuple of (`IPerson`, `IHasGitRepositories`, | ||
403 | 119 | `IGitRepository`). | ||
404 | 120 | """ | ||
405 | 121 | if owner is None or name != "+git": | ||
406 | 122 | raise InvalidNamespace("/".join(segments.traversed)) | ||
407 | 123 | try: | ||
408 | 124 | repository_name = next(segments) | ||
409 | 125 | except StopIteration: | ||
410 | 126 | raise InvalidNamespace("/".join(segments.traversed)) | ||
411 | 127 | repository = self.getNamespace(owner).getByName(repository_name) | ||
412 | 128 | if repository is None: | ||
413 | 129 | raise NoSuchGitRepository(repository_name) | ||
414 | 130 | return owner, self.context, repository | ||
415 | 131 | |||
416 | 132 | |||
417 | 133 | class ProjectGitTraversable(_BaseGitTraversable): | ||
418 | 134 | """Git repository traversable for projects. | ||
419 | 135 | |||
420 | 136 | From here, you can traverse to a named project repository. | ||
421 | 137 | """ | ||
422 | 138 | |||
423 | 139 | adapts(IProduct) | ||
424 | 140 | implements(IGitTraversable) | ||
425 | 141 | |||
426 | 142 | def getNamespace(self, owner): | ||
427 | 143 | return getUtility(IGitNamespaceSet).get(owner, project=self.context) | ||
428 | 144 | |||
429 | 145 | |||
430 | 146 | class DistributionGitTraversable(_BaseGitTraversable): | ||
431 | 147 | """Git repository traversable for distributions. | ||
432 | 148 | |||
433 | 149 | From here, you can traverse to a distribution source package. | ||
434 | 150 | """ | ||
435 | 151 | |||
436 | 152 | adapts(IDistribution) | ||
437 | 153 | implements(IGitTraversable) | ||
438 | 154 | |||
439 | 155 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
440 | 156 | def traverse(self, owner, name, segments): | ||
441 | 157 | """See `IGitTraversable`. | ||
442 | 158 | |||
443 | 159 | :raises InvalidNamespace: If 'name' is not '+source' or there are no | ||
444 | 160 | further segments. | ||
445 | 161 | :raises NoSuchSourcePackageName: If the segment after '+source' | ||
446 | 162 | doesn't match an existing source package name. | ||
447 | 163 | :return: A tuple of (`IPerson`, `IDistributionSourcePackage`, None). | ||
448 | 164 | """ | ||
449 | 165 | # Distributions don't support named repositories themselves, so | ||
450 | 166 | # ignore the base traverse method. | ||
451 | 167 | if name != "+source": | ||
452 | 168 | raise InvalidNamespace("/".join(segments.traversed)) | ||
453 | 169 | try: | ||
454 | 170 | spn_name = next(segments) | ||
455 | 171 | except StopIteration: | ||
456 | 172 | raise InvalidNamespace("/".join(segments.traversed)) | ||
457 | 173 | distro_source_package = self.context.getSourcePackage(spn_name) | ||
458 | 174 | if distro_source_package is None: | ||
459 | 175 | raise NoSuchSourcePackageName(spn_name) | ||
460 | 176 | return owner, distro_source_package, None | ||
461 | 177 | |||
462 | 178 | |||
463 | 179 | class DistributionSourcePackageGitTraversable(_BaseGitTraversable): | ||
464 | 180 | """Git repository traversable for distribution source packages. | ||
465 | 181 | |||
466 | 182 | From here, you can traverse to a named package repository. | ||
467 | 183 | """ | ||
468 | 184 | |||
469 | 185 | adapts(IDistributionSourcePackage) | ||
470 | 186 | implements(IGitTraversable) | ||
471 | 187 | |||
472 | 188 | def getNamespace(self, owner): | ||
473 | 189 | return getUtility(IGitNamespaceSet).get( | ||
474 | 190 | owner, distribution=self.context.distribution, | ||
475 | 191 | sourcepackagename=self.context.sourcepackagename) | ||
476 | 192 | |||
477 | 193 | |||
478 | 194 | class PersonGitTraversable(_BaseGitTraversable): | ||
479 | 195 | """Git repository traversable for people. | ||
480 | 196 | |||
481 | 197 | From here, you can traverse to a named personal repository, or to a | ||
482 | 198 | project or a distribution with a person context. | ||
483 | 199 | """ | ||
484 | 200 | |||
485 | 201 | adapts(IPerson) | ||
486 | 202 | implements(IGitTraversable) | ||
487 | 203 | |||
488 | 204 | def getNamespace(self, owner): | ||
489 | 205 | return getUtility(IGitNamespaceSet).get(owner) | ||
490 | 206 | |||
491 | 207 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
492 | 208 | def traverse(self, owner, name, segments): | ||
493 | 209 | """See `IGitTraversable`. | ||
494 | 210 | |||
495 | 211 | :raises InvalidNamespace: If 'name' is '+git' and there are no | ||
496 | 212 | further segments. | ||
497 | 213 | :raises InvalidProductName: If 'name' is not '+git' and is not a | ||
498 | 214 | valid name. | ||
499 | 215 | :raises NoSuchGitRepository: If the segment after '+git' doesn't | ||
500 | 216 | match an existing Git repository. | ||
501 | 217 | :raises NoSuchProduct: If 'name' is not '+git' and doesn't match an | ||
502 | 218 | existing pillar. | ||
503 | 219 | :return: A tuple of (`IPerson`, `IHasGitRepositories`, | ||
504 | 220 | `IGitRepository`). | ||
505 | 221 | """ | ||
506 | 222 | if name == "+git": | ||
507 | 223 | return super(PersonGitTraversable, self).traverse( | ||
508 | 224 | owner, name, segments) | ||
509 | 225 | else: | ||
510 | 226 | if not valid_name(name): | ||
511 | 227 | raise InvalidProductName(name) | ||
512 | 228 | pillar = getUtility(IPillarNameSet).getByName(name) | ||
513 | 229 | if pillar is None: | ||
514 | 230 | # Actually, the pillar is no such *anything*. | ||
515 | 231 | raise NoSuchProduct(name) | ||
516 | 232 | return owner, pillar, None | ||
517 | 233 | |||
518 | 234 | |||
519 | 235 | class SegmentIterator: | ||
520 | 236 | """An iterator that remembers the elements it has traversed.""" | ||
521 | 237 | |||
522 | 238 | def __init__(self, iterator): | ||
523 | 239 | self._iterator = iterator | ||
524 | 240 | self.traversed = [] | ||
525 | 241 | |||
526 | 242 | def next(self): | ||
527 | 243 | segment = next(self._iterator) | ||
528 | 244 | if not isinstance(segment, unicode): | ||
529 | 245 | segment = segment.decode("US-ASCII") | ||
530 | 246 | self.traversed.append(segment) | ||
531 | 247 | return segment | ||
532 | 248 | |||
533 | 249 | |||
534 | 250 | class GitTraverser: | ||
535 | 251 | """Utility for traversing to objects that can have Git repositories.""" | ||
536 | 252 | |||
537 | 253 | implements(IGitTraverser) | ||
538 | 254 | |||
539 | 255 | def traverse(self, segments): | ||
540 | 256 | """See `IGitTraverser`.""" | ||
541 | 257 | owner = None | ||
542 | 258 | target = None | ||
543 | 259 | repository = None | ||
544 | 260 | traversable = RootGitTraversable() | ||
545 | 261 | segments_iter = SegmentIterator(segments) | ||
546 | 262 | while traversable is not None: | ||
547 | 263 | try: | ||
548 | 264 | name = next(segments_iter) | ||
549 | 265 | except StopIteration: | ||
550 | 266 | break | ||
551 | 267 | owner, target, repository = traversable.traverse( | ||
552 | 268 | owner, name, segments_iter) | ||
553 | 269 | if repository is not None: | ||
554 | 270 | break | ||
555 | 271 | traversable = adapt(target, IGitTraversable) | ||
556 | 272 | if target is None or not IHasGitRepositories.providedBy(target): | ||
557 | 273 | raise InvalidNamespace("/".join(segments_iter.traversed)) | ||
558 | 274 | return owner, target, repository | ||
559 | 275 | |||
560 | 276 | def traverse_path(self, path): | ||
561 | 277 | """See `IGitTraverser`.""" | ||
562 | 278 | segments = iter(path.split("/")) | ||
563 | 279 | owner, target, repository = self.traverse(segments) | ||
564 | 280 | if list(segments): | ||
565 | 281 | raise InvalidNamespace(path) | ||
566 | 282 | return owner, target, repository | ||
567 | 283 | |||
568 | 284 | |||
569 | 285 | class GitLookup: | ||
570 | 286 | """Utility for looking up Git repositories.""" | ||
571 | 287 | |||
572 | 288 | implements(IGitLookup) | ||
573 | 289 | |||
574 | 290 | def get(self, repository_id, default=None): | ||
575 | 291 | """See `IGitLookup`.""" | ||
576 | 292 | repository = IStore(GitRepository).get(GitRepository, repository_id) | ||
577 | 293 | if repository is None: | ||
578 | 294 | return default | ||
579 | 295 | return repository | ||
580 | 296 | |||
581 | 297 | @staticmethod | ||
582 | 298 | def uriToPath(uri): | ||
583 | 299 | """See `IGitLookup`.""" | ||
584 | 300 | schemes = ('git', 'git+ssh', 'https', 'ssh') | ||
585 | 301 | codehosting_host = URI(config.codehosting.git_anon_root).host | ||
586 | 302 | if ((uri.scheme in schemes and uri.host == codehosting_host) or | ||
587 | 303 | (uri.scheme == "lp" and uri.host is None)): | ||
588 | 304 | return uri.path.lstrip("/") | ||
589 | 305 | else: | ||
590 | 306 | return None | ||
591 | 307 | |||
592 | 308 | def getByUrl(self, url): | ||
593 | 309 | """See `IGitLookup`.""" | ||
594 | 310 | if url is None: | ||
595 | 311 | return None | ||
596 | 312 | url = url.rstrip("/") | ||
597 | 313 | try: | ||
598 | 314 | uri = URI(url) | ||
599 | 315 | except InvalidURIError: | ||
600 | 316 | return None | ||
601 | 317 | |||
602 | 318 | path = self.uriToPath(uri) | ||
603 | 319 | if path is None: | ||
604 | 320 | return None | ||
605 | 321 | return self.getByPath(path) | ||
606 | 322 | |||
607 | 323 | def getByUniqueName(self, unique_name): | ||
608 | 324 | """See `IGitLookup`.""" | ||
609 | 325 | try: | ||
610 | 326 | if unique_name.startswith("~"): | ||
611 | 327 | segments = iter(unique_name.split("/")) | ||
612 | 328 | _, _, repository = getUtility(IGitTraverser).traverse(segments) | ||
613 | 329 | if repository is None or list(segments): | ||
614 | 330 | raise InvalidNamespace(unique_name) | ||
615 | 331 | return repository | ||
616 | 332 | except (InvalidNamespace, NameLookupFailed): | ||
617 | 333 | pass | ||
618 | 334 | return None | ||
619 | 335 | |||
620 | 336 | def getByPath(self, path): | ||
621 | 337 | """See `IGitLookup`.""" | ||
622 | 338 | traverser = getUtility(IGitTraverser) | ||
623 | 339 | try: | ||
624 | 340 | owner, target, repository = traverser.traverse_path(path) | ||
625 | 341 | except (InvalidNamespace, InvalidProductName, NameLookupFailed): | ||
626 | 342 | return None | ||
627 | 343 | if repository is not None: | ||
628 | 344 | return repository | ||
629 | 345 | repository_set = getUtility(IGitRepositorySet) | ||
630 | 346 | if owner is None: | ||
631 | 347 | return repository_set.getDefaultRepository(target) | ||
632 | 348 | else: | ||
633 | 349 | return repository_set.getDefaultRepositoryForOwner(owner, target) | ||
634 | 0 | 350 | ||
635 | === modified file 'lib/lp/code/model/gitnamespace.py' | |||
636 | --- lib/lp/code/model/gitnamespace.py 2015-02-26 13:43:51 +0000 | |||
637 | +++ lib/lp/code/model/gitnamespace.py 2015-02-27 10:23:01 +0000 | |||
638 | @@ -30,8 +30,6 @@ | |||
639 | 30 | GitRepositoryCreatorNotMemberOfOwnerTeam, | 30 | GitRepositoryCreatorNotMemberOfOwnerTeam, |
640 | 31 | GitRepositoryCreatorNotOwner, | 31 | GitRepositoryCreatorNotOwner, |
641 | 32 | GitRepositoryExists, | 32 | GitRepositoryExists, |
642 | 33 | InvalidNamespace, | ||
643 | 34 | NoSuchGitRepository, | ||
644 | 35 | ) | 33 | ) |
645 | 36 | from lp.code.interfaces.gitnamespace import ( | 34 | from lp.code.interfaces.gitnamespace import ( |
646 | 37 | IGitNamespace, | 35 | IGitNamespace, |
647 | @@ -50,27 +48,9 @@ | |||
648 | 50 | ) | 48 | ) |
649 | 51 | from lp.code.model.gitrepository import GitRepository | 49 | from lp.code.model.gitrepository import GitRepository |
650 | 52 | from lp.registry.enums import PersonVisibility | 50 | from lp.registry.enums import PersonVisibility |
651 | 53 | from lp.registry.errors import NoSuchSourcePackageName | ||
652 | 54 | from lp.registry.interfaces.distribution import ( | ||
653 | 55 | IDistribution, | ||
654 | 56 | IDistributionSet, | ||
655 | 57 | NoSuchDistribution, | ||
656 | 58 | ) | ||
657 | 59 | from lp.registry.interfaces.distributionsourcepackage import ( | 51 | from lp.registry.interfaces.distributionsourcepackage import ( |
658 | 60 | IDistributionSourcePackage, | 52 | IDistributionSourcePackage, |
659 | 61 | ) | 53 | ) |
660 | 62 | from lp.registry.interfaces.person import ( | ||
661 | 63 | IPersonSet, | ||
662 | 64 | NoSuchPerson, | ||
663 | 65 | ) | ||
664 | 66 | from lp.registry.interfaces.pillar import IPillarNameSet | ||
665 | 67 | from lp.registry.interfaces.product import ( | ||
666 | 68 | IProduct, | ||
667 | 69 | IProductSet, | ||
668 | 70 | NoSuchProduct, | ||
669 | 71 | ) | ||
670 | 72 | from lp.registry.interfaces.projectgroup import IProjectGroup | ||
671 | 73 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet | ||
672 | 74 | from lp.services.database.constants import DEFAULT | 54 | from lp.services.database.constants import DEFAULT |
673 | 75 | from lp.services.database.interfaces import IStore | 55 | from lp.services.database.interfaces import IStore |
674 | 76 | from lp.services.propertycache import get_property_cache | 56 | from lp.services.propertycache import get_property_cache |
675 | @@ -397,142 +377,3 @@ | |||
676 | 397 | person, distribution.getSourcePackage(sourcepackagename)) | 377 | person, distribution.getSourcePackage(sourcepackagename)) |
677 | 398 | else: | 378 | else: |
678 | 399 | return PersonalGitNamespace(person) | 379 | return PersonalGitNamespace(person) |
679 | 400 | |||
680 | 401 | def _findOrRaise(self, error, name, finder, *args): | ||
681 | 402 | if name is None: | ||
682 | 403 | return None | ||
683 | 404 | args = list(args) | ||
684 | 405 | args.append(name) | ||
685 | 406 | result = finder(*args) | ||
686 | 407 | if result is None: | ||
687 | 408 | raise error(name) | ||
688 | 409 | return result | ||
689 | 410 | |||
690 | 411 | def _findPerson(self, person_name): | ||
691 | 412 | return self._findOrRaise( | ||
692 | 413 | NoSuchPerson, person_name, getUtility(IPersonSet).getByName) | ||
693 | 414 | |||
694 | 415 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
695 | 416 | def _findPillar(self, pillar_name): | ||
696 | 417 | """Find and return the pillar with the given name. | ||
697 | 418 | |||
698 | 419 | If the given name is '+git' (indicating a personal repository) or | ||
699 | 420 | None, return None. | ||
700 | 421 | |||
701 | 422 | :raise NoSuchProduct if there's no pillar with the given name or it | ||
702 | 423 | is a project group. | ||
703 | 424 | """ | ||
704 | 425 | if pillar_name == "+git": | ||
705 | 426 | return None | ||
706 | 427 | pillar = self._findOrRaise( | ||
707 | 428 | NoSuchProduct, pillar_name, getUtility(IPillarNameSet).getByName) | ||
708 | 429 | if IProjectGroup.providedBy(pillar): | ||
709 | 430 | raise NoSuchProduct(pillar_name) | ||
710 | 431 | return pillar | ||
711 | 432 | |||
712 | 433 | def _findProject(self, project_name): | ||
713 | 434 | return self._findOrRaise( | ||
714 | 435 | NoSuchProduct, project_name, getUtility(IProductSet).getByName) | ||
715 | 436 | |||
716 | 437 | def _findDistribution(self, distribution_name): | ||
717 | 438 | return self._findOrRaise( | ||
718 | 439 | NoSuchDistribution, distribution_name, | ||
719 | 440 | getUtility(IDistributionSet).getByName) | ||
720 | 441 | |||
721 | 442 | def _findSourcePackageName(self, sourcepackagename_name): | ||
722 | 443 | return self._findOrRaise( | ||
723 | 444 | NoSuchSourcePackageName, sourcepackagename_name, | ||
724 | 445 | getUtility(ISourcePackageNameSet).queryByName) | ||
725 | 446 | |||
726 | 447 | def _realize(self, names): | ||
727 | 448 | """Turn a dict of object names into a dict of objects. | ||
728 | 449 | |||
729 | 450 | Takes the results of `IGitNamespaceSet.parse` and turns them into a | ||
730 | 451 | dict where the values are Launchpad objects. | ||
731 | 452 | """ | ||
732 | 453 | data = {} | ||
733 | 454 | data["person"] = self._findPerson(names["person"]) | ||
734 | 455 | data["project"] = self._findProject(names["project"]) | ||
735 | 456 | data["distribution"] = self._findDistribution(names["distribution"]) | ||
736 | 457 | data["sourcepackagename"] = self._findSourcePackageName( | ||
737 | 458 | names["sourcepackagename"]) | ||
738 | 459 | return data | ||
739 | 460 | |||
740 | 461 | def interpret(self, person, project, distribution, sourcepackagename): | ||
741 | 462 | names = dict( | ||
742 | 463 | person=person, project=project, distribution=distribution, | ||
743 | 464 | sourcepackagename=sourcepackagename) | ||
744 | 465 | data = self._realize(names) | ||
745 | 466 | return self.get(**data) | ||
746 | 467 | |||
747 | 468 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
748 | 469 | def parse(self, namespace_name): | ||
749 | 470 | """See `IGitNamespaceSet`.""" | ||
750 | 471 | data = dict( | ||
751 | 472 | person=None, project=None, distribution=None, | ||
752 | 473 | sourcepackagename=None) | ||
753 | 474 | tokens = namespace_name.split("/") | ||
754 | 475 | if len(tokens) == 1: | ||
755 | 476 | data["person"] = tokens[0] | ||
756 | 477 | elif len(tokens) == 2: | ||
757 | 478 | data["person"] = tokens[0] | ||
758 | 479 | data["project"] = tokens[1] | ||
759 | 480 | elif len(tokens) == 4 and tokens[2] == "+source": | ||
760 | 481 | data["person"] = tokens[0] | ||
761 | 482 | data["distribution"] = tokens[1] | ||
762 | 483 | data["sourcepackagename"] = tokens[3] | ||
763 | 484 | else: | ||
764 | 485 | raise InvalidNamespace(namespace_name) | ||
765 | 486 | if not data["person"].startswith("~"): | ||
766 | 487 | raise InvalidNamespace(namespace_name) | ||
767 | 488 | data["person"] = data["person"][1:] | ||
768 | 489 | return data | ||
769 | 490 | |||
770 | 491 | def lookup(self, namespace_name): | ||
771 | 492 | """See `IGitNamespaceSet`.""" | ||
772 | 493 | names = self.parse(namespace_name) | ||
773 | 494 | return self.interpret(**names) | ||
774 | 495 | |||
775 | 496 | # Marker for references to Git URL layouts: ##GITNAMESPACE## | ||
776 | 497 | def traverse(self, segments): | ||
777 | 498 | """See `IGitNamespaceSet`.""" | ||
778 | 499 | traversed_segments = [] | ||
779 | 500 | |||
780 | 501 | def get_next_segment(): | ||
781 | 502 | try: | ||
782 | 503 | result = segments.next() | ||
783 | 504 | except StopIteration: | ||
784 | 505 | raise InvalidNamespace("/".join(traversed_segments)) | ||
785 | 506 | if result is None: | ||
786 | 507 | raise AssertionError("None segment passed to traverse()") | ||
787 | 508 | if not isinstance(result, unicode): | ||
788 | 509 | result = result.decode("US-ASCII") | ||
789 | 510 | traversed_segments.append(result) | ||
790 | 511 | return result | ||
791 | 512 | |||
792 | 513 | person_name = get_next_segment() | ||
793 | 514 | person = self._findPerson(person_name) | ||
794 | 515 | pillar_name = get_next_segment() | ||
795 | 516 | pillar = self._findPillar(pillar_name) | ||
796 | 517 | if pillar is None: | ||
797 | 518 | namespace = self.get(person) | ||
798 | 519 | git_literal = pillar_name | ||
799 | 520 | elif IProduct.providedBy(pillar): | ||
800 | 521 | namespace = self.get(person, project=pillar) | ||
801 | 522 | git_literal = get_next_segment() | ||
802 | 523 | else: | ||
803 | 524 | source_literal = get_next_segment() | ||
804 | 525 | if source_literal != "+source": | ||
805 | 526 | raise InvalidNamespace("/".join(traversed_segments)) | ||
806 | 527 | sourcepackagename_name = get_next_segment() | ||
807 | 528 | sourcepackagename = self._findSourcePackageName( | ||
808 | 529 | sourcepackagename_name) | ||
809 | 530 | namespace = self.get( | ||
810 | 531 | person, distribution=IDistribution(pillar), | ||
811 | 532 | sourcepackagename=sourcepackagename) | ||
812 | 533 | git_literal = get_next_segment() | ||
813 | 534 | if git_literal != "+git": | ||
814 | 535 | raise InvalidNamespace("/".join(traversed_segments)) | ||
815 | 536 | repository_name = get_next_segment() | ||
816 | 537 | return self._findOrRaise( | ||
817 | 538 | NoSuchGitRepository, repository_name, namespace.getByName) | ||
818 | 539 | 380 | ||
819 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
820 | --- lib/lp/code/model/gitrepository.py 2015-02-26 11:34:47 +0000 | |||
821 | +++ lib/lp/code/model/gitrepository.py 2015-02-27 10:23:01 +0000 | |||
822 | @@ -39,6 +39,7 @@ | |||
823 | 39 | GitDefaultConflict, | 39 | GitDefaultConflict, |
824 | 40 | GitTargetError, | 40 | GitTargetError, |
825 | 41 | ) | 41 | ) |
826 | 42 | from lp.code.interfaces.gitlookup import IGitLookup | ||
827 | 42 | from lp.code.interfaces.gitnamespace import ( | 43 | from lp.code.interfaces.gitnamespace import ( |
828 | 43 | get_git_namespace, | 44 | get_git_namespace, |
829 | 44 | IGitNamespacePolicy, | 45 | IGitNamespacePolicy, |
830 | @@ -357,8 +358,10 @@ | |||
831 | 357 | 358 | ||
832 | 358 | def getByPath(self, user, path): | 359 | def getByPath(self, user, path): |
833 | 359 | """See `IGitRepositorySet`.""" | 360 | """See `IGitRepositorySet`.""" |
836 | 360 | # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place. | 361 | repository = getUtility(IGitLookup).getByPath(path) |
837 | 361 | raise NotImplementedError | 362 | if repository is not None and repository.visibleByUser(user): |
838 | 363 | return repository | ||
839 | 364 | return None | ||
840 | 362 | 365 | ||
841 | 363 | def getDefaultRepository(self, target): | 366 | def getDefaultRepository(self, target): |
842 | 364 | """See `IGitRepositorySet`.""" | 367 | """See `IGitRepositorySet`.""" |
843 | 365 | 368 | ||
844 | === added file 'lib/lp/code/model/tests/test_gitlookup.py' | |||
845 | --- lib/lp/code/model/tests/test_gitlookup.py 1970-01-01 00:00:00 +0000 | |||
846 | +++ lib/lp/code/model/tests/test_gitlookup.py 2015-02-27 10:23:01 +0000 | |||
847 | @@ -0,0 +1,449 @@ | |||
848 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | ||
849 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
850 | 3 | |||
851 | 4 | """Tests for the IGitLookup implementation.""" | ||
852 | 5 | |||
853 | 6 | __metaclass__ = type | ||
854 | 7 | |||
855 | 8 | from lazr.uri import URI | ||
856 | 9 | from zope.component import getUtility | ||
857 | 10 | |||
858 | 11 | from lp.code.errors import ( | ||
859 | 12 | InvalidNamespace, | ||
860 | 13 | NoSuchGitRepository, | ||
861 | 14 | ) | ||
862 | 15 | from lp.code.interfaces.gitlookup import ( | ||
863 | 16 | IGitLookup, | ||
864 | 17 | IGitTraverser, | ||
865 | 18 | ) | ||
866 | 19 | from lp.code.interfaces.gitrepository import IGitRepositorySet | ||
867 | 20 | from lp.registry.errors import NoSuchSourcePackageName | ||
868 | 21 | from lp.registry.interfaces.person import NoSuchPerson | ||
869 | 22 | from lp.registry.interfaces.product import ( | ||
870 | 23 | InvalidProductName, | ||
871 | 24 | NoSuchProduct, | ||
872 | 25 | ) | ||
873 | 26 | from lp.services.config import config | ||
874 | 27 | from lp.testing import ( | ||
875 | 28 | person_logged_in, | ||
876 | 29 | TestCaseWithFactory, | ||
877 | 30 | ) | ||
878 | 31 | from lp.testing.layers import DatabaseFunctionalLayer | ||
879 | 32 | |||
880 | 33 | |||
881 | 34 | class TestGetByUniqueName(TestCaseWithFactory): | ||
882 | 35 | """Tests for `IGitLookup.getByUniqueName`.""" | ||
883 | 36 | |||
884 | 37 | layer = DatabaseFunctionalLayer | ||
885 | 38 | |||
886 | 39 | def setUp(self): | ||
887 | 40 | super(TestGetByUniqueName, self).setUp() | ||
888 | 41 | self.lookup = getUtility(IGitLookup) | ||
889 | 42 | |||
890 | 43 | def test_not_found(self): | ||
891 | 44 | unused_name = self.factory.getUniqueString() | ||
892 | 45 | self.assertIsNone(self.lookup.getByUniqueName(unused_name)) | ||
893 | 46 | |||
894 | 47 | def test_project(self): | ||
895 | 48 | repository = self.factory.makeGitRepository() | ||
896 | 49 | self.assertEqual( | ||
897 | 50 | repository, self.lookup.getByUniqueName(repository.unique_name)) | ||
898 | 51 | |||
899 | 52 | def test_package(self): | ||
900 | 53 | dsp = self.factory.makeDistributionSourcePackage() | ||
901 | 54 | repository = self.factory.makeGitRepository(target=dsp) | ||
902 | 55 | self.assertEqual( | ||
903 | 56 | repository, self.lookup.getByUniqueName(repository.unique_name)) | ||
904 | 57 | |||
905 | 58 | def test_personal(self): | ||
906 | 59 | owner = self.factory.makePerson() | ||
907 | 60 | repository = self.factory.makeGitRepository(owner=owner, target=owner) | ||
908 | 61 | self.assertEqual( | ||
909 | 62 | repository, self.lookup.getByUniqueName(repository.unique_name)) | ||
910 | 63 | |||
911 | 64 | |||
912 | 65 | class TestGetByPath(TestCaseWithFactory): | ||
913 | 66 | """Test `IGitLookup.getByPath`.""" | ||
914 | 67 | |||
915 | 68 | layer = DatabaseFunctionalLayer | ||
916 | 69 | |||
917 | 70 | def setUp(self): | ||
918 | 71 | super(TestGetByPath, self).setUp() | ||
919 | 72 | self.lookup = getUtility(IGitLookup) | ||
920 | 73 | |||
921 | 74 | def test_project(self): | ||
922 | 75 | repository = self.factory.makeGitRepository() | ||
923 | 76 | self.assertEqual( | ||
924 | 77 | repository, self.lookup.getByPath(repository.unique_name)) | ||
925 | 78 | |||
926 | 79 | def test_project_default(self): | ||
927 | 80 | repository = self.factory.makeGitRepository() | ||
928 | 81 | with person_logged_in(repository.target.owner): | ||
929 | 82 | getUtility(IGitRepositorySet).setDefaultRepository( | ||
930 | 83 | repository.target, repository) | ||
931 | 84 | self.assertEqual( | ||
932 | 85 | repository, self.lookup.getByPath(repository.shortened_path)) | ||
933 | 86 | |||
934 | 87 | def test_package(self): | ||
935 | 88 | dsp = self.factory.makeDistributionSourcePackage() | ||
936 | 89 | repository = self.factory.makeGitRepository(target=dsp) | ||
937 | 90 | self.assertEqual( | ||
938 | 91 | repository, self.lookup.getByPath(repository.unique_name)) | ||
939 | 92 | |||
940 | 93 | def test_package_default(self): | ||
941 | 94 | dsp = self.factory.makeDistributionSourcePackage() | ||
942 | 95 | repository = self.factory.makeGitRepository(target=dsp) | ||
943 | 96 | with person_logged_in(repository.target.distribution.owner): | ||
944 | 97 | getUtility(IGitRepositorySet).setDefaultRepository( | ||
945 | 98 | repository.target, repository) | ||
946 | 99 | self.assertEqual( | ||
947 | 100 | repository, self.lookup.getByPath(repository.shortened_path)) | ||
948 | 101 | |||
949 | 102 | def test_personal(self): | ||
950 | 103 | owner = self.factory.makePerson() | ||
951 | 104 | repository = self.factory.makeGitRepository(owner=owner, target=owner) | ||
952 | 105 | self.assertEqual( | ||
953 | 106 | repository, self.lookup.getByPath(repository.unique_name)) | ||
954 | 107 | |||
955 | 108 | def test_invalid_namespace(self): | ||
956 | 109 | # If `getByPath` is given a path to something with no default Git | ||
957 | 110 | # repository, such as a distribution, it returns None. | ||
958 | 111 | distro = self.factory.makeDistribution() | ||
959 | 112 | self.assertIsNone(self.lookup.getByPath(distro.name)) | ||
960 | 113 | |||
961 | 114 | def test_no_default_git_repository(self): | ||
962 | 115 | # If `getByPath` is given a path to something that could have a Git | ||
963 | 116 | # repository but doesn't, it returns None. | ||
964 | 117 | project = self.factory.makeProduct() | ||
965 | 118 | self.assertIsNone(self.lookup.getByPath(project.name)) | ||
966 | 119 | |||
967 | 120 | |||
968 | 121 | class TestGetByUrl(TestCaseWithFactory): | ||
969 | 122 | """Test `IGitLookup.getByUrl`.""" | ||
970 | 123 | |||
971 | 124 | layer = DatabaseFunctionalLayer | ||
972 | 125 | |||
973 | 126 | def setUp(self): | ||
974 | 127 | super(TestGetByUrl, self).setUp() | ||
975 | 128 | self.lookup = getUtility(IGitLookup) | ||
976 | 129 | |||
977 | 130 | def makeProjectRepository(self): | ||
978 | 131 | owner = self.factory.makePerson(name="aa") | ||
979 | 132 | project = self.factory.makeProduct(name="bb") | ||
980 | 133 | return self.factory.makeGitRepository( | ||
981 | 134 | owner=owner, target=project, name=u"cc") | ||
982 | 135 | |||
983 | 136 | def test_getByUrl_with_none(self): | ||
984 | 137 | # getByUrl returns None if given None. | ||
985 | 138 | self.assertIsNone(self.lookup.getByUrl(None)) | ||
986 | 139 | |||
987 | 140 | def assertUrlMatches(self, url, repository): | ||
988 | 141 | self.assertEqual(repository, self.lookup.getByUrl(url)) | ||
989 | 142 | |||
990 | 143 | def test_getByUrl_with_trailing_slash(self): | ||
991 | 144 | # Trailing slashes are stripped from the URL prior to searching. | ||
992 | 145 | repository = self.makeProjectRepository() | ||
993 | 146 | self.assertUrlMatches( | ||
994 | 147 | "git://git.launchpad.dev/~aa/bb/+git/cc/", repository) | ||
995 | 148 | |||
996 | 149 | def test_getByUrl_with_git(self): | ||
997 | 150 | # getByUrl recognises LP repositories for git URLs. | ||
998 | 151 | repository = self.makeProjectRepository() | ||
999 | 152 | self.assertUrlMatches( | ||
1000 | 153 | "git://git.launchpad.dev/~aa/bb/+git/cc", repository) | ||
1001 | 154 | |||
1002 | 155 | def test_getByUrl_with_git_ssh(self): | ||
1003 | 156 | # getByUrl recognises LP repositories for git+ssh URLs. | ||
1004 | 157 | repository = self.makeProjectRepository() | ||
1005 | 158 | self.assertUrlMatches( | ||
1006 | 159 | "git+ssh://git.launchpad.dev/~aa/bb/+git/cc", repository) | ||
1007 | 160 | |||
1008 | 161 | def test_getByUrl_with_https(self): | ||
1009 | 162 | # getByUrl recognises LP repositories for https URLs. | ||
1010 | 163 | repository = self.makeProjectRepository() | ||
1011 | 164 | self.assertUrlMatches( | ||
1012 | 165 | "https://git.launchpad.dev/~aa/bb/+git/cc", repository) | ||
1013 | 166 | |||
1014 | 167 | def test_getByUrl_with_ssh(self): | ||
1015 | 168 | # getByUrl recognises LP repositories for ssh URLs. | ||
1016 | 169 | repository = self.makeProjectRepository() | ||
1017 | 170 | self.assertUrlMatches( | ||
1018 | 171 | "ssh://git.launchpad.dev/~aa/bb/+git/cc", repository) | ||
1019 | 172 | |||
1020 | 173 | def test_getByUrl_with_ftp(self): | ||
1021 | 174 | # getByUrl does not recognise LP repositories for ftp URLs. | ||
1022 | 175 | self.makeProjectRepository() | ||
1023 | 176 | self.assertIsNone( | ||
1024 | 177 | self.lookup.getByUrl("ftp://git.launchpad.dev/~aa/bb/+git/cc")) | ||
1025 | 178 | |||
1026 | 179 | def test_getByUrl_with_lp(self): | ||
1027 | 180 | # getByUrl supports lp: URLs. | ||
1028 | 181 | url = "lp:~aa/bb/+git/cc" | ||
1029 | 182 | self.assertIsNone(self.lookup.getByUrl(url)) | ||
1030 | 183 | repository = self.makeProjectRepository() | ||
1031 | 184 | self.assertUrlMatches(url, repository) | ||
1032 | 185 | |||
1033 | 186 | def test_getByUrl_with_default(self): | ||
1034 | 187 | # getByUrl honours default repositories when looking up URLs. | ||
1035 | 188 | repository = self.makeProjectRepository() | ||
1036 | 189 | with person_logged_in(repository.target.owner): | ||
1037 | 190 | getUtility(IGitRepositorySet).setDefaultRepository( | ||
1038 | 191 | repository.target, repository) | ||
1039 | 192 | self.assertUrlMatches("lp:bb", repository) | ||
1040 | 193 | |||
1041 | 194 | def test_uriToPath(self): | ||
1042 | 195 | # uriToPath only supports our own URLs with certain schemes. | ||
1043 | 196 | uri = URI(config.codehosting.git_anon_root) | ||
1044 | 197 | uri.path = "/~foo/bar/baz" | ||
1045 | 198 | # Test valid schemes. | ||
1046 | 199 | for scheme in ("git", "git+ssh", "https", "ssh"): | ||
1047 | 200 | uri.scheme = scheme | ||
1048 | 201 | self.assertEqual("~foo/bar/baz", self.lookup.uriToPath(uri)) | ||
1049 | 202 | # Test an invalid scheme. | ||
1050 | 203 | uri.scheme = "ftp" | ||
1051 | 204 | self.assertIsNone(self.lookup.uriToPath(uri)) | ||
1052 | 205 | # Test valid scheme but invalid domain. | ||
1053 | 206 | uri.scheme = 'sftp' | ||
1054 | 207 | uri.host = 'example.com' | ||
1055 | 208 | self.assertIsNone(self.lookup.uriToPath(uri)) | ||
1056 | 209 | |||
1057 | 210 | |||
1058 | 211 | class TestGitTraverser(TestCaseWithFactory): | ||
1059 | 212 | """Tests for the repository traverser.""" | ||
1060 | 213 | |||
1061 | 214 | layer = DatabaseFunctionalLayer | ||
1062 | 215 | |||
1063 | 216 | def setUp(self): | ||
1064 | 217 | super(TestGitTraverser, self).setUp() | ||
1065 | 218 | self.traverser = getUtility(IGitTraverser) | ||
1066 | 219 | |||
1067 | 220 | def assertTraverses(self, path, owner, target, repository=None): | ||
1068 | 221 | self.assertEqual( | ||
1069 | 222 | (owner, target, repository), self.traverser.traverse_path(path)) | ||
1070 | 223 | |||
1071 | 224 | def test_nonexistent_project(self): | ||
1072 | 225 | # `traverse_path` raises `NoSuchProduct` when resolving a path of | ||
1073 | 226 | # 'project' if the project doesn't exist. | ||
1074 | 227 | self.assertRaises(NoSuchProduct, self.traverser.traverse_path, "bb") | ||
1075 | 228 | |||
1076 | 229 | def test_invalid_project(self): | ||
1077 | 230 | # `traverse_path` raises `InvalidProductName` when resolving a path | ||
1078 | 231 | # for a completely invalid default project repository. | ||
1079 | 232 | self.assertRaises( | ||
1080 | 233 | InvalidProductName, self.traverser.traverse_path, "b") | ||
1081 | 234 | |||
1082 | 235 | def test_project(self): | ||
1083 | 236 | # `traverse_path` resolves the name of a project to the project itself. | ||
1084 | 237 | project = self.factory.makeProduct() | ||
1085 | 238 | self.assertTraverses(project.name, None, project) | ||
1086 | 239 | |||
1087 | 240 | def test_project_no_named_repositories(self): | ||
1088 | 241 | # Projects do not have named repositories without an owner context, | ||
1089 | 242 | # so trying to traverse to them raises `InvalidNamespace`. | ||
1090 | 243 | project = self.factory.makeProduct() | ||
1091 | 244 | repository = self.factory.makeGitRepository(target=project) | ||
1092 | 245 | self.assertRaises( | ||
1093 | 246 | InvalidNamespace, self.traverser.traverse_path, | ||
1094 | 247 | "%s/+git/%s" % (project.name, repository.name)) | ||
1095 | 248 | |||
1096 | 249 | def test_no_such_distribution(self): | ||
1097 | 250 | # `traverse_path` raises `NoSuchProduct` if the distribution doesn't | ||
1098 | 251 | # exist. That's because it can't tell the difference between the | ||
1099 | 252 | # name of a project that doesn't exist and the name of a | ||
1100 | 253 | # distribution that doesn't exist. | ||
1101 | 254 | self.assertRaises( | ||
1102 | 255 | NoSuchProduct, self.traverser.traverse_path, | ||
1103 | 256 | "distro/+source/package") | ||
1104 | 257 | |||
1105 | 258 | def test_missing_sourcepackagename(self): | ||
1106 | 259 | # `traverse_path` raises `InvalidNamespace` if there are no segments | ||
1107 | 260 | # after '+source'. | ||
1108 | 261 | self.factory.makeDistribution(name="distro") | ||
1109 | 262 | self.assertRaises( | ||
1110 | 263 | InvalidNamespace, self.traverser.traverse_path, "distro/+source") | ||
1111 | 264 | |||
1112 | 265 | def test_no_such_sourcepackagename(self): | ||
1113 | 266 | # `traverse_path` raises `NoSuchSourcePackageName` if the package in | ||
1114 | 267 | # distro/+source/package doesn't exist. | ||
1115 | 268 | self.factory.makeDistribution(name="distro") | ||
1116 | 269 | self.assertRaises( | ||
1117 | 270 | NoSuchSourcePackageName, self.traverser.traverse_path, | ||
1118 | 271 | "distro/+source/nonexistent") | ||
1119 | 272 | |||
1120 | 273 | def test_package(self): | ||
1121 | 274 | # `traverse_path` resolves 'distro/+source/package' to the | ||
1122 | 275 | # distribution source package. | ||
1123 | 276 | dsp = self.factory.makeDistributionSourcePackage() | ||
1124 | 277 | path = "%s/+source/%s" % ( | ||
1125 | 278 | dsp.distribution.name, dsp.sourcepackagename.name) | ||
1126 | 279 | self.assertTraverses(path, None, dsp) | ||
1127 | 280 | |||
1128 | 281 | def test_package_no_named_repositories(self): | ||
1129 | 282 | # Packages do not have named repositories without an owner context, | ||
1130 | 283 | # so trying to traverse to them raises `InvalidNamespace`. | ||
1131 | 284 | dsp = self.factory.makeDistributionSourcePackage() | ||
1132 | 285 | repository = self.factory.makeGitRepository(target=dsp) | ||
1133 | 286 | self.assertRaises( | ||
1134 | 287 | InvalidNamespace, self.traverser.traverse_path, | ||
1135 | 288 | "%s/+source/%s/+git/%s" % ( | ||
1136 | 289 | dsp.distribution.name, dsp.sourcepackagename.name, | ||
1137 | 290 | repository.name)) | ||
1138 | 291 | |||
1139 | 292 | def test_nonexistent_person(self): | ||
1140 | 293 | # `traverse_path` raises `NoSuchPerson` when resolving a path of | ||
1141 | 294 | # '~person/project' if the person doesn't exist. | ||
1142 | 295 | self.assertRaises( | ||
1143 | 296 | NoSuchPerson, self.traverser.traverse_path, "~person/bb") | ||
1144 | 297 | |||
1145 | 298 | def test_nonexistent_person_project(self): | ||
1146 | 299 | # `traverse_path` raises `NoSuchProduct` when resolving a path of | ||
1147 | 300 | # '~person/project' if the project doesn't exist. | ||
1148 | 301 | self.factory.makePerson(name="person") | ||
1149 | 302 | self.assertRaises( | ||
1150 | 303 | NoSuchProduct, self.traverser.traverse_path, "~person/bb") | ||
1151 | 304 | |||
1152 | 305 | def test_invalid_person_project(self): | ||
1153 | 306 | # `traverse_path` raises `InvalidProductName` when resolving a path | ||
1154 | 307 | # for a person and a completely invalid default project repository. | ||
1155 | 308 | self.factory.makePerson(name="person") | ||
1156 | 309 | self.assertRaises( | ||
1157 | 310 | InvalidProductName, self.traverser.traverse_path, "~person/b") | ||
1158 | 311 | |||
1159 | 312 | def test_invalid_person_project_group(self): | ||
1160 | 313 | # Project groups do not have repositories, so `traverse_path` raises | ||
1161 | 314 | # `InvalidNamespace` when asked to traverse to them. | ||
1162 | 315 | person = self.factory.makePerson() | ||
1163 | 316 | project_group = self.factory.makeProject() | ||
1164 | 317 | self.assertRaises( | ||
1165 | 318 | InvalidNamespace, self.traverser.traverse_path, | ||
1166 | 319 | "~%s/%s/+git/repository" % (person.name, project_group.name)) | ||
1167 | 320 | |||
1168 | 321 | def test_person_missing_repository_name(self): | ||
1169 | 322 | # `traverse_path` raises `InvalidNamespace` if there are no segments | ||
1170 | 323 | # after '+git'. | ||
1171 | 324 | self.factory.makePerson(name="person") | ||
1172 | 325 | self.assertRaises( | ||
1173 | 326 | InvalidNamespace, self.traverser.traverse_path, "~person/+git") | ||
1174 | 327 | |||
1175 | 328 | def test_person_no_such_repository(self): | ||
1176 | 329 | # `traverse_path` raises `NoSuchGitRepository` if the repository in | ||
1177 | 330 | # project/+git/repository doesn't exist. | ||
1178 | 331 | self.factory.makePerson(name="person") | ||
1179 | 332 | self.assertRaises( | ||
1180 | 333 | NoSuchGitRepository, self.traverser.traverse_path, | ||
1181 | 334 | "~person/+git/repository") | ||
1182 | 335 | |||
1183 | 336 | def test_person_repository(self): | ||
1184 | 337 | # `traverse_path` resolves an existing project repository. | ||
1185 | 338 | person = self.factory.makePerson(name="person") | ||
1186 | 339 | repository = self.factory.makeGitRepository( | ||
1187 | 340 | owner=person, target=person, name=u"repository") | ||
1188 | 341 | self.assertTraverses( | ||
1189 | 342 | "~person/+git/repository", person, person, repository) | ||
1190 | 343 | |||
1191 | 344 | def test_person_project(self): | ||
1192 | 345 | # `traverse_path` resolves '~person/project' to the person and the | ||
1193 | 346 | # project. | ||
1194 | 347 | person = self.factory.makePerson() | ||
1195 | 348 | project = self.factory.makeProduct() | ||
1196 | 349 | self.assertTraverses( | ||
1197 | 350 | "~%s/%s" % (person.name, project.name), person, project) | ||
1198 | 351 | |||
1199 | 352 | def test_person_project_missing_repository_name(self): | ||
1200 | 353 | # `traverse_path` raises `InvalidNamespace` if there are no segments | ||
1201 | 354 | # after '+git'. | ||
1202 | 355 | person = self.factory.makePerson() | ||
1203 | 356 | project = self.factory.makeProduct() | ||
1204 | 357 | self.assertRaises( | ||
1205 | 358 | InvalidNamespace, self.traverser.traverse_path, | ||
1206 | 359 | "~%s/%s/+git" % (person.name, project.name)) | ||
1207 | 360 | |||
1208 | 361 | def test_person_project_no_such_repository(self): | ||
1209 | 362 | # `traverse_path` raises `NoSuchGitRepository` if the repository in | ||
1210 | 363 | # ~person/project/+git/repository doesn't exist. | ||
1211 | 364 | person = self.factory.makePerson() | ||
1212 | 365 | project = self.factory.makeProduct() | ||
1213 | 366 | self.assertRaises( | ||
1214 | 367 | NoSuchGitRepository, self.traverser.traverse_path, | ||
1215 | 368 | "~%s/%s/+git/nonexistent" % (person.name, project.name)) | ||
1216 | 369 | |||
1217 | 370 | def test_person_project_repository(self): | ||
1218 | 371 | # `traverse_path` resolves an existing person-project repository. | ||
1219 | 372 | person = self.factory.makePerson() | ||
1220 | 373 | project = self.factory.makeProduct() | ||
1221 | 374 | repository = self.factory.makeGitRepository( | ||
1222 | 375 | owner=person, target=project) | ||
1223 | 376 | self.assertTraverses( | ||
1224 | 377 | "~%s/%s/+git/%s" % (person.name, project.name, repository.name), | ||
1225 | 378 | person, project, repository) | ||
1226 | 379 | |||
1227 | 380 | def test_no_such_person_distribution(self): | ||
1228 | 381 | # `traverse_path` raises `NoSuchProduct` when resolving a path of | ||
1229 | 382 | # '~person/distro' if the distribution doesn't exist. That's | ||
1230 | 383 | # because it can't tell the difference between the name of a project | ||
1231 | 384 | # that doesn't exist and the name of a distribution that doesn't | ||
1232 | 385 | # exist. | ||
1233 | 386 | self.factory.makePerson(name="person") | ||
1234 | 387 | self.assertRaises( | ||
1235 | 388 | NoSuchProduct, self.traverser.traverse_path, | ||
1236 | 389 | "~person/distro/+source/package") | ||
1237 | 390 | |||
1238 | 391 | def test_missing_person_sourcepackagename(self): | ||
1239 | 392 | # `traverse_path` raises `InvalidNamespace` if there are no segments | ||
1240 | 393 | # after '+source' in a person-DSP path. | ||
1241 | 394 | self.factory.makePerson(name="person") | ||
1242 | 395 | self.factory.makeDistribution(name="distro") | ||
1243 | 396 | self.assertRaises( | ||
1244 | 397 | InvalidNamespace, self.traverser.traverse_path, | ||
1245 | 398 | "~person/distro/+source") | ||
1246 | 399 | |||
1247 | 400 | def test_no_such_person_sourcepackagename(self): | ||
1248 | 401 | # `traverse_path` raises `NoSuchSourcePackageName` if the package in | ||
1249 | 402 | # ~person/distro/+source/package doesn't exist. | ||
1250 | 403 | self.factory.makePerson(name="person") | ||
1251 | 404 | self.factory.makeDistribution(name="distro") | ||
1252 | 405 | self.assertRaises( | ||
1253 | 406 | NoSuchSourcePackageName, self.traverser.traverse_path, | ||
1254 | 407 | "~person/distro/+source/nonexistent") | ||
1255 | 408 | |||
1256 | 409 | def test_person_package(self): | ||
1257 | 410 | # `traverse_path` resolves '~person/distro/+source/package' to the | ||
1258 | 411 | # person and the DSP. | ||
1259 | 412 | person = self.factory.makePerson() | ||
1260 | 413 | dsp = self.factory.makeDistributionSourcePackage() | ||
1261 | 414 | path = "~%s/%s/+source/%s" % ( | ||
1262 | 415 | person.name, dsp.distribution.name, dsp.sourcepackagename.name) | ||
1263 | 416 | self.assertTraverses(path, person, dsp) | ||
1264 | 417 | |||
1265 | 418 | def test_person_package_missing_repository_name(self): | ||
1266 | 419 | # `traverse_path` raises `InvalidNamespace` if there are no segments | ||
1267 | 420 | # after '+git'. | ||
1268 | 421 | person = self.factory.makePerson() | ||
1269 | 422 | dsp = self.factory.makeDistributionSourcePackage() | ||
1270 | 423 | self.assertRaises( | ||
1271 | 424 | InvalidNamespace, self.traverser.traverse_path, | ||
1272 | 425 | "~%s/%s/+source/%s/+git" % ( | ||
1273 | 426 | person.name, dsp.distribution.name, | ||
1274 | 427 | dsp.sourcepackagename.name)) | ||
1275 | 428 | |||
1276 | 429 | def test_person_package_no_such_repository(self): | ||
1277 | 430 | # `traverse_path` raises `NoSuchGitRepository` if the repository in | ||
1278 | 431 | # ~person/project/+git/repository doesn't exist. | ||
1279 | 432 | person = self.factory.makePerson() | ||
1280 | 433 | dsp = self.factory.makeDistributionSourcePackage() | ||
1281 | 434 | self.assertRaises( | ||
1282 | 435 | NoSuchGitRepository, self.traverser.traverse_path, | ||
1283 | 436 | "~%s/%s/+source/%s/+git/nonexistent" % ( | ||
1284 | 437 | person.name, dsp.distribution.name, | ||
1285 | 438 | dsp.sourcepackagename.name)) | ||
1286 | 439 | |||
1287 | 440 | def test_person_package_repository(self): | ||
1288 | 441 | # `traverse_path` resolves an existing person-package repository. | ||
1289 | 442 | person = self.factory.makePerson() | ||
1290 | 443 | dsp = self.factory.makeDistributionSourcePackage() | ||
1291 | 444 | repository = self.factory.makeGitRepository(owner=person, target=dsp) | ||
1292 | 445 | self.assertTraverses( | ||
1293 | 446 | "~%s/%s/+source/%s/+git/%s" % ( | ||
1294 | 447 | person.name, dsp.distribution.name, dsp.sourcepackagename.name, | ||
1295 | 448 | repository.name), | ||
1296 | 449 | person, dsp, repository) | ||
1297 | 0 | 450 | ||
1298 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
1299 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-02-26 11:34:47 +0000 | |||
1300 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-27 10:23:01 +0000 | |||
1301 | @@ -512,6 +512,19 @@ | |||
1302 | 512 | # GitRepositorySet instances provide IGitRepositorySet. | 512 | # GitRepositorySet instances provide IGitRepositorySet. |
1303 | 513 | verifyObject(IGitRepositorySet, self.repository_set) | 513 | verifyObject(IGitRepositorySet, self.repository_set) |
1304 | 514 | 514 | ||
1305 | 515 | def test_getByPath(self): | ||
1306 | 516 | # getByPath returns a repository matching the path that it's given. | ||
1307 | 517 | a = self.factory.makeGitRepository() | ||
1308 | 518 | self.factory.makeGitRepository() | ||
1309 | 519 | repository = self.repository_set.getByPath(a.owner, a.shortened_path) | ||
1310 | 520 | self.assertEqual(a, repository) | ||
1311 | 521 | |||
1312 | 522 | def test_getByPath_not_found(self): | ||
1313 | 523 | # If a repository cannot be found for a path, then getByPath returns | ||
1314 | 524 | # None. | ||
1315 | 525 | person = self.factory.makePerson() | ||
1316 | 526 | self.assertIsNone(self.repository_set.getByPath(person, "nonexistent")) | ||
1317 | 527 | |||
1318 | 515 | def test_setDefaultRepository_refuses_person(self): | 528 | def test_setDefaultRepository_refuses_person(self): |
1319 | 516 | # setDefaultRepository refuses if the target is a person. | 529 | # setDefaultRepository refuses if the target is a person. |
1320 | 517 | person = self.factory.makePerson() | 530 | person = self.factory.makePerson() |