Merge lp:~cjwatson/launchpad/git-collection 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: 17365
Proposed branch: lp:~cjwatson/launchpad/git-collection
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-lookup
Diff against target: 1309 lines (+1279/-0)
5 files modified
lib/lp/code/adapters/gitcollection.py (+62/-0)
lib/lp/code/configure.zcml (+49/-0)
lib/lp/code/interfaces/gitcollection.py (+125/-0)
lib/lp/code/model/gitcollection.py (+327/-0)
lib/lp/code/model/tests/test_gitcollection.py (+716/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-collection
Reviewer Review Type Date Requested Status
William Grant code 2015-02-23 Approve on 2015-02-25
Review via email: mp+250646@code.launchpad.net

Commit message

Add support for collections of Git repositories.

Description of the change

Add support for collections of Git repositories.

This is mostly a much-reduced version of BranchLookup. It's also the last of the big core chunks of the Git repository model; after this it should be possible to work in more digestible pieces rather than thousand-line sections of entirely new code.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/code/adapters/gitcollection.py'
2--- lib/lp/code/adapters/gitcollection.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/code/adapters/gitcollection.py 2015-02-26 17:21:27 +0000
4@@ -0,0 +1,62 @@
5+# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Adapters for different objects to Git repository collections."""
9+
10+__metaclass__ = type
11+__all__ = [
12+ 'git_collection_for_distribution',
13+ 'git_collection_for_distro_source_package',
14+ 'git_collection_for_person',
15+ 'git_collection_for_person_distro_source_package',
16+ 'git_collection_for_person_product',
17+ 'git_collection_for_project',
18+ 'git_collection_for_project_group',
19+ ]
20+
21+
22+from zope.component import getUtility
23+
24+from lp.code.interfaces.gitcollection import IAllGitRepositories
25+
26+
27+def git_collection_for_project(project):
28+ """Adapt a product to a Git repository collection."""
29+ return getUtility(IAllGitRepositories).inProject(project)
30+
31+
32+def git_collection_for_project_group(project_group):
33+ """Adapt a project group to a Git repository collection."""
34+ return getUtility(IAllGitRepositories).inProjectGroup(project_group)
35+
36+
37+def git_collection_for_distribution(distribution):
38+ """Adapt a distribution to a Git repository collection."""
39+ return getUtility(IAllGitRepositories).inDistribution(distribution)
40+
41+
42+def git_collection_for_distro_source_package(distro_source_package):
43+ """Adapt a distro_source_package to a Git repository collection."""
44+ return getUtility(IAllGitRepositories).inDistributionSourcePackage(
45+ distro_source_package)
46+
47+
48+def git_collection_for_person(person):
49+ """Adapt a person to a Git repository collection."""
50+ return getUtility(IAllGitRepositories).ownedBy(person)
51+
52+
53+def git_collection_for_person_product(person_product):
54+ """Adapt a PersonProduct to a Git repository collection."""
55+ collection = getUtility(IAllGitRepositories).ownedBy(person_product.person)
56+ collection = collection.inProject(person_product.product)
57+ return collection
58+
59+
60+def git_collection_for_person_distro_source_package(person_dsp):
61+ """Adapt a PersonDistributionSourcePackage to a Git repository
62+ collection."""
63+ collection = getUtility(IAllGitRepositories).ownedBy(person_dsp.person)
64+ collection = collection.inDistributionSourcePackage(
65+ person_dsp.distro_source_package)
66+ return collection
67
68=== modified file 'lib/lp/code/configure.zcml'
69--- lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000
70+++ lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000
71@@ -858,6 +858,55 @@
72 <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
73 </securedutility>
74
75+ <!-- GitCollection -->
76+
77+ <class class="lp.code.model.gitcollection.GenericGitCollection">
78+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
79+ </class>
80+ <class class="lp.code.model.gitcollection.AnonymousGitCollection">
81+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
82+ </class>
83+ <class class="lp.code.model.gitcollection.VisibleGitCollection">
84+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
85+ </class>
86+ <adapter
87+ for="storm.store.Store"
88+ provides="lp.code.interfaces.gitcollection.IGitCollection"
89+ factory="lp.code.model.gitcollection.GenericGitCollection"/>
90+ <adapter
91+ for="lp.registry.interfaces.product.IProduct"
92+ provides="lp.code.interfaces.gitcollection.IGitCollection"
93+ factory="lp.code.adapters.gitcollection.git_collection_for_project"/>
94+ <adapter
95+ for="lp.registry.interfaces.projectgroup.IProjectGroup"
96+ provides="lp.code.interfaces.gitcollection.IGitCollection"
97+ factory="lp.code.adapters.gitcollection.git_collection_for_project_group"/>
98+ <adapter
99+ for="lp.registry.interfaces.distribution.IDistribution"
100+ provides="lp.code.interfaces.gitcollection.IGitCollection"
101+ factory="lp.code.adapters.gitcollection.git_collection_for_distribution"/>
102+ <adapter
103+ for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
104+ provides="lp.code.interfaces.gitcollection.IGitCollection"
105+ factory="lp.code.adapters.gitcollection.git_collection_for_distro_source_package"/>
106+ <adapter
107+ for="lp.registry.interfaces.person.IPerson"
108+ provides="lp.code.interfaces.gitcollection.IGitCollection"
109+ factory="lp.code.adapters.gitcollection.git_collection_for_person"/>
110+ <adapter
111+ for="lp.registry.interfaces.personproduct.IPersonProduct"
112+ provides="lp.code.interfaces.gitcollection.IGitCollection"
113+ factory="lp.code.adapters.gitcollection.git_collection_for_person_product"/>
114+ <adapter
115+ for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
116+ provides="lp.code.interfaces.gitcollection.IGitCollection"
117+ factory="lp.code.adapters.gitcollection.git_collection_for_person_distro_source_package"/>
118+ <securedutility
119+ class="lp.code.model.gitcollection.GenericGitCollection"
120+ provides="lp.code.interfaces.gitcollection.IAllGitRepositories">
121+ <allow interface="lp.code.interfaces.gitcollection.IAllGitRepositories"/>
122+ </securedutility>
123+
124 <!-- Default Git repositories -->
125
126 <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />
127
128=== added file 'lib/lp/code/interfaces/gitcollection.py'
129--- lib/lp/code/interfaces/gitcollection.py 1970-01-01 00:00:00 +0000
130+++ lib/lp/code/interfaces/gitcollection.py 2015-02-26 17:21:27 +0000
131@@ -0,0 +1,125 @@
132+# Copyright 2015 Canonical Ltd. This software is licensed under the
133+# GNU Affero General Public License version 3 (see the file LICENSE).
134+
135+"""A collection of Git repositories.
136+
137+See `IGitCollection` for more details.
138+"""
139+
140+__metaclass__ = type
141+__all__ = [
142+ 'IAllGitRepositories',
143+ 'IGitCollection',
144+ 'InvalidGitFilter',
145+ ]
146+
147+from zope.interface import Interface
148+
149+
150+class InvalidGitFilter(Exception):
151+ """Raised when an `IGitCollection` cannot apply the given filter."""
152+
153+
154+class IGitCollection(Interface):
155+ """A collection of Git repositories.
156+
157+ An `IGitCollection` is an immutable collection of Git repositories. It
158+ has two kinds of methods: filter methods and query methods.
159+
160+ Query methods get information about the contents of the collection. See
161+ `IGitCollection.count` and `IGitCollection.getRepositories`.
162+
163+ Filter methods return new IGitCollection instances that have some sort
164+ of restriction. Examples include `ownedBy`, `visibleByUser` and
165+ `inProject`.
166+
167+ Implementations of this interface are not 'content classes'. That is, they
168+ do not correspond to a particular row in the database.
169+
170+ This interface is intended for use within Launchpad, not to be exported as
171+ a public API.
172+ """
173+
174+ def count():
175+ """The number of repositories in this collection."""
176+
177+ def is_empty():
178+ """Is this collection empty?"""
179+
180+ def ownerCounts():
181+ """Return the number of different repository owners.
182+
183+ :return: a tuple (individual_count, team_count) containing the
184+ number of individuals and teams that own repositories in this
185+ collection.
186+ """
187+
188+ def getRepositories(eager_load=False):
189+ """Return a result set of all repositories in this collection.
190+
191+ The returned result set will also join across the specified tables
192+ as defined by the arguments to this function. These extra tables
193+ are joined specifically to allow the caller to sort on values not in
194+ the GitRepository table itself.
195+
196+ :param eager_load: If True trigger eager loading of all the related
197+ objects in the collection.
198+ """
199+
200+ def getRepositoryIds():
201+ """Return a result set of all repository ids in this collection."""
202+
203+ def getTeamsWithRepositories(person):
204+ """Return the teams that person is a member of that have
205+ repositories."""
206+
207+ def inProject(project):
208+ """Restrict the collection to repositories in 'project'."""
209+
210+ def inProjectGroup(projectgroup):
211+ """Restrict the collection to repositories in 'projectgroup'."""
212+
213+ def inDistribution(distribution):
214+ """Restrict the collection to repositories in 'distribution'."""
215+
216+ def inDistributionSourcePackage(distro_source_package):
217+ """Restrict to repositories in a package for a distribution."""
218+
219+ def isPersonal():
220+ """Restrict the collection to personal repositories."""
221+
222+ def isPrivate():
223+ """Restrict the collection to private repositories."""
224+
225+ def isExclusive():
226+ """Restrict the collection to repositories owned by exclusive
227+ people."""
228+
229+ def ownedBy(person):
230+ """Restrict the collection to repositories owned by 'person'."""
231+
232+ def ownedByTeamMember(person):
233+ """Restrict the collection to repositories owned by 'person' or a
234+ team of which person is a member.
235+ """
236+
237+ def registeredBy(person):
238+ """Restrict the collection to repositories registered by 'person'."""
239+
240+ def search(term):
241+ """Search the collection for repositories matching 'term'.
242+
243+ :param term: A string.
244+ :return: A `ResultSet` of repositories that matched.
245+ """
246+
247+ def visibleByUser(person):
248+ """Restrict the collection to repositories that person is allowed to
249+ see."""
250+
251+ def withIds(*repository_ids):
252+ """Restrict the collection to repositories with the specified ids."""
253+
254+
255+class IAllGitRepositories(IGitCollection):
256+ """A `IGitCollection` representing all Git repositories in Launchpad."""
257
258=== added file 'lib/lp/code/model/gitcollection.py'
259--- lib/lp/code/model/gitcollection.py 1970-01-01 00:00:00 +0000
260+++ lib/lp/code/model/gitcollection.py 2015-02-26 17:21:27 +0000
261@@ -0,0 +1,327 @@
262+# Copyright 2015 Canonical Ltd. This software is licensed under the
263+# GNU Affero General Public License version 3 (see the file LICENSE).
264+
265+"""Implementations of `IGitCollection`."""
266+
267+__metaclass__ = type
268+__all__ = [
269+ 'GenericGitCollection',
270+ ]
271+
272+from lazr.uri import (
273+ InvalidURIError,
274+ URI,
275+ )
276+from storm.expr import (
277+ Count,
278+ In,
279+ Join,
280+ Select,
281+ )
282+from zope.component import getUtility
283+from zope.interface import implements
284+
285+from lp.app.enums import PRIVATE_INFORMATION_TYPES
286+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
287+from lp.code.interfaces.gitcollection import (
288+ IGitCollection,
289+ InvalidGitFilter,
290+ )
291+from lp.code.interfaces.gitlookup import IGitLookup
292+from lp.code.interfaces.gitrepository import (
293+ user_has_special_git_repository_access,
294+ )
295+from lp.code.model.gitrepository import (
296+ GitRepository,
297+ get_git_repository_privacy_filter,
298+ )
299+from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
300+from lp.registry.model.person import Person
301+from lp.registry.model.product import Product
302+from lp.registry.model.teammembership import TeamParticipation
303+from lp.services.database.bulk import load_related
304+from lp.services.database.decoratedresultset import DecoratedResultSet
305+from lp.services.database.interfaces import IStore
306+from lp.services.propertycache import get_property_cache
307+
308+
309+class GenericGitCollection:
310+ """See `IGitCollection`."""
311+
312+ implements(IGitCollection)
313+
314+ def __init__(self, store=None, filter_expressions=None, tables=None):
315+ """Construct a `GenericGitCollection`.
316+
317+ :param store: The store to look in for repositories. If not
318+ specified, use the default store.
319+ :param filter_expressions: A list of Storm expressions to
320+ restrict the repositories in the collection. If unspecified,
321+ then there will be no restrictions on the result set. That is,
322+ all repositories in the store will be in the collection.
323+ :param tables: A dict of Storm tables to the Join expression. If an
324+ expression in filter_expressions refers to a table, then that
325+ table *must* be in this list.
326+ """
327+ self._store = store
328+ if filter_expressions is None:
329+ filter_expressions = []
330+ self._filter_expressions = list(filter_expressions)
331+ if tables is None:
332+ tables = {}
333+ self._tables = tables
334+ self._user = None
335+
336+ def count(self):
337+ """See `IGitCollection`."""
338+ return self.getRepositories(eager_load=False).count()
339+
340+ def is_empty(self):
341+ """See `IGitCollection`."""
342+ return self.getRepositories(eager_load=False).is_empty()
343+
344+ def ownerCounts(self):
345+ """See `IGitCollection`."""
346+ is_team = Person.teamowner != None
347+ owners = self._getRepositorySelect((GitRepository.owner_id,))
348+ counts = dict(self.store.find(
349+ (is_team, Count(Person.id)),
350+ Person.id.is_in(owners)).group_by(is_team))
351+ return (counts.get(False, 0), counts.get(True, 0))
352+
353+ @property
354+ def store(self):
355+ # Although you might think we could set the default value for store
356+ # in the constructor, we can't. The IStore utility is not available
357+ # at the time that the ZCML is parsed, which means we get an error
358+ # if this code is in the constructor.
359+ # -- JonathanLange 2009-02-17.
360+ if self._store is None:
361+ return IStore(GitRepository)
362+ else:
363+ return self._store
364+
365+ def _filterBy(self, expressions, table=None, join=None):
366+ """Return a subset of this collection, filtered by 'expressions'."""
367+ # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
368+ # for explicit 'tables' by harnessing Storm's table inference system.
369+ # See http://paste.ubuntu.com/118711/ for one way to do that.
370+ if table is not None and join is None:
371+ raise InvalidGitFilter("Cannot specify a table without a join.")
372+ if expressions is None:
373+ expressions = []
374+ tables = self._tables.copy()
375+ if table is not None:
376+ tables[table] = join
377+ return self.__class__(
378+ self.store, self._filter_expressions + expressions, tables)
379+
380+ def _getRepositorySelect(self, columns=(GitRepository.id,)):
381+ """Return a Storm 'Select' for columns in this collection."""
382+ repositories = self.getRepositories(
383+ eager_load=False, find_expr=columns)
384+ return repositories.get_plain_result_set()._get_select()
385+
386+ def _getRepositoryExpressions(self):
387+ """Return the where expressions for this collection."""
388+ return (self._filter_expressions +
389+ self._getRepositoryVisibilityExpression())
390+
391+ def _getRepositoryVisibilityExpression(self):
392+ """Return the where clauses for visibility."""
393+ return []
394+
395+ def getRepositories(self, find_expr=GitRepository, eager_load=False):
396+ """See `IGitCollection`."""
397+ tables = [GitRepository] + list(set(self._tables.values()))
398+ expressions = self._getRepositoryExpressions()
399+ resultset = self.store.using(*tables).find(find_expr, *expressions)
400+
401+ def do_eager_load(rows):
402+ repository_ids = set(repository.id for repository in rows)
403+ if not repository_ids:
404+ return
405+ load_related(Product, rows, ['project_id'])
406+ # So far have only needed the persons for their canonical_url - no
407+ # need for validity etc in the API call.
408+ load_related(Person, rows, ['owner_id', 'registrant_id'])
409+
410+ def cache_permission(repository):
411+ if self._user:
412+ get_property_cache(repository)._known_viewers = set(
413+ [self._user.id])
414+ return repository
415+
416+ eager_load_hook = (
417+ do_eager_load if eager_load and find_expr == GitRepository
418+ else None)
419+ return DecoratedResultSet(
420+ resultset, pre_iter_hook=eager_load_hook,
421+ result_decorator=cache_permission)
422+
423+ def getRepositoryIds(self):
424+ """See `IGitCollection`."""
425+ return self.getRepositories(
426+ find_expr=GitRepository.id).get_plain_result_set()
427+
428+ def getTeamsWithRepositories(self, person):
429+ """See `IGitCollection`."""
430+ # This method doesn't entirely fit with the intent of the
431+ # GitCollection conceptual model, but we're not quite sure how to
432+ # fix it just yet.
433+ repository_query = self._getRepositorySelect((GitRepository.owner_id,))
434+ return self.store.find(
435+ Person,
436+ Person.id == TeamParticipation.teamID,
437+ TeamParticipation.person == person,
438+ TeamParticipation.team != person,
439+ Person.id.is_in(repository_query))
440+
441+ def inProject(self, project):
442+ """See `IGitCollection`."""
443+ return self._filterBy([GitRepository.project == project])
444+
445+ def inProjectGroup(self, projectgroup):
446+ """See `IGitCollection`."""
447+ return self._filterBy(
448+ [Product.projectgroup == projectgroup.id],
449+ table=Product,
450+ join=Join(Product, GitRepository.project == Product.id))
451+
452+ def inDistribution(self, distribution):
453+ """See `IGitCollection`."""
454+ return self._filterBy([GitRepository.distribution == distribution])
455+
456+ def inDistributionSourcePackage(self, distro_source_package):
457+ """See `IGitCollection`."""
458+ distribution = distro_source_package.distribution
459+ sourcepackagename = distro_source_package.sourcepackagename
460+ return self._filterBy(
461+ [GitRepository.distribution == distribution,
462+ GitRepository.sourcepackagename == sourcepackagename])
463+
464+ def isPersonal(self):
465+ """See `IGitCollection`."""
466+ return self._filterBy(
467+ [GitRepository.project == None,
468+ GitRepository.distribution == None])
469+
470+ def isPrivate(self):
471+ """See `IGitCollection`."""
472+ return self._filterBy(
473+ [GitRepository.information_type.is_in(PRIVATE_INFORMATION_TYPES)])
474+
475+ def isExclusive(self):
476+ """See `IGitCollection`."""
477+ return self._filterBy(
478+ [Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY)],
479+ table=Person,
480+ join=Join(Person, GitRepository.owner_id == Person.id))
481+
482+ def ownedBy(self, person):
483+ """See `IGitCollection`."""
484+ return self._filterBy([GitRepository.owner == person])
485+
486+ def ownedByTeamMember(self, person):
487+ """See `IGitCollection`."""
488+ subquery = Select(
489+ TeamParticipation.teamID,
490+ where=TeamParticipation.personID == person.id)
491+ return self._filterBy([In(GitRepository.owner_id, subquery)])
492+
493+ def registeredBy(self, person):
494+ """See `IGitCollection`."""
495+ return self._filterBy([GitRepository.registrant == person])
496+
497+ def _getExactMatch(self, term):
498+ # Look up the repository by its URL, which handles both shortcuts
499+ # and unique names.
500+ repository = getUtility(IGitLookup).getByUrl(term)
501+ if repository is not None:
502+ return repository
503+ # Fall back to searching by unique_name, stripping out the path if
504+ # it's a URI.
505+ try:
506+ path = URI(term).path.strip("/")
507+ except InvalidURIError:
508+ path = term
509+ return getUtility(IGitLookup).getByUniqueName(path)
510+
511+ def search(self, term):
512+ """See `IGitCollection`."""
513+ repository = self._getExactMatch(term)
514+ if repository:
515+ collection = self._filterBy([GitRepository.id == repository.id])
516+ else:
517+ term = unicode(term)
518+ # Filter by name.
519+ field = GitRepository.name
520+ # Except if the term contains /, when we use unique_name.
521+ # XXX cjwatson 2015-02-06: Disabled until the URL format settles
522+ # down, at which point we can make GitRepository.unique_name a
523+ # trigger-maintained column rather than a property.
524+ #if '/' in term:
525+ # field = GitRepository.unique_name
526+ collection = self._filterBy(
527+ [field.lower().contains_string(term.lower())])
528+ return collection.getRepositories(eager_load=False).order_by(
529+ GitRepository.name, GitRepository.id)
530+
531+ def visibleByUser(self, person):
532+ """See `IGitCollection`."""
533+ if (person == LAUNCHPAD_SERVICES or
534+ user_has_special_git_repository_access(person)):
535+ return self
536+ if person is None:
537+ return AnonymousGitCollection(
538+ self._store, self._filter_expressions, self._tables)
539+ return VisibleGitCollection(
540+ person, self._store, self._filter_expressions, self._tables)
541+
542+ def withIds(self, *repository_ids):
543+ """See `IGitCollection`."""
544+ return self._filterBy([GitRepository.id.is_in(repository_ids)])
545+
546+
547+class AnonymousGitCollection(GenericGitCollection):
548+ """Repository collection that only shows public repositories."""
549+
550+ def _getRepositoryVisibilityExpression(self):
551+ """Return the where clauses for visibility."""
552+ return get_git_repository_privacy_filter(None)
553+
554+
555+class VisibleGitCollection(GenericGitCollection):
556+ """A repository collection that has special logic for visibility."""
557+
558+ def __init__(self, user, store=None, filter_expressions=None, tables=None):
559+ super(VisibleGitCollection, self).__init__(
560+ store=store, filter_expressions=filter_expressions, tables=tables)
561+ self._user = user
562+
563+ def _filterBy(self, expressions, table=None, join=None):
564+ """Return a subset of this collection, filtered by 'expressions'."""
565+ # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
566+ # for explicit 'tables' by harnessing Storm's table inference system.
567+ # See http://paste.ubuntu.com/118711/ for one way to do that.
568+ if table is not None and join is None:
569+ raise InvalidGitFilter("Cannot specify a table without a join.")
570+ if expressions is None:
571+ expressions = []
572+ tables = self._tables.copy()
573+ if table is not None:
574+ tables[table] = join
575+ return self.__class__(
576+ self._user, self.store, self._filter_expressions + expressions)
577+
578+ def _getRepositoryVisibilityExpression(self):
579+ """Return the where clauses for visibility."""
580+ return get_git_repository_privacy_filter(self._user)
581+
582+ def visibleByUser(self, person):
583+ """See `IGitCollection`."""
584+ if person == self._user:
585+ return self
586+ raise InvalidGitFilter(
587+ "Cannot filter for Git repositories visible by user %r, already "
588+ "filtering for %r" % (person, self._user))
589
590=== added file 'lib/lp/code/model/tests/test_gitcollection.py'
591--- lib/lp/code/model/tests/test_gitcollection.py 1970-01-01 00:00:00 +0000
592+++ lib/lp/code/model/tests/test_gitcollection.py 2015-02-26 17:21:27 +0000
593@@ -0,0 +1,716 @@
594+# Copyright 2015 Canonical Ltd. This software is licensed under the
595+# GNU Affero General Public License version 3 (see the file LICENSE).
596+
597+"""Tests for Git repository collections."""
598+
599+__metaclass__ = type
600+
601+from testtools.matchers import Equals
602+from zope.component import getUtility
603+from zope.security.proxy import removeSecurityProxy
604+
605+from lp.app.enums import InformationType
606+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
607+from lp.app.interfaces.services import IService
608+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
609+from lp.code.interfaces.gitcollection import (
610+ IAllGitRepositories,
611+ IGitCollection,
612+ )
613+from lp.code.interfaces.gitrepository import IGitRepositorySet
614+from lp.code.model.gitcollection import GenericGitCollection
615+from lp.code.model.gitrepository import GitRepository
616+from lp.registry.enums import PersonVisibility
617+from lp.registry.interfaces.person import TeamMembershipPolicy
618+from lp.registry.model.persondistributionsourcepackage import (
619+ PersonDistributionSourcePackage,
620+ )
621+from lp.registry.model.personproduct import PersonProduct
622+from lp.services.database.interfaces import IStore
623+from lp.testing import (
624+ person_logged_in,
625+ StormStatementRecorder,
626+ TestCaseWithFactory,
627+ )
628+from lp.testing.layers import DatabaseFunctionalLayer
629+from lp.testing.matchers import HasQueryCount
630+
631+
632+class TestGitCollectionAdaptation(TestCaseWithFactory):
633+ """Check that certain objects can be adapted to a Git repository
634+ collection."""
635+
636+ layer = DatabaseFunctionalLayer
637+
638+ def assertCollection(self, target):
639+ self.assertIsNotNone(IGitCollection(target, None))
640+
641+ def test_project(self):
642+ # A project can be adapted to a Git repository collection.
643+ self.assertCollection(self.factory.makeProduct())
644+
645+ def test_project_group(self):
646+ # A project group can be adapted to a Git repository collection.
647+ self.assertCollection(self.factory.makeProject())
648+
649+ def test_distribution(self):
650+ # A distribution can be adapted to a Git repository collection.
651+ self.assertCollection(self.factory.makeDistribution())
652+
653+ def test_distribution_source_package(self):
654+ # A distribution source package can be adapted to a Git repository
655+ # collection.
656+ self.assertCollection(self.factory.makeDistributionSourcePackage())
657+
658+ def test_person(self):
659+ # A person can be adapted to a Git repository collection.
660+ self.assertCollection(self.factory.makePerson())
661+
662+ def test_person_product(self):
663+ # A PersonProduct can be adapted to a Git repository collection.
664+ project = self.factory.makeProduct()
665+ self.assertCollection(PersonProduct(project.owner, project))
666+
667+ def test_person_distribution_source_package(self):
668+ # A PersonDistributionSourcePackage can be adapted to a Git
669+ # repository collection.
670+ dsp = self.factory.makeDistributionSourcePackage()
671+ self.assertCollection(
672+ PersonDistributionSourcePackage(dsp.distribution.owner, dsp))
673+
674+
675+class TestGenericGitCollection(TestCaseWithFactory):
676+
677+ layer = DatabaseFunctionalLayer
678+
679+ def setUp(self):
680+ super(TestGenericGitCollection, self).setUp()
681+ self.store = IStore(GitRepository)
682+
683+ def test_provides_gitcollection(self):
684+ # `GenericGitCollection` provides the `IGitCollection`
685+ # interface.
686+ self.assertProvides(GenericGitCollection(self.store), IGitCollection)
687+
688+ def test_getRepositories_no_filter_no_repositories(self):
689+ # If no filter is specified, then the collection is of all
690+ # repositories in Launchpad. By default, there are no repositories.
691+ collection = GenericGitCollection(self.store)
692+ self.assertEqual([], list(collection.getRepositories()))
693+
694+ def test_getRepositories_no_filter(self):
695+ # If no filter is specified, then the collection is of all
696+ # repositories in Launchpad.
697+ collection = GenericGitCollection(self.store)
698+ repository = self.factory.makeGitRepository()
699+ self.assertEqual([repository], list(collection.getRepositories()))
700+
701+ def test_getRepositories_project_filter(self):
702+ # If the specified filter is for the repositories of a particular
703+ # project, then the collection contains only repositories of that
704+ # project.
705+ repository = self.factory.makeGitRepository()
706+ self.factory.makeGitRepository()
707+ collection = GenericGitCollection(
708+ self.store, [GitRepository.project == repository.target])
709+ self.assertEqual([repository], list(collection.getRepositories()))
710+
711+ def test_getRepositories_caches_viewers(self):
712+ # getRepositories() caches the user as a known viewer so that
713+ # repository.visibleByUser() does not have to hit the database.
714+ collection = GenericGitCollection(self.store)
715+ owner = self.factory.makePerson()
716+ project = self.factory.makeProduct()
717+ repository = self.factory.makeGitRepository(
718+ owner=owner, target=project,
719+ information_type=InformationType.USERDATA)
720+ someone = self.factory.makePerson()
721+ with person_logged_in(owner):
722+ getUtility(IService, 'sharing').ensureAccessGrants(
723+ [someone], owner, gitrepositories=[repository],
724+ ignore_permissions=True)
725+ [repository] = list(collection.visibleByUser(
726+ someone).getRepositories())
727+ with StormStatementRecorder() as recorder:
728+ self.assertTrue(repository.visibleByUser(someone))
729+ self.assertThat(recorder, HasQueryCount(Equals(0)))
730+
731+ def test_getRepositoryIds(self):
732+ repository = self.factory.makeGitRepository()
733+ self.factory.makeGitRepository()
734+ collection = GenericGitCollection(
735+ self.store, [GitRepository.project == repository.target])
736+ self.assertEqual([repository.id], list(collection.getRepositoryIds()))
737+
738+ def test_count(self):
739+ # The 'count' property of a collection is the number of elements in
740+ # the collection.
741+ collection = GenericGitCollection(self.store)
742+ self.assertEqual(0, collection.count())
743+ for i in range(3):
744+ self.factory.makeGitRepository()
745+ self.assertEqual(3, collection.count())
746+
747+ def test_count_respects_filter(self):
748+ # If a collection is a subset of all possible repositories, then the
749+ # count will be the size of that subset. That is, 'count' respects
750+ # any filters that are applied.
751+ repository = self.factory.makeGitRepository()
752+ self.factory.makeGitRepository()
753+ collection = GenericGitCollection(
754+ self.store, [GitRepository.project == repository.target])
755+ self.assertEqual(1, collection.count())
756+
757+
758+class TestGitCollectionFilters(TestCaseWithFactory):
759+
760+ layer = DatabaseFunctionalLayer
761+
762+ def setUp(self):
763+ TestCaseWithFactory.setUp(self)
764+ self.all_repositories = getUtility(IAllGitRepositories)
765+
766+ def test_order_by_repository_name(self):
767+ # The result of getRepositories() can be ordered by
768+ # `GitRepository.name`, no matter what filters are applied.
769+ aardvark = self.factory.makeProduct(name='aardvark')
770+ badger = self.factory.makeProduct(name='badger')
771+ repository_a = self.factory.makeGitRepository(target=aardvark)
772+ repository_b = self.factory.makeGitRepository(target=badger)
773+ person = self.factory.makePerson()
774+ repository_c = self.factory.makeGitRepository(
775+ owner=person, target=person)
776+ self.assertEqual(
777+ sorted([repository_a, repository_b, repository_c]),
778+ sorted(self.all_repositories.getRepositories()
779+ .order_by(GitRepository.name)))
780+
781+ def test_count_respects_visibleByUser_filter(self):
782+ # IGitCollection.count() returns the number of repositories that
783+ # getRepositories() yields, even when the visibleByUser filter is
784+ # applied.
785+ repository = self.factory.makeGitRepository()
786+ self.factory.makeGitRepository(
787+ information_type=InformationType.USERDATA)
788+ collection = self.all_repositories.visibleByUser(repository.owner)
789+ self.assertEqual(1, collection.getRepositories().count())
790+ self.assertEqual(1, len(list(collection.getRepositories())))
791+ self.assertEqual(1, collection.count())
792+
793+ def test_ownedBy(self):
794+ # 'ownedBy' returns a new collection restricted to repositories
795+ # owned by the given person.
796+ repository = self.factory.makeGitRepository()
797+ self.factory.makeGitRepository()
798+ collection = self.all_repositories.ownedBy(repository.owner)
799+ self.assertEqual([repository], list(collection.getRepositories()))
800+
801+ def test_ownedByTeamMember(self):
802+ # 'ownedBy' returns a new collection restricted to repositories
803+ # owned by any team of which the given person is a member.
804+ person = self.factory.makePerson()
805+ team = self.factory.makeTeam(members=[person])
806+ repository = self.factory.makeGitRepository(owner=team)
807+ self.factory.makeGitRepository()
808+ collection = self.all_repositories.ownedByTeamMember(person)
809+ self.assertEqual([repository], list(collection.getRepositories()))
810+
811+ def test_in_project(self):
812+ # 'inProject' returns a new collection restricted to repositories in
813+ # the given project.
814+ #
815+ # NOTE: JonathanLange 2009-02-11: Maybe this should be a more
816+ # generic method called 'onTarget' that takes a person, package or
817+ # project.
818+ repository = self.factory.makeGitRepository()
819+ self.factory.makeGitRepository()
820+ collection = self.all_repositories.inProject(repository.target)
821+ self.assertEqual([repository], list(collection.getRepositories()))
822+
823+ def test_inProjectGroup(self):
824+ # 'inProjectGroup' returns a new collection restricted to
825+ # repositories in the given project group.
826+ repository = self.factory.makeGitRepository()
827+ self.factory.makeGitRepository()
828+ projectgroup = self.factory.makeProject()
829+ removeSecurityProxy(repository.target).projectgroup = projectgroup
830+ collection = self.all_repositories.inProjectGroup(projectgroup)
831+ self.assertEqual([repository], list(collection.getRepositories()))
832+
833+ def test_isExclusive(self):
834+ # 'isExclusive' is restricted to repositories owned by exclusive
835+ # teams and users.
836+ user = self.factory.makePerson()
837+ team = self.factory.makeTeam(
838+ membership_policy=TeamMembershipPolicy.RESTRICTED)
839+ other_team = self.factory.makeTeam(
840+ membership_policy=TeamMembershipPolicy.OPEN)
841+ team_repository = self.factory.makeGitRepository(owner=team)
842+ user_repository = self.factory.makeGitRepository(owner=user)
843+ self.factory.makeGitRepository(owner=other_team)
844+ collection = self.all_repositories.isExclusive()
845+ self.assertContentEqual(
846+ [team_repository, user_repository],
847+ list(collection.getRepositories()))
848+
849+ def test_inProject_and_isExclusive(self):
850+ # 'inProject' and 'isExclusive' can combine to form a collection
851+ # that is restricted to repositories of a particular project owned
852+ # by exclusive teams and users.
853+ team = self.factory.makeTeam(
854+ membership_policy=TeamMembershipPolicy.RESTRICTED)
855+ other_team = self.factory.makeTeam(
856+ membership_policy=TeamMembershipPolicy.OPEN)
857+ project = self.factory.makeProduct()
858+ repository = self.factory.makeGitRepository(target=project, owner=team)
859+ self.factory.makeGitRepository(owner=team)
860+ self.factory.makeGitRepository(target=project, owner=other_team)
861+ collection = self.all_repositories.inProject(project).isExclusive()
862+ self.assertEqual([repository], list(collection.getRepositories()))
863+ collection = self.all_repositories.isExclusive().inProject(project)
864+ self.assertEqual([repository], list(collection.getRepositories()))
865+
866+ def test_ownedBy_and_inProject(self):
867+ # 'ownedBy' and 'inProject' can combine to form a collection that is
868+ # restricted to repositories of a particular project owned by a
869+ # particular person.
870+ person = self.factory.makePerson()
871+ project = self.factory.makeProduct()
872+ repository = self.factory.makeGitRepository(
873+ target=project, owner=person)
874+ self.factory.makeGitRepository(owner=person)
875+ self.factory.makeGitRepository(target=project)
876+ collection = self.all_repositories.inProject(project).ownedBy(person)
877+ self.assertEqual([repository], list(collection.getRepositories()))
878+ collection = self.all_repositories.ownedBy(person).inProject(project)
879+ self.assertEqual([repository], list(collection.getRepositories()))
880+
881+ def test_ownedBy_and_isPrivate(self):
882+ # 'ownedBy' and 'isPrivate' can combine to form a collection that is
883+ # restricted to private repositories owned by a particular person.
884+ person = self.factory.makePerson()
885+ project = self.factory.makeProduct()
886+ repository = self.factory.makeGitRepository(
887+ target=project, owner=person,
888+ information_type=InformationType.USERDATA)
889+ self.factory.makeGitRepository(owner=person)
890+ self.factory.makeGitRepository(target=project)
891+ collection = self.all_repositories.isPrivate().ownedBy(person)
892+ self.assertEqual([repository], list(collection.getRepositories()))
893+ collection = self.all_repositories.ownedBy(person).isPrivate()
894+ self.assertEqual([repository], list(collection.getRepositories()))
895+
896+ def test_ownedByTeamMember_and_inProject(self):
897+ # 'ownedBy' and 'inProject' can combine to form a collection that is
898+ # restricted to repositories of a particular project owned by a
899+ # particular person or team of which the person is a member.
900+ person = self.factory.makePerson()
901+ team = self.factory.makeTeam(members=[person])
902+ project = self.factory.makeProduct()
903+ repository = self.factory.makeGitRepository(
904+ target=project, owner=person)
905+ repository2 = self.factory.makeGitRepository(
906+ target=project, owner=team)
907+ self.factory.makeGitRepository(owner=person)
908+ self.factory.makeGitRepository(target=project)
909+ project_repositories = self.all_repositories.inProject(project)
910+ collection = project_repositories.ownedByTeamMember(person)
911+ self.assertContentEqual(
912+ [repository, repository2], collection.getRepositories())
913+ person_repositories = self.all_repositories.ownedByTeamMember(person)
914+ collection = person_repositories.inProject(project)
915+ self.assertContentEqual(
916+ [repository, repository2], collection.getRepositories())
917+
918+ def test_in_distribution(self):
919+ # 'inDistribution' returns a new collection that only has
920+ # repositories that are package repositories associated with the
921+ # distribution specified.
922+ distro = self.factory.makeDistribution()
923+ # Make two repositories in the same distribution, but different
924+ # source packages.
925+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
926+ repository = self.factory.makeGitRepository(target=dsp)
927+ dsp2 = self.factory.makeDistributionSourcePackage(distribution=distro)
928+ repository2 = self.factory.makeGitRepository(target=dsp2)
929+ # Another repository in a different distribution.
930+ self.factory.makeGitRepository(
931+ target=self.factory.makeDistributionSourcePackage())
932+ # And a project repository.
933+ self.factory.makeGitRepository()
934+ collection = self.all_repositories.inDistribution(distro)
935+ self.assertEqual(
936+ sorted([repository, repository2]),
937+ sorted(collection.getRepositories()))
938+
939+ def test_in_distribution_source_package(self):
940+ # 'inDistributionSourcePackage' returns a new collection that only
941+ # has repositories for the source package in the distribution.
942+ distro = self.factory.makeDistribution()
943+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
944+ dsp_other_distro = self.factory.makeDistributionSourcePackage()
945+ repository = self.factory.makeGitRepository(target=dsp)
946+ repository2 = self.factory.makeGitRepository(target=dsp)
947+ self.factory.makeGitRepository(target=dsp_other_distro)
948+ self.factory.makeGitRepository()
949+ collection = self.all_repositories.inDistributionSourcePackage(dsp)
950+ self.assertEqual(
951+ sorted([repository, repository2]),
952+ sorted(collection.getRepositories()))
953+
954+ def test_withIds(self):
955+ # 'withIds' returns a new collection that only has repositories with
956+ # the given ids.
957+ repository1 = self.factory.makeGitRepository()
958+ repository2 = self.factory.makeGitRepository()
959+ self.factory.makeGitRepository()
960+ ids = [repository1.id, repository2.id]
961+ collection = self.all_repositories.withIds(*ids)
962+ self.assertEqual(
963+ sorted([repository1, repository2]),
964+ sorted(collection.getRepositories()))
965+
966+ def test_registeredBy(self):
967+ # 'registeredBy' returns a new collection that only has repositories
968+ # that were registered by the given user.
969+ registrant = self.factory.makePerson()
970+ repository = self.factory.makeGitRepository(
971+ owner=registrant, registrant=registrant)
972+ removeSecurityProxy(repository).owner = self.factory.makePerson()
973+ self.factory.makeGitRepository()
974+ collection = self.all_repositories.registeredBy(registrant)
975+ self.assertEqual([repository], list(collection.getRepositories()))
976+
977+
978+class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory):
979+
980+ layer = DatabaseFunctionalLayer
981+
982+ def setUp(self):
983+ TestCaseWithFactory.setUp(self)
984+ self.public_repository = self.factory.makeGitRepository(name=u'public')
985+ self.private_repository = self.factory.makeGitRepository(
986+ name=u'private', information_type=InformationType.USERDATA)
987+ self.all_repositories = getUtility(IAllGitRepositories)
988+
989+ def test_all_repositories(self):
990+ # Without the visibleByUser filter, all repositories are in the
991+ # collection.
992+ self.assertEqual(
993+ sorted([self.public_repository, self.private_repository]),
994+ sorted(self.all_repositories.getRepositories()))
995+
996+ def test_anonymous_sees_only_public(self):
997+ # Anonymous users can see only public repositories.
998+ repositories = self.all_repositories.visibleByUser(None)
999+ self.assertEqual(
1000+ [self.public_repository], list(repositories.getRepositories()))
1001+
1002+ def test_visibility_then_project(self):
1003+ # We can apply other filters after applying the visibleByUser filter.
1004+ # Create another public repository.
1005+ self.factory.makeGitRepository()
1006+ repositories = self.all_repositories.visibleByUser(None).inProject(
1007+ self.public_repository.target).getRepositories()
1008+ self.assertEqual([self.public_repository], list(repositories))
1009+
1010+ def test_random_person_sees_only_public(self):
1011+ # Logged in users with no special permissions can see only public
1012+ # repositories.
1013+ person = self.factory.makePerson()
1014+ repositories = self.all_repositories.visibleByUser(person)
1015+ self.assertEqual(
1016+ [self.public_repository], list(repositories.getRepositories()))
1017+
1018+ def test_owner_sees_own_repositories(self):
1019+ # Users can always see the repositories that they own, as well as public
1020+ # repositories.
1021+ owner = removeSecurityProxy(self.private_repository).owner
1022+ repositories = self.all_repositories.visibleByUser(owner)
1023+ self.assertEqual(
1024+ sorted([self.public_repository, self.private_repository]),
1025+ sorted(repositories.getRepositories()))
1026+
1027+ def test_launchpad_services_sees_all(self):
1028+ # The LAUNCHPAD_SERVICES special user sees *everything*.
1029+ repositories = self.all_repositories.visibleByUser(LAUNCHPAD_SERVICES)
1030+ self.assertEqual(
1031+ sorted(self.all_repositories.getRepositories()),
1032+ sorted(repositories.getRepositories()))
1033+
1034+ def test_admins_see_all(self):
1035+ # Launchpad administrators see *everything*.
1036+ admin = self.factory.makePerson()
1037+ admin_team = removeSecurityProxy(
1038+ getUtility(ILaunchpadCelebrities).admin)
1039+ admin_team.addMember(admin, admin_team.teamowner)
1040+ repositories = self.all_repositories.visibleByUser(admin)
1041+ self.assertEqual(
1042+ sorted(self.all_repositories.getRepositories()),
1043+ sorted(repositories.getRepositories()))
1044+
1045+ def test_private_teams_see_own_private_personal_repositories(self):
1046+ # Private teams are given an access grant to see their private
1047+ # personal repositories.
1048+ team_owner = self.factory.makePerson()
1049+ team = self.factory.makeTeam(
1050+ visibility=PersonVisibility.PRIVATE,
1051+ membership_policy=TeamMembershipPolicy.MODERATED,
1052+ owner=team_owner)
1053+ with person_logged_in(team_owner):
1054+ personal_repository = self.factory.makeGitRepository(
1055+ owner=team, target=team,
1056+ information_type=InformationType.USERDATA)
1057+ # The team is automatically subscribed to the repository since
1058+ # they are the owner. We want to unsubscribe them so that they
1059+ # lose access conferred via subscription and rely instead on the
1060+ # APG.
1061+ # XXX cjwatson 2015-02-05: Uncomment this once
1062+ # GitRepositorySubscriptions exist.
1063+ #personal_repository.unsubscribe(team, team_owner, True)
1064+ # Make another personal repository the team can't see.
1065+ other_person = self.factory.makePerson()
1066+ self.factory.makeGitRepository(
1067+ owner=other_person, target=other_person,
1068+ information_type=InformationType.USERDATA)
1069+ repositories = self.all_repositories.visibleByUser(team)
1070+ self.assertEqual(
1071+ sorted([self.public_repository, personal_repository]),
1072+ sorted(repositories.getRepositories()))
1073+
1074+
1075+class TestSearch(TestCaseWithFactory):
1076+ """Tests for IGitCollection.search()."""
1077+
1078+ layer = DatabaseFunctionalLayer
1079+
1080+ def setUp(self):
1081+ TestCaseWithFactory.setUp(self)
1082+ self.collection = getUtility(IAllGitRepositories)
1083+
1084+ def test_exact_match_unique_name(self):
1085+ # If you search for a unique name of a repository that exists,
1086+ # you'll get a single result with a repository with that repository
1087+ # name.
1088+ repository = self.factory.makeGitRepository()
1089+ self.factory.makeGitRepository()
1090+ search_results = self.collection.search(repository.unique_name)
1091+ self.assertEqual([repository], list(search_results))
1092+
1093+ def test_unique_name_match_not_in_collection(self):
1094+ # If you search for a unique name of a repository that does not
1095+ # exist, you'll get an empty result set.
1096+ repository = self.factory.makeGitRepository()
1097+ collection = self.collection.inProject(self.factory.makeProduct())
1098+ search_results = collection.search(repository.unique_name)
1099+ self.assertEqual([], list(search_results))
1100+
1101+ def test_exact_match_launchpad_url(self):
1102+ # If you search for the Launchpad URL of a repository, and there is
1103+ # a repository with that URL, then you get a single result with that
1104+ # repository.
1105+ repository = self.factory.makeGitRepository()
1106+ self.factory.makeGitRepository()
1107+ search_results = self.collection.search(repository.getCodebrowseUrl())
1108+ self.assertEqual([repository], list(search_results))
1109+
1110+ def test_exact_match_with_lp_colon_url(self):
1111+ repository = self.factory.makeGitRepository()
1112+ lp_name = 'lp:' + repository.unique_name
1113+ search_results = self.collection.search(lp_name)
1114+ self.assertEqual([repository], list(search_results))
1115+
1116+ def test_exact_match_bad_url(self):
1117+ search_results = self.collection.search('http:hahafail')
1118+ self.assertEqual([], list(search_results))
1119+
1120+ def test_exact_match_git_identity(self):
1121+ # If you search for the Git identity of a repository, then you get a
1122+ # single result with that repository.
1123+ repository = self.factory.makeGitRepository()
1124+ self.factory.makeGitRepository()
1125+ search_results = self.collection.search(repository.git_identity)
1126+ self.assertEqual([repository], list(search_results))
1127+
1128+ def test_exact_match_git_identity_development_focus(self):
1129+ # If you search for the development focus and it is set, you get a
1130+ # single result with the development focus repository.
1131+ fooix = self.factory.makeProduct(name='fooix')
1132+ repository = self.factory.makeGitRepository(
1133+ owner=fooix.owner, target=fooix)
1134+ with person_logged_in(fooix.owner):
1135+ getUtility(IGitRepositorySet).setDefaultRepository(
1136+ fooix, repository)
1137+ self.factory.makeGitRepository()
1138+ search_results = self.collection.search('lp:fooix')
1139+ self.assertEqual([repository], list(search_results))
1140+
1141+ def test_bad_match_git_identity_development_focus(self):
1142+ # If you search for the development focus for a project where one
1143+ # isn't set, you get an empty search result.
1144+ fooix = self.factory.makeProduct(name='fooix')
1145+ self.factory.makeGitRepository(target=fooix)
1146+ self.factory.makeGitRepository()
1147+ search_results = self.collection.search('lp:fooix')
1148+ self.assertEqual([], list(search_results))
1149+
1150+ def test_bad_match_git_identity_no_project(self):
1151+ # If you search for the development focus for a project where one
1152+ # isn't set, you get an empty search result.
1153+ self.factory.makeGitRepository()
1154+ search_results = self.collection.search('lp:fooix')
1155+ self.assertEqual([], list(search_results))
1156+
1157+ def test_exact_match_url_trailing_slash(self):
1158+ # Sometimes, users are inconsiderately unaware of our arbitrary
1159+ # database restrictions and will put trailing slashes on their
1160+ # search queries. Rather bravely, we refuse to explode in this
1161+ # case.
1162+ repository = self.factory.makeGitRepository()
1163+ self.factory.makeGitRepository()
1164+ search_results = self.collection.search(
1165+ repository.getCodebrowseUrl() + '/')
1166+ self.assertEqual([repository], list(search_results))
1167+
1168+ def test_match_exact_repository_name(self):
1169+ # search returns all repositories with the same name as the search
1170+ # term.
1171+ repository1 = self.factory.makeGitRepository(name=u'foo')
1172+ repository2 = self.factory.makeGitRepository(name=u'foo')
1173+ self.factory.makeGitRepository()
1174+ search_results = self.collection.search('foo')
1175+ self.assertEqual(
1176+ sorted([repository1, repository2]), sorted(search_results))
1177+
1178+ def disabled_test_match_against_unique_name(self):
1179+ # XXX cjwatson 2015-02-06: Disabled until the URL format settles
1180+ # down.
1181+ repository = self.factory.makeGitRepository(name=u'fooa')
1182+ search_term = repository.target.name + '/foo'
1183+ search_results = self.collection.search(search_term)
1184+ self.assertEqual([repository], list(search_results))
1185+
1186+ def test_match_sub_repository_name(self):
1187+ # search returns all repositories which have a name of which the
1188+ # search term is a substring.
1189+ repository1 = self.factory.makeGitRepository(name=u'afoo')
1190+ repository2 = self.factory.makeGitRepository(name=u'foob')
1191+ self.factory.makeGitRepository()
1192+ search_results = self.collection.search('foo')
1193+ self.assertEqual(
1194+ sorted([repository1, repository2]), sorted(search_results))
1195+
1196+ def test_match_ignores_case(self):
1197+ repository = self.factory.makeGitRepository(name=u'foobar')
1198+ search_results = self.collection.search('FOOBAR')
1199+ self.assertEqual([repository], list(search_results))
1200+
1201+ def test_dont_match_project_if_in_project(self):
1202+ # If the container is restricted to the project, then we don't match
1203+ # the project name.
1204+ project = self.factory.makeProduct('foo')
1205+ repository1 = self.factory.makeGitRepository(
1206+ target=project, name=u'foo')
1207+ self.factory.makeGitRepository(target=project, name=u'bar')
1208+ search_results = self.collection.inProject(project).search('foo')
1209+ self.assertEqual([repository1], list(search_results))
1210+
1211+
1212+class TestGetTeamsWithRepositories(TestCaseWithFactory):
1213+ """Test the IGitCollection.getTeamsWithRepositories method."""
1214+
1215+ layer = DatabaseFunctionalLayer
1216+
1217+ def setUp(self):
1218+ TestCaseWithFactory.setUp(self)
1219+ self.all_repositories = getUtility(IAllGitRepositories)
1220+
1221+ def test_no_teams(self):
1222+ # If the user is not a member of any teams, there are no results,
1223+ # even if the person owns a repository themselves.
1224+ person = self.factory.makePerson()
1225+ self.factory.makeGitRepository(owner=person)
1226+ teams = list(self.all_repositories.getTeamsWithRepositories(person))
1227+ self.assertEqual([], teams)
1228+
1229+ def test_team_repositories(self):
1230+ # Return the teams that the user is in and that have repositories.
1231+ person = self.factory.makePerson()
1232+ team = self.factory.makeTeam(owner=person)
1233+ self.factory.makeGitRepository(owner=team)
1234+ # Make another team that person is in that has no repositories.
1235+ self.factory.makeTeam(owner=person)
1236+ teams = list(self.all_repositories.getTeamsWithRepositories(person))
1237+ self.assertEqual([team], teams)
1238+
1239+ def test_respects_restrictions(self):
1240+ # Create a team with repositories on a project, and another
1241+ # repository in a different namespace owned by a different team that
1242+ # the person is a member of. Restricting the collection will return
1243+ # just the teams that have repositories in that restricted
1244+ # collection.
1245+ person = self.factory.makePerson()
1246+ team1 = self.factory.makeTeam(owner=person)
1247+ repository = self.factory.makeGitRepository(owner=team1)
1248+ # Make another team that person is in that owns a repository in a
1249+ # different namespace to the namespace of the repository owned by team1.
1250+ team2 = self.factory.makeTeam(owner=person)
1251+ self.factory.makeGitRepository(owner=team2)
1252+ collection = self.all_repositories.inProject(repository.target)
1253+ teams = list(collection.getTeamsWithRepositories(person))
1254+ self.assertEqual([team1], teams)
1255+
1256+
1257+class TestGitCollectionOwnerCounts(TestCaseWithFactory):
1258+ """Test IGitCollection.ownerCounts."""
1259+
1260+ layer = DatabaseFunctionalLayer
1261+
1262+ def setUp(self):
1263+ TestCaseWithFactory.setUp(self)
1264+ self.all_repositories = getUtility(IAllGitRepositories)
1265+
1266+ def test_no_repositories(self):
1267+ # If there are no repositories, we should get zero counts for both.
1268+ person_count, team_count = self.all_repositories.ownerCounts()
1269+ self.assertEqual(0, person_count)
1270+ self.assertEqual(0, team_count)
1271+
1272+ def test_individual_repository_owners(self):
1273+ # Repositories owned by an individual are returned as the first part
1274+ # of the tuple.
1275+ self.factory.makeGitRepository()
1276+ self.factory.makeGitRepository()
1277+ person_count, team_count = self.all_repositories.ownerCounts()
1278+ self.assertEqual(2, person_count)
1279+ self.assertEqual(0, team_count)
1280+
1281+ def test_team_repository_owners(self):
1282+ # Repositories owned by teams are returned as the second part of the
1283+ # tuple.
1284+ self.factory.makeGitRepository(owner=self.factory.makeTeam())
1285+ self.factory.makeGitRepository(owner=self.factory.makeTeam())
1286+ person_count, team_count = self.all_repositories.ownerCounts()
1287+ self.assertEqual(0, person_count)
1288+ self.assertEqual(2, team_count)
1289+
1290+ def test_multiple_repositories_owned_counted_once(self):
1291+ # Confirming that a person that owns multiple repositories only gets
1292+ # counted once.
1293+ individual = self.factory.makePerson()
1294+ team = self.factory.makeTeam()
1295+ for owner in [individual, individual, team, team]:
1296+ self.factory.makeGitRepository(owner=owner)
1297+ person_count, team_count = self.all_repositories.ownerCounts()
1298+ self.assertEqual(1, person_count)
1299+ self.assertEqual(1, team_count)
1300+
1301+ def test_counts_limited_by_collection(self):
1302+ # For collections that are constrained in some way, we only get
1303+ # counts for the constrained collection.
1304+ r1 = self.factory.makeGitRepository()
1305+ project = r1.target
1306+ self.factory.makeGitRepository()
1307+ collection = self.all_repositories.inProject(project)
1308+ person_count, team_count = collection.ownerCounts()
1309+ self.assertEqual(1, person_count)