launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18355
[Merge] lp:~cjwatson/launchpad/git-mp-collection into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-mp-collection into lp:launchpad with lp:~cjwatson/launchpad/git-mp-create as a prerequisite.
Commit message:
Add merge proposal support to GitCollection.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1445017 in Launchpad itself: "Support for Launchpad Git merge proposals "
https://bugs.launchpad.net/launchpad/+bug/1445017
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-mp-collection/+merge/257145
Add merge proposal support to GitCollection. This will be needed to support various views shortly.
The non-trivial database handling in GitCollection consists almost entirely of undoing simplifications I applied to it relative to BranchCollection when we didn't need merge proposal support. Now that we do, we need all the asymmetric filter stuff as well.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-mp-collection into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/gitcollection.py'
--- lib/lp/code/interfaces/gitcollection.py 2015-04-15 18:34:25 +0000
+++ lib/lp/code/interfaces/gitcollection.py 2015-04-22 16:49:36 +0000
@@ -69,6 +69,42 @@
def getRepositoryIds():
"""Return a result set of all repository ids in this collection."""
+ # XXX cjwatson 2015-04-16: Add something like for_repositories or
+ # for_refs once we know exactly what we need.
+ def getMergeProposals(statuses=None, target_repository=None,
+ target_path=None, eager_load=False):
+ """Return a result set of merge proposals for the repositories in
+ this collection.
+
+ :param statuses: If specified, only return merge proposals with these
+ statuses. If not, return all merge proposals.
+ :param target_repository: If specified, only return merge proposals
+ that target the specified repository.
+ :param target_path: If specified, only return merge proposals that
+ target the specified path.
+ :param eager_load: If True, preloads all the related information for
+ merge proposals like PreviewDiffs and GitRepositories.
+ """
+
+ def getMergeProposalsForPerson(person, status=None):
+ """Proposals for `person`.
+
+ Return the proposals for repositories owned by `person` or where
+ `person` is reviewing or been asked to review.
+ """
+
+ def getMergeProposalsForReviewer(reviewer, status=None):
+ """Return a result set of merge proposals for the given reviewer.
+
+ That is, all merge proposals that 'reviewer' has voted on or has
+ been invited to vote on.
+
+ :param reviewer: An `IPerson` who is a reviewer.
+ :param status: An iterable of queue_status of the proposals to
+ return. If None is specified, all the proposals of all possible
+ states are returned.
+ """
+
def getTeamsWithRepositories(person):
"""Return the teams that person is a member of that have
repositories."""
@@ -117,6 +153,15 @@
"""Restrict the collection to repositories subscribed to by
'person'."""
+ def targetedBy(person, since=None):
+ """Restrict the collection to repositories targeted by person.
+
+ A repository is targeted by a person if that person has registered a
+ merge proposal with a reference in that repository as the target.
+
+ :param since: If supplied, ignore merge proposals before this date.
+ """
+
def visibleByUser(person):
"""Restrict the collection to repositories that person is allowed to
see."""
=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py 2015-04-22 16:49:35 +0000
+++ lib/lp/code/model/branchmergeproposal.py 2015-04-22 16:49:36 +0000
@@ -1054,16 +1054,19 @@
# Circular imports.
from lp.code.model.branch import Branch
from lp.code.model.branchcollection import GenericBranchCollection
+ from lp.code.model.gitcollection import GenericGitCollection
+ from lp.code.model.gitrepository import GitRepository
ids = set()
source_branch_ids = set()
+ source_git_repository_ids = set()
person_ids = set()
for mp in branch_merge_proposals:
ids.add(mp.id)
if mp.source_branchID is not None:
source_branch_ids.add(mp.source_branchID)
- # XXX cjwatson 2015-04-22: Implement for Git once GitCollection
- # supports MPs.
+ if mp.source_git_repositoryID is not None:
+ source_git_repository_ids.add(mp.source_git_repositoryID)
person_ids.add(mp.registrantID)
person_ids.add(mp.merge_reporterID)
@@ -1071,11 +1074,15 @@
Branch, branch_merge_proposals, (
"target_branchID", "prerequisite_branchID",
"source_branchID"))
+ repositories = load_related(
+ GitRepository, branch_merge_proposals, (
+ "target_git_repositoryID", "prerequisite_git_repositoryID",
+ "source_git_repositoryID"))
# The stacked on branches are used to check branch visibility.
GenericBranchCollection.preloadVisibleStackedOnBranches(
branches, user)
- if len(branches) == 0:
+ if len(branches) == 0 and len(repositories) == 0:
return
# Pre-load PreviewDiffs and Diffs.
@@ -1090,17 +1097,22 @@
cache = get_property_cache(previewdiff.branch_merge_proposal)
cache.preview_diff = previewdiff
- # Add source branch owners' to the list of pre-loaded persons.
+ # Add source branch/repository owners' to the list of pre-loaded
+ # persons.
person_ids.update(
branch.ownerID for branch in branches
if branch.id in source_branch_ids)
+ person_ids.update(
+ repository.owner_id for repository in repositories
+ if repository.id in source_git_repository_ids)
# Pre-load Person and ValidPersonCache.
list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
person_ids, need_validity=True))
- # Pre-load branches' data.
+ # Pre-load branches'/repositories' data.
GenericBranchCollection.preloadDataForBranches(branches)
+ GenericGitCollection.preloadDataForRepositories(repositories)
class BranchMergeProposalGetter:
=== modified file 'lib/lp/code/model/gitcollection.py'
--- lib/lp/code/model/gitcollection.py 2015-04-15 18:34:25 +0000
+++ lib/lp/code/model/gitcollection.py 2015-04-22 16:49:36 +0000
@@ -8,16 +8,25 @@
'GenericGitCollection',
]
+from functools import partial
+
from lazr.uri import (
InvalidURIError,
URI,
)
from storm.expr import (
+ And,
Count,
+ Desc,
In,
Join,
+ LeftJoin,
Select,
+ SQL,
+ With,
)
+from storm.info import ClassAlias
+from storm.store import EmptyResultSet
from zope.component import getUtility
from zope.interface import implements
@@ -31,14 +40,19 @@
from lp.code.interfaces.gitrepository import (
user_has_special_git_repository_access,
)
+from lp.code.model.branchmergeproposal import BranchMergeProposal
+from lp.code.model.codereviewcomment import CodeReviewComment
+from lp.code.model.codereviewvote import CodeReviewVoteReference
from lp.code.model.gitrepository import (
GitRepository,
get_git_repository_privacy_filter,
)
from lp.code.model.gitsubscription import GitSubscription
from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
+from lp.registry.model.distribution import Distribution
from lp.registry.model.person import Person
from lp.registry.model.product import Product
+from lp.registry.model.sourcepackagename import SourcePackageName
from lp.registry.model.teammembership import TeamParticipation
from lp.services.database.bulk import load_related
from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -51,7 +65,8 @@
implements(IGitCollection)
- def __init__(self, store=None, filter_expressions=None, tables=None):
+ def __init__(self, store=None, filter_expressions=None, tables=None,
+ asymmetric_filter_expressions=None, asymmetric_tables=None):
"""Construct a `GenericGitCollection`.
:param store: The store to look in for repositories. If not
@@ -63,6 +78,10 @@
: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.
+ :param asymmetric_filter_expressions: As per filter_expressions but
+ only applies to one side of reflexive joins.
+ :param asymmetric_tables: As per tables, for
+ asymmetric_filter_expressions.
"""
self._store = store
if filter_expressions is None:
@@ -71,6 +90,13 @@
if tables is None:
tables = {}
self._tables = tables
+ if asymmetric_filter_expressions is None:
+ asymmetric_filter_expressions = []
+ self._asymmetric_filter_expressions = list(
+ asymmetric_filter_expressions)
+ if asymmetric_tables is None:
+ asymmetric_tables = {}
+ self._asymmetric_tables = asymmetric_tables
self._user = None
def count(self):
@@ -102,8 +128,13 @@
else:
return self._store
- def _filterBy(self, expressions, table=None, join=None):
- """Return a subset of this collection, filtered by 'expressions'."""
+ def _filterBy(self, expressions, table=None, join=None, symmetric=True):
+ """Return a subset of this collection, filtered by 'expressions'.
+
+ :param symmetric: If True, this filter will apply to both sides of
+ merge proposal lookups and any other lookups that join
+ GitRepository back onto GitRepository.
+ """
# 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.
@@ -112,10 +143,23 @@
if expressions is None:
expressions = []
tables = self._tables.copy()
+ asymmetric_tables = self._asymmetric_tables.copy()
+ if symmetric:
+ if table is not None:
+ tables[table] = join
+ symmetric_expr = self._filter_expressions + expressions
+ asymmetric_expr = list(self._asymmetric_filter_expressions)
+ else:
+ if table is not None:
+ asymmetric_tables[table] = join
+ symmetric_expr = list(self._filter_expressions)
+ asymmetric_expr = (
+ self._asymmetric_filter_expressions + expressions)
if table is not None:
tables[table] = join
return self.__class__(
- self.store, self._filter_expressions + expressions, tables)
+ self.store, symmetric_expr, tables,
+ asymmetric_expr, asymmetric_tables)
def _getRepositorySelect(self, columns=(GitRepository.id,)):
"""Return a Storm 'Select' for columns in this collection."""
@@ -126,15 +170,25 @@
def _getRepositoryExpressions(self):
"""Return the where expressions for this collection."""
return (self._filter_expressions +
+ self._asymmetric_filter_expressions +
self._getRepositoryVisibilityExpression())
- def _getRepositoryVisibilityExpression(self):
+ def _getRepositoryVisibilityExpression(self, repository_class=None):
"""Return the where clauses for visibility."""
return []
+ @staticmethod
+ def preloadDataForRepositories(repositories):
+ """Preload repositories' cached associated targets."""
+ load_related(Distribution, repositories, ['distribution_id'])
+ load_related(SourcePackageName, repositories, ['sourcepackagename_id'])
+ load_related(Product, repositories, ['project_id'])
+
def getRepositories(self, find_expr=GitRepository, eager_load=False):
"""See `IGitCollection`."""
- tables = [GitRepository] + list(set(self._tables.values()))
+ all_tables = set(
+ self._tables.values() + self._asymmetric_tables.values())
+ tables = [GitRepository] + list(all_tables)
expressions = self._getRepositoryExpressions()
resultset = self.store.using(*tables).find(find_expr, *expressions)
@@ -165,6 +219,148 @@
return self.getRepositories(
find_expr=GitRepository.id).get_plain_result_set()
+ def getMergeProposals(self, statuses=None, target_repository=None,
+ target_path=None, merged_revision_ids=None,
+ eager_load=False):
+ """See `IGitCollection`."""
+ if merged_revision_ids is not None and not merged_revision_ids:
+ # We have an empty revision list, so we can shortcut.
+ return EmptyResultSet()
+ elif (self._asymmetric_filter_expressions or
+ target_repository is not None or
+ target_path is not None or
+ merged_revision_ids is not None):
+ return self._naiveGetMergeProposals(
+ statuses, target_repository, target_path, merged_revision_ids,
+ eager_load=eager_load)
+ else:
+ # When examining merge proposals in a scope, this is a moderately
+ # effective set of constrained queries. It is not effective when
+ # unscoped or when tight constraints on repositories are present.
+ return self._scopedGetMergeProposals(
+ statuses, eager_load=eager_load)
+
+ def _naiveGetMergeProposals(self, statuses=None, target_repository=None,
+ target_path=None, merged_revision_ids=None,
+ eager_load=False):
+ Target = ClassAlias(GitRepository, "target")
+ extra_tables = list(set(
+ self._tables.values() + self._asymmetric_tables.values()))
+ tables = [GitRepository] + extra_tables + [
+ Join(BranchMergeProposal, And(
+ GitRepository.id ==
+ BranchMergeProposal.source_git_repositoryID,
+ *(self._filter_expressions +
+ self._asymmetric_filter_expressions))),
+ Join(Target,
+ Target.id == BranchMergeProposal.target_git_repositoryID),
+ ]
+ expressions = self._getRepositoryVisibilityExpression()
+ expressions.extend(self._getRepositoryVisibilityExpression(Target))
+ if target_repository is not None:
+ expressions.append(
+ BranchMergeProposal.target_git_repository == target_repository)
+ if target_path is not None:
+ expressions.append(
+ BranchMergeProposal.target_git_path == target_path)
+ if merged_revision_ids is not None:
+ expressions.append(
+ BranchMergeProposal.merged_revision_id.is_in(
+ merged_revision_ids))
+ if statuses is not None:
+ expressions.append(
+ BranchMergeProposal.queue_status.is_in(statuses))
+ resultset = self.store.using(*tables).find(
+ BranchMergeProposal, *expressions)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
+ def _scopedGetMergeProposals(self, statuses, eager_load=False):
+ expressions = (
+ self._filter_expressions
+ + self._getRepositoryVisibilityExpression())
+ with_expr = With(
+ "candidate_repositories",
+ Select(
+ GitRepository.id,
+ tables=[GitRepository] + self._tables.values(),
+ where=And(*expressions) if expressions else True))
+ expressions = [SQL("""
+ source_git_repository IN
+ (SELECT id FROM candidate_repositories) AND
+ target_git_repository IN
+ (SELECT id FROM candidate_repositories)""")]
+ tables = [BranchMergeProposal]
+ if self._asymmetric_filter_expressions:
+ # Need to filter on GitRepository beyond the with constraints.
+ expressions += self._asymmetric_filter_expressions
+ expressions.append(
+ BranchMergeProposal.source_git_repositoryID ==
+ GitRepository.id)
+ tables.append(GitRepository)
+ tables.extend(self._asymmetric_tables.values())
+ if statuses is not None:
+ expressions.append(
+ BranchMergeProposal.queue_status.is_in(statuses))
+ resultset = self.store.with_(with_expr).using(*tables).find(
+ BranchMergeProposal, *expressions)
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
+ def getMergeProposalsForPerson(self, person, status=None,
+ eager_load=False):
+ """See `IGitCollection`."""
+ # We want to limit the proposals to those where the source repository
+ # is limited by the defined collection.
+ owned = self.ownedBy(person).getMergeProposals(status)
+ reviewing = self.getMergeProposalsForReviewer(person, status)
+ resultset = owned.union(reviewing)
+
+ if not eager_load:
+ return resultset
+ else:
+ loader = partial(
+ BranchMergeProposal.preloadDataForBMPs, user=self._user)
+ return DecoratedResultSet(resultset, pre_iter_hook=loader)
+
+ def getMergeProposalsForReviewer(self, reviewer, status=None):
+ """See `IGitCollection`."""
+ tables = [
+ BranchMergeProposal,
+ Join(CodeReviewVoteReference,
+ CodeReviewVoteReference.branch_merge_proposalID == \
+ BranchMergeProposal.id),
+ LeftJoin(CodeReviewComment,
+ CodeReviewVoteReference.commentID == CodeReviewComment.id)]
+
+ expressions = [
+ CodeReviewVoteReference.reviewer == reviewer,
+ BranchMergeProposal.source_git_repositoryID.is_in(
+ self._getRepositorySelect())]
+ visibility = self._getRepositoryVisibilityExpression()
+ if visibility:
+ expressions.append(
+ BranchMergeProposal.target_git_repositoryID.is_in(
+ Select(GitRepository.id, visibility)))
+ if status is not None:
+ expressions.append(
+ BranchMergeProposal.queue_status.is_in(status))
+ proposals = self.store.using(*tables).find(
+ BranchMergeProposal, *expressions)
+ # Apply sorting here as we can't do it in the browser code. We need to
+ # think carefully about the best places to do this, but not here nor
+ # now.
+ proposals.order_by(Desc(CodeReviewComment.vote))
+ return proposals
+
def getTeamsWithRepositories(self, person):
"""See `IGitCollection`."""
# This method doesn't entirely fit with the intent of the
@@ -221,18 +417,20 @@
def ownedBy(self, person):
"""See `IGitCollection`."""
- return self._filterBy([GitRepository.owner == person])
+ return self._filterBy([GitRepository.owner == person], symmetric=False)
def ownedByTeamMember(self, person):
"""See `IGitCollection`."""
subquery = Select(
TeamParticipation.teamID,
where=TeamParticipation.personID == person.id)
- return self._filterBy([In(GitRepository.owner_id, subquery)])
+ return self._filterBy(
+ [In(GitRepository.owner_id, subquery)], symmetric=False)
def registeredBy(self, person):
"""See `IGitCollection`."""
- return self._filterBy([GitRepository.registrant == person])
+ return self._filterBy(
+ [GitRepository.registrant == person], symmetric=False)
def _getExactMatch(self, term):
# Look up the repository by its URL, which handles both shortcuts
@@ -274,7 +472,21 @@
[GitSubscription.person == person],
table=GitSubscription,
join=Join(GitSubscription,
- GitSubscription.repository == GitRepository.id))
+ GitSubscription.repository == GitRepository.id),
+ symmetric=False)
+
+ def targetedBy(self, person, since=None):
+ """See `IGitCollection`."""
+ clauses = [BranchMergeProposal.registrant == person]
+ if since is not None:
+ clauses.append(BranchMergeProposal.date_created >= since)
+ return self._filterBy(
+ clauses,
+ table=BranchMergeProposal,
+ join=Join(
+ BranchMergeProposal,
+ BranchMergeProposal.target_git_repository == GitRepository.id),
+ symmetric=False)
def visibleByUser(self, person):
"""See `IGitCollection`."""
@@ -283,33 +495,46 @@
return self
if person is None:
return AnonymousGitCollection(
- self._store, self._filter_expressions, self._tables)
+ self._store, self._filter_expressions, self._tables,
+ self._asymmetric_filter_expressions, self._asymmetric_tables)
return VisibleGitCollection(
- person, self._store, self._filter_expressions, self._tables)
+ person, self._store, self._filter_expressions, self._tables,
+ self._asymmetric_filter_expressions, self._asymmetric_tables)
def withIds(self, *repository_ids):
"""See `IGitCollection`."""
- return self._filterBy([GitRepository.id.is_in(repository_ids)])
+ return self._filterBy(
+ [GitRepository.id.is_in(repository_ids)], symmetric=False)
class AnonymousGitCollection(GenericGitCollection):
"""Repository collection that only shows public repositories."""
- def _getRepositoryVisibilityExpression(self):
+ def _getRepositoryVisibilityExpression(self,
+ repository_class=GitRepository):
"""Return the where clauses for visibility."""
- return get_git_repository_privacy_filter(None)
+ return get_git_repository_privacy_filter(
+ None, repository_class=repository_class)
class VisibleGitCollection(GenericGitCollection):
"""A repository collection that has special logic for visibility."""
- def __init__(self, user, store=None, filter_expressions=None, tables=None):
+ def __init__(self, user, store=None, filter_expressions=None, tables=None,
+ asymmetric_filter_expressions=None, asymmetric_tables=None):
super(VisibleGitCollection, self).__init__(
- store=store, filter_expressions=filter_expressions, tables=tables)
+ store=store, filter_expressions=filter_expressions, tables=tables,
+ asymmetric_filter_expressions=asymmetric_filter_expressions,
+ asymmetric_tables=asymmetric_tables)
self._user = user
- def _filterBy(self, expressions, table=None, join=None):
- """Return a subset of this collection, filtered by 'expressions'."""
+ def _filterBy(self, expressions, table=None, join=None, symmetric=True):
+ """Return a subset of this collection, filtered by 'expressions'.
+
+ :param symmetric: If True this filter will apply to both sides of
+ merge proposal lookups and any other lookups that join
+ GitRepository back onto GitRepository.
+ """
# 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.
@@ -318,14 +543,30 @@
if expressions is None:
expressions = []
tables = self._tables.copy()
- if table is not None:
- tables[table] = join
+ asymmetric_tables = self._asymmetric_tables.copy()
+ if symmetric:
+ if table is not None:
+ tables[table] = join
+ symmetric_expr = self._filter_expressions + expressions
+ asymmetric_expr = list(self._asymmetric_filter_expressions)
+ else:
+ if table is not None:
+ asymmetric_tables[table] = join
+ symmetric_expr = list(self._filter_expressions)
+ asymmetric_expr = self._asymmetric_filter_expressions + expressions
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)
+ self._user, self.store, symmetric_expr, tables,
+ asymmetric_expr, asymmetric_tables)
+
+ def _getRepositoryVisibilityExpression(self,
+ repository_class=GitRepository):
+ """Return the where clauses for visibility.
+
+ :param repository_class: The GitRepository class to use - permits
+ using ClassAliases.
+ """
+ return get_git_repository_privacy_filter(
+ self._user, repository_class=repository_class)
def visibleByUser(self, person):
"""See `IGitCollection`."""
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-04-22 16:49:35 +0000
+++ lib/lp/code/model/gitrepository.py 2015-04-22 16:49:36 +0000
@@ -850,8 +850,8 @@
return []
-def get_git_repository_privacy_filter(user):
- public_filter = GitRepository.information_type.is_in(
+def get_git_repository_privacy_filter(user, repository_class=GitRepository):
+ public_filter = repository_class.information_type.is_in(
PUBLIC_INFORMATION_TYPES)
if user is None:
@@ -859,7 +859,7 @@
artifact_grant_query = Coalesce(
ArrayIntersects(
- SQL("GitRepository.access_grants"),
+ SQL("%s.access_grants" % repository_class.__storm_table__),
Select(
ArrayAgg(TeamParticipation.teamID),
tables=TeamParticipation,
@@ -868,7 +868,7 @@
policy_grant_query = Coalesce(
ArrayIntersects(
- Array(SQL("GitRepository.access_policy")),
+ Array(SQL("%s.access_policy" % repository_class.__storm_table__)),
Select(
ArrayAgg(AccessPolicyGrant.policy_id),
tables=(AccessPolicyGrant,
=== modified file 'lib/lp/code/model/tests/test_gitcollection.py'
--- lib/lp/code/model/tests/test_gitcollection.py 2015-04-15 18:34:25 +0000
+++ lib/lp/code/model/tests/test_gitcollection.py 2015-04-22 16:49:36 +0000
@@ -5,7 +5,18 @@
__metaclass__ = type
+from datetime import (
+ datetime,
+ timedelta,
+ )
+from operator import attrgetter
+
+import pytz
from testtools.matchers import Equals
+from storm.store import (
+ EmptyResultSet,
+ Store,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -13,6 +24,7 @@
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IService
from lp.code.enums import (
+ BranchMergeProposalStatus,
BranchSubscriptionDiffSize,
BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel,
@@ -42,7 +54,10 @@
StormStatementRecorder,
TestCaseWithFactory,
)
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ LaunchpadFunctionalLayer,
+ )
from lp.testing.matchers import HasQueryCount
@@ -406,6 +421,263 @@
collection = self.all_repositories.subscribedBy(subscriber)
self.assertEqual([repository], list(collection.getRepositories()))
+ def test_targetedBy(self):
+ # Only repositories that are merge targets are returned.
+ [target_ref] = self.factory.makeGitRefs()
+ registrant = self.factory.makePerson()
+ self.factory.makeBranchMergeProposalForGit(
+ target_ref=target_ref, registrant=registrant)
+ # And another not registered by registrant.
+ self.factory.makeBranchMergeProposalForGit()
+ collection = self.all_repositories.targetedBy(registrant)
+ self.assertEqual(
+ [target_ref.repository], list(collection.getRepositories()))
+
+ def test_targetedBy_since(self):
+ # Ignore proposals created before 'since'.
+ bmp = self.factory.makeBranchMergeProposalForGit()
+ date_created = self.factory.getUniqueDate()
+ removeSecurityProxy(bmp).date_created = date_created
+ registrant = bmp.registrant
+ repositories = self.all_repositories.targetedBy(
+ registrant, since=date_created)
+ self.assertEqual(
+ [bmp.target_git_repository], list(repositories.getRepositories()))
+ since = self.factory.getUniqueDate()
+ repositories = self.all_repositories.targetedBy(
+ registrant, since=since)
+ self.assertEqual([], list(repositories.getRepositories()))
+
+
+class TestBranchMergeProposals(TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ TestCaseWithFactory.setUp(self)
+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_empty_branch_merge_proposals(self):
+ proposals = self.all_repositories.getMergeProposals()
+ self.assertEqual([], list(proposals))
+
+ def test_empty_revisions_shortcut(self):
+ # If you explicitly pass an empty collection of revision numbers,
+ # the method shortcuts and gives you an empty result set. In this
+ # way, merged_revnos=None (the default) has a very different behaviour
+ # than merged_revnos=[]: the first is no restriction, while the second
+ # excludes everything.
+ self.factory.makeBranchMergeProposalForGit()
+ proposals = self.all_repositories.getMergeProposals(
+ merged_revision_ids=[])
+ self.assertEqual([], list(proposals))
+ self.assertIsInstance(proposals, EmptyResultSet)
+
+ def test_some_branch_merge_proposals(self):
+ mp = self.factory.makeBranchMergeProposalForGit()
+ proposals = self.all_repositories.getMergeProposals()
+ self.assertEqual([mp], list(proposals))
+
+ def test_just_owned_branch_merge_proposals(self):
+ # If the collection only includes branches owned by a person, the
+ # getMergeProposals() will only return merge proposals for source
+ # branches that are owned by that person.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ [ref1] = self.factory.makeGitRefs(target=project, owner=person)
+ [ref2] = self.factory.makeGitRefs(target=project, owner=person)
+ [ref3] = self.factory.makeGitRefs(target=project)
+ self.factory.makeGitRefs(target=project)
+ [target] = self.factory.makeGitRefs(target=project)
+ mp1 = self.factory.makeBranchMergeProposalForGit(
+ target_ref=target, source_ref=ref1)
+ mp2 = self.factory.makeBranchMergeProposalForGit(
+ target_ref=target, source_ref=ref2)
+ self.factory.makeBranchMergeProposalForGit(
+ target_ref=target, source_ref=ref3)
+ collection = self.all_repositories.ownedBy(person)
+ proposals = collection.getMergeProposals()
+ self.assertEqual(sorted([mp1, mp2]), sorted(proposals))
+
+ def test_preloading_for_previewdiff(self):
+ project = self.factory.makeProduct()
+ [target] = self.factory.makeGitRefs(target=project)
+ owner = self.factory.makePerson()
+ [ref1] = self.factory.makeGitRefs(target=project, owner=owner)
+ [ref2] = self.factory.makeGitRefs(target=project, owner=owner)
+ bmp1 = self.factory.makeBranchMergeProposalForGit(
+ target_ref=target, source_ref=ref1)
+ bmp2 = self.factory.makeBranchMergeProposalForGit(
+ target_ref=target, source_ref=ref2)
+ old_date = datetime.now(pytz.UTC) - timedelta(hours=1)
+ self.factory.makePreviewDiff(
+ merge_proposal=bmp1, date_created=old_date)
+ previewdiff1 = self.factory.makePreviewDiff(merge_proposal=bmp1)
+ self.factory.makePreviewDiff(
+ merge_proposal=bmp2, date_created=old_date)
+ previewdiff2 = self.factory.makePreviewDiff(merge_proposal=bmp2)
+ Store.of(bmp1).flush()
+ Store.of(bmp1).invalidate()
+ collection = self.all_repositories.ownedBy(owner)
+ [pre_bmp1, pre_bmp2] = sorted(
+ collection.getMergeProposals(eager_load=True),
+ key=attrgetter('id'))
+ with StormStatementRecorder() as recorder:
+ self.assertEqual(
+ removeSecurityProxy(pre_bmp1.preview_diff).id, previewdiff1.id)
+ self.assertEqual(
+ removeSecurityProxy(pre_bmp2.preview_diff).id, previewdiff2.id)
+ self.assertThat(recorder, HasQueryCount(Equals(0)))
+
+ def test_merge_proposals_in_project(self):
+ mp1 = self.factory.makeBranchMergeProposalForGit()
+ self.factory.makeBranchMergeProposalForGit()
+ project = mp1.source_git_ref.target
+ collection = self.all_repositories.inProject(project)
+ proposals = collection.getMergeProposals()
+ self.assertEqual([mp1], list(proposals))
+
+ def test_target_branch_private(self):
+ # The target branch must be in the branch collection, as must the
+ # source branch.
+ registrant = self.factory.makePerson()
+ mp1 = self.factory.makeBranchMergeProposalForGit(registrant=registrant)
+ naked_repository = removeSecurityProxy(mp1.target_git_repository)
+ naked_repository.transitionToInformationType(
+ InformationType.USERDATA, registrant, verify_policy=False)
+ collection = self.all_repositories.visibleByUser(None)
+ proposals = collection.getMergeProposals()
+ self.assertEqual([], list(proposals))
+
+ def test_status_restriction(self):
+ mp1 = self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.WORK_IN_PROGRESS)
+ mp2 = self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+ self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.CODE_APPROVED)
+ proposals = self.all_repositories.getMergeProposals(
+ [BranchMergeProposalStatus.WORK_IN_PROGRESS,
+ BranchMergeProposalStatus.NEEDS_REVIEW])
+ self.assertEqual(sorted([mp1, mp2]), sorted(proposals))
+
+ def test_status_restriction_with_project_filter(self):
+ # getMergeProposals returns the merge proposals with a particular
+ # status that are _inside_ the repository collection. mp1 is in the
+ # product with NEEDS_REVIEW, mp2 is outside of the project and mp3
+ # has an excluded status.
+ mp1 = self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+ self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+ project = mp1.source_git_ref.target
+ [ref1] = self.factory.makeGitRefs(target=project)
+ [ref2] = self.factory.makeGitRefs(target=project)
+ self.factory.makeBranchMergeProposalForGit(
+ target_ref=ref1, source_ref=ref2,
+ set_state=BranchMergeProposalStatus.CODE_APPROVED)
+ collection = self.all_repositories.inProject(project)
+ proposals = collection.getMergeProposals(
+ [BranchMergeProposalStatus.NEEDS_REVIEW])
+ self.assertEqual([mp1], list(proposals))
+
+ def test_specifying_target_repository(self):
+ # If the target_repository is specified but not the target_path,
+ # only merge proposals where that repository is the target are
+ # returned.
+ [ref1, ref2] = self.factory.makeGitRefs(
+ paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
+ mp1 = self.factory.makeBranchMergeProposalForGit(target_ref=ref1)
+ mp2 = self.factory.makeBranchMergeProposalForGit(target_ref=ref2)
+ self.factory.makeBranchMergeProposalForGit()
+ proposals = self.all_repositories.getMergeProposals(
+ target_repository=mp1.target_git_repository)
+ self.assertEqual(sorted([mp1, mp2]), sorted(proposals))
+
+ def test_specifying_target_ref(self):
+ # If the target_repository and target_path are specified, only merge
+ # proposals where that ref is the target are returned.
+ mp1 = self.factory.makeBranchMergeProposalForGit()
+ self.factory.makeBranchMergeProposalForGit()
+ proposals = self.all_repositories.getMergeProposals(
+ target_repository=mp1.target_git_repository,
+ target_path=mp1.target_git_path)
+ self.assertEqual([mp1], list(proposals))
+
+
+class TestBranchMergeProposalsForReviewer(TestCaseWithFactory):
+ """Tests for IGitCollection.getProposalsForReviewer()."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ # Use the admin user as we don't care about who can and can't call
+ # nominate reviewer in this test.
+ TestCaseWithFactory.setUp(self, 'admin@xxxxxxxxxxxxx')
+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
+ self.all_repositories = getUtility(IAllGitRepositories)
+
+ def test_getProposalsForReviewer(self):
+ reviewer = self.factory.makePerson()
+ proposal = self.factory.makeBranchMergeProposalForGit()
+ proposal.nominateReviewer(reviewer, reviewer)
+ self.factory.makeBranchMergeProposalForGit()
+ proposals = self.all_repositories.getMergeProposalsForReviewer(
+ reviewer)
+ self.assertEqual([proposal], list(proposals))
+
+ def test_getProposalsForReviewer_filter_status(self):
+ reviewer = self.factory.makePerson()
+ proposal1 = self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+ proposal1.nominateReviewer(reviewer, reviewer)
+ proposal2 = self.factory.makeBranchMergeProposalForGit(
+ set_state=BranchMergeProposalStatus.WORK_IN_PROGRESS)
+ proposal2.nominateReviewer(reviewer, reviewer)
+ proposals = self.all_repositories.getMergeProposalsForReviewer(
+ reviewer, [BranchMergeProposalStatus.NEEDS_REVIEW])
+ self.assertEqual([proposal1], list(proposals))
+
+ def test_getProposalsForReviewer_anonymous(self):
+ # Don't include proposals if the target branch is private for
+ # anonymous views.
+ reviewer = self.factory.makePerson()
+ [target_ref] = self.factory.makeGitRefs(
+ information_type=InformationType.USERDATA)
+ proposal = self.factory.makeBranchMergeProposalForGit(
+ target_ref=target_ref)
+ proposal.nominateReviewer(reviewer, reviewer)
+ proposals = self.all_repositories.visibleByUser(
+ None).getMergeProposalsForReviewer(reviewer)
+ self.assertEqual([], list(proposals))
+
+ def test_getProposalsForReviewer_anonymous_source_private(self):
+ # Don't include proposals if the source branch is private for
+ # anonymous views.
+ reviewer = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ [source_ref] = self.factory.makeGitRefs(
+ target=project, information_type=InformationType.USERDATA)
+ [target_ref] = self.factory.makeGitRefs(target=project)
+ proposal = self.factory.makeBranchMergeProposalForGit(
+ source_ref=source_ref, target_ref=target_ref)
+ proposal.nominateReviewer(reviewer, reviewer)
+ proposals = self.all_repositories.visibleByUser(
+ None).getMergeProposalsForReviewer(reviewer)
+ self.assertEqual([], list(proposals))
+
+ def test_getProposalsForReviewer_for_product(self):
+ reviewer = self.factory.makePerson()
+ proposal = self.factory.makeBranchMergeProposalForGit()
+ proposal.nominateReviewer(reviewer, reviewer)
+ proposal2 = self.factory.makeBranchMergeProposalForGit()
+ proposal2.nominateReviewer(reviewer, reviewer)
+ proposals = self.all_repositories.inProject(
+ proposal.merge_source.target).getMergeProposalsForReviewer(
+ reviewer)
+ self.assertEqual([proposal], list(proposals))
+
class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory):
Follow ups