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

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
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 Approve
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.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/code/adapters/gitcollection.py'
--- lib/lp/code/adapters/gitcollection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/adapters/gitcollection.py 2015-02-26 17:21:27 +0000
@@ -0,0 +1,62 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Adapters for different objects to Git repository collections."""
5
6__metaclass__ = type
7__all__ = [
8 'git_collection_for_distribution',
9 'git_collection_for_distro_source_package',
10 'git_collection_for_person',
11 'git_collection_for_person_distro_source_package',
12 'git_collection_for_person_product',
13 'git_collection_for_project',
14 'git_collection_for_project_group',
15 ]
16
17
18from zope.component import getUtility
19
20from lp.code.interfaces.gitcollection import IAllGitRepositories
21
22
23def git_collection_for_project(project):
24 """Adapt a product to a Git repository collection."""
25 return getUtility(IAllGitRepositories).inProject(project)
26
27
28def git_collection_for_project_group(project_group):
29 """Adapt a project group to a Git repository collection."""
30 return getUtility(IAllGitRepositories).inProjectGroup(project_group)
31
32
33def git_collection_for_distribution(distribution):
34 """Adapt a distribution to a Git repository collection."""
35 return getUtility(IAllGitRepositories).inDistribution(distribution)
36
37
38def git_collection_for_distro_source_package(distro_source_package):
39 """Adapt a distro_source_package to a Git repository collection."""
40 return getUtility(IAllGitRepositories).inDistributionSourcePackage(
41 distro_source_package)
42
43
44def git_collection_for_person(person):
45 """Adapt a person to a Git repository collection."""
46 return getUtility(IAllGitRepositories).ownedBy(person)
47
48
49def git_collection_for_person_product(person_product):
50 """Adapt a PersonProduct to a Git repository collection."""
51 collection = getUtility(IAllGitRepositories).ownedBy(person_product.person)
52 collection = collection.inProject(person_product.product)
53 return collection
54
55
56def git_collection_for_person_distro_source_package(person_dsp):
57 """Adapt a PersonDistributionSourcePackage to a Git repository
58 collection."""
59 collection = getUtility(IAllGitRepositories).ownedBy(person_dsp.person)
60 collection = collection.inDistributionSourcePackage(
61 person_dsp.distro_source_package)
62 return collection
063
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000
+++ lib/lp/code/configure.zcml 2015-02-26 17:21:27 +0000
@@ -858,6 +858,55 @@
858 <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />858 <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
859 </securedutility>859 </securedutility>
860860
861 <!-- GitCollection -->
862
863 <class class="lp.code.model.gitcollection.GenericGitCollection">
864 <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
865 </class>
866 <class class="lp.code.model.gitcollection.AnonymousGitCollection">
867 <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
868 </class>
869 <class class="lp.code.model.gitcollection.VisibleGitCollection">
870 <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
871 </class>
872 <adapter
873 for="storm.store.Store"
874 provides="lp.code.interfaces.gitcollection.IGitCollection"
875 factory="lp.code.model.gitcollection.GenericGitCollection"/>
876 <adapter
877 for="lp.registry.interfaces.product.IProduct"
878 provides="lp.code.interfaces.gitcollection.IGitCollection"
879 factory="lp.code.adapters.gitcollection.git_collection_for_project"/>
880 <adapter
881 for="lp.registry.interfaces.projectgroup.IProjectGroup"
882 provides="lp.code.interfaces.gitcollection.IGitCollection"
883 factory="lp.code.adapters.gitcollection.git_collection_for_project_group"/>
884 <adapter
885 for="lp.registry.interfaces.distribution.IDistribution"
886 provides="lp.code.interfaces.gitcollection.IGitCollection"
887 factory="lp.code.adapters.gitcollection.git_collection_for_distribution"/>
888 <adapter
889 for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
890 provides="lp.code.interfaces.gitcollection.IGitCollection"
891 factory="lp.code.adapters.gitcollection.git_collection_for_distro_source_package"/>
892 <adapter
893 for="lp.registry.interfaces.person.IPerson"
894 provides="lp.code.interfaces.gitcollection.IGitCollection"
895 factory="lp.code.adapters.gitcollection.git_collection_for_person"/>
896 <adapter
897 for="lp.registry.interfaces.personproduct.IPersonProduct"
898 provides="lp.code.interfaces.gitcollection.IGitCollection"
899 factory="lp.code.adapters.gitcollection.git_collection_for_person_product"/>
900 <adapter
901 for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
902 provides="lp.code.interfaces.gitcollection.IGitCollection"
903 factory="lp.code.adapters.gitcollection.git_collection_for_person_distro_source_package"/>
904 <securedutility
905 class="lp.code.model.gitcollection.GenericGitCollection"
906 provides="lp.code.interfaces.gitcollection.IAllGitRepositories">
907 <allow interface="lp.code.interfaces.gitcollection.IAllGitRepositories"/>
908 </securedutility>
909
861 <!-- Default Git repositories -->910 <!-- Default Git repositories -->
862911
863 <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />912 <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />
864913
=== added file 'lib/lp/code/interfaces/gitcollection.py'
--- lib/lp/code/interfaces/gitcollection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitcollection.py 2015-02-26 17:21:27 +0000
@@ -0,0 +1,125 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""A collection of Git repositories.
5
6See `IGitCollection` for more details.
7"""
8
9__metaclass__ = type
10__all__ = [
11 'IAllGitRepositories',
12 'IGitCollection',
13 'InvalidGitFilter',
14 ]
15
16from zope.interface import Interface
17
18
19class InvalidGitFilter(Exception):
20 """Raised when an `IGitCollection` cannot apply the given filter."""
21
22
23class IGitCollection(Interface):
24 """A collection of Git repositories.
25
26 An `IGitCollection` is an immutable collection of Git repositories. It
27 has two kinds of methods: filter methods and query methods.
28
29 Query methods get information about the contents of the collection. See
30 `IGitCollection.count` and `IGitCollection.getRepositories`.
31
32 Filter methods return new IGitCollection instances that have some sort
33 of restriction. Examples include `ownedBy`, `visibleByUser` and
34 `inProject`.
35
36 Implementations of this interface are not 'content classes'. That is, they
37 do not correspond to a particular row in the database.
38
39 This interface is intended for use within Launchpad, not to be exported as
40 a public API.
41 """
42
43 def count():
44 """The number of repositories in this collection."""
45
46 def is_empty():
47 """Is this collection empty?"""
48
49 def ownerCounts():
50 """Return the number of different repository owners.
51
52 :return: a tuple (individual_count, team_count) containing the
53 number of individuals and teams that own repositories in this
54 collection.
55 """
56
57 def getRepositories(eager_load=False):
58 """Return a result set of all repositories in this collection.
59
60 The returned result set will also join across the specified tables
61 as defined by the arguments to this function. These extra tables
62 are joined specifically to allow the caller to sort on values not in
63 the GitRepository table itself.
64
65 :param eager_load: If True trigger eager loading of all the related
66 objects in the collection.
67 """
68
69 def getRepositoryIds():
70 """Return a result set of all repository ids in this collection."""
71
72 def getTeamsWithRepositories(person):
73 """Return the teams that person is a member of that have
74 repositories."""
75
76 def inProject(project):
77 """Restrict the collection to repositories in 'project'."""
78
79 def inProjectGroup(projectgroup):
80 """Restrict the collection to repositories in 'projectgroup'."""
81
82 def inDistribution(distribution):
83 """Restrict the collection to repositories in 'distribution'."""
84
85 def inDistributionSourcePackage(distro_source_package):
86 """Restrict to repositories in a package for a distribution."""
87
88 def isPersonal():
89 """Restrict the collection to personal repositories."""
90
91 def isPrivate():
92 """Restrict the collection to private repositories."""
93
94 def isExclusive():
95 """Restrict the collection to repositories owned by exclusive
96 people."""
97
98 def ownedBy(person):
99 """Restrict the collection to repositories owned by 'person'."""
100
101 def ownedByTeamMember(person):
102 """Restrict the collection to repositories owned by 'person' or a
103 team of which person is a member.
104 """
105
106 def registeredBy(person):
107 """Restrict the collection to repositories registered by 'person'."""
108
109 def search(term):
110 """Search the collection for repositories matching 'term'.
111
112 :param term: A string.
113 :return: A `ResultSet` of repositories that matched.
114 """
115
116 def visibleByUser(person):
117 """Restrict the collection to repositories that person is allowed to
118 see."""
119
120 def withIds(*repository_ids):
121 """Restrict the collection to repositories with the specified ids."""
122
123
124class IAllGitRepositories(IGitCollection):
125 """A `IGitCollection` representing all Git repositories in Launchpad."""
0126
=== added file 'lib/lp/code/model/gitcollection.py'
--- lib/lp/code/model/gitcollection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitcollection.py 2015-02-26 17:21:27 +0000
@@ -0,0 +1,327 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Implementations of `IGitCollection`."""
5
6__metaclass__ = type
7__all__ = [
8 'GenericGitCollection',
9 ]
10
11from lazr.uri import (
12 InvalidURIError,
13 URI,
14 )
15from storm.expr import (
16 Count,
17 In,
18 Join,
19 Select,
20 )
21from zope.component import getUtility
22from zope.interface import implements
23
24from lp.app.enums import PRIVATE_INFORMATION_TYPES
25from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
26from lp.code.interfaces.gitcollection import (
27 IGitCollection,
28 InvalidGitFilter,
29 )
30from lp.code.interfaces.gitlookup import IGitLookup
31from lp.code.interfaces.gitrepository import (
32 user_has_special_git_repository_access,
33 )
34from lp.code.model.gitrepository import (
35 GitRepository,
36 get_git_repository_privacy_filter,
37 )
38from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
39from lp.registry.model.person import Person
40from lp.registry.model.product import Product
41from lp.registry.model.teammembership import TeamParticipation
42from lp.services.database.bulk import load_related
43from lp.services.database.decoratedresultset import DecoratedResultSet
44from lp.services.database.interfaces import IStore
45from lp.services.propertycache import get_property_cache
46
47
48class GenericGitCollection:
49 """See `IGitCollection`."""
50
51 implements(IGitCollection)
52
53 def __init__(self, store=None, filter_expressions=None, tables=None):
54 """Construct a `GenericGitCollection`.
55
56 :param store: The store to look in for repositories. If not
57 specified, use the default store.
58 :param filter_expressions: A list of Storm expressions to
59 restrict the repositories in the collection. If unspecified,
60 then there will be no restrictions on the result set. That is,
61 all repositories in the store will be in the collection.
62 :param tables: A dict of Storm tables to the Join expression. If an
63 expression in filter_expressions refers to a table, then that
64 table *must* be in this list.
65 """
66 self._store = store
67 if filter_expressions is None:
68 filter_expressions = []
69 self._filter_expressions = list(filter_expressions)
70 if tables is None:
71 tables = {}
72 self._tables = tables
73 self._user = None
74
75 def count(self):
76 """See `IGitCollection`."""
77 return self.getRepositories(eager_load=False).count()
78
79 def is_empty(self):
80 """See `IGitCollection`."""
81 return self.getRepositories(eager_load=False).is_empty()
82
83 def ownerCounts(self):
84 """See `IGitCollection`."""
85 is_team = Person.teamowner != None
86 owners = self._getRepositorySelect((GitRepository.owner_id,))
87 counts = dict(self.store.find(
88 (is_team, Count(Person.id)),
89 Person.id.is_in(owners)).group_by(is_team))
90 return (counts.get(False, 0), counts.get(True, 0))
91
92 @property
93 def store(self):
94 # Although you might think we could set the default value for store
95 # in the constructor, we can't. The IStore utility is not available
96 # at the time that the ZCML is parsed, which means we get an error
97 # if this code is in the constructor.
98 # -- JonathanLange 2009-02-17.
99 if self._store is None:
100 return IStore(GitRepository)
101 else:
102 return self._store
103
104 def _filterBy(self, expressions, table=None, join=None):
105 """Return a subset of this collection, filtered by 'expressions'."""
106 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
107 # for explicit 'tables' by harnessing Storm's table inference system.
108 # See http://paste.ubuntu.com/118711/ for one way to do that.
109 if table is not None and join is None:
110 raise InvalidGitFilter("Cannot specify a table without a join.")
111 if expressions is None:
112 expressions = []
113 tables = self._tables.copy()
114 if table is not None:
115 tables[table] = join
116 return self.__class__(
117 self.store, self._filter_expressions + expressions, tables)
118
119 def _getRepositorySelect(self, columns=(GitRepository.id,)):
120 """Return a Storm 'Select' for columns in this collection."""
121 repositories = self.getRepositories(
122 eager_load=False, find_expr=columns)
123 return repositories.get_plain_result_set()._get_select()
124
125 def _getRepositoryExpressions(self):
126 """Return the where expressions for this collection."""
127 return (self._filter_expressions +
128 self._getRepositoryVisibilityExpression())
129
130 def _getRepositoryVisibilityExpression(self):
131 """Return the where clauses for visibility."""
132 return []
133
134 def getRepositories(self, find_expr=GitRepository, eager_load=False):
135 """See `IGitCollection`."""
136 tables = [GitRepository] + list(set(self._tables.values()))
137 expressions = self._getRepositoryExpressions()
138 resultset = self.store.using(*tables).find(find_expr, *expressions)
139
140 def do_eager_load(rows):
141 repository_ids = set(repository.id for repository in rows)
142 if not repository_ids:
143 return
144 load_related(Product, rows, ['project_id'])
145 # So far have only needed the persons for their canonical_url - no
146 # need for validity etc in the API call.
147 load_related(Person, rows, ['owner_id', 'registrant_id'])
148
149 def cache_permission(repository):
150 if self._user:
151 get_property_cache(repository)._known_viewers = set(
152 [self._user.id])
153 return repository
154
155 eager_load_hook = (
156 do_eager_load if eager_load and find_expr == GitRepository
157 else None)
158 return DecoratedResultSet(
159 resultset, pre_iter_hook=eager_load_hook,
160 result_decorator=cache_permission)
161
162 def getRepositoryIds(self):
163 """See `IGitCollection`."""
164 return self.getRepositories(
165 find_expr=GitRepository.id).get_plain_result_set()
166
167 def getTeamsWithRepositories(self, person):
168 """See `IGitCollection`."""
169 # This method doesn't entirely fit with the intent of the
170 # GitCollection conceptual model, but we're not quite sure how to
171 # fix it just yet.
172 repository_query = self._getRepositorySelect((GitRepository.owner_id,))
173 return self.store.find(
174 Person,
175 Person.id == TeamParticipation.teamID,
176 TeamParticipation.person == person,
177 TeamParticipation.team != person,
178 Person.id.is_in(repository_query))
179
180 def inProject(self, project):
181 """See `IGitCollection`."""
182 return self._filterBy([GitRepository.project == project])
183
184 def inProjectGroup(self, projectgroup):
185 """See `IGitCollection`."""
186 return self._filterBy(
187 [Product.projectgroup == projectgroup.id],
188 table=Product,
189 join=Join(Product, GitRepository.project == Product.id))
190
191 def inDistribution(self, distribution):
192 """See `IGitCollection`."""
193 return self._filterBy([GitRepository.distribution == distribution])
194
195 def inDistributionSourcePackage(self, distro_source_package):
196 """See `IGitCollection`."""
197 distribution = distro_source_package.distribution
198 sourcepackagename = distro_source_package.sourcepackagename
199 return self._filterBy(
200 [GitRepository.distribution == distribution,
201 GitRepository.sourcepackagename == sourcepackagename])
202
203 def isPersonal(self):
204 """See `IGitCollection`."""
205 return self._filterBy(
206 [GitRepository.project == None,
207 GitRepository.distribution == None])
208
209 def isPrivate(self):
210 """See `IGitCollection`."""
211 return self._filterBy(
212 [GitRepository.information_type.is_in(PRIVATE_INFORMATION_TYPES)])
213
214 def isExclusive(self):
215 """See `IGitCollection`."""
216 return self._filterBy(
217 [Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY)],
218 table=Person,
219 join=Join(Person, GitRepository.owner_id == Person.id))
220
221 def ownedBy(self, person):
222 """See `IGitCollection`."""
223 return self._filterBy([GitRepository.owner == person])
224
225 def ownedByTeamMember(self, person):
226 """See `IGitCollection`."""
227 subquery = Select(
228 TeamParticipation.teamID,
229 where=TeamParticipation.personID == person.id)
230 return self._filterBy([In(GitRepository.owner_id, subquery)])
231
232 def registeredBy(self, person):
233 """See `IGitCollection`."""
234 return self._filterBy([GitRepository.registrant == person])
235
236 def _getExactMatch(self, term):
237 # Look up the repository by its URL, which handles both shortcuts
238 # and unique names.
239 repository = getUtility(IGitLookup).getByUrl(term)
240 if repository is not None:
241 return repository
242 # Fall back to searching by unique_name, stripping out the path if
243 # it's a URI.
244 try:
245 path = URI(term).path.strip("/")
246 except InvalidURIError:
247 path = term
248 return getUtility(IGitLookup).getByUniqueName(path)
249
250 def search(self, term):
251 """See `IGitCollection`."""
252 repository = self._getExactMatch(term)
253 if repository:
254 collection = self._filterBy([GitRepository.id == repository.id])
255 else:
256 term = unicode(term)
257 # Filter by name.
258 field = GitRepository.name
259 # Except if the term contains /, when we use unique_name.
260 # XXX cjwatson 2015-02-06: Disabled until the URL format settles
261 # down, at which point we can make GitRepository.unique_name a
262 # trigger-maintained column rather than a property.
263 #if '/' in term:
264 # field = GitRepository.unique_name
265 collection = self._filterBy(
266 [field.lower().contains_string(term.lower())])
267 return collection.getRepositories(eager_load=False).order_by(
268 GitRepository.name, GitRepository.id)
269
270 def visibleByUser(self, person):
271 """See `IGitCollection`."""
272 if (person == LAUNCHPAD_SERVICES or
273 user_has_special_git_repository_access(person)):
274 return self
275 if person is None:
276 return AnonymousGitCollection(
277 self._store, self._filter_expressions, self._tables)
278 return VisibleGitCollection(
279 person, self._store, self._filter_expressions, self._tables)
280
281 def withIds(self, *repository_ids):
282 """See `IGitCollection`."""
283 return self._filterBy([GitRepository.id.is_in(repository_ids)])
284
285
286class AnonymousGitCollection(GenericGitCollection):
287 """Repository collection that only shows public repositories."""
288
289 def _getRepositoryVisibilityExpression(self):
290 """Return the where clauses for visibility."""
291 return get_git_repository_privacy_filter(None)
292
293
294class VisibleGitCollection(GenericGitCollection):
295 """A repository collection that has special logic for visibility."""
296
297 def __init__(self, user, store=None, filter_expressions=None, tables=None):
298 super(VisibleGitCollection, self).__init__(
299 store=store, filter_expressions=filter_expressions, tables=tables)
300 self._user = user
301
302 def _filterBy(self, expressions, table=None, join=None):
303 """Return a subset of this collection, filtered by 'expressions'."""
304 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
305 # for explicit 'tables' by harnessing Storm's table inference system.
306 # See http://paste.ubuntu.com/118711/ for one way to do that.
307 if table is not None and join is None:
308 raise InvalidGitFilter("Cannot specify a table without a join.")
309 if expressions is None:
310 expressions = []
311 tables = self._tables.copy()
312 if table is not None:
313 tables[table] = join
314 return self.__class__(
315 self._user, self.store, self._filter_expressions + expressions)
316
317 def _getRepositoryVisibilityExpression(self):
318 """Return the where clauses for visibility."""
319 return get_git_repository_privacy_filter(self._user)
320
321 def visibleByUser(self, person):
322 """See `IGitCollection`."""
323 if person == self._user:
324 return self
325 raise InvalidGitFilter(
326 "Cannot filter for Git repositories visible by user %r, already "
327 "filtering for %r" % (person, self._user))
0328
=== added file 'lib/lp/code/model/tests/test_gitcollection.py'
--- lib/lp/code/model/tests/test_gitcollection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitcollection.py 2015-02-26 17:21:27 +0000
@@ -0,0 +1,716 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for Git repository collections."""
5
6__metaclass__ = type
7
8from testtools.matchers import Equals
9from zope.component import getUtility
10from zope.security.proxy import removeSecurityProxy
11
12from lp.app.enums import InformationType
13from lp.app.interfaces.launchpad import ILaunchpadCelebrities
14from lp.app.interfaces.services import IService
15from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
16from lp.code.interfaces.gitcollection import (
17 IAllGitRepositories,
18 IGitCollection,
19 )
20from lp.code.interfaces.gitrepository import IGitRepositorySet
21from lp.code.model.gitcollection import GenericGitCollection
22from lp.code.model.gitrepository import GitRepository
23from lp.registry.enums import PersonVisibility
24from lp.registry.interfaces.person import TeamMembershipPolicy
25from lp.registry.model.persondistributionsourcepackage import (
26 PersonDistributionSourcePackage,
27 )
28from lp.registry.model.personproduct import PersonProduct
29from lp.services.database.interfaces import IStore
30from lp.testing import (
31 person_logged_in,
32 StormStatementRecorder,
33 TestCaseWithFactory,
34 )
35from lp.testing.layers import DatabaseFunctionalLayer
36from lp.testing.matchers import HasQueryCount
37
38
39class TestGitCollectionAdaptation(TestCaseWithFactory):
40 """Check that certain objects can be adapted to a Git repository
41 collection."""
42
43 layer = DatabaseFunctionalLayer
44
45 def assertCollection(self, target):
46 self.assertIsNotNone(IGitCollection(target, None))
47
48 def test_project(self):
49 # A project can be adapted to a Git repository collection.
50 self.assertCollection(self.factory.makeProduct())
51
52 def test_project_group(self):
53 # A project group can be adapted to a Git repository collection.
54 self.assertCollection(self.factory.makeProject())
55
56 def test_distribution(self):
57 # A distribution can be adapted to a Git repository collection.
58 self.assertCollection(self.factory.makeDistribution())
59
60 def test_distribution_source_package(self):
61 # A distribution source package can be adapted to a Git repository
62 # collection.
63 self.assertCollection(self.factory.makeDistributionSourcePackage())
64
65 def test_person(self):
66 # A person can be adapted to a Git repository collection.
67 self.assertCollection(self.factory.makePerson())
68
69 def test_person_product(self):
70 # A PersonProduct can be adapted to a Git repository collection.
71 project = self.factory.makeProduct()
72 self.assertCollection(PersonProduct(project.owner, project))
73
74 def test_person_distribution_source_package(self):
75 # A PersonDistributionSourcePackage can be adapted to a Git
76 # repository collection.
77 dsp = self.factory.makeDistributionSourcePackage()
78 self.assertCollection(
79 PersonDistributionSourcePackage(dsp.distribution.owner, dsp))
80
81
82class TestGenericGitCollection(TestCaseWithFactory):
83
84 layer = DatabaseFunctionalLayer
85
86 def setUp(self):
87 super(TestGenericGitCollection, self).setUp()
88 self.store = IStore(GitRepository)
89
90 def test_provides_gitcollection(self):
91 # `GenericGitCollection` provides the `IGitCollection`
92 # interface.
93 self.assertProvides(GenericGitCollection(self.store), IGitCollection)
94
95 def test_getRepositories_no_filter_no_repositories(self):
96 # If no filter is specified, then the collection is of all
97 # repositories in Launchpad. By default, there are no repositories.
98 collection = GenericGitCollection(self.store)
99 self.assertEqual([], list(collection.getRepositories()))
100
101 def test_getRepositories_no_filter(self):
102 # If no filter is specified, then the collection is of all
103 # repositories in Launchpad.
104 collection = GenericGitCollection(self.store)
105 repository = self.factory.makeGitRepository()
106 self.assertEqual([repository], list(collection.getRepositories()))
107
108 def test_getRepositories_project_filter(self):
109 # If the specified filter is for the repositories of a particular
110 # project, then the collection contains only repositories of that
111 # project.
112 repository = self.factory.makeGitRepository()
113 self.factory.makeGitRepository()
114 collection = GenericGitCollection(
115 self.store, [GitRepository.project == repository.target])
116 self.assertEqual([repository], list(collection.getRepositories()))
117
118 def test_getRepositories_caches_viewers(self):
119 # getRepositories() caches the user as a known viewer so that
120 # repository.visibleByUser() does not have to hit the database.
121 collection = GenericGitCollection(self.store)
122 owner = self.factory.makePerson()
123 project = self.factory.makeProduct()
124 repository = self.factory.makeGitRepository(
125 owner=owner, target=project,
126 information_type=InformationType.USERDATA)
127 someone = self.factory.makePerson()
128 with person_logged_in(owner):
129 getUtility(IService, 'sharing').ensureAccessGrants(
130 [someone], owner, gitrepositories=[repository],
131 ignore_permissions=True)
132 [repository] = list(collection.visibleByUser(
133 someone).getRepositories())
134 with StormStatementRecorder() as recorder:
135 self.assertTrue(repository.visibleByUser(someone))
136 self.assertThat(recorder, HasQueryCount(Equals(0)))
137
138 def test_getRepositoryIds(self):
139 repository = self.factory.makeGitRepository()
140 self.factory.makeGitRepository()
141 collection = GenericGitCollection(
142 self.store, [GitRepository.project == repository.target])
143 self.assertEqual([repository.id], list(collection.getRepositoryIds()))
144
145 def test_count(self):
146 # The 'count' property of a collection is the number of elements in
147 # the collection.
148 collection = GenericGitCollection(self.store)
149 self.assertEqual(0, collection.count())
150 for i in range(3):
151 self.factory.makeGitRepository()
152 self.assertEqual(3, collection.count())
153
154 def test_count_respects_filter(self):
155 # If a collection is a subset of all possible repositories, then the
156 # count will be the size of that subset. That is, 'count' respects
157 # any filters that are applied.
158 repository = self.factory.makeGitRepository()
159 self.factory.makeGitRepository()
160 collection = GenericGitCollection(
161 self.store, [GitRepository.project == repository.target])
162 self.assertEqual(1, collection.count())
163
164
165class TestGitCollectionFilters(TestCaseWithFactory):
166
167 layer = DatabaseFunctionalLayer
168
169 def setUp(self):
170 TestCaseWithFactory.setUp(self)
171 self.all_repositories = getUtility(IAllGitRepositories)
172
173 def test_order_by_repository_name(self):
174 # The result of getRepositories() can be ordered by
175 # `GitRepository.name`, no matter what filters are applied.
176 aardvark = self.factory.makeProduct(name='aardvark')
177 badger = self.factory.makeProduct(name='badger')
178 repository_a = self.factory.makeGitRepository(target=aardvark)
179 repository_b = self.factory.makeGitRepository(target=badger)
180 person = self.factory.makePerson()
181 repository_c = self.factory.makeGitRepository(
182 owner=person, target=person)
183 self.assertEqual(
184 sorted([repository_a, repository_b, repository_c]),
185 sorted(self.all_repositories.getRepositories()
186 .order_by(GitRepository.name)))
187
188 def test_count_respects_visibleByUser_filter(self):
189 # IGitCollection.count() returns the number of repositories that
190 # getRepositories() yields, even when the visibleByUser filter is
191 # applied.
192 repository = self.factory.makeGitRepository()
193 self.factory.makeGitRepository(
194 information_type=InformationType.USERDATA)
195 collection = self.all_repositories.visibleByUser(repository.owner)
196 self.assertEqual(1, collection.getRepositories().count())
197 self.assertEqual(1, len(list(collection.getRepositories())))
198 self.assertEqual(1, collection.count())
199
200 def test_ownedBy(self):
201 # 'ownedBy' returns a new collection restricted to repositories
202 # owned by the given person.
203 repository = self.factory.makeGitRepository()
204 self.factory.makeGitRepository()
205 collection = self.all_repositories.ownedBy(repository.owner)
206 self.assertEqual([repository], list(collection.getRepositories()))
207
208 def test_ownedByTeamMember(self):
209 # 'ownedBy' returns a new collection restricted to repositories
210 # owned by any team of which the given person is a member.
211 person = self.factory.makePerson()
212 team = self.factory.makeTeam(members=[person])
213 repository = self.factory.makeGitRepository(owner=team)
214 self.factory.makeGitRepository()
215 collection = self.all_repositories.ownedByTeamMember(person)
216 self.assertEqual([repository], list(collection.getRepositories()))
217
218 def test_in_project(self):
219 # 'inProject' returns a new collection restricted to repositories in
220 # the given project.
221 #
222 # NOTE: JonathanLange 2009-02-11: Maybe this should be a more
223 # generic method called 'onTarget' that takes a person, package or
224 # project.
225 repository = self.factory.makeGitRepository()
226 self.factory.makeGitRepository()
227 collection = self.all_repositories.inProject(repository.target)
228 self.assertEqual([repository], list(collection.getRepositories()))
229
230 def test_inProjectGroup(self):
231 # 'inProjectGroup' returns a new collection restricted to
232 # repositories in the given project group.
233 repository = self.factory.makeGitRepository()
234 self.factory.makeGitRepository()
235 projectgroup = self.factory.makeProject()
236 removeSecurityProxy(repository.target).projectgroup = projectgroup
237 collection = self.all_repositories.inProjectGroup(projectgroup)
238 self.assertEqual([repository], list(collection.getRepositories()))
239
240 def test_isExclusive(self):
241 # 'isExclusive' is restricted to repositories owned by exclusive
242 # teams and users.
243 user = self.factory.makePerson()
244 team = self.factory.makeTeam(
245 membership_policy=TeamMembershipPolicy.RESTRICTED)
246 other_team = self.factory.makeTeam(
247 membership_policy=TeamMembershipPolicy.OPEN)
248 team_repository = self.factory.makeGitRepository(owner=team)
249 user_repository = self.factory.makeGitRepository(owner=user)
250 self.factory.makeGitRepository(owner=other_team)
251 collection = self.all_repositories.isExclusive()
252 self.assertContentEqual(
253 [team_repository, user_repository],
254 list(collection.getRepositories()))
255
256 def test_inProject_and_isExclusive(self):
257 # 'inProject' and 'isExclusive' can combine to form a collection
258 # that is restricted to repositories of a particular project owned
259 # by exclusive teams and users.
260 team = self.factory.makeTeam(
261 membership_policy=TeamMembershipPolicy.RESTRICTED)
262 other_team = self.factory.makeTeam(
263 membership_policy=TeamMembershipPolicy.OPEN)
264 project = self.factory.makeProduct()
265 repository = self.factory.makeGitRepository(target=project, owner=team)
266 self.factory.makeGitRepository(owner=team)
267 self.factory.makeGitRepository(target=project, owner=other_team)
268 collection = self.all_repositories.inProject(project).isExclusive()
269 self.assertEqual([repository], list(collection.getRepositories()))
270 collection = self.all_repositories.isExclusive().inProject(project)
271 self.assertEqual([repository], list(collection.getRepositories()))
272
273 def test_ownedBy_and_inProject(self):
274 # 'ownedBy' and 'inProject' can combine to form a collection that is
275 # restricted to repositories of a particular project owned by a
276 # particular person.
277 person = self.factory.makePerson()
278 project = self.factory.makeProduct()
279 repository = self.factory.makeGitRepository(
280 target=project, owner=person)
281 self.factory.makeGitRepository(owner=person)
282 self.factory.makeGitRepository(target=project)
283 collection = self.all_repositories.inProject(project).ownedBy(person)
284 self.assertEqual([repository], list(collection.getRepositories()))
285 collection = self.all_repositories.ownedBy(person).inProject(project)
286 self.assertEqual([repository], list(collection.getRepositories()))
287
288 def test_ownedBy_and_isPrivate(self):
289 # 'ownedBy' and 'isPrivate' can combine to form a collection that is
290 # restricted to private repositories owned by a particular person.
291 person = self.factory.makePerson()
292 project = self.factory.makeProduct()
293 repository = self.factory.makeGitRepository(
294 target=project, owner=person,
295 information_type=InformationType.USERDATA)
296 self.factory.makeGitRepository(owner=person)
297 self.factory.makeGitRepository(target=project)
298 collection = self.all_repositories.isPrivate().ownedBy(person)
299 self.assertEqual([repository], list(collection.getRepositories()))
300 collection = self.all_repositories.ownedBy(person).isPrivate()
301 self.assertEqual([repository], list(collection.getRepositories()))
302
303 def test_ownedByTeamMember_and_inProject(self):
304 # 'ownedBy' and 'inProject' can combine to form a collection that is
305 # restricted to repositories of a particular project owned by a
306 # particular person or team of which the person is a member.
307 person = self.factory.makePerson()
308 team = self.factory.makeTeam(members=[person])
309 project = self.factory.makeProduct()
310 repository = self.factory.makeGitRepository(
311 target=project, owner=person)
312 repository2 = self.factory.makeGitRepository(
313 target=project, owner=team)
314 self.factory.makeGitRepository(owner=person)
315 self.factory.makeGitRepository(target=project)
316 project_repositories = self.all_repositories.inProject(project)
317 collection = project_repositories.ownedByTeamMember(person)
318 self.assertContentEqual(
319 [repository, repository2], collection.getRepositories())
320 person_repositories = self.all_repositories.ownedByTeamMember(person)
321 collection = person_repositories.inProject(project)
322 self.assertContentEqual(
323 [repository, repository2], collection.getRepositories())
324
325 def test_in_distribution(self):
326 # 'inDistribution' returns a new collection that only has
327 # repositories that are package repositories associated with the
328 # distribution specified.
329 distro = self.factory.makeDistribution()
330 # Make two repositories in the same distribution, but different
331 # source packages.
332 dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
333 repository = self.factory.makeGitRepository(target=dsp)
334 dsp2 = self.factory.makeDistributionSourcePackage(distribution=distro)
335 repository2 = self.factory.makeGitRepository(target=dsp2)
336 # Another repository in a different distribution.
337 self.factory.makeGitRepository(
338 target=self.factory.makeDistributionSourcePackage())
339 # And a project repository.
340 self.factory.makeGitRepository()
341 collection = self.all_repositories.inDistribution(distro)
342 self.assertEqual(
343 sorted([repository, repository2]),
344 sorted(collection.getRepositories()))
345
346 def test_in_distribution_source_package(self):
347 # 'inDistributionSourcePackage' returns a new collection that only
348 # has repositories for the source package in the distribution.
349 distro = self.factory.makeDistribution()
350 dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
351 dsp_other_distro = self.factory.makeDistributionSourcePackage()
352 repository = self.factory.makeGitRepository(target=dsp)
353 repository2 = self.factory.makeGitRepository(target=dsp)
354 self.factory.makeGitRepository(target=dsp_other_distro)
355 self.factory.makeGitRepository()
356 collection = self.all_repositories.inDistributionSourcePackage(dsp)
357 self.assertEqual(
358 sorted([repository, repository2]),
359 sorted(collection.getRepositories()))
360
361 def test_withIds(self):
362 # 'withIds' returns a new collection that only has repositories with
363 # the given ids.
364 repository1 = self.factory.makeGitRepository()
365 repository2 = self.factory.makeGitRepository()
366 self.factory.makeGitRepository()
367 ids = [repository1.id, repository2.id]
368 collection = self.all_repositories.withIds(*ids)
369 self.assertEqual(
370 sorted([repository1, repository2]),
371 sorted(collection.getRepositories()))
372
373 def test_registeredBy(self):
374 # 'registeredBy' returns a new collection that only has repositories
375 # that were registered by the given user.
376 registrant = self.factory.makePerson()
377 repository = self.factory.makeGitRepository(
378 owner=registrant, registrant=registrant)
379 removeSecurityProxy(repository).owner = self.factory.makePerson()
380 self.factory.makeGitRepository()
381 collection = self.all_repositories.registeredBy(registrant)
382 self.assertEqual([repository], list(collection.getRepositories()))
383
384
385class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory):
386
387 layer = DatabaseFunctionalLayer
388
389 def setUp(self):
390 TestCaseWithFactory.setUp(self)
391 self.public_repository = self.factory.makeGitRepository(name=u'public')
392 self.private_repository = self.factory.makeGitRepository(
393 name=u'private', information_type=InformationType.USERDATA)
394 self.all_repositories = getUtility(IAllGitRepositories)
395
396 def test_all_repositories(self):
397 # Without the visibleByUser filter, all repositories are in the
398 # collection.
399 self.assertEqual(
400 sorted([self.public_repository, self.private_repository]),
401 sorted(self.all_repositories.getRepositories()))
402
403 def test_anonymous_sees_only_public(self):
404 # Anonymous users can see only public repositories.
405 repositories = self.all_repositories.visibleByUser(None)
406 self.assertEqual(
407 [self.public_repository], list(repositories.getRepositories()))
408
409 def test_visibility_then_project(self):
410 # We can apply other filters after applying the visibleByUser filter.
411 # Create another public repository.
412 self.factory.makeGitRepository()
413 repositories = self.all_repositories.visibleByUser(None).inProject(
414 self.public_repository.target).getRepositories()
415 self.assertEqual([self.public_repository], list(repositories))
416
417 def test_random_person_sees_only_public(self):
418 # Logged in users with no special permissions can see only public
419 # repositories.
420 person = self.factory.makePerson()
421 repositories = self.all_repositories.visibleByUser(person)
422 self.assertEqual(
423 [self.public_repository], list(repositories.getRepositories()))
424
425 def test_owner_sees_own_repositories(self):
426 # Users can always see the repositories that they own, as well as public
427 # repositories.
428 owner = removeSecurityProxy(self.private_repository).owner
429 repositories = self.all_repositories.visibleByUser(owner)
430 self.assertEqual(
431 sorted([self.public_repository, self.private_repository]),
432 sorted(repositories.getRepositories()))
433
434 def test_launchpad_services_sees_all(self):
435 # The LAUNCHPAD_SERVICES special user sees *everything*.
436 repositories = self.all_repositories.visibleByUser(LAUNCHPAD_SERVICES)
437 self.assertEqual(
438 sorted(self.all_repositories.getRepositories()),
439 sorted(repositories.getRepositories()))
440
441 def test_admins_see_all(self):
442 # Launchpad administrators see *everything*.
443 admin = self.factory.makePerson()
444 admin_team = removeSecurityProxy(
445 getUtility(ILaunchpadCelebrities).admin)
446 admin_team.addMember(admin, admin_team.teamowner)
447 repositories = self.all_repositories.visibleByUser(admin)
448 self.assertEqual(
449 sorted(self.all_repositories.getRepositories()),
450 sorted(repositories.getRepositories()))
451
452 def test_private_teams_see_own_private_personal_repositories(self):
453 # Private teams are given an access grant to see their private
454 # personal repositories.
455 team_owner = self.factory.makePerson()
456 team = self.factory.makeTeam(
457 visibility=PersonVisibility.PRIVATE,
458 membership_policy=TeamMembershipPolicy.MODERATED,
459 owner=team_owner)
460 with person_logged_in(team_owner):
461 personal_repository = self.factory.makeGitRepository(
462 owner=team, target=team,
463 information_type=InformationType.USERDATA)
464 # The team is automatically subscribed to the repository since
465 # they are the owner. We want to unsubscribe them so that they
466 # lose access conferred via subscription and rely instead on the
467 # APG.
468 # XXX cjwatson 2015-02-05: Uncomment this once
469 # GitRepositorySubscriptions exist.
470 #personal_repository.unsubscribe(team, team_owner, True)
471 # Make another personal repository the team can't see.
472 other_person = self.factory.makePerson()
473 self.factory.makeGitRepository(
474 owner=other_person, target=other_person,
475 information_type=InformationType.USERDATA)
476 repositories = self.all_repositories.visibleByUser(team)
477 self.assertEqual(
478 sorted([self.public_repository, personal_repository]),
479 sorted(repositories.getRepositories()))
480
481
482class TestSearch(TestCaseWithFactory):
483 """Tests for IGitCollection.search()."""
484
485 layer = DatabaseFunctionalLayer
486
487 def setUp(self):
488 TestCaseWithFactory.setUp(self)
489 self.collection = getUtility(IAllGitRepositories)
490
491 def test_exact_match_unique_name(self):
492 # If you search for a unique name of a repository that exists,
493 # you'll get a single result with a repository with that repository
494 # name.
495 repository = self.factory.makeGitRepository()
496 self.factory.makeGitRepository()
497 search_results = self.collection.search(repository.unique_name)
498 self.assertEqual([repository], list(search_results))
499
500 def test_unique_name_match_not_in_collection(self):
501 # If you search for a unique name of a repository that does not
502 # exist, you'll get an empty result set.
503 repository = self.factory.makeGitRepository()
504 collection = self.collection.inProject(self.factory.makeProduct())
505 search_results = collection.search(repository.unique_name)
506 self.assertEqual([], list(search_results))
507
508 def test_exact_match_launchpad_url(self):
509 # If you search for the Launchpad URL of a repository, and there is
510 # a repository with that URL, then you get a single result with that
511 # repository.
512 repository = self.factory.makeGitRepository()
513 self.factory.makeGitRepository()
514 search_results = self.collection.search(repository.getCodebrowseUrl())
515 self.assertEqual([repository], list(search_results))
516
517 def test_exact_match_with_lp_colon_url(self):
518 repository = self.factory.makeGitRepository()
519 lp_name = 'lp:' + repository.unique_name
520 search_results = self.collection.search(lp_name)
521 self.assertEqual([repository], list(search_results))
522
523 def test_exact_match_bad_url(self):
524 search_results = self.collection.search('http:hahafail')
525 self.assertEqual([], list(search_results))
526
527 def test_exact_match_git_identity(self):
528 # If you search for the Git identity of a repository, then you get a
529 # single result with that repository.
530 repository = self.factory.makeGitRepository()
531 self.factory.makeGitRepository()
532 search_results = self.collection.search(repository.git_identity)
533 self.assertEqual([repository], list(search_results))
534
535 def test_exact_match_git_identity_development_focus(self):
536 # If you search for the development focus and it is set, you get a
537 # single result with the development focus repository.
538 fooix = self.factory.makeProduct(name='fooix')
539 repository = self.factory.makeGitRepository(
540 owner=fooix.owner, target=fooix)
541 with person_logged_in(fooix.owner):
542 getUtility(IGitRepositorySet).setDefaultRepository(
543 fooix, repository)
544 self.factory.makeGitRepository()
545 search_results = self.collection.search('lp:fooix')
546 self.assertEqual([repository], list(search_results))
547
548 def test_bad_match_git_identity_development_focus(self):
549 # If you search for the development focus for a project where one
550 # isn't set, you get an empty search result.
551 fooix = self.factory.makeProduct(name='fooix')
552 self.factory.makeGitRepository(target=fooix)
553 self.factory.makeGitRepository()
554 search_results = self.collection.search('lp:fooix')
555 self.assertEqual([], list(search_results))
556
557 def test_bad_match_git_identity_no_project(self):
558 # If you search for the development focus for a project where one
559 # isn't set, you get an empty search result.
560 self.factory.makeGitRepository()
561 search_results = self.collection.search('lp:fooix')
562 self.assertEqual([], list(search_results))
563
564 def test_exact_match_url_trailing_slash(self):
565 # Sometimes, users are inconsiderately unaware of our arbitrary
566 # database restrictions and will put trailing slashes on their
567 # search queries. Rather bravely, we refuse to explode in this
568 # case.
569 repository = self.factory.makeGitRepository()
570 self.factory.makeGitRepository()
571 search_results = self.collection.search(
572 repository.getCodebrowseUrl() + '/')
573 self.assertEqual([repository], list(search_results))
574
575 def test_match_exact_repository_name(self):
576 # search returns all repositories with the same name as the search
577 # term.
578 repository1 = self.factory.makeGitRepository(name=u'foo')
579 repository2 = self.factory.makeGitRepository(name=u'foo')
580 self.factory.makeGitRepository()
581 search_results = self.collection.search('foo')
582 self.assertEqual(
583 sorted([repository1, repository2]), sorted(search_results))
584
585 def disabled_test_match_against_unique_name(self):
586 # XXX cjwatson 2015-02-06: Disabled until the URL format settles
587 # down.
588 repository = self.factory.makeGitRepository(name=u'fooa')
589 search_term = repository.target.name + '/foo'
590 search_results = self.collection.search(search_term)
591 self.assertEqual([repository], list(search_results))
592
593 def test_match_sub_repository_name(self):
594 # search returns all repositories which have a name of which the
595 # search term is a substring.
596 repository1 = self.factory.makeGitRepository(name=u'afoo')
597 repository2 = self.factory.makeGitRepository(name=u'foob')
598 self.factory.makeGitRepository()
599 search_results = self.collection.search('foo')
600 self.assertEqual(
601 sorted([repository1, repository2]), sorted(search_results))
602
603 def test_match_ignores_case(self):
604 repository = self.factory.makeGitRepository(name=u'foobar')
605 search_results = self.collection.search('FOOBAR')
606 self.assertEqual([repository], list(search_results))
607
608 def test_dont_match_project_if_in_project(self):
609 # If the container is restricted to the project, then we don't match
610 # the project name.
611 project = self.factory.makeProduct('foo')
612 repository1 = self.factory.makeGitRepository(
613 target=project, name=u'foo')
614 self.factory.makeGitRepository(target=project, name=u'bar')
615 search_results = self.collection.inProject(project).search('foo')
616 self.assertEqual([repository1], list(search_results))
617
618
619class TestGetTeamsWithRepositories(TestCaseWithFactory):
620 """Test the IGitCollection.getTeamsWithRepositories method."""
621
622 layer = DatabaseFunctionalLayer
623
624 def setUp(self):
625 TestCaseWithFactory.setUp(self)
626 self.all_repositories = getUtility(IAllGitRepositories)
627
628 def test_no_teams(self):
629 # If the user is not a member of any teams, there are no results,
630 # even if the person owns a repository themselves.
631 person = self.factory.makePerson()
632 self.factory.makeGitRepository(owner=person)
633 teams = list(self.all_repositories.getTeamsWithRepositories(person))
634 self.assertEqual([], teams)
635
636 def test_team_repositories(self):
637 # Return the teams that the user is in and that have repositories.
638 person = self.factory.makePerson()
639 team = self.factory.makeTeam(owner=person)
640 self.factory.makeGitRepository(owner=team)
641 # Make another team that person is in that has no repositories.
642 self.factory.makeTeam(owner=person)
643 teams = list(self.all_repositories.getTeamsWithRepositories(person))
644 self.assertEqual([team], teams)
645
646 def test_respects_restrictions(self):
647 # Create a team with repositories on a project, and another
648 # repository in a different namespace owned by a different team that
649 # the person is a member of. Restricting the collection will return
650 # just the teams that have repositories in that restricted
651 # collection.
652 person = self.factory.makePerson()
653 team1 = self.factory.makeTeam(owner=person)
654 repository = self.factory.makeGitRepository(owner=team1)
655 # Make another team that person is in that owns a repository in a
656 # different namespace to the namespace of the repository owned by team1.
657 team2 = self.factory.makeTeam(owner=person)
658 self.factory.makeGitRepository(owner=team2)
659 collection = self.all_repositories.inProject(repository.target)
660 teams = list(collection.getTeamsWithRepositories(person))
661 self.assertEqual([team1], teams)
662
663
664class TestGitCollectionOwnerCounts(TestCaseWithFactory):
665 """Test IGitCollection.ownerCounts."""
666
667 layer = DatabaseFunctionalLayer
668
669 def setUp(self):
670 TestCaseWithFactory.setUp(self)
671 self.all_repositories = getUtility(IAllGitRepositories)
672
673 def test_no_repositories(self):
674 # If there are no repositories, we should get zero counts for both.
675 person_count, team_count = self.all_repositories.ownerCounts()
676 self.assertEqual(0, person_count)
677 self.assertEqual(0, team_count)
678
679 def test_individual_repository_owners(self):
680 # Repositories owned by an individual are returned as the first part
681 # of the tuple.
682 self.factory.makeGitRepository()
683 self.factory.makeGitRepository()
684 person_count, team_count = self.all_repositories.ownerCounts()
685 self.assertEqual(2, person_count)
686 self.assertEqual(0, team_count)
687
688 def test_team_repository_owners(self):
689 # Repositories owned by teams are returned as the second part of the
690 # tuple.
691 self.factory.makeGitRepository(owner=self.factory.makeTeam())
692 self.factory.makeGitRepository(owner=self.factory.makeTeam())
693 person_count, team_count = self.all_repositories.ownerCounts()
694 self.assertEqual(0, person_count)
695 self.assertEqual(2, team_count)
696
697 def test_multiple_repositories_owned_counted_once(self):
698 # Confirming that a person that owns multiple repositories only gets
699 # counted once.
700 individual = self.factory.makePerson()
701 team = self.factory.makeTeam()
702 for owner in [individual, individual, team, team]:
703 self.factory.makeGitRepository(owner=owner)
704 person_count, team_count = self.all_repositories.ownerCounts()
705 self.assertEqual(1, person_count)
706 self.assertEqual(1, team_count)
707
708 def test_counts_limited_by_collection(self):
709 # For collections that are constrained in some way, we only get
710 # counts for the constrained collection.
711 r1 = self.factory.makeGitRepository()
712 project = r1.target
713 self.factory.makeGitRepository()
714 collection = self.all_repositories.inProject(project)
715 person_count, team_count = collection.ownerCounts()
716 self.assertEqual(1, person_count)