Merge lp:~cjwatson/launchpad/git-collection into lp:launchpad
- git-collection
- Merge into devel
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 |
Related bugs: |
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
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) |