launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17940
[Merge] lp:~cjwatson/launchpad/git-collection into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-collection into lp:launchpad with lp:~cjwatson/launchpad/git-lookup as a prerequisite.
Commit message:
Add support for collections of Git repositories.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1032731 in Launchpad itself: "Support for Launchpad-hosted Git repositories"
https://bugs.launchpad.net/launchpad/+bug/1032731
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-collection/+merge/250646
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-collection into lp:launchpad.
=== 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-23 16:14:41 +0000
@@ -0,0 +1,62 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Adapters for different objects to Git repository collections."""
+
+__metaclass__ = type
+__all__ = [
+ 'git_collection_for_distribution',
+ 'git_collection_for_distro_source_package',
+ 'git_collection_for_person',
+ 'git_collection_for_person_distro_source_package',
+ 'git_collection_for_person_product',
+ 'git_collection_for_project',
+ 'git_collection_for_project_group',
+ ]
+
+
+from zope.component import getUtility
+
+from lp.code.interfaces.gitcollection import IAllGitRepositories
+
+
+def git_collection_for_project(project):
+ """Adapt a product to a Git repository collection."""
+ return getUtility(IAllGitRepositories).inProject(project)
+
+
+def git_collection_for_project_group(project_group):
+ """Adapt a project group to a Git repository collection."""
+ return getUtility(IAllGitRepositories).inProjectGroup(project_group)
+
+
+def git_collection_for_distribution(distribution):
+ """Adapt a distribution to a Git repository collection."""
+ return getUtility(IAllGitRepositories).inDistribution(distribution)
+
+
+def git_collection_for_distro_source_package(distro_source_package):
+ """Adapt a distro_source_package to a Git repository collection."""
+ return getUtility(IAllGitRepositories).inDistributionSourcePackage(
+ distro_source_package)
+
+
+def git_collection_for_person(person):
+ """Adapt a person to a Git repository collection."""
+ return getUtility(IAllGitRepositories).ownedBy(person)
+
+
+def git_collection_for_person_product(person_product):
+ """Adapt a PersonProduct to a Git repository collection."""
+ collection = getUtility(IAllGitRepositories).ownedBy(person_product.person)
+ collection = collection.inProject(person_product.product)
+ return collection
+
+
+def git_collection_for_person_distro_source_package(person_dsp):
+ """Adapt a PersonDistributionSourcePackage to a Git repository
+ collection."""
+ collection = getUtility(IAllGitRepositories).ownedBy(person_dsp.person)
+ collection = collection.inDistributionSourcePackage(
+ person_dsp.distro_source_package)
+ return collection
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-23 16:14:41 +0000
+++ lib/lp/code/configure.zcml 2015-02-23 16:14:41 +0000
@@ -858,6 +858,55 @@
<allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
</securedutility>
+ <!-- GitCollection -->
+
+ <class class="lp.code.model.gitcollection.GenericGitCollection">
+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
+ </class>
+ <class class="lp.code.model.gitcollection.AnonymousGitCollection">
+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
+ </class>
+ <class class="lp.code.model.gitcollection.VisibleGitCollection">
+ <allow interface="lp.code.interfaces.gitcollection.IGitCollection"/>
+ </class>
+ <adapter
+ for="storm.store.Store"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.model.gitcollection.GenericGitCollection"/>
+ <adapter
+ for="lp.registry.interfaces.product.IProduct"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_project"/>
+ <adapter
+ for="lp.registry.interfaces.projectgroup.IProjectGroup"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_project_group"/>
+ <adapter
+ for="lp.registry.interfaces.distribution.IDistribution"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_distribution"/>
+ <adapter
+ for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_distro_source_package"/>
+ <adapter
+ for="lp.registry.interfaces.person.IPerson"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_person"/>
+ <adapter
+ for="lp.registry.interfaces.personproduct.IPersonProduct"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_person_product"/>
+ <adapter
+ for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
+ provides="lp.code.interfaces.gitcollection.IGitCollection"
+ factory="lp.code.adapters.gitcollection.git_collection_for_person_distro_source_package"/>
+ <securedutility
+ class="lp.code.model.gitcollection.GenericGitCollection"
+ provides="lp.code.interfaces.gitcollection.IAllGitRepositories">
+ <allow interface="lp.code.interfaces.gitcollection.IAllGitRepositories"/>
+ </securedutility>
+
<!-- Default Git repositories -->
<adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />
=== 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-23 16:14:41 +0000
@@ -0,0 +1,125 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A collection of Git repositories.
+
+See `IGitCollection` for more details.
+"""
+
+__metaclass__ = type
+__all__ = [
+ 'IAllGitRepositories',
+ 'IGitCollection',
+ 'InvalidGitFilter',
+ ]
+
+from zope.interface import Interface
+
+
+class InvalidGitFilter(Exception):
+ """Raised when an `IGitCollection` cannot apply the given filter."""
+
+
+class IGitCollection(Interface):
+ """A collection of Git repositories.
+
+ An `IGitCollection` is an immutable collection of Git repositories. It
+ has two kinds of methods: filter methods and query methods.
+
+ Query methods get information about the contents of the collection. See
+ `IGitCollection.count` and `IGitCollection.getRepositories`.
+
+ Filter methods return new IGitCollection instances that have some sort
+ of restriction. Examples include `ownedBy`, `visibleByUser` and
+ `inProject`.
+
+ Implementations of this interface are not 'content classes'. That is, they
+ do not correspond to a particular row in the database.
+
+ This interface is intended for use within Launchpad, not to be exported as
+ a public API.
+ """
+
+ def count():
+ """The number of repositories in this collection."""
+
+ def is_empty():
+ """Is this collection empty?"""
+
+ def ownerCounts():
+ """Return the number of different repository owners.
+
+ :return: a tuple (individual_count, team_count) containing the
+ number of individuals and teams that own repositories in this
+ collection.
+ """
+
+ def getRepositories(eager_load=False):
+ """Return a result set of all repositories in this collection.
+
+ The returned result set will also join across the specified tables
+ as defined by the arguments to this function. These extra tables
+ are joined specifically to allow the caller to sort on values not in
+ the GitRepository table itself.
+
+ :param eager_load: If True trigger eager loading of all the related
+ objects in the collection.
+ """
+
+ def getRepositoryIds():
+ """Return a result set of all repository ids in this collection."""
+
+ def getTeamsWithRepositories(person):
+ """Return the teams that person is a member of that have
+ repositories."""
+
+ def inProject(project):
+ """Restrict the collection to repositories in 'project'."""
+
+ def inProjectGroup(projectgroup):
+ """Restrict the collection to repositories in 'projectgroup'."""
+
+ def inDistribution(distribution):
+ """Restrict the collection to repositories in 'distribution'."""
+
+ def inDistributionSourcePackage(distro_source_package):
+ """Restrict to repositories in a package for a distribution."""
+
+ def isPersonal():
+ """Restrict the collection to personal repositories."""
+
+ def isPrivate():
+ """Restrict the collection to private repositories."""
+
+ def isExclusive():
+ """Restrict the collection to repositories owned by exclusive
+ people."""
+
+ def ownedBy(person):
+ """Restrict the collection to repositories owned by 'person'."""
+
+ def ownedByTeamMember(person):
+ """Restrict the collection to repositories owned by 'person' or a
+ team of which person is a member.
+ """
+
+ def registeredBy(person):
+ """Restrict the collection to repositories registered by 'person'."""
+
+ def search(term):
+ """Search the collection for repositories matching 'term'.
+
+ :param term: A string.
+ :return: A `ResultSet` of repositories that matched.
+ """
+
+ def visibleByUser(person):
+ """Restrict the collection to repositories that person is allowed to
+ see."""
+
+ def withIds(*repository_ids):
+ """Restrict the collection to repositories with the specified ids."""
+
+
+class IAllGitRepositories(IGitCollection):
+ """A `IGitCollection` representing all Git repositories in Launchpad."""
=== 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-23 16:14:41 +0000
@@ -0,0 +1,327 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Implementations of `IGitCollection`."""
+
+__metaclass__ = type
+__all__ = [
+ 'GenericGitCollection',
+ ]
+
+from lazr.uri import (
+ InvalidURIError,
+ URI,
+ )
+from storm.expr import (
+ Count,
+ In,
+ Join,
+ Select,
+ )
+from zope.component import getUtility
+from zope.interface import implements
+
+from lp.app.enums import PRIVATE_INFORMATION_TYPES
+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
+from lp.code.interfaces.gitcollection import (
+ IGitCollection,
+ InvalidGitFilter,
+ )
+from lp.code.interfaces.gitlookup import IGitLookup
+from lp.code.interfaces.gitrepository import (
+ user_has_special_git_repository_access,
+ )
+from lp.code.model.gitrepository import (
+ GitRepository,
+ get_git_repository_privacy_filter,
+ )
+from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
+from lp.registry.model.person import Person
+from lp.registry.model.product import Product
+from lp.registry.model.teammembership import TeamParticipation
+from lp.services.database.bulk import load_related
+from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.database.interfaces import IStore
+from lp.services.propertycache import get_property_cache
+
+
+class GenericGitCollection:
+ """See `IGitCollection`."""
+
+ implements(IGitCollection)
+
+ def __init__(self, store=None, filter_expressions=None, tables=None):
+ """Construct a `GenericGitCollection`.
+
+ :param store: The store to look in for repositories. If not
+ specified, use the default store.
+ :param filter_expressions: A list of Storm expressions to
+ restrict the repositories in the collection. If unspecified,
+ then there will be no restrictions on the result set. That is,
+ all repositories in the store will be in the collection.
+ :param tables: A dict of Storm tables to the Join expression. If an
+ expression in filter_expressions refers to a table, then that
+ table *must* be in this list.
+ """
+ self._store = store
+ if filter_expressions is None:
+ filter_expressions = []
+ self._filter_expressions = list(filter_expressions)
+ if tables is None:
+ tables = {}
+ self._tables = tables
+ self._user = None
+
+ def count(self):
+ """See `IGitCollection`."""
+ return self.getRepositories(eager_load=False).count()
+
+ def is_empty(self):
+ """See `IGitCollection`."""
+ return self.getRepositories(eager_load=False).is_empty()
+
+ def ownerCounts(self):
+ """See `IGitCollection`."""
+ is_team = Person.teamowner != None
+ owners = self._getRepositorySelect((GitRepository.owner_id,))
+ counts = dict(self.store.find(
+ (is_team, Count(Person.id)),
+ Person.id.is_in(owners)).group_by(is_team))
+ return (counts.get(False, 0), counts.get(True, 0))
+
+ @property
+ def store(self):
+ # Although you might think we could set the default value for store
+ # in the constructor, we can't. The IStore utility is not available
+ # at the time that the ZCML is parsed, which means we get an error
+ # if this code is in the constructor.
+ # -- JonathanLange 2009-02-17.
+ if self._store is None:
+ return IStore(GitRepository)
+ else:
+ return self._store
+
+ def _filterBy(self, expressions, table=None, join=None):
+ """Return a subset of this collection, filtered by 'expressions'."""
+ # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
+ # for explicit 'tables' by harnessing Storm's table inference system.
+ # See http://paste.ubuntu.com/118711/ for one way to do that.
+ if table is not None and join is None:
+ raise InvalidGitFilter("Cannot specify a table without a join.")
+ if expressions is None:
+ expressions = []
+ tables = self._tables.copy()
+ if table is not None:
+ tables[table] = join
+ return self.__class__(
+ self.store, self._filter_expressions + expressions, tables)
+
+ def _getRepositorySelect(self, columns=(GitRepository.id,)):
+ """Return a Storm 'Select' for columns in this collection."""
+ repositories = self.getRepositories(
+ eager_load=False, find_expr=columns)
+ return repositories.get_plain_result_set()._get_select()
+
+ def _getRepositoryExpressions(self):
+ """Return the where expressions for this collection."""
+ return (self._filter_expressions +
+ self._getRepositoryVisibilityExpression())
+
+ def _getRepositoryVisibilityExpression(self):
+ """Return the where clauses for visibility."""
+ return []
+
+ def getRepositories(self, find_expr=GitRepository, eager_load=False):
+ """See `IGitCollection`."""
+ tables = [GitRepository] + list(set(self._tables.values()))
+ expressions = self._getRepositoryExpressions()
+ resultset = self.store.using(*tables).find(find_expr, *expressions)
+
+ def do_eager_load(rows):
+ repository_ids = set(repository.id for repository in rows)
+ if not repository_ids:
+ return
+ load_related(Product, rows, ['project_id'])
+ # So far have only needed the persons for their canonical_url - no
+ # need for validity etc in the API call.
+ load_related(Person, rows, ['owner_id', 'registrant_id'])
+
+ def cache_permission(repository):
+ if self._user:
+ get_property_cache(repository)._known_viewers = set(
+ [self._user.id])
+ return repository
+
+ eager_load_hook = (
+ do_eager_load if eager_load and find_expr == GitRepository
+ else None)
+ return DecoratedResultSet(
+ resultset, pre_iter_hook=eager_load_hook,
+ result_decorator=cache_permission)
+
+ def getRepositoryIds(self):
+ """See `IGitCollection`."""
+ return self.getRepositories(
+ find_expr=GitRepository.id).get_plain_result_set()
+
+ def getTeamsWithRepositories(self, person):
+ """See `IGitCollection`."""
+ # This method doesn't entirely fit with the intent of the
+ # GitCollection conceptual model, but we're not quite sure how to
+ # fix it just yet.
+ repository_query = self._getRepositorySelect((GitRepository.owner_id,))
+ return self.store.find(
+ Person,
+ Person.id == TeamParticipation.teamID,
+ TeamParticipation.person == person,
+ TeamParticipation.team != person,
+ Person.id.is_in(repository_query))
+
+ def inProject(self, project):
+ """See `IGitCollection`."""
+ return self._filterBy([GitRepository.project == project])
+
+ def inProjectGroup(self, projectgroup):
+ """See `IGitCollection`."""
+ return self._filterBy(
+ [Product.projectgroup == projectgroup.id],
+ table=Product,
+ join=Join(Product, GitRepository.project == Product.id))
+
+ def inDistribution(self, distribution):
+ """See `IGitCollection`."""
+ return self._filterBy([GitRepository.distribution == distribution])
+
+ def inDistributionSourcePackage(self, distro_source_package):
+ """See `IGitCollection`."""
+ distribution = distro_source_package.distribution
+ sourcepackagename = distro_source_package.sourcepackagename
+ return self._filterBy(
+ [GitRepository.distribution == distribution,
+ GitRepository.sourcepackagename == sourcepackagename])
+
+ def isPersonal(self):
+ """See `IGitCollection`."""
+ return self._filterBy(
+ [GitRepository.project == None,
+ GitRepository.distribution == None])
+
+ def isPrivate(self):
+ """See `IGitCollection`."""
+ return self._filterBy(
+ [GitRepository.information_type.is_in(PRIVATE_INFORMATION_TYPES)])
+
+ def isExclusive(self):
+ """See `IGitCollection`."""
+ return self._filterBy(
+ [Person.membership_policy.is_in(EXCLUSIVE_TEAM_POLICY)],
+ table=Person,
+ join=Join(Person, GitRepository.owner_id == Person.id))
+
+ def ownedBy(self, person):
+ """See `IGitCollection`."""
+ return self._filterBy([GitRepository.owner == person])
+
+ def ownedByTeamMember(self, person):
+ """See `IGitCollection`."""
+ subquery = Select(
+ TeamParticipation.teamID,
+ where=TeamParticipation.personID == person.id)
+ return self._filterBy([In(GitRepository.owner_id, subquery)])
+
+ def registeredBy(self, person):
+ """See `IGitCollection`."""
+ return self._filterBy([GitRepository.registrant == person])
+
+ def _getExactMatch(self, term):
+ # Look up the repository by its URL, which handles both shortcuts
+ # and unique names.
+ repository = getUtility(IGitLookup).getByUrl(term)
+ if repository is not None:
+ return repository
+ # Fall back to searching by unique_name, stripping out the path if
+ # it's a URI.
+ try:
+ path = URI(term).path.strip("/")
+ except InvalidURIError:
+ path = term
+ return getUtility(IGitLookup).getByUniqueName(path)
+
+ def search(self, term):
+ """See `IGitCollection`."""
+ repository = self._getExactMatch(term)
+ if repository:
+ collection = self._filterBy([GitRepository.id == repository.id])
+ else:
+ term = unicode(term)
+ # Filter by name.
+ field = GitRepository.name
+ # Except if the term contains /, when we use unique_name.
+ # XXX cjwatson 2015-02-06: Disabled until the URL format settles
+ # down, at which point we can make GitRepository.unique_name a
+ # trigger-maintained column rather than a property.
+ #if '/' in term:
+ # field = GitRepository.unique_name
+ collection = self._filterBy(
+ [field.lower().contains_string(term.lower())])
+ return collection.getRepositories(eager_load=False).order_by(
+ GitRepository.name, GitRepository.id)
+
+ def visibleByUser(self, person):
+ """See `IGitCollection`."""
+ if (person == LAUNCHPAD_SERVICES or
+ user_has_special_git_repository_access(person)):
+ return self
+ if person is None:
+ return AnonymousGitCollection(
+ self._store, self._filter_expressions, self._tables)
+ return VisibleGitCollection(
+ person, self._store, self._filter_expressions, self._tables)
+
+ def withIds(self, *repository_ids):
+ """See `IGitCollection`."""
+ return self._filterBy([GitRepository.id.is_in(repository_ids)])
+
+
+class AnonymousGitCollection(GenericGitCollection):
+ """Repository collection that only shows public repositories."""
+
+ def _getRepositoryVisibilityExpression(self):
+ """Return the where clauses for visibility."""
+ return get_git_repository_privacy_filter(None)
+
+
+class VisibleGitCollection(GenericGitCollection):
+ """A repository collection that has special logic for visibility."""
+
+ def __init__(self, user, store=None, filter_expressions=None, tables=None):
+ super(VisibleGitCollection, self).__init__(
+ store=store, filter_expressions=filter_expressions, tables=tables)
+ self._user = user
+
+ def _filterBy(self, expressions, table=None, join=None):
+ """Return a subset of this collection, filtered by 'expressions'."""
+ # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
+ # for explicit 'tables' by harnessing Storm's table inference system.
+ # See http://paste.ubuntu.com/118711/ for one way to do that.
+ if table is not None and join is None:
+ raise InvalidGitFilter("Cannot specify a table without a join.")
+ if expressions is None:
+ expressions = []
+ tables = self._tables.copy()
+ if table is not None:
+ tables[table] = join
+ return self.__class__(
+ self._user, self.store, self._filter_expressions + expressions)
+
+ def _getRepositoryVisibilityExpression(self):
+ """Return the where clauses for visibility."""
+ return get_git_repository_privacy_filter(self._user)
+
+ def visibleByUser(self, person):
+ """See `IGitCollection`."""
+ if person == self._user:
+ return self
+ raise InvalidGitFilter(
+ "Cannot filter for Git repositories visible by user %r, already "
+ "filtering for %r" % (person, self._user))
=== 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-23 16:14:41 +0000
@@ -0,0 +1,715 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Git repository collections."""
+
+__metaclass__ = type
+
+from testtools.matchers import Equals
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.app.enums import InformationType
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.interfaces.services import IService
+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
+from lp.code.interfaces.gitcollection import (
+ IAllGitRepositories,
+ IGitCollection,
+ )
+from lp.code.model.gitcollection import GenericGitCollection
+from lp.code.model.gitrepository import GitRepository
+from lp.registry.enums import PersonVisibility
+from lp.registry.interfaces.person import TeamMembershipPolicy
+from lp.registry.model.persondistributionsourcepackage import (
+ PersonDistributionSourcePackage,
+ )
+from lp.registry.model.personproduct import PersonProduct
+from lp.services.database.interfaces import IStore
+from lp.services.webapp.publisher import canonical_url
+from lp.testing import (
+ person_logged_in,
+ StormStatementRecorder,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
+
+
+class TestGitCollectionAdaptation(TestCaseWithFactory):
+ """Check that certain objects can be adapted to a Git repository
+ collection."""
+
+ layer = DatabaseFunctionalLayer
+
+ def assertCollection(self, target):
+ self.assertIsNotNone(IGitCollection(target, None))
+
+ def test_project(self):
+ # A project can be adapted to a Git repository collection.
+ self.assertCollection(self.factory.makeProduct())
+
+ def test_project_group(self):
+ # A project group can be adapted to a Git repository collection.
+ self.assertCollection(self.factory.makeProject())
+
+ def test_distribution(self):
+ # A distribution can be adapted to a Git repository collection.
+ self.assertCollection(self.factory.makeDistribution())
+
+ def test_distribution_source_package(self):
+ # A distribution source package can be adapted to a Git repository
+ # collection.
+ self.assertCollection(self.factory.makeDistributionSourcePackage())
+
+ def test_person(self):
+ # A person can be adapted to a Git repository collection.
+ self.assertCollection(self.factory.makePerson())
+
+ def test_person_product(self):
+ # A PersonProduct can be adapted to a Git repository collection.
+ project = self.factory.makeProduct()
+ self.assertCollection(PersonProduct(project.owner, project))
+
+ def test_person_distribution_source_package(self):
+ # A PersonDistributionSourcePackage can be adapted to a Git
+ # repository collection.
+ dsp = self.factory.makeDistributionSourcePackage()
+ self.assertCollection(
+ PersonDistributionSourcePackage(dsp.distribution.owner, dsp))
+
+
+class TestGenericGitCollection(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGenericGitCollection, self).setUp()
+ self.store = IStore(GitRepository)
+
+ def test_provides_gitcollection(self):
+ # `GenericGitCollection` provides the `IGitCollection`
+ # interface.
+ self.assertProvides(GenericGitCollection(self.store), IGitCollection)
+
+ def test_getRepositories_no_filter_no_repositories(self):
+ # If no filter is specified, then the collection is of all
+ # repositories in Launchpad. By default, there are no repositories.
+ collection = GenericGitCollection(self.store)
+ self.assertEqual([], list(collection.getRepositories()))
+
+ def test_getRepositories_no_filter(self):
+ # If no filter is specified, then the collection is of all
+ # repositories in Launchpad.
+ collection = GenericGitCollection(self.store)
+ repository = self.factory.makeGitRepository()
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_getRepositories_project_filter(self):
+ # If the specified filter is for the repositories of a particular
+ # project, then the collection contains only repositories of that
+ # project.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ collection = GenericGitCollection(
+ self.store, [GitRepository.project == repository.target])
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_getRepositories_caches_viewers(self):
+ # getRepositories() caches the user as a known viewer so that
+ # repository.visibleByUser() does not have to hit the database.
+ collection = GenericGitCollection(self.store)
+ owner = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=owner, target=project,
+ information_type=InformationType.USERDATA)
+ someone = self.factory.makePerson()
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').ensureAccessGrants(
+ [someone], owner, gitrepositories=[repository],
+ ignore_permissions=True)
+ [repository] = list(collection.visibleByUser(
+ someone).getRepositories())
+ with StormStatementRecorder() as recorder:
+ self.assertTrue(repository.visibleByUser(someone))
+ self.assertThat(recorder, HasQueryCount(Equals(0)))
+
+ def test_getRepositoryIds(self):
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ collection = GenericGitCollection(
+ self.store, [GitRepository.project == repository.target])
+ self.assertEqual([repository.id], list(collection.getRepositoryIds()))
+
+ def test_count(self):
+ # The 'count' property of a collection is the number of elements in
+ # the collection.
+ collection = GenericGitCollection(self.store)
+ self.assertEqual(0, collection.count())
+ for i in range(3):
+ self.factory.makeGitRepository()
+ self.assertEqual(3, collection.count())
+
+ def test_count_respects_filter(self):
+ # If a collection is a subset of all possible repositories, then the
+ # count will be the size of that subset. That is, 'count' respects
+ # any filters that are applied.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ collection = GenericGitCollection(
+ self.store, [GitRepository.project == repository.target])
+ self.assertEqual(1, collection.count())
+
+
+class TestGitCollectionFilters(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_order_by_repository_name(self):
+ # The result of getRepositories() can be ordered by
+ # `GitRepository.name`, no matter what filters are applied.
+ aardvark = self.factory.makeProduct(name='aardvark')
+ badger = self.factory.makeProduct(name='badger')
+ repository_a = self.factory.makeGitRepository(target=aardvark)
+ repository_b = self.factory.makeGitRepository(target=badger)
+ person = self.factory.makePerson()
+ repository_c = self.factory.makeGitRepository(
+ owner=person, target=person)
+ self.assertEqual(
+ sorted([repository_a, repository_b, repository_c]),
+ sorted(self.all_repositories.getRepositories()
+ .order_by(GitRepository.name)))
+
+ def test_count_respects_visibleByUser_filter(self):
+ # IGitCollection.count() returns the number of repositories that
+ # getRepositories() yields, even when the visibleByUser filter is
+ # applied.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository(
+ information_type=InformationType.USERDATA)
+ collection = self.all_repositories.visibleByUser(repository.owner)
+ self.assertEqual(1, collection.getRepositories().count())
+ self.assertEqual(1, len(list(collection.getRepositories())))
+ self.assertEqual(1, collection.count())
+
+ def test_ownedBy(self):
+ # 'ownedBy' returns a new collection restricted to repositories
+ # owned by the given person.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.ownedBy(repository.owner)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_ownedByTeamMember(self):
+ # 'ownedBy' returns a new collection restricted to repositories
+ # owned by any team of which the given person is a member.
+ person = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[person])
+ repository = self.factory.makeGitRepository(owner=team)
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.ownedByTeamMember(person)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_in_project(self):
+ # 'inProject' returns a new collection restricted to repositories in
+ # the given project.
+ #
+ # NOTE: JonathanLange 2009-02-11: Maybe this should be a more
+ # generic method called 'onTarget' that takes a person, package or
+ # project.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.inProject(repository.target)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_inProjectGroup(self):
+ # 'inProjectGroup' returns a new collection restricted to
+ # repositories in the given project group.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ projectgroup = self.factory.makeProject()
+ removeSecurityProxy(repository.target).projectgroup = projectgroup
+ collection = self.all_repositories.inProjectGroup(projectgroup)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_isExclusive(self):
+ # 'isExclusive' is restricted to repositories owned by exclusive
+ # teams and users.
+ user = self.factory.makePerson()
+ team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.RESTRICTED)
+ other_team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.OPEN)
+ team_repository = self.factory.makeGitRepository(owner=team)
+ user_repository = self.factory.makeGitRepository(owner=user)
+ self.factory.makeGitRepository(owner=other_team)
+ collection = self.all_repositories.isExclusive()
+ self.assertContentEqual(
+ [team_repository, user_repository],
+ list(collection.getRepositories()))
+
+ def test_inProject_and_isExclusive(self):
+ # 'inProject' and 'isExclusive' can combine to form a collection
+ # that is restricted to repositories of a particular project owned
+ # by exclusive teams and users.
+ team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.RESTRICTED)
+ other_team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.OPEN)
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(target=project, owner=team)
+ self.factory.makeGitRepository(owner=team)
+ self.factory.makeGitRepository(target=project, owner=other_team)
+ collection = self.all_repositories.inProject(project).isExclusive()
+ self.assertEqual([repository], list(collection.getRepositories()))
+ collection = self.all_repositories.isExclusive().inProject(project)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_ownedBy_and_inProject(self):
+ # 'ownedBy' and 'inProject' can combine to form a collection that is
+ # restricted to repositories of a particular project owned by a
+ # particular person.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ target=project, owner=person)
+ self.factory.makeGitRepository(owner=person)
+ self.factory.makeGitRepository(target=project)
+ collection = self.all_repositories.inProject(project).ownedBy(person)
+ self.assertEqual([repository], list(collection.getRepositories()))
+ collection = self.all_repositories.ownedBy(person).inProject(project)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_ownedBy_and_isPrivate(self):
+ # 'ownedBy' and 'isPrivate' can combine to form a collection that is
+ # restricted to private repositories owned by a particular person.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ target=project, owner=person,
+ information_type=InformationType.USERDATA)
+ self.factory.makeGitRepository(owner=person)
+ self.factory.makeGitRepository(target=project)
+ collection = self.all_repositories.isPrivate().ownedBy(person)
+ self.assertEqual([repository], list(collection.getRepositories()))
+ collection = self.all_repositories.ownedBy(person).isPrivate()
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+ def test_ownedByTeamMember_and_inProject(self):
+ # 'ownedBy' and 'inProject' can combine to form a collection that is
+ # restricted to repositories of a particular project owned by a
+ # particular person or team of which the person is a member.
+ person = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[person])
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ target=project, owner=person)
+ repository2 = self.factory.makeGitRepository(
+ target=project, owner=team)
+ self.factory.makeGitRepository(owner=person)
+ self.factory.makeGitRepository(target=project)
+ project_repositories = self.all_repositories.inProject(project)
+ collection = project_repositories.ownedByTeamMember(person)
+ self.assertContentEqual(
+ [repository, repository2], collection.getRepositories())
+ person_repositories = self.all_repositories.ownedByTeamMember(person)
+ collection = person_repositories.inProject(project)
+ self.assertContentEqual(
+ [repository, repository2], collection.getRepositories())
+
+ def test_in_distribution(self):
+ # 'inDistribution' returns a new collection that only has
+ # repositories that are package repositories associated with the
+ # distribution specified.
+ distro = self.factory.makeDistribution()
+ # Make two repositories in the same distribution, but different
+ # source packages.
+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
+ repository = self.factory.makeGitRepository(target=dsp)
+ dsp2 = self.factory.makeDistributionSourcePackage(distribution=distro)
+ repository2 = self.factory.makeGitRepository(target=dsp2)
+ # Another repository in a different distribution.
+ self.factory.makeGitRepository(
+ target=self.factory.makeDistributionSourcePackage())
+ # And a project repository.
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.inDistribution(distro)
+ self.assertEqual(
+ sorted([repository, repository2]),
+ sorted(collection.getRepositories()))
+
+ def test_in_distribution_source_package(self):
+ # 'inDistributionSourcePackage' returns a new collection that only
+ # has repositories for the source package in the distribution.
+ distro = self.factory.makeDistribution()
+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
+ dsp_other_distro = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ repository2 = self.factory.makeGitRepository(target=dsp)
+ self.factory.makeGitRepository(target=dsp_other_distro)
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.inDistributionSourcePackage(dsp)
+ self.assertEqual(
+ sorted([repository, repository2]),
+ sorted(collection.getRepositories()))
+
+ def test_withIds(self):
+ # 'withIds' returns a new collection that only has repositories with
+ # the given ids.
+ repository1 = self.factory.makeGitRepository()
+ repository2 = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ ids = [repository1.id, repository2.id]
+ collection = self.all_repositories.withIds(*ids)
+ self.assertEqual(
+ sorted([repository1, repository2]),
+ sorted(collection.getRepositories()))
+
+ def test_registeredBy(self):
+ # 'registeredBy' returns a new collection that only has repositories
+ # that were registered by the given user.
+ registrant = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=registrant, registrant=registrant)
+ removeSecurityProxy(repository).owner = self.factory.makePerson()
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.registeredBy(registrant)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
+
+class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.public_repository = self.factory.makeGitRepository(name=u'public')
+ self.private_repository = self.factory.makeGitRepository(
+ name=u'private', information_type=InformationType.USERDATA)
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_all_repositories(self):
+ # Without the visibleByUser filter, all repositories are in the
+ # collection.
+ self.assertEqual(
+ sorted([self.public_repository, self.private_repository]),
+ sorted(self.all_repositories.getRepositories()))
+
+ def test_anonymous_sees_only_public(self):
+ # Anonymous users can see only public repositories.
+ repositories = self.all_repositories.visibleByUser(None)
+ self.assertEqual(
+ [self.public_repository], list(repositories.getRepositories()))
+
+ def test_visibility_then_project(self):
+ # We can apply other filters after applying the visibleByUser filter.
+ # Create another public repository.
+ self.factory.makeGitRepository()
+ repositories = self.all_repositories.visibleByUser(None).inProject(
+ self.public_repository.target).getRepositories()
+ self.assertEqual([self.public_repository], list(repositories))
+
+ def test_random_person_sees_only_public(self):
+ # Logged in users with no special permissions can see only public
+ # repositories.
+ person = self.factory.makePerson()
+ repositories = self.all_repositories.visibleByUser(person)
+ self.assertEqual(
+ [self.public_repository], list(repositories.getRepositories()))
+
+ def test_owner_sees_own_repositories(self):
+ # Users can always see the repositories that they own, as well as public
+ # repositories.
+ owner = removeSecurityProxy(self.private_repository).owner
+ repositories = self.all_repositories.visibleByUser(owner)
+ self.assertEqual(
+ sorted([self.public_repository, self.private_repository]),
+ sorted(repositories.getRepositories()))
+
+ def test_launchpad_services_sees_all(self):
+ # The LAUNCHPAD_SERVICES special user sees *everything*.
+ repositories = self.all_repositories.visibleByUser(LAUNCHPAD_SERVICES)
+ self.assertEqual(
+ sorted(self.all_repositories.getRepositories()),
+ sorted(repositories.getRepositories()))
+
+ def test_admins_see_all(self):
+ # Launchpad administrators see *everything*.
+ admin = self.factory.makePerson()
+ admin_team = removeSecurityProxy(
+ getUtility(ILaunchpadCelebrities).admin)
+ admin_team.addMember(admin, admin_team.teamowner)
+ repositories = self.all_repositories.visibleByUser(admin)
+ self.assertEqual(
+ sorted(self.all_repositories.getRepositories()),
+ sorted(repositories.getRepositories()))
+
+ def test_private_teams_see_own_private_personal_repositories(self):
+ # Private teams are given an access grant to see their private
+ # personal repositories.
+ team_owner = self.factory.makePerson()
+ team = self.factory.makeTeam(
+ visibility=PersonVisibility.PRIVATE,
+ membership_policy=TeamMembershipPolicy.MODERATED,
+ owner=team_owner)
+ with person_logged_in(team_owner):
+ personal_repository = self.factory.makeGitRepository(
+ owner=team, target=team,
+ information_type=InformationType.USERDATA)
+ # The team is automatically subscribed to the repository since
+ # they are the owner. We want to unsubscribe them so that they
+ # lose access conferred via subscription and rely instead on the
+ # APG.
+ # XXX cjwatson 2015-02-05: Uncomment this once
+ # GitRepositorySubscriptions exist.
+ #personal_repository.unsubscribe(team, team_owner, True)
+ # Make another personal repository the team can't see.
+ other_person = self.factory.makePerson()
+ self.factory.makeGitRepository(
+ owner=other_person, target=other_person,
+ information_type=InformationType.USERDATA)
+ repositories = self.all_repositories.visibleByUser(team)
+ self.assertEqual(
+ sorted([self.public_repository, personal_repository]),
+ sorted(repositories.getRepositories()))
+
+
+class TestSearch(TestCaseWithFactory):
+ """Tests for IGitCollection.search()."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.collection = getUtility(IAllGitRepositories)
+
+ def test_exact_match_unique_name(self):
+ # If you search for a unique name of a repository that exists,
+ # you'll get a single result with a repository with that repository
+ # name.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ search_results = self.collection.search(repository.unique_name)
+ self.assertEqual([repository], list(search_results))
+
+ def test_unique_name_match_not_in_collection(self):
+ # If you search for a unique name of a repository that does not
+ # exist, you'll get an empty result set.
+ repository = self.factory.makeGitRepository()
+ collection = self.collection.inProject(self.factory.makeProduct())
+ search_results = collection.search(repository.unique_name)
+ self.assertEqual([], list(search_results))
+
+ def test_exact_match_launchpad_url(self):
+ # If you search for the Launchpad URL of a repository, and there is
+ # a repository with that URL, then you get a single result with that
+ # repository.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ search_results = self.collection.search(repository.getCodebrowseUrl())
+ self.assertEqual([repository], list(search_results))
+
+ def test_exact_match_with_lp_colon_url(self):
+ repository = self.factory.makeGitRepository()
+ lp_name = 'lp:' + repository.unique_name
+ search_results = self.collection.search(lp_name)
+ self.assertEqual([repository], list(search_results))
+
+ def test_exact_match_bad_url(self):
+ search_results = self.collection.search('http:hahafail')
+ self.assertEqual([], list(search_results))
+
+ def test_exact_match_git_identity(self):
+ # If you search for the Git identity of a repository, then you get a
+ # single result with that repository.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ search_results = self.collection.search(repository.git_identity)
+ self.assertEqual([repository], list(search_results))
+
+ def test_exact_match_git_identity_development_focus(self):
+ # If you search for the development focus and it is set, you get a
+ # single result with the development focus repository.
+ fooix = self.factory.makeProduct(name='fooix')
+ repository = self.factory.makeGitRepository(
+ owner=fooix.owner, target=fooix)
+ with person_logged_in(fooix.owner):
+ fooix.setDefaultGitRepository(repository)
+ self.factory.makeGitRepository()
+ search_results = self.collection.search('lp:fooix')
+ self.assertEqual([repository], list(search_results))
+
+ def test_bad_match_git_identity_development_focus(self):
+ # If you search for the development focus for a project where one
+ # isn't set, you get an empty search result.
+ fooix = self.factory.makeProduct(name='fooix')
+ self.factory.makeGitRepository(target=fooix)
+ self.factory.makeGitRepository()
+ search_results = self.collection.search('lp:fooix')
+ self.assertEqual([], list(search_results))
+
+ def test_bad_match_git_identity_no_project(self):
+ # If you search for the development focus for a project where one
+ # isn't set, you get an empty search result.
+ self.factory.makeGitRepository()
+ search_results = self.collection.search('lp:fooix')
+ self.assertEqual([], list(search_results))
+
+ def test_exact_match_url_trailing_slash(self):
+ # Sometimes, users are inconsiderately unaware of our arbitrary
+ # database restrictions and will put trailing slashes on their
+ # search queries. Rather bravely, we refuse to explode in this
+ # case.
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ search_results = self.collection.search(
+ repository.getCodebrowseUrl() + '/')
+ self.assertEqual([repository], list(search_results))
+
+ def test_match_exact_repository_name(self):
+ # search returns all repositories with the same name as the search
+ # term.
+ repository1 = self.factory.makeGitRepository(name=u'foo')
+ repository2 = self.factory.makeGitRepository(name=u'foo')
+ self.factory.makeGitRepository()
+ search_results = self.collection.search('foo')
+ self.assertEqual(
+ sorted([repository1, repository2]), sorted(search_results))
+
+ def disabled_test_match_against_unique_name(self):
+ # XXX cjwatson 2015-02-06: Disabled until the URL format settles
+ # down.
+ repository = self.factory.makeGitRepository(name=u'fooa')
+ search_term = repository.target.name + '/foo'
+ search_results = self.collection.search(search_term)
+ self.assertEqual([repository], list(search_results))
+
+ def test_match_sub_repository_name(self):
+ # search returns all repositories which have a name of which the
+ # search term is a substring.
+ repository1 = self.factory.makeGitRepository(name=u'afoo')
+ repository2 = self.factory.makeGitRepository(name=u'foob')
+ self.factory.makeGitRepository()
+ search_results = self.collection.search('foo')
+ self.assertEqual(
+ sorted([repository1, repository2]), sorted(search_results))
+
+ def test_match_ignores_case(self):
+ repository = self.factory.makeGitRepository(name=u'foobar')
+ search_results = self.collection.search('FOOBAR')
+ self.assertEqual([repository], list(search_results))
+
+ def test_dont_match_project_if_in_project(self):
+ # If the container is restricted to the project, then we don't match
+ # the project name.
+ project = self.factory.makeProduct('foo')
+ repository1 = self.factory.makeGitRepository(
+ target=project, name=u'foo')
+ self.factory.makeGitRepository(target=project, name=u'bar')
+ search_results = self.collection.inProject(project).search('foo')
+ self.assertEqual([repository1], list(search_results))
+
+
+class TestGetTeamsWithRepositories(TestCaseWithFactory):
+ """Test the IGitCollection.getTeamsWithRepositories method."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_no_teams(self):
+ # If the user is not a member of any teams, there are no results,
+ # even if the person owns a repository themselves.
+ person = self.factory.makePerson()
+ self.factory.makeGitRepository(owner=person)
+ teams = list(self.all_repositories.getTeamsWithRepositories(person))
+ self.assertEqual([], teams)
+
+ def test_team_repositories(self):
+ # Return the teams that the user is in and that have repositories.
+ person = self.factory.makePerson()
+ team = self.factory.makeTeam(owner=person)
+ self.factory.makeGitRepository(owner=team)
+ # Make another team that person is in that has no repositories.
+ self.factory.makeTeam(owner=person)
+ teams = list(self.all_repositories.getTeamsWithRepositories(person))
+ self.assertEqual([team], teams)
+
+ def test_respects_restrictions(self):
+ # Create a team with repositories on a project, and another
+ # repository in a different namespace owned by a different team that
+ # the person is a member of. Restricting the collection will return
+ # just the teams that have repositories in that restricted
+ # collection.
+ person = self.factory.makePerson()
+ team1 = self.factory.makeTeam(owner=person)
+ repository = self.factory.makeGitRepository(owner=team1)
+ # Make another team that person is in that owns a repository in a
+ # different namespace to the namespace of the repository owned by team1.
+ team2 = self.factory.makeTeam(owner=person)
+ self.factory.makeGitRepository(owner=team2)
+ collection = self.all_repositories.inProject(repository.target)
+ teams = list(collection.getTeamsWithRepositories(person))
+ self.assertEqual([team1], teams)
+
+
+class TestGitCollectionOwnerCounts(TestCaseWithFactory):
+ """Test IGitCollection.ownerCounts."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_no_repositories(self):
+ # If there are no repositories, we should get zero counts for both.
+ person_count, team_count = self.all_repositories.ownerCounts()
+ self.assertEqual(0, person_count)
+ self.assertEqual(0, team_count)
+
+ def test_individual_repository_owners(self):
+ # Repositories owned by an individual are returned as the first part
+ # of the tuple.
+ self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ person_count, team_count = self.all_repositories.ownerCounts()
+ self.assertEqual(2, person_count)
+ self.assertEqual(0, team_count)
+
+ def test_team_repository_owners(self):
+ # Repositories owned by teams are returned as the second part of the
+ # tuple.
+ self.factory.makeGitRepository(owner=self.factory.makeTeam())
+ self.factory.makeGitRepository(owner=self.factory.makeTeam())
+ person_count, team_count = self.all_repositories.ownerCounts()
+ self.assertEqual(0, person_count)
+ self.assertEqual(2, team_count)
+
+ def test_multiple_repositories_owned_counted_once(self):
+ # Confirming that a person that owns multiple repositories only gets
+ # counted once.
+ individual = self.factory.makePerson()
+ team = self.factory.makeTeam()
+ for owner in [individual, individual, team, team]:
+ self.factory.makeGitRepository(owner=owner)
+ person_count, team_count = self.all_repositories.ownerCounts()
+ self.assertEqual(1, person_count)
+ self.assertEqual(1, team_count)
+
+ def test_counts_limited_by_collection(self):
+ # For collections that are constrained in some way, we only get
+ # counts for the constrained collection.
+ r1 = self.factory.makeGitRepository()
+ project = r1.target
+ self.factory.makeGitRepository()
+ collection = self.all_repositories.inProject(project)
+ person_count, team_count = collection.ownerCounts()
+ self.assertEqual(1, person_count)
Follow ups