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

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