Merge lp:~cjwatson/launchpad/git-lookup into lp:launchpad

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
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
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-20 16:28:06 +0000
+++ lib/lp/code/configure.zcml 2015-02-27 10:23:01 +0000
@@ -865,6 +865,24 @@
865 <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />865 <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
866 <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />866 <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
867867
868 <class class="lp.code.model.gitlookup.GitLookup">
869 <allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
870 </class>
871 <securedutility
872 class="lp.code.model.gitlookup.GitLookup"
873 provides="lp.code.interfaces.gitlookup.IGitLookup">
874 <allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
875 </securedutility>
876 <securedutility
877 class="lp.code.model.gitlookup.GitTraverser"
878 provides="lp.code.interfaces.gitlookup.IGitTraverser">
879 <allow interface="lp.code.interfaces.gitlookup.IGitTraverser" />
880 </securedutility>
881 <adapter factory="lp.code.model.gitlookup.PersonGitTraversable" />
882 <adapter factory="lp.code.model.gitlookup.ProjectGitTraversable" />
883 <adapter factory="lp.code.model.gitlookup.DistributionGitTraversable" />
884 <adapter factory="lp.code.model.gitlookup.DistributionSourcePackageGitTraversable" />
885
868 <lp:help-folder folder="help" name="+help-code" />886 <lp:help-folder folder="help" name="+help-code" />
869887
870 <!-- Diffs -->888 <!-- Diffs -->
871889
=== added file 'lib/lp/code/interfaces/gitlookup.py'
--- lib/lp/code/interfaces/gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitlookup.py 2015-02-27 10:23:01 +0000
@@ -0,0 +1,137 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Utility for looking up Git repositories by name."""
5
6__metaclass__ = type
7__all__ = [
8 'IGitLookup',
9 'IGitTraversable',
10 'IGitTraverser',
11 ]
12
13from zope.interface import Interface
14
15
16class IGitTraversable(Interface):
17 """A thing that can be traversed to find a thing with a Git repository."""
18
19 def traverse(owner, name, segments):
20 """Return the object beneath this one that matches 'name'.
21
22 :param owner: The current `IPerson` context, or None.
23 :param name: The name of the object being traversed to.
24 :param segments: An iterator over remaining path segments.
25 :return: A tuple of
26 * an `IPerson`, or None;
27 * an `IGitTraversable`;
28 * an `IGitRepository`, or None; if this is non-None then
29 traversing should stop.
30 """
31
32
33class IGitTraverser(Interface):
34 """Utility for traversing to an object that can have a Git repository."""
35
36 def traverse(segments):
37 """Traverse to the object referred to by a prefix of the 'segments'
38 iterable.
39
40 :raises InvalidNamespace: If the path cannot be parsed as a
41 repository namespace.
42 :raises InvalidProductName: If the project component of the path is
43 not a valid name.
44 :raises NoSuchGitRepository: If there is a '+git' segment, but the
45 following segment doesn't match an existing Git repository.
46 :raises NoSuchPerson: If the first segment of the path begins with a
47 '~', but we can't find a person matching the remainder.
48 :raises NoSuchProduct: If we can't find a project that matches the
49 project component of the path.
50 :raises NoSuchSourcePackageName: If the source package referred to
51 does not exist.
52
53 :return: A tuple of::
54 * an `IPerson`, or None;
55 * an `IHasGitRepositories`;
56 * an `IGitRepository`, or None.
57 """
58
59 def traverse_path(path):
60 """Traverse to the object referred to by 'path'.
61
62 All segments of 'path' must be consumed.
63
64 :raises InvalidNamespace: If the path cannot be parsed as a
65 repository namespace.
66 :raises InvalidProductName: If the project component of the path is
67 not a valid name.
68 :raises NoSuchGitRepository: If there is a '+git' segment, but the
69 following segment doesn't match an existing Git repository.
70 :raises NoSuchPerson: If the first segment of the path begins with a
71 '~', but we can't find a person matching the remainder.
72 :raises NoSuchProduct: If we can't find a project that matches the
73 project component of the path.
74 :raises NoSuchSourcePackageName: If the source package referred to
75 does not exist.
76
77 :return: A tuple of::
78 * an `IPerson`, or None;
79 * an `IHasGitRepositories`;
80 * an `IGitRepository`, or None.
81 """
82
83
84class IGitLookup(Interface):
85 """Utility for looking up a Git repository by name."""
86
87 def get(repository_id, default=None):
88 """Return the repository with the given id.
89
90 Return the default value if there is no such repository.
91 """
92
93 def getByUniqueName(unique_name):
94 """Find a repository by its unique name.
95
96 Unique names have one of the following forms:
97 ~OWNER/PROJECT/+git/NAME
98 ~OWNER/DISTRO/+source/SOURCE/+git/NAME
99 ~OWNER/+git/NAME
100
101 :return: An `IGitRepository`, or None.
102 """
103
104 def uriToPath(uri):
105 """Return the path for the URI, if the URI is on codehosting.
106
107 This does not ensure that the path is valid.
108
109 :param uri: An instance of lazr.uri.URI
110 :return: The path if possible; None if the URI is not a valid
111 codehosting URI.
112 """
113
114 def getByUrl(url):
115 """Find a repository by URL.
116
117 Either from the URL on git.launchpad.net (various schemes) or the
118 lp: URL (which relies on client-side configuration).
119 """
120
121 def getByPath(path):
122 """Find a repository by its path.
123
124 Any of these forms may be used, with or without a leading slash:
125 Unique names:
126 ~OWNER/PROJECT/+git/NAME
127 ~OWNER/DISTRO/+source/SOURCE/+git/NAME
128 ~OWNER/+git/NAME
129 Owner-target default aliases:
130 ~OWNER/PROJECT
131 ~OWNER/DISTRO/+source/SOURCE
132 Official aliases:
133 PROJECT
134 DISTRO/+source/SOURCE
135
136 :return: An `IGitRepository`, or None.
137 """
0138
=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
--- lib/lp/code/interfaces/gitnamespace.py 2015-02-26 13:43:51 +0000
+++ lib/lp/code/interfaces/gitnamespace.py 2015-02-27 10:23:01 +0000
@@ -148,83 +148,6 @@
148 def get(person, project=None, distribution=None, sourcepackagename=None):148 def get(person, project=None, distribution=None, sourcepackagename=None):
149 """Return the appropriate `IGitNamespace` for the given objects."""149 """Return the appropriate `IGitNamespace` for the given objects."""
150150
151 def interpret(person, project, distribution, sourcepackagename):
152 """Like `get`, but takes names of objects.
153
154 :raise NoSuchPerson: If the person referred to cannot be found.
155 :raise NoSuchProduct: If the project referred to cannot be found.
156 :raise NoSuchDistribution: If the distribution referred to cannot be
157 found.
158 :raise NoSuchSourcePackageName: If the sourcepackagename referred to
159 cannot be found.
160 :return: An `IGitNamespace`.
161 """
162
163 def parse(namespace_name):
164 """Parse 'namespace_name' into its components.
165
166 The name of a namespace is actually a path containing many elements,
167 each of which maps to a particular kind of object in Launchpad.
168 Elements that can appear in a namespace name are: 'person',
169 'project', 'distribution', and 'sourcepackagename'.
170
171 `parse` returns a dict which maps the names of these elements (e.g.
172 'person', 'project') to the values of these elements (e.g. 'mark',
173 'firefox'). If the given path doesn't include a particular kind of
174 element, the dict maps that element name to None.
175
176 For example::
177 parse('~foo/bar') => {
178 'person': 'foo', 'project': 'bar', 'distribution': None,
179 'sourcepackagename': None,
180 }
181
182 If the given 'namespace_name' cannot be parsed, then we raise an
183 `InvalidNamespace` error.
184
185 :raise InvalidNamespace: If the name is too long, too short, or
186 malformed.
187 :return: A dict with keys matching each component in
188 'namespace_name'.
189 """
190
191 def lookup(namespace_name):
192 """Return the `IGitNamespace` for 'namespace_name'.
193
194 :raise InvalidNamespace: if namespace_name cannot be parsed.
195 :raise NoSuchPerson: if the person referred to cannot be found.
196 :raise NoSuchProduct: if the project referred to cannot be found.
197 :raise NoSuchDistribution: if the distribution referred to cannot be
198 found.
199 :raise NoSuchSourcePackageName: if the sourcepackagename referred to
200 cannot be found.
201 :return: An `IGitNamespace`.
202 """
203
204 def traverse(segments):
205 """Look up the Git repository at the path given by 'segments'.
206
207 The iterable 'segments' will be consumed until a repository is
208 found. As soon as a repository is found, the repository will be
209 returned and the consumption of segments will stop. Thus, there
210 will often be unconsumed segments that can be used for further
211 traversal.
212
213 :param segments: An iterable of URL segments, a prefix of which
214 identifies a Git repository. The first segment is the username,
215 *not* preceded by a '~`.
216 :raise InvalidNamespace: if there are not enough segments to define a
217 repository.
218 :raise NoSuchPerson: if the person referred to cannot be found.
219 :raise NoSuchProduct: if the product or distro referred to cannot be
220 found.
221 :raise NoSuchDistribution: if the distribution referred to cannot be
222 found.
223 :raise NoSuchSourcePackageName: if the sourcepackagename referred to
224 cannot be found.
225 :return: `IGitRepository`.
226 """
227
228151
229def get_git_namespace(target, owner):152def get_git_namespace(target, owner):
230 if IProduct.providedBy(target):153 if IProduct.providedBy(target):
231154
=== modified file 'lib/lp/code/model/branchlookup.py'
--- lib/lp/code/model/branchlookup.py 2015-01-29 13:09:37 +0000
+++ lib/lp/code/model/branchlookup.py 2015-02-27 10:23:01 +0000
@@ -72,13 +72,13 @@
72from lp.services.webapp.authorization import check_permission72from lp.services.webapp.authorization import check_permission
7373
7474
75def adapt(provided, interface):75def adapt(obj, interface):
76 """Adapt 'obj' to 'interface', using multi-adapters if necessary."""76 """Adapt 'obj' to 'interface', using multi-adapters if necessary."""
77 required = interface(provided, None)77 required = interface(obj, None)
78 if required is not None:78 if required is not None:
79 return required79 return required
80 try:80 try:
81 return queryMultiAdapter(provided, interface)81 return queryMultiAdapter(obj, interface)
82 except TypeError:82 except TypeError:
83 return None83 return None
8484
8585
=== added file 'lib/lp/code/model/gitlookup.py'
--- lib/lp/code/model/gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitlookup.py 2015-02-27 10:23:01 +0000
@@ -0,0 +1,349 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Database implementation of the Git repository lookup utility."""
5
6__metaclass__ = type
7# This module doesn't export anything. If you want to look up Git
8# repositories by name, then get the IGitLookup utility.
9__all__ = []
10
11from lazr.uri import (
12 InvalidURIError,
13 URI,
14 )
15from zope.component import (
16 adapts,
17 getUtility,
18 queryMultiAdapter,
19 )
20from zope.interface import implements
21
22from lp.app.errors import NameLookupFailed
23from lp.app.validators.name import valid_name
24from lp.code.errors import (
25 InvalidNamespace,
26 NoSuchGitRepository,
27 )
28from lp.code.interfaces.gitlookup import (
29 IGitLookup,
30 IGitTraversable,
31 IGitTraverser,
32 )
33from lp.code.interfaces.gitnamespace import IGitNamespaceSet
34from lp.code.interfaces.gitrepository import IGitRepositorySet
35from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
36from lp.code.model.gitrepository import GitRepository
37from lp.registry.errors import NoSuchSourcePackageName
38from lp.registry.interfaces.distribution import IDistribution
39from lp.registry.interfaces.distributionsourcepackage import (
40 IDistributionSourcePackage,
41 )
42from lp.registry.interfaces.person import (
43 IPerson,
44 IPersonSet,
45 NoSuchPerson,
46 )
47from lp.registry.interfaces.pillar import IPillarNameSet
48from lp.registry.interfaces.product import (
49 InvalidProductName,
50 IProduct,
51 NoSuchProduct,
52 )
53from lp.services.config import config
54from lp.services.database.interfaces import IStore
55
56
57def adapt(obj, interface):
58 """Adapt 'obj' to 'interface', using multi-adapters if necessary."""
59 required = interface(obj, None)
60 if required is not None:
61 return required
62 try:
63 return queryMultiAdapter(obj, interface)
64 except TypeError:
65 return None
66
67
68class RootGitTraversable:
69 """Root traversable for Git repository objects.
70
71 Corresponds to '/' in the path. From here, you can traverse to a
72 project or a distribution, optionally with a person context as well.
73 """
74
75 implements(IGitTraversable)
76
77 # Marker for references to Git URL layouts: ##GITNAMESPACE##
78 def traverse(self, owner, name, segments):
79 """See `IGitTraversable`.
80
81 :raises InvalidProductName: If 'name' is not a valid name.
82 :raises NoSuchPerson: If 'name' begins with a '~', but the remainder
83 doesn't match an existing person.
84 :raises NoSuchProduct: If 'name' doesn't match an existing pillar.
85 :return: A tuple of (`IPerson`, `IPillar`, None).
86 """
87 assert owner is None
88 if name.startswith("~"):
89 owner_name = name[1:]
90 owner = getUtility(IPersonSet).getByName(owner_name)
91 if owner is None:
92 raise NoSuchPerson(owner_name)
93 return owner, owner, None
94 else:
95 if not valid_name(name):
96 raise InvalidProductName(name)
97 pillar = getUtility(IPillarNameSet).getByName(name)
98 if pillar is None:
99 # Actually, the pillar is no such *anything*.
100 raise NoSuchProduct(name)
101 return owner, pillar, None
102
103
104class _BaseGitTraversable:
105 """Base class for traversable implementations."""
106
107 def __init__(self, context):
108 self.context = context
109
110 # Marker for references to Git URL layouts: ##GITNAMESPACE##
111 def traverse(self, owner, name, segments):
112 """See `IGitTraversable`.
113
114 :raises InvalidNamespace: If 'name' is not '+git', or there is no
115 owner, or there are no further segments.
116 :raises NoSuchGitRepository: If the segment after '+git' doesn't
117 match an existing Git repository.
118 :return: A tuple of (`IPerson`, `IHasGitRepositories`,
119 `IGitRepository`).
120 """
121 if owner is None or name != "+git":
122 raise InvalidNamespace("/".join(segments.traversed))
123 try:
124 repository_name = next(segments)
125 except StopIteration:
126 raise InvalidNamespace("/".join(segments.traversed))
127 repository = self.getNamespace(owner).getByName(repository_name)
128 if repository is None:
129 raise NoSuchGitRepository(repository_name)
130 return owner, self.context, repository
131
132
133class ProjectGitTraversable(_BaseGitTraversable):
134 """Git repository traversable for projects.
135
136 From here, you can traverse to a named project repository.
137 """
138
139 adapts(IProduct)
140 implements(IGitTraversable)
141
142 def getNamespace(self, owner):
143 return getUtility(IGitNamespaceSet).get(owner, project=self.context)
144
145
146class DistributionGitTraversable(_BaseGitTraversable):
147 """Git repository traversable for distributions.
148
149 From here, you can traverse to a distribution source package.
150 """
151
152 adapts(IDistribution)
153 implements(IGitTraversable)
154
155 # Marker for references to Git URL layouts: ##GITNAMESPACE##
156 def traverse(self, owner, name, segments):
157 """See `IGitTraversable`.
158
159 :raises InvalidNamespace: If 'name' is not '+source' or there are no
160 further segments.
161 :raises NoSuchSourcePackageName: If the segment after '+source'
162 doesn't match an existing source package name.
163 :return: A tuple of (`IPerson`, `IDistributionSourcePackage`, None).
164 """
165 # Distributions don't support named repositories themselves, so
166 # ignore the base traverse method.
167 if name != "+source":
168 raise InvalidNamespace("/".join(segments.traversed))
169 try:
170 spn_name = next(segments)
171 except StopIteration:
172 raise InvalidNamespace("/".join(segments.traversed))
173 distro_source_package = self.context.getSourcePackage(spn_name)
174 if distro_source_package is None:
175 raise NoSuchSourcePackageName(spn_name)
176 return owner, distro_source_package, None
177
178
179class DistributionSourcePackageGitTraversable(_BaseGitTraversable):
180 """Git repository traversable for distribution source packages.
181
182 From here, you can traverse to a named package repository.
183 """
184
185 adapts(IDistributionSourcePackage)
186 implements(IGitTraversable)
187
188 def getNamespace(self, owner):
189 return getUtility(IGitNamespaceSet).get(
190 owner, distribution=self.context.distribution,
191 sourcepackagename=self.context.sourcepackagename)
192
193
194class PersonGitTraversable(_BaseGitTraversable):
195 """Git repository traversable for people.
196
197 From here, you can traverse to a named personal repository, or to a
198 project or a distribution with a person context.
199 """
200
201 adapts(IPerson)
202 implements(IGitTraversable)
203
204 def getNamespace(self, owner):
205 return getUtility(IGitNamespaceSet).get(owner)
206
207 # Marker for references to Git URL layouts: ##GITNAMESPACE##
208 def traverse(self, owner, name, segments):
209 """See `IGitTraversable`.
210
211 :raises InvalidNamespace: If 'name' is '+git' and there are no
212 further segments.
213 :raises InvalidProductName: If 'name' is not '+git' and is not a
214 valid name.
215 :raises NoSuchGitRepository: If the segment after '+git' doesn't
216 match an existing Git repository.
217 :raises NoSuchProduct: If 'name' is not '+git' and doesn't match an
218 existing pillar.
219 :return: A tuple of (`IPerson`, `IHasGitRepositories`,
220 `IGitRepository`).
221 """
222 if name == "+git":
223 return super(PersonGitTraversable, self).traverse(
224 owner, name, segments)
225 else:
226 if not valid_name(name):
227 raise InvalidProductName(name)
228 pillar = getUtility(IPillarNameSet).getByName(name)
229 if pillar is None:
230 # Actually, the pillar is no such *anything*.
231 raise NoSuchProduct(name)
232 return owner, pillar, None
233
234
235class SegmentIterator:
236 """An iterator that remembers the elements it has traversed."""
237
238 def __init__(self, iterator):
239 self._iterator = iterator
240 self.traversed = []
241
242 def next(self):
243 segment = next(self._iterator)
244 if not isinstance(segment, unicode):
245 segment = segment.decode("US-ASCII")
246 self.traversed.append(segment)
247 return segment
248
249
250class GitTraverser:
251 """Utility for traversing to objects that can have Git repositories."""
252
253 implements(IGitTraverser)
254
255 def traverse(self, segments):
256 """See `IGitTraverser`."""
257 owner = None
258 target = None
259 repository = None
260 traversable = RootGitTraversable()
261 segments_iter = SegmentIterator(segments)
262 while traversable is not None:
263 try:
264 name = next(segments_iter)
265 except StopIteration:
266 break
267 owner, target, repository = traversable.traverse(
268 owner, name, segments_iter)
269 if repository is not None:
270 break
271 traversable = adapt(target, IGitTraversable)
272 if target is None or not IHasGitRepositories.providedBy(target):
273 raise InvalidNamespace("/".join(segments_iter.traversed))
274 return owner, target, repository
275
276 def traverse_path(self, path):
277 """See `IGitTraverser`."""
278 segments = iter(path.split("/"))
279 owner, target, repository = self.traverse(segments)
280 if list(segments):
281 raise InvalidNamespace(path)
282 return owner, target, repository
283
284
285class GitLookup:
286 """Utility for looking up Git repositories."""
287
288 implements(IGitLookup)
289
290 def get(self, repository_id, default=None):
291 """See `IGitLookup`."""
292 repository = IStore(GitRepository).get(GitRepository, repository_id)
293 if repository is None:
294 return default
295 return repository
296
297 @staticmethod
298 def uriToPath(uri):
299 """See `IGitLookup`."""
300 schemes = ('git', 'git+ssh', 'https', 'ssh')
301 codehosting_host = URI(config.codehosting.git_anon_root).host
302 if ((uri.scheme in schemes and uri.host == codehosting_host) or
303 (uri.scheme == "lp" and uri.host is None)):
304 return uri.path.lstrip("/")
305 else:
306 return None
307
308 def getByUrl(self, url):
309 """See `IGitLookup`."""
310 if url is None:
311 return None
312 url = url.rstrip("/")
313 try:
314 uri = URI(url)
315 except InvalidURIError:
316 return None
317
318 path = self.uriToPath(uri)
319 if path is None:
320 return None
321 return self.getByPath(path)
322
323 def getByUniqueName(self, unique_name):
324 """See `IGitLookup`."""
325 try:
326 if unique_name.startswith("~"):
327 segments = iter(unique_name.split("/"))
328 _, _, repository = getUtility(IGitTraverser).traverse(segments)
329 if repository is None or list(segments):
330 raise InvalidNamespace(unique_name)
331 return repository
332 except (InvalidNamespace, NameLookupFailed):
333 pass
334 return None
335
336 def getByPath(self, path):
337 """See `IGitLookup`."""
338 traverser = getUtility(IGitTraverser)
339 try:
340 owner, target, repository = traverser.traverse_path(path)
341 except (InvalidNamespace, InvalidProductName, NameLookupFailed):
342 return None
343 if repository is not None:
344 return repository
345 repository_set = getUtility(IGitRepositorySet)
346 if owner is None:
347 return repository_set.getDefaultRepository(target)
348 else:
349 return repository_set.getDefaultRepositoryForOwner(owner, target)
0350
=== modified file 'lib/lp/code/model/gitnamespace.py'
--- lib/lp/code/model/gitnamespace.py 2015-02-26 13:43:51 +0000
+++ lib/lp/code/model/gitnamespace.py 2015-02-27 10:23:01 +0000
@@ -30,8 +30,6 @@
30 GitRepositoryCreatorNotMemberOfOwnerTeam,30 GitRepositoryCreatorNotMemberOfOwnerTeam,
31 GitRepositoryCreatorNotOwner,31 GitRepositoryCreatorNotOwner,
32 GitRepositoryExists,32 GitRepositoryExists,
33 InvalidNamespace,
34 NoSuchGitRepository,
35 )33 )
36from lp.code.interfaces.gitnamespace import (34from lp.code.interfaces.gitnamespace import (
37 IGitNamespace,35 IGitNamespace,
@@ -50,27 +48,9 @@
50 )48 )
51from lp.code.model.gitrepository import GitRepository49from lp.code.model.gitrepository import GitRepository
52from lp.registry.enums import PersonVisibility50from lp.registry.enums import PersonVisibility
53from lp.registry.errors import NoSuchSourcePackageName
54from lp.registry.interfaces.distribution import (
55 IDistribution,
56 IDistributionSet,
57 NoSuchDistribution,
58 )
59from lp.registry.interfaces.distributionsourcepackage import (51from lp.registry.interfaces.distributionsourcepackage import (
60 IDistributionSourcePackage,52 IDistributionSourcePackage,
61 )53 )
62from lp.registry.interfaces.person import (
63 IPersonSet,
64 NoSuchPerson,
65 )
66from lp.registry.interfaces.pillar import IPillarNameSet
67from lp.registry.interfaces.product import (
68 IProduct,
69 IProductSet,
70 NoSuchProduct,
71 )
72from lp.registry.interfaces.projectgroup import IProjectGroup
73from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
74from lp.services.database.constants import DEFAULT54from lp.services.database.constants import DEFAULT
75from lp.services.database.interfaces import IStore55from lp.services.database.interfaces import IStore
76from lp.services.propertycache import get_property_cache56from lp.services.propertycache import get_property_cache
@@ -397,142 +377,3 @@
397 person, distribution.getSourcePackage(sourcepackagename))377 person, distribution.getSourcePackage(sourcepackagename))
398 else:378 else:
399 return PersonalGitNamespace(person)379 return PersonalGitNamespace(person)
400
401 def _findOrRaise(self, error, name, finder, *args):
402 if name is None:
403 return None
404 args = list(args)
405 args.append(name)
406 result = finder(*args)
407 if result is None:
408 raise error(name)
409 return result
410
411 def _findPerson(self, person_name):
412 return self._findOrRaise(
413 NoSuchPerson, person_name, getUtility(IPersonSet).getByName)
414
415 # Marker for references to Git URL layouts: ##GITNAMESPACE##
416 def _findPillar(self, pillar_name):
417 """Find and return the pillar with the given name.
418
419 If the given name is '+git' (indicating a personal repository) or
420 None, return None.
421
422 :raise NoSuchProduct if there's no pillar with the given name or it
423 is a project group.
424 """
425 if pillar_name == "+git":
426 return None
427 pillar = self._findOrRaise(
428 NoSuchProduct, pillar_name, getUtility(IPillarNameSet).getByName)
429 if IProjectGroup.providedBy(pillar):
430 raise NoSuchProduct(pillar_name)
431 return pillar
432
433 def _findProject(self, project_name):
434 return self._findOrRaise(
435 NoSuchProduct, project_name, getUtility(IProductSet).getByName)
436
437 def _findDistribution(self, distribution_name):
438 return self._findOrRaise(
439 NoSuchDistribution, distribution_name,
440 getUtility(IDistributionSet).getByName)
441
442 def _findSourcePackageName(self, sourcepackagename_name):
443 return self._findOrRaise(
444 NoSuchSourcePackageName, sourcepackagename_name,
445 getUtility(ISourcePackageNameSet).queryByName)
446
447 def _realize(self, names):
448 """Turn a dict of object names into a dict of objects.
449
450 Takes the results of `IGitNamespaceSet.parse` and turns them into a
451 dict where the values are Launchpad objects.
452 """
453 data = {}
454 data["person"] = self._findPerson(names["person"])
455 data["project"] = self._findProject(names["project"])
456 data["distribution"] = self._findDistribution(names["distribution"])
457 data["sourcepackagename"] = self._findSourcePackageName(
458 names["sourcepackagename"])
459 return data
460
461 def interpret(self, person, project, distribution, sourcepackagename):
462 names = dict(
463 person=person, project=project, distribution=distribution,
464 sourcepackagename=sourcepackagename)
465 data = self._realize(names)
466 return self.get(**data)
467
468 # Marker for references to Git URL layouts: ##GITNAMESPACE##
469 def parse(self, namespace_name):
470 """See `IGitNamespaceSet`."""
471 data = dict(
472 person=None, project=None, distribution=None,
473 sourcepackagename=None)
474 tokens = namespace_name.split("/")
475 if len(tokens) == 1:
476 data["person"] = tokens[0]
477 elif len(tokens) == 2:
478 data["person"] = tokens[0]
479 data["project"] = tokens[1]
480 elif len(tokens) == 4 and tokens[2] == "+source":
481 data["person"] = tokens[0]
482 data["distribution"] = tokens[1]
483 data["sourcepackagename"] = tokens[3]
484 else:
485 raise InvalidNamespace(namespace_name)
486 if not data["person"].startswith("~"):
487 raise InvalidNamespace(namespace_name)
488 data["person"] = data["person"][1:]
489 return data
490
491 def lookup(self, namespace_name):
492 """See `IGitNamespaceSet`."""
493 names = self.parse(namespace_name)
494 return self.interpret(**names)
495
496 # Marker for references to Git URL layouts: ##GITNAMESPACE##
497 def traverse(self, segments):
498 """See `IGitNamespaceSet`."""
499 traversed_segments = []
500
501 def get_next_segment():
502 try:
503 result = segments.next()
504 except StopIteration:
505 raise InvalidNamespace("/".join(traversed_segments))
506 if result is None:
507 raise AssertionError("None segment passed to traverse()")
508 if not isinstance(result, unicode):
509 result = result.decode("US-ASCII")
510 traversed_segments.append(result)
511 return result
512
513 person_name = get_next_segment()
514 person = self._findPerson(person_name)
515 pillar_name = get_next_segment()
516 pillar = self._findPillar(pillar_name)
517 if pillar is None:
518 namespace = self.get(person)
519 git_literal = pillar_name
520 elif IProduct.providedBy(pillar):
521 namespace = self.get(person, project=pillar)
522 git_literal = get_next_segment()
523 else:
524 source_literal = get_next_segment()
525 if source_literal != "+source":
526 raise InvalidNamespace("/".join(traversed_segments))
527 sourcepackagename_name = get_next_segment()
528 sourcepackagename = self._findSourcePackageName(
529 sourcepackagename_name)
530 namespace = self.get(
531 person, distribution=IDistribution(pillar),
532 sourcepackagename=sourcepackagename)
533 git_literal = get_next_segment()
534 if git_literal != "+git":
535 raise InvalidNamespace("/".join(traversed_segments))
536 repository_name = get_next_segment()
537 return self._findOrRaise(
538 NoSuchGitRepository, repository_name, namespace.getByName)
539380
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-02-26 11:34:47 +0000
+++ lib/lp/code/model/gitrepository.py 2015-02-27 10:23:01 +0000
@@ -39,6 +39,7 @@
39 GitDefaultConflict,39 GitDefaultConflict,
40 GitTargetError,40 GitTargetError,
41 )41 )
42from lp.code.interfaces.gitlookup import IGitLookup
42from lp.code.interfaces.gitnamespace import (43from lp.code.interfaces.gitnamespace import (
43 get_git_namespace,44 get_git_namespace,
44 IGitNamespacePolicy,45 IGitNamespacePolicy,
@@ -357,8 +358,10 @@
357358
358 def getByPath(self, user, path):359 def getByPath(self, user, path):
359 """See `IGitRepositorySet`."""360 """See `IGitRepositorySet`."""
360 # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.361 repository = getUtility(IGitLookup).getByPath(path)
361 raise NotImplementedError362 if repository is not None and repository.visibleByUser(user):
363 return repository
364 return None
362365
363 def getDefaultRepository(self, target):366 def getDefaultRepository(self, target):
364 """See `IGitRepositorySet`."""367 """See `IGitRepositorySet`."""
365368
=== added file 'lib/lp/code/model/tests/test_gitlookup.py'
--- lib/lp/code/model/tests/test_gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitlookup.py 2015-02-27 10:23:01 +0000
@@ -0,0 +1,449 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the IGitLookup implementation."""
5
6__metaclass__ = type
7
8from lazr.uri import URI
9from zope.component import getUtility
10
11from lp.code.errors import (
12 InvalidNamespace,
13 NoSuchGitRepository,
14 )
15from lp.code.interfaces.gitlookup import (
16 IGitLookup,
17 IGitTraverser,
18 )
19from lp.code.interfaces.gitrepository import IGitRepositorySet
20from lp.registry.errors import NoSuchSourcePackageName
21from lp.registry.interfaces.person import NoSuchPerson
22from lp.registry.interfaces.product import (
23 InvalidProductName,
24 NoSuchProduct,
25 )
26from lp.services.config import config
27from lp.testing import (
28 person_logged_in,
29 TestCaseWithFactory,
30 )
31from lp.testing.layers import DatabaseFunctionalLayer
32
33
34class TestGetByUniqueName(TestCaseWithFactory):
35 """Tests for `IGitLookup.getByUniqueName`."""
36
37 layer = DatabaseFunctionalLayer
38
39 def setUp(self):
40 super(TestGetByUniqueName, self).setUp()
41 self.lookup = getUtility(IGitLookup)
42
43 def test_not_found(self):
44 unused_name = self.factory.getUniqueString()
45 self.assertIsNone(self.lookup.getByUniqueName(unused_name))
46
47 def test_project(self):
48 repository = self.factory.makeGitRepository()
49 self.assertEqual(
50 repository, self.lookup.getByUniqueName(repository.unique_name))
51
52 def test_package(self):
53 dsp = self.factory.makeDistributionSourcePackage()
54 repository = self.factory.makeGitRepository(target=dsp)
55 self.assertEqual(
56 repository, self.lookup.getByUniqueName(repository.unique_name))
57
58 def test_personal(self):
59 owner = self.factory.makePerson()
60 repository = self.factory.makeGitRepository(owner=owner, target=owner)
61 self.assertEqual(
62 repository, self.lookup.getByUniqueName(repository.unique_name))
63
64
65class TestGetByPath(TestCaseWithFactory):
66 """Test `IGitLookup.getByPath`."""
67
68 layer = DatabaseFunctionalLayer
69
70 def setUp(self):
71 super(TestGetByPath, self).setUp()
72 self.lookup = getUtility(IGitLookup)
73
74 def test_project(self):
75 repository = self.factory.makeGitRepository()
76 self.assertEqual(
77 repository, self.lookup.getByPath(repository.unique_name))
78
79 def test_project_default(self):
80 repository = self.factory.makeGitRepository()
81 with person_logged_in(repository.target.owner):
82 getUtility(IGitRepositorySet).setDefaultRepository(
83 repository.target, repository)
84 self.assertEqual(
85 repository, self.lookup.getByPath(repository.shortened_path))
86
87 def test_package(self):
88 dsp = self.factory.makeDistributionSourcePackage()
89 repository = self.factory.makeGitRepository(target=dsp)
90 self.assertEqual(
91 repository, self.lookup.getByPath(repository.unique_name))
92
93 def test_package_default(self):
94 dsp = self.factory.makeDistributionSourcePackage()
95 repository = self.factory.makeGitRepository(target=dsp)
96 with person_logged_in(repository.target.distribution.owner):
97 getUtility(IGitRepositorySet).setDefaultRepository(
98 repository.target, repository)
99 self.assertEqual(
100 repository, self.lookup.getByPath(repository.shortened_path))
101
102 def test_personal(self):
103 owner = self.factory.makePerson()
104 repository = self.factory.makeGitRepository(owner=owner, target=owner)
105 self.assertEqual(
106 repository, self.lookup.getByPath(repository.unique_name))
107
108 def test_invalid_namespace(self):
109 # If `getByPath` is given a path to something with no default Git
110 # repository, such as a distribution, it returns None.
111 distro = self.factory.makeDistribution()
112 self.assertIsNone(self.lookup.getByPath(distro.name))
113
114 def test_no_default_git_repository(self):
115 # If `getByPath` is given a path to something that could have a Git
116 # repository but doesn't, it returns None.
117 project = self.factory.makeProduct()
118 self.assertIsNone(self.lookup.getByPath(project.name))
119
120
121class TestGetByUrl(TestCaseWithFactory):
122 """Test `IGitLookup.getByUrl`."""
123
124 layer = DatabaseFunctionalLayer
125
126 def setUp(self):
127 super(TestGetByUrl, self).setUp()
128 self.lookup = getUtility(IGitLookup)
129
130 def makeProjectRepository(self):
131 owner = self.factory.makePerson(name="aa")
132 project = self.factory.makeProduct(name="bb")
133 return self.factory.makeGitRepository(
134 owner=owner, target=project, name=u"cc")
135
136 def test_getByUrl_with_none(self):
137 # getByUrl returns None if given None.
138 self.assertIsNone(self.lookup.getByUrl(None))
139
140 def assertUrlMatches(self, url, repository):
141 self.assertEqual(repository, self.lookup.getByUrl(url))
142
143 def test_getByUrl_with_trailing_slash(self):
144 # Trailing slashes are stripped from the URL prior to searching.
145 repository = self.makeProjectRepository()
146 self.assertUrlMatches(
147 "git://git.launchpad.dev/~aa/bb/+git/cc/", repository)
148
149 def test_getByUrl_with_git(self):
150 # getByUrl recognises LP repositories for git URLs.
151 repository = self.makeProjectRepository()
152 self.assertUrlMatches(
153 "git://git.launchpad.dev/~aa/bb/+git/cc", repository)
154
155 def test_getByUrl_with_git_ssh(self):
156 # getByUrl recognises LP repositories for git+ssh URLs.
157 repository = self.makeProjectRepository()
158 self.assertUrlMatches(
159 "git+ssh://git.launchpad.dev/~aa/bb/+git/cc", repository)
160
161 def test_getByUrl_with_https(self):
162 # getByUrl recognises LP repositories for https URLs.
163 repository = self.makeProjectRepository()
164 self.assertUrlMatches(
165 "https://git.launchpad.dev/~aa/bb/+git/cc", repository)
166
167 def test_getByUrl_with_ssh(self):
168 # getByUrl recognises LP repositories for ssh URLs.
169 repository = self.makeProjectRepository()
170 self.assertUrlMatches(
171 "ssh://git.launchpad.dev/~aa/bb/+git/cc", repository)
172
173 def test_getByUrl_with_ftp(self):
174 # getByUrl does not recognise LP repositories for ftp URLs.
175 self.makeProjectRepository()
176 self.assertIsNone(
177 self.lookup.getByUrl("ftp://git.launchpad.dev/~aa/bb/+git/cc"))
178
179 def test_getByUrl_with_lp(self):
180 # getByUrl supports lp: URLs.
181 url = "lp:~aa/bb/+git/cc"
182 self.assertIsNone(self.lookup.getByUrl(url))
183 repository = self.makeProjectRepository()
184 self.assertUrlMatches(url, repository)
185
186 def test_getByUrl_with_default(self):
187 # getByUrl honours default repositories when looking up URLs.
188 repository = self.makeProjectRepository()
189 with person_logged_in(repository.target.owner):
190 getUtility(IGitRepositorySet).setDefaultRepository(
191 repository.target, repository)
192 self.assertUrlMatches("lp:bb", repository)
193
194 def test_uriToPath(self):
195 # uriToPath only supports our own URLs with certain schemes.
196 uri = URI(config.codehosting.git_anon_root)
197 uri.path = "/~foo/bar/baz"
198 # Test valid schemes.
199 for scheme in ("git", "git+ssh", "https", "ssh"):
200 uri.scheme = scheme
201 self.assertEqual("~foo/bar/baz", self.lookup.uriToPath(uri))
202 # Test an invalid scheme.
203 uri.scheme = "ftp"
204 self.assertIsNone(self.lookup.uriToPath(uri))
205 # Test valid scheme but invalid domain.
206 uri.scheme = 'sftp'
207 uri.host = 'example.com'
208 self.assertIsNone(self.lookup.uriToPath(uri))
209
210
211class TestGitTraverser(TestCaseWithFactory):
212 """Tests for the repository traverser."""
213
214 layer = DatabaseFunctionalLayer
215
216 def setUp(self):
217 super(TestGitTraverser, self).setUp()
218 self.traverser = getUtility(IGitTraverser)
219
220 def assertTraverses(self, path, owner, target, repository=None):
221 self.assertEqual(
222 (owner, target, repository), self.traverser.traverse_path(path))
223
224 def test_nonexistent_project(self):
225 # `traverse_path` raises `NoSuchProduct` when resolving a path of
226 # 'project' if the project doesn't exist.
227 self.assertRaises(NoSuchProduct, self.traverser.traverse_path, "bb")
228
229 def test_invalid_project(self):
230 # `traverse_path` raises `InvalidProductName` when resolving a path
231 # for a completely invalid default project repository.
232 self.assertRaises(
233 InvalidProductName, self.traverser.traverse_path, "b")
234
235 def test_project(self):
236 # `traverse_path` resolves the name of a project to the project itself.
237 project = self.factory.makeProduct()
238 self.assertTraverses(project.name, None, project)
239
240 def test_project_no_named_repositories(self):
241 # Projects do not have named repositories without an owner context,
242 # so trying to traverse to them raises `InvalidNamespace`.
243 project = self.factory.makeProduct()
244 repository = self.factory.makeGitRepository(target=project)
245 self.assertRaises(
246 InvalidNamespace, self.traverser.traverse_path,
247 "%s/+git/%s" % (project.name, repository.name))
248
249 def test_no_such_distribution(self):
250 # `traverse_path` raises `NoSuchProduct` if the distribution doesn't
251 # exist. That's because it can't tell the difference between the
252 # name of a project that doesn't exist and the name of a
253 # distribution that doesn't exist.
254 self.assertRaises(
255 NoSuchProduct, self.traverser.traverse_path,
256 "distro/+source/package")
257
258 def test_missing_sourcepackagename(self):
259 # `traverse_path` raises `InvalidNamespace` if there are no segments
260 # after '+source'.
261 self.factory.makeDistribution(name="distro")
262 self.assertRaises(
263 InvalidNamespace, self.traverser.traverse_path, "distro/+source")
264
265 def test_no_such_sourcepackagename(self):
266 # `traverse_path` raises `NoSuchSourcePackageName` if the package in
267 # distro/+source/package doesn't exist.
268 self.factory.makeDistribution(name="distro")
269 self.assertRaises(
270 NoSuchSourcePackageName, self.traverser.traverse_path,
271 "distro/+source/nonexistent")
272
273 def test_package(self):
274 # `traverse_path` resolves 'distro/+source/package' to the
275 # distribution source package.
276 dsp = self.factory.makeDistributionSourcePackage()
277 path = "%s/+source/%s" % (
278 dsp.distribution.name, dsp.sourcepackagename.name)
279 self.assertTraverses(path, None, dsp)
280
281 def test_package_no_named_repositories(self):
282 # Packages do not have named repositories without an owner context,
283 # so trying to traverse to them raises `InvalidNamespace`.
284 dsp = self.factory.makeDistributionSourcePackage()
285 repository = self.factory.makeGitRepository(target=dsp)
286 self.assertRaises(
287 InvalidNamespace, self.traverser.traverse_path,
288 "%s/+source/%s/+git/%s" % (
289 dsp.distribution.name, dsp.sourcepackagename.name,
290 repository.name))
291
292 def test_nonexistent_person(self):
293 # `traverse_path` raises `NoSuchPerson` when resolving a path of
294 # '~person/project' if the person doesn't exist.
295 self.assertRaises(
296 NoSuchPerson, self.traverser.traverse_path, "~person/bb")
297
298 def test_nonexistent_person_project(self):
299 # `traverse_path` raises `NoSuchProduct` when resolving a path of
300 # '~person/project' if the project doesn't exist.
301 self.factory.makePerson(name="person")
302 self.assertRaises(
303 NoSuchProduct, self.traverser.traverse_path, "~person/bb")
304
305 def test_invalid_person_project(self):
306 # `traverse_path` raises `InvalidProductName` when resolving a path
307 # for a person and a completely invalid default project repository.
308 self.factory.makePerson(name="person")
309 self.assertRaises(
310 InvalidProductName, self.traverser.traverse_path, "~person/b")
311
312 def test_invalid_person_project_group(self):
313 # Project groups do not have repositories, so `traverse_path` raises
314 # `InvalidNamespace` when asked to traverse to them.
315 person = self.factory.makePerson()
316 project_group = self.factory.makeProject()
317 self.assertRaises(
318 InvalidNamespace, self.traverser.traverse_path,
319 "~%s/%s/+git/repository" % (person.name, project_group.name))
320
321 def test_person_missing_repository_name(self):
322 # `traverse_path` raises `InvalidNamespace` if there are no segments
323 # after '+git'.
324 self.factory.makePerson(name="person")
325 self.assertRaises(
326 InvalidNamespace, self.traverser.traverse_path, "~person/+git")
327
328 def test_person_no_such_repository(self):
329 # `traverse_path` raises `NoSuchGitRepository` if the repository in
330 # project/+git/repository doesn't exist.
331 self.factory.makePerson(name="person")
332 self.assertRaises(
333 NoSuchGitRepository, self.traverser.traverse_path,
334 "~person/+git/repository")
335
336 def test_person_repository(self):
337 # `traverse_path` resolves an existing project repository.
338 person = self.factory.makePerson(name="person")
339 repository = self.factory.makeGitRepository(
340 owner=person, target=person, name=u"repository")
341 self.assertTraverses(
342 "~person/+git/repository", person, person, repository)
343
344 def test_person_project(self):
345 # `traverse_path` resolves '~person/project' to the person and the
346 # project.
347 person = self.factory.makePerson()
348 project = self.factory.makeProduct()
349 self.assertTraverses(
350 "~%s/%s" % (person.name, project.name), person, project)
351
352 def test_person_project_missing_repository_name(self):
353 # `traverse_path` raises `InvalidNamespace` if there are no segments
354 # after '+git'.
355 person = self.factory.makePerson()
356 project = self.factory.makeProduct()
357 self.assertRaises(
358 InvalidNamespace, self.traverser.traverse_path,
359 "~%s/%s/+git" % (person.name, project.name))
360
361 def test_person_project_no_such_repository(self):
362 # `traverse_path` raises `NoSuchGitRepository` if the repository in
363 # ~person/project/+git/repository doesn't exist.
364 person = self.factory.makePerson()
365 project = self.factory.makeProduct()
366 self.assertRaises(
367 NoSuchGitRepository, self.traverser.traverse_path,
368 "~%s/%s/+git/nonexistent" % (person.name, project.name))
369
370 def test_person_project_repository(self):
371 # `traverse_path` resolves an existing person-project repository.
372 person = self.factory.makePerson()
373 project = self.factory.makeProduct()
374 repository = self.factory.makeGitRepository(
375 owner=person, target=project)
376 self.assertTraverses(
377 "~%s/%s/+git/%s" % (person.name, project.name, repository.name),
378 person, project, repository)
379
380 def test_no_such_person_distribution(self):
381 # `traverse_path` raises `NoSuchProduct` when resolving a path of
382 # '~person/distro' if the distribution doesn't exist. That's
383 # because it can't tell the difference between the name of a project
384 # that doesn't exist and the name of a distribution that doesn't
385 # exist.
386 self.factory.makePerson(name="person")
387 self.assertRaises(
388 NoSuchProduct, self.traverser.traverse_path,
389 "~person/distro/+source/package")
390
391 def test_missing_person_sourcepackagename(self):
392 # `traverse_path` raises `InvalidNamespace` if there are no segments
393 # after '+source' in a person-DSP path.
394 self.factory.makePerson(name="person")
395 self.factory.makeDistribution(name="distro")
396 self.assertRaises(
397 InvalidNamespace, self.traverser.traverse_path,
398 "~person/distro/+source")
399
400 def test_no_such_person_sourcepackagename(self):
401 # `traverse_path` raises `NoSuchSourcePackageName` if the package in
402 # ~person/distro/+source/package doesn't exist.
403 self.factory.makePerson(name="person")
404 self.factory.makeDistribution(name="distro")
405 self.assertRaises(
406 NoSuchSourcePackageName, self.traverser.traverse_path,
407 "~person/distro/+source/nonexistent")
408
409 def test_person_package(self):
410 # `traverse_path` resolves '~person/distro/+source/package' to the
411 # person and the DSP.
412 person = self.factory.makePerson()
413 dsp = self.factory.makeDistributionSourcePackage()
414 path = "~%s/%s/+source/%s" % (
415 person.name, dsp.distribution.name, dsp.sourcepackagename.name)
416 self.assertTraverses(path, person, dsp)
417
418 def test_person_package_missing_repository_name(self):
419 # `traverse_path` raises `InvalidNamespace` if there are no segments
420 # after '+git'.
421 person = self.factory.makePerson()
422 dsp = self.factory.makeDistributionSourcePackage()
423 self.assertRaises(
424 InvalidNamespace, self.traverser.traverse_path,
425 "~%s/%s/+source/%s/+git" % (
426 person.name, dsp.distribution.name,
427 dsp.sourcepackagename.name))
428
429 def test_person_package_no_such_repository(self):
430 # `traverse_path` raises `NoSuchGitRepository` if the repository in
431 # ~person/project/+git/repository doesn't exist.
432 person = self.factory.makePerson()
433 dsp = self.factory.makeDistributionSourcePackage()
434 self.assertRaises(
435 NoSuchGitRepository, self.traverser.traverse_path,
436 "~%s/%s/+source/%s/+git/nonexistent" % (
437 person.name, dsp.distribution.name,
438 dsp.sourcepackagename.name))
439
440 def test_person_package_repository(self):
441 # `traverse_path` resolves an existing person-package repository.
442 person = self.factory.makePerson()
443 dsp = self.factory.makeDistributionSourcePackage()
444 repository = self.factory.makeGitRepository(owner=person, target=dsp)
445 self.assertTraverses(
446 "~%s/%s/+source/%s/+git/%s" % (
447 person.name, dsp.distribution.name, dsp.sourcepackagename.name,
448 repository.name),
449 person, dsp, repository)
0450
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-02-26 11:34:47 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-27 10:23:01 +0000
@@ -512,6 +512,19 @@
512 # GitRepositorySet instances provide IGitRepositorySet.512 # GitRepositorySet instances provide IGitRepositorySet.
513 verifyObject(IGitRepositorySet, self.repository_set)513 verifyObject(IGitRepositorySet, self.repository_set)
514514
515 def test_getByPath(self):
516 # getByPath returns a repository matching the path that it's given.
517 a = self.factory.makeGitRepository()
518 self.factory.makeGitRepository()
519 repository = self.repository_set.getByPath(a.owner, a.shortened_path)
520 self.assertEqual(a, repository)
521
522 def test_getByPath_not_found(self):
523 # If a repository cannot be found for a path, then getByPath returns
524 # None.
525 person = self.factory.makePerson()
526 self.assertIsNone(self.repository_set.getByPath(person, "nonexistent"))
527
515 def test_setDefaultRepository_refuses_person(self):528 def test_setDefaultRepository_refuses_person(self):
516 # setDefaultRepository refuses if the target is a person.529 # setDefaultRepository refuses if the target is a person.
517 person = self.factory.makePerson()530 person = self.factory.makePerson()