← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/dependent-merges into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/dependent-merges into lp:launchpad.

Commit message:
Add Branch:+dependent-merges and GitRef:+dependent-merges views.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #496056 in Launchpad itself: "Dependent branch list is empty"
  https://bugs.launchpad.net/launchpad/+bug/496056

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/dependent-merges/+merge/274132

Add Branch:+dependent-merges and GitRef:+dependent-merges views, making the "<n> branch/branches dependent on this one" links useful again.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/dependent-merges into lp:launchpad.
=== modified file 'lib/lp/code/browser/branchmergeproposallisting.py'
--- lib/lp/code/browser/branchmergeproposallisting.py	2015-10-04 16:38:18 +0000
+++ lib/lp/code/browser/branchmergeproposallisting.py	2015-10-12 13:06:46 +0000
@@ -8,6 +8,7 @@
 __all__ = [
     'ActiveReviewsView',
     'BranchActiveReviewsView',
+    'BranchDependentMergesView',
     'BranchMergeProposalListingItem',
     'BranchMergeProposalListingView',
     'PersonActiveReviewsView',
@@ -264,6 +265,15 @@
                 self.context.displayname, self.status_value.title)
 
 
+class BranchDependentMergesView(BranchMergeProposalListingView):
+    """Branch merge proposals that list this branch as a prerequisite."""
+
+    def getVisibleProposalsForUser(self):
+        """See `BranchMergeProposalListingView`."""
+        return self.context.getDependentMergeProposals(
+            self.status_filter, self.user, eager_load=True)
+
+
 class ActiveReviewsView(BranchMergeProposalListingView):
     """Branch merge proposals for a context that are needing review."""
 

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2015-09-18 15:41:08 +0000
+++ lib/lp/code/browser/configure.zcml	2015-10-12 13:06:46 +0000
@@ -503,6 +503,12 @@
         name="+activereviews"
         template="../templates/active-reviews.pt"/>
     <browser:page
+        for="lp.code.interfaces.branch.IBranch"
+        class="lp.code.browser.branchmergeproposallisting.BranchDependentMergesView"
+        permission="zope.Public"
+        name="+dependent-merges"
+        template="../templates/branchmergeproposal-generic-listing.pt"/>
+    <browser:page
         for="lp.code.interfaces.branch.IBranchBatchNavigator"
         name="+branch-listing"
         template="../templates/branch-listing.pt"
@@ -909,6 +915,12 @@
         permission="zope.Public"
         name="+activereviews"
         template="../templates/active-reviews.pt"/>
+    <browser:page
+        for="lp.code.interfaces.gitref.IGitRef"
+        class="lp.code.browser.branchmergeproposallisting.BranchDependentMergesView"
+        permission="zope.Public"
+        name="+dependent-merges"
+        template="../templates/branchmergeproposal-generic-listing.pt"/>
     <browser:menus
         classes="GitRefContextMenu"
         module="lp.code.browser.gitref"/>

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2015-09-30 10:25:58 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2015-10-12 13:06:46 +0000
@@ -364,6 +364,51 @@
         self.assertIn("has no merge proposals", browser.contents)
 
 
+class DependentMergesTestMixin(BranchMergeProposalListingTestMixin):
+
+    view_name = '+dependent-merges'
+
+    def makeBzrMergeProposal(self):
+        information_type = (
+            InformationType.USERDATA if self.supports_privacy else None)
+        prerequisite = self.bzr_branch
+        if prerequisite is None:
+            prerequisite = self.factory.makeBranch(
+                target=self.bzr_target, information_type=information_type)
+        target = self.factory.makeBranch(
+            target=self.bzr_target, information_type=information_type)
+        source = self.factory.makeBranch(
+            target=self.bzr_target, owner=self.owner,
+            information_type=information_type)
+        return self.factory.makeBranchMergeProposal(
+            source_branch=source, target_branch=target,
+            prerequisite_branch=prerequisite,
+            set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+
+    def makeGitMergeProposal(self):
+        information_type = (
+            InformationType.USERDATA if self.supports_privacy else None)
+        prerequisite = self.git_ref
+        if prerequisite is None:
+            [prerequisite] = self.factory.makeGitRefs(
+                target=self.git_target, information_type=information_type)
+        [target] = self.factory.makeGitRefs(
+            target=self.git_target, information_type=information_type)
+        [source] = self.factory.makeGitRefs(
+            target=self.git_target, owner=self.owner,
+            information_type=information_type)
+        return self.factory.makeBranchMergeProposalForGit(
+            source_ref=source, target_ref=target,
+            prerequisite_ref=prerequisite,
+            set_state=BranchMergeProposalStatus.NEEDS_REVIEW)
+
+    def test_none(self):
+        """The dependent merges view should be enabled for the target."""
+        browser = self.getViewBrowser(
+            self.context, self.view_name, rootsite='code', user=self.user)
+        self.assertIn("has no merge proposals", browser.contents)
+
+
 class ActiveReviewsTestMixin(BranchMergeProposalListingTestMixin):
 
     view_name = '+activereviews'
@@ -524,6 +569,18 @@
     pass
 
 
+class TestBranchDependentMerges(
+        BranchContextMixin, DependentMergesTestMixin, BrowserTestCase):
+
+    pass
+
+
+class TestGitRefDependentMerges(
+        GitRefContextMixin, DependentMergesTestMixin, BrowserTestCase):
+
+    pass
+
+
 class TestProductActiveReviews(
         ProductContextMixin, ActiveReviewsTestMixin, BrowserTestCase):
 

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2015-09-29 15:54:05 +0000
+++ lib/lp/code/interfaces/branch.py	2015-10-12 13:06:46 +0000
@@ -661,6 +661,10 @@
                           merged_revnos=None, eager_load=False):
         """Return matching BranchMergeProposals."""
 
+    def getDependentMergeProposals(status=None, visible_by_user=None,
+                                   eager_load=False):
+        """Return BranchMergeProposals dependent on merging this branch."""
+
     def getMergeProposalByID(id):
         """Return this branch's merge proposal with this id, or None."""
 

=== modified file 'lib/lp/code/interfaces/branchcollection.py'
--- lib/lp/code/interfaces/branchcollection.py	2015-09-17 11:12:42 +0000
+++ lib/lp/code/interfaces/branchcollection.py	2015-10-12 13:06:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """A collection of branches.
@@ -73,8 +73,8 @@
     def getBranchIds():
         """Return a result set of all branch ids in this collection."""
 
-    def getMergeProposals(statuses=None, for_branches=None,
-                          target_branch=None, eager_load=False):
+    def getMergeProposals(statuses=None, for_branches=None, target_branch=None,
+                          prerequisite_branch=None, eager_load=False):
         """Return a result set of merge proposals for the branches in this
         collection.
 
@@ -85,6 +85,8 @@
             branch is one of the branches specified.
         :param target_branch: If specified, only return merge proposals
             that target the specified branch.
+        :param prerequisite_branch: If specified, only return merge proposals
+            that require the specified branch to be merged first.
         :param eager_load: If True, preloads all the related information for
             merge proposals like PreviewDiffs and Branches.
         """

=== modified file 'lib/lp/code/interfaces/gitcollection.py'
--- lib/lp/code/interfaces/gitcollection.py	2015-09-21 09:59:13 +0000
+++ lib/lp/code/interfaces/gitcollection.py	2015-10-12 13:06:46 +0000
@@ -74,7 +74,8 @@
     # 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):
+                          target_path=None, prerequisite_repository=None,
+                          prerequisite_path=None, eager_load=False):
         """Return a result set of merge proposals for the repositories in
         this collection.
 
@@ -84,6 +85,12 @@
             that target the specified repository.
         :param target_path: If specified, only return merge proposals that
             target the specified path.
+        :param prerequisite_repository: If specified, only return merge
+            proposals that require a reference in the specified repository to
+            be merged first.
+        :param prerequisite_path: If specified, only return merge proposals
+            that require a reference with the specified path to be merged
+            first.
         :param eager_load: If True, preloads all the related information for
             merge proposals like PreviewDiffs and GitRepositories.
         """

=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py	2015-09-30 08:58:44 +0000
+++ lib/lp/code/interfaces/gitref.py	2015-10-12 13:06:46 +0000
@@ -305,6 +305,10 @@
                           merged_revision_ids=None, eager_load=False):
         """Return matching BranchMergeProposals."""
 
+    def getDependentMergeProposals(status=None, visible_by_user=None,
+                                   eager_load=False):
+        """Return BranchMergeProposals dependent on merging this reference."""
+
     def getMergeProposalByID(id):
         """Return this reference's merge proposal with this id, or None."""
 

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2015-10-06 16:08:27 +0000
+++ lib/lp/code/model/branch.py	2015-10-12 13:06:46 +0000
@@ -526,6 +526,19 @@
             status, target_branch=self, merged_revnos=merged_revnos,
             eager_load=eager_load)
 
+    def getDependentMergeProposals(self, status=None, visible_by_user=None,
+                                   eager_load=False):
+        """See `IBranch`."""
+        if not status:
+            status = (
+                BranchMergeProposalStatus.CODE_APPROVED,
+                BranchMergeProposalStatus.NEEDS_REVIEW,
+                BranchMergeProposalStatus.WORK_IN_PROGRESS)
+
+        collection = getUtility(IAllBranches).visibleByUser(visible_by_user)
+        return collection.getMergeProposals(
+            status, prerequisite_branch=self, eager_load=eager_load)
+
     def getMergeProposalByID(self, id):
         """See `IBranch`."""
         return Store.of(self).find(

=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py	2015-09-17 11:12:42 +0000
+++ lib/lp/code/model/branchcollection.py	2015-10-12 13:06:46 +0000
@@ -307,8 +307,9 @@
         return self.getBranches(find_expr=Branch.id).get_plain_result_set()
 
     def getMergeProposals(self, statuses=None, for_branches=None,
-                          target_branch=None, merged_revnos=None,
-                          merged_revision=None, eager_load=False):
+                          target_branch=None, prerequisite_branch=None,
+                          merged_revnos=None, merged_revision=None,
+                          eager_load=False):
         """See `IBranchCollection`."""
         if for_branches is not None and not for_branches:
             # We have an empty branches list, so we can shortcut.
@@ -319,11 +320,12 @@
         elif (self._asymmetric_filter_expressions or
             for_branches is not None or
             target_branch is not None or
+            prerequisite_branch is not None or
             merged_revnos is not None or
             merged_revision is not None):
             return self._naiveGetMergeProposals(
-                statuses, for_branches, target_branch, merged_revnos,
-                merged_revision, eager_load=eager_load)
+                statuses, for_branches, target_branch, prerequisite_branch,
+                merged_revnos, merged_revision, 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
@@ -332,8 +334,9 @@
                 statuses, eager_load=eager_load)
 
     def _naiveGetMergeProposals(self, statuses=None, for_branches=None,
-                                target_branch=None, merged_revnos=None,
-                                merged_revision=None, eager_load=False):
+                                target_branch=None, prerequisite_branch=None,
+                                merged_revnos=None, merged_revision=None,
+                                eager_load=False):
         Target = ClassAlias(Branch, "target")
         extra_tables = list(set(
             self._tables.values() + self._asymmetric_tables.values()))
@@ -353,6 +356,9 @@
         if target_branch is not None:
             expressions.append(
                 BranchMergeProposal.target_branch == target_branch)
+        if prerequisite_branch is not None:
+            expressions.append(
+                BranchMergeProposal.prerequisite_branch == prerequisite_branch)
         if merged_revnos is not None:
             expressions.append(
                 BranchMergeProposal.merged_revno.is_in(merged_revnos))

=== modified file 'lib/lp/code/model/gitcollection.py'
--- lib/lp/code/model/gitcollection.py	2015-09-21 09:59:13 +0000
+++ lib/lp/code/model/gitcollection.py	2015-10-12 13:06:46 +0000
@@ -45,8 +45,8 @@
 from lp.code.model.codereviewcomment import CodeReviewComment
 from lp.code.model.codereviewvote import CodeReviewVoteReference
 from lp.code.model.gitrepository import (
+    get_git_repository_privacy_filter,
     GitRepository,
-    get_git_repository_privacy_filter,
     )
 from lp.code.model.gitsubscription import GitSubscription
 from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
@@ -237,7 +237,8 @@
             find_expr=GitRepository.id).get_plain_result_set()
 
     def getMergeProposals(self, statuses=None, target_repository=None,
-                          target_path=None, merged_revision_ids=None,
+                          target_path=None, prerequisite_repository=None,
+                          prerequisite_path=None, merged_revision_ids=None,
                           eager_load=False):
         """See `IGitCollection`."""
         if merged_revision_ids is not None and not merged_revision_ids:
@@ -246,10 +247,13 @@
         elif (self._asymmetric_filter_expressions or
             target_repository is not None or
             target_path is not None or
+            prerequisite_repository is not None or
+            prerequisite_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)
+                statuses, target_repository, target_path,
+                prerequisite_repository, prerequisite_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
@@ -258,8 +262,9 @@
                 statuses, eager_load=eager_load)
 
     def _naiveGetMergeProposals(self, statuses=None, target_repository=None,
-                                target_path=None, merged_revision_ids=None,
-                                eager_load=False):
+                                target_path=None, prerequisite_repository=None,
+                                prerequisite_path=None,
+                                merged_revision_ids=None, eager_load=False):
         Target = ClassAlias(GitRepository, "target")
         extra_tables = list(set(
             self._tables.values() + self._asymmetric_tables.values()))
@@ -280,6 +285,13 @@
         if target_path is not None:
             expressions.append(
                 BranchMergeProposal.target_git_path == target_path)
+        if prerequisite_repository is not None:
+            expressions.append(
+                BranchMergeProposal.prerequisite_git_repository ==
+                    prerequisite_repository)
+        if prerequisite_path is not None:
+            expressions.append(
+                BranchMergeProposal.prerequisite_git_path == prerequisite_path)
         if merged_revision_ids is not None:
             expressions.append(
                 BranchMergeProposal.merged_revision_id.is_in(

=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py	2015-10-06 16:08:27 +0000
+++ lib/lp/code/model/gitref.py	2015-10-12 13:06:46 +0000
@@ -222,6 +222,21 @@
             status, target_repository=self.repository, target_path=self.path,
             merged_revision_ids=merged_revision_ids, eager_load=eager_load)
 
+    def getDependentMergeProposals(self, status=None, visible_by_user=None,
+                                   eager_load=False):
+        """See `IGitRef`."""
+        if not status:
+            status = (
+                BranchMergeProposalStatus.CODE_APPROVED,
+                BranchMergeProposalStatus.NEEDS_REVIEW,
+                BranchMergeProposalStatus.WORK_IN_PROGRESS)
+
+        collection = getUtility(IAllGitRepositories).visibleByUser(
+            visible_by_user)
+        return collection.getMergeProposals(
+            status, prerequisite_repository=self.repository,
+            prerequisite_path=self.path, eager_load=eager_load)
+
     def getMergeProposalByID(self, id):
         """See `IGitRef`."""
         return self.landing_targets.find(BranchMergeProposal.id == id).one()

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2015-09-29 00:17:48 +0000
+++ lib/lp/code/model/tests/test_branch.py	2015-10-12 13:06:46 +0000
@@ -698,6 +698,20 @@
             [product.development_focus],
             list(branch.associatedProductSeries()))
 
+    def test_getMergeProposals(self):
+        target_branch = self.factory.makeProductBranch()
+        bmp = self.factory.makeBranchMergeProposal(target_branch=target_branch)
+        self.factory.makeBranchMergeProposal()
+        self.assertEqual([bmp], list(target_branch.getMergeProposals()))
+
+    def test_getDependentMergeProposals(self):
+        prerequisite_branch = self.factory.makeProductBranch()
+        bmp = self.factory.makeBranchMergeProposal(
+            prerequisite_branch=prerequisite_branch)
+        self.factory.makeBranchMergeProposal()
+        self.assertEqual(
+            [bmp], list(prerequisite_branch.getDependentMergeProposals()))
+
 
 class TestBranchUpgrade(TestCaseWithFactory):
     """Test the upgrade functionalities of branches."""

=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py	2015-04-22 12:03:05 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py	2015-10-12 13:06:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for branch collections."""
@@ -1052,6 +1052,17 @@
             target_branch=mp1.target_branch)
         self.assertEqual([mp1], list(proposals))
 
+    def test_specifying_prerequisite_branch(self):
+        # If the prerequisite_branch is specified, only merge proposals
+        # where that branch is the prerequisite are returned.
+        prerequisite = self.factory.makeProductBranch()
+        mp1 = self.factory.makeBranchMergeProposal(
+            prerequisite_branch=prerequisite)
+        self.factory.makeBranchMergeProposal()
+        proposals = self.all_branches.getMergeProposals(
+            prerequisite_branch=prerequisite)
+        self.assertEqual([mp1], list(proposals))
+
 
 class TestBranchMergeProposalsForReviewer(TestCaseWithFactory):
     """Tests for IBranchCollection.getProposalsForReviewer()."""

=== modified file 'lib/lp/code/model/tests/test_gitcollection.py'
--- lib/lp/code/model/tests/test_gitcollection.py	2015-05-14 13:57:51 +0000
+++ lib/lp/code/model/tests/test_gitcollection.py	2015-10-12 13:06:46 +0000
@@ -12,11 +12,11 @@
 from operator import attrgetter
 
 import pytz
-from testtools.matchers import Equals
 from storm.store import (
     EmptyResultSet,
     Store,
     )
+from testtools.matchers import Equals
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -598,6 +598,32 @@
             target_path=mp1.target_git_path)
         self.assertEqual([mp1], list(proposals))
 
+    def test_specifying_prerequisite_repository(self):
+        # If the prerequisite_repository is specified but not the
+        # prerequisite_path, only merge proposals where that repository is
+        # the prerequisite are returned.
+        [ref1, ref2] = self.factory.makeGitRefs(
+            paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
+        mp1 = self.factory.makeBranchMergeProposalForGit(prerequisite_ref=ref1)
+        mp2 = self.factory.makeBranchMergeProposalForGit(prerequisite_ref=ref2)
+        self.factory.makeBranchMergeProposalForGit()
+        proposals = self.all_repositories.getMergeProposals(
+            prerequisite_repository=ref1.repository)
+        self.assertEqual(sorted([mp1, mp2]), sorted(proposals))
+
+    def test_specifying_prerequisite_ref(self):
+        # If the prerequisite_repository and prerequisite_path are
+        # specified, only merge proposals where that ref is the prerequisite
+        # are returned.
+        [prerequisite] = self.factory.makeGitRefs()
+        mp1 = self.factory.makeBranchMergeProposalForGit(
+            prerequisite_ref=prerequisite)
+        self.factory.makeBranchMergeProposalForGit()
+        proposals = self.all_repositories.getMergeProposals(
+            prerequisite_repository=prerequisite.repository,
+            prerequisite_path=prerequisite.path)
+        self.assertEqual([mp1], list(proposals))
+
 
 class TestBranchMergeProposalsForReviewer(TestCaseWithFactory):
     """Tests for IGitCollection.getProposalsForReviewer()."""

=== modified file 'lib/lp/code/model/tests/test_gitref.py'
--- lib/lp/code/model/tests/test_gitref.py	2015-06-05 11:05:03 +0000
+++ lib/lp/code/model/tests/test_gitref.py	2015-10-12 13:06:46 +0000
@@ -39,8 +39,17 @@
     def test_getMergeProposals(self):
         [target_ref] = self.factory.makeGitRefs()
         bmp = self.factory.makeBranchMergeProposalForGit(target_ref=target_ref)
+        self.factory.makeBranchMergeProposalForGit()
         self.assertEqual([bmp], list(target_ref.getMergeProposals()))
 
+    def test_getDependentMergeProposals(self):
+        [prerequisite_ref] = self.factory.makeGitRefs()
+        bmp = self.factory.makeBranchMergeProposalForGit(
+            prerequisite_ref=prerequisite_ref)
+        self.factory.makeBranchMergeProposalForGit()
+        self.assertEqual(
+            [bmp], list(prerequisite_ref.getDependentMergeProposals()))
+
     def test_implements_IInformationType(self):
         [ref] = self.factory.makeGitRefs()
         verifyObject(IInformationType, ref)

=== modified file 'lib/lp/code/templates/branch-pending-merges.pt'
--- lib/lp/code/templates/branch-pending-merges.pt	2015-04-19 12:56:32 +0000
+++ lib/lp/code/templates/branch-pending-merges.pt	2015-10-12 13:06:46 +0000
@@ -29,7 +29,7 @@
 
       <div id="dependent-branches" tal:condition="context/dependent_branches">
         <img src="/@@/merge-proposal-icon" />
-        <a href="+merges" tal:content="structure view/dependent_branch_count_text">
+        <a href="+dependent-merges" tal:content="structure view/dependent_branch_count_text">
           1 branch
         </a>
         dependent on this one.

=== modified file 'lib/lp/code/templates/gitref-pending-merges.pt'
--- lib/lp/code/templates/gitref-pending-merges.pt	2015-04-28 16:48:22 +0000
+++ lib/lp/code/templates/gitref-pending-merges.pt	2015-10-12 13:06:46 +0000
@@ -24,7 +24,7 @@
 
       <div id="dependent-landings" tal:condition="view/dependent_landings">
         <img src="/@@/merge-proposal-icon" />
-        <a href="+merges" tal:content="structure view/dependent_landing_count_text">
+        <a href="+dependent-merges" tal:content="structure view/dependent_landing_count_text">
           1 branch
         </a>
         dependent on this one.


Follow ups