← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/branch-transitively_private-attribute into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/branch-transitively_private-attribute into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #849850 in Launchpad itself: "Branch needs a transitively_private attribute"
  https://bugs.launchpad.net/launchpad/+bug/849850

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/branch-transitively_private-attribute/+merge/75325

This branch can only be deployed after the following db-devel branches have been deployed to lp.net:
lp:~wallyworld/launchpad/branch-privacy-trigger
lp:~wallyworld/launchpad/branch-transitively-private-column

Bug 568121 requires that public branches stacked on private branches are shown as private. This mp introduces a new Branch attribute: transitively_private. The attribute is maintained by a db trigger and allows both queries and application code to use a common attribute for determining whether a branch is truly private. This branch allows some previously deployed code to recursively look at stacked on branches to populate the "private" attribute to be replaced with a delegated call to transitively_private.

== Implementation ==

Essentially: simply replace uses of Branch.private with Branch.transitively_private in Storm queries and also hand coded SQL.
Also a bit of code to ensure that when branch.setPrivate() is called, branch.transitively_private is invalidated on that instance so that it is reloaded from the db.

== Tests ==

Existing tests using private branches were altered so that some of the private branches were made transitively private.

Changed tests in:
  - test_branchcollection
  - test_branchlookup
  - test_revision
  - test_branchmergeproposal
  - test_branch

Plus added tests:
  TestUpdateRevisionCacheForBranch
  - test_revisions_for_transitive_private_branch_marked_private
  - test_existing_transitive_private_revisions_with_public_branch

  TestGetByLPPath
  - test_transitive_private_branch
  - test_transitive_private_linked_branch

== Lint ==

 Linting changed files:
  lib/lp/code/model/branch.py
  lib/lp/code/model/branchcollection.py
  lib/lp/code/model/branchlookup.py
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/revision.py
  lib/lp/code/model/tests/test_branch.py
  lib/lp/code/model/tests/test_branchcollection.py
  lib/lp/code/model/tests/test_branchlookup.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/model/tests/test_revision.py
  lib/lp/registry/model/distribution.py
  lib/lp/registry/tests/test_person.py
  lib/lp/testing/factory.py

./lib/lp/code/model/tests/test_branchcollection.py
     759: local variable 'mp' is assigned to but never used
     770: local variable 'mp' is assigned to but never used
./lib/lp/code/model/tests/test_revision.py
     312: local variable 'b1' is assigned to but never used
     506: local variable 'rev2' is assigned to but never used
     529: local variable 'rev2' is assigned to but never used
     537: local variable 'rev3' is assigned to but never used
     556: local variable 'rev1' is assigned to but never used
     565: local variable 'rev2' is assigned to but never used
     635: local variable 'last_scanned_id' is assigned to but never used

-- 
https://code.launchpad.net/~wallyworld/launchpad/branch-transitively_private-attribute/+merge/75325
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/branch-transitively_private-attribute into lp:launchpad.
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2011-09-12 07:29:31 +0000
+++ lib/lp/code/model/branch.py	2011-09-14 11:51:42 +0000
@@ -175,22 +175,14 @@
     # of the state of any stacked on branches.
     explicitly_private = BoolCol(
         default=False, notNull=True, dbName='private')
+    # A branch is transitively private if it is private or it is stacked on a
+    # transitively private branch. The value of this attribute is maintained
+    # by a database trigger.
+    transitively_private = BoolCol(dbName='transitively_private')
 
     @property
     def private(self):
-        return self.explicitly_private or self._isStackedOnPrivate()
-
-    def _isStackedOnPrivate(self, checked_branches=None):
-        # Return True if any of this branch's stacked_on branches is private.
-        is_stacked_on_private = False
-        if self.stacked_on is not None:
-            checked_branches = checked_branches or []
-            checked_branches.append(self)
-            if self.stacked_on not in checked_branches:
-                is_stacked_on_private = (
-                    self.stacked_on.explicitly_private or
-                    self.stacked_on._isStackedOnPrivate(checked_branches))
-        return is_stacked_on_private
+        return self.transitively_private
 
     def setPrivate(self, private, user):
         """See `IBranch`."""
@@ -205,6 +197,12 @@
             if not private and not policy.canBranchesBePublic():
                 raise BranchCannotBePublic()
         self.explicitly_private = private
+        # If this branch is private, then it is also transitively_private
+        # otherwise we need to reload the value.
+        if private:
+            self.transitively_private = True
+        else:
+            self.transitively_private = AutoReload
 
     registrant = ForeignKey(
         dbName='registrant', foreignKey='Person',
@@ -1419,6 +1417,7 @@
     naked_branch.unique_name = AutoReload
     naked_branch.owner_name = AutoReload
     naked_branch.target_suffix = AutoReload
+    naked_branch.transitively_private = AutoReload
 
 
 def branch_modified_subscriber(branch, event):

=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py	2011-09-13 05:23:16 +0000
+++ lib/lp/code/model/branchcollection.py	2011-09-14 11:51:42 +0000
@@ -368,7 +368,7 @@
         scope_tables = [Branch] + self._tables.values()
         scope_expressions = self._branch_filter_expressions
         select = self.store.using(*scope_tables).find(
-            (Branch.id, Branch.explicitly_private, Branch.ownerID),
+            (Branch.id, Branch.transitively_private, Branch.ownerID),
             *scope_expressions)
         branches_query = select._get_select()
         with_expr = [With("scope_branches", branches_query)
@@ -719,7 +719,7 @@
 
     def _getBranchVisibilityExpression(self, branch_class=Branch):
         """Return the where clauses for visibility."""
-        return [branch_class.explicitly_private == False]
+        return [branch_class.transitively_private == False]
 
     def _getCandidateBranchesWith(self):
         """Return WITH clauses defining candidate branches.
@@ -730,7 +730,8 @@
         # Anonymous users get public branches only.
         return [
             With("candidate_branches",
-                SQL("select id from scope_branches where not private"))
+                SQL("""select id from scope_branches
+                    where not transitively_private"""))
             ]
 
 
@@ -818,7 +819,7 @@
                        BranchSubscription.person ==
                        TeamParticipation.teamID,
                        TeamParticipation.person == person,
-                       Branch.explicitly_private == True)))
+                       Branch.transitively_private == True)))
         return private_branches
 
     def _getBranchVisibilityExpression(self, branch_class=Branch):
@@ -827,7 +828,7 @@
         :param branch_class: The Branch class to use - permits using
             ClassAliases.
         """
-        public_branches = branch_class.explicitly_private == False
+        public_branches = branch_class.transitively_private == False
         if self._private_branch_ids is None:
             # Public only.
             return [public_branches]
@@ -848,21 +849,24 @@
             # Really an anonymous sitation
             return [
                 With("candidate_branches",
-                    SQL("select id from scope_branches where not private"))
+                    SQL("""
+                        select id from scope_branches
+                        where not transitively_private"""))
                 ]
         return [
             With("teams", self.store.find(TeamParticipation.teamID,
                 TeamParticipation.personID == person.id)._get_select()),
             With("private_branches", SQL("""
                 SELECT scope_branches.id FROM scope_branches WHERE
-                scope_branches.private AND (
+                scope_branches.transitively_private AND (
                     (scope_branches.owner in (select team from teams) OR
                      EXISTS(SELECT true from BranchSubscription, teams WHERE
                          branchsubscription.branch = scope_branches.id AND
                          branchsubscription.person = teams.team)))""")),
             With("candidate_branches", SQL("""
                 (SELECT id FROM private_branches) UNION
-                (select id FROM scope_branches WHERE not private)"""))
+                (select id FROM scope_branches
+                WHERE not transitively_private)"""))
             ]
 
     def visibleByUser(self, person):

=== modified file 'lib/lp/code/model/branchlookup.py'
--- lib/lp/code/model/branchlookup.py	2011-08-29 07:55:48 +0000
+++ lib/lp/code/model/branchlookup.py	2011-09-14 11:51:42 +0000
@@ -299,7 +299,7 @@
         result = store.find(
             (Branch.id),
             Branch.id == branch_id,
-            Branch.explicitly_private == False).one()
+            Branch.transitively_private == False).one()
         if result is None:
             return None, None
         else:
@@ -316,7 +316,7 @@
         result = store.find(
             (Branch.id, Branch.unique_name),
             Branch.unique_name.is_in(prefixes),
-            Branch.explicitly_private == False).one()
+            Branch.transitively_private == False).one()
         if result is None:
             return None, None
         else:

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2011-08-26 00:43:31 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2011-09-14 11:51:42 +0000
@@ -184,9 +184,10 @@
     @property
     def private(self):
         return (
-            self.source_branch.private or self.target_branch.private or
+            self.source_branch.transitively_private or
+            self.target_branch.transitively_private or
             (self.prerequisite_branch is not None and
-             self.prerequisite_branch.private))
+             self.prerequisite_branch.transitively_private))
 
     reviewer = ForeignKey(
         dbName='reviewer', foreignKey='Person',

=== modified file 'lib/lp/code/model/revision.py'
--- lib/lp/code/model/revision.py	2011-08-29 07:55:48 +0000
+++ lib/lp/code/model/revision.py	2011-09-14 11:51:42 +0000
@@ -162,7 +162,7 @@
             self.id == BranchRevision.revision_id,
             BranchRevision.branch_id == Branch.id)
         if not allow_private:
-            query = And(query, Not(Branch.explicitly_private))
+            query = And(query, Not(Branch.transitively_private))
         if not allow_junk:
             query = And(
                 query,
@@ -478,7 +478,7 @@
             Revision,
             And(revision_time_limit(day_limit),
                 person_condition,
-                Not(Branch.explicitly_private)))
+                Not(Branch.transitively_private)))
         result_set.config(distinct=True)
         return result_set.order_by(Desc(Revision.revision_date))
 
@@ -497,7 +497,7 @@
             ]
 
         conditions = And(revision_time_limit(day_limit),
-                         Not(Branch.explicitly_private))
+                         Not(Branch.transitively_private))
 
         if IProduct.providedBy(obj):
             conditions = And(conditions, Branch.product == obj)

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2011-09-06 05:58:25 +0000
+++ lib/lp/code/model/tests/test_branch.py	2011-09-14 11:51:42 +0000
@@ -2240,6 +2240,7 @@
         stacked_on = self.factory.makeBranch(private=True)
         branch = self.factory.makeBranch(stacked_on=stacked_on, private=False)
         self.assertTrue(branch.private)
+        self.assertTrue(removeSecurityProxy(branch).transitively_private)
         self.assertFalse(branch.explicitly_private)
 
     def test_private_stacked_on_public_is_private(self):
@@ -2247,6 +2248,7 @@
         stacked_on = self.factory.makeBranch(private=False)
         branch = self.factory.makeBranch(stacked_on=stacked_on, private=True)
         self.assertTrue(branch.private)
+        self.assertTrue(removeSecurityProxy(branch).transitively_private)
         self.assertTrue(branch.explicitly_private)
 
 
@@ -2265,6 +2267,8 @@
         self.assertFalse(branch.private)
         branch.setPrivate(False, branch.owner)
         self.assertFalse(branch.private)
+        self.assertFalse(removeSecurityProxy(branch).transitively_private)
+        self.assertFalse(branch.explicitly_private)
 
     def test_public_to_private_allowed(self):
         # If there is a privacy policy allowing the branch owner to have
@@ -2274,6 +2278,8 @@
             branch.owner, BranchVisibilityRule.PRIVATE)
         branch.setPrivate(True, branch.owner)
         self.assertTrue(branch.private)
+        self.assertTrue(removeSecurityProxy(branch).transitively_private)
+        self.assertTrue(branch.explicitly_private)
 
     def test_public_to_private_not_allowed(self):
         # If there are no privacy policies allowing private branches, then
@@ -2292,6 +2298,8 @@
         admins = getUtility(ILaunchpadCelebrities).admin
         branch.setPrivate(True, admins.teamowner)
         self.assertTrue(branch.private)
+        self.assertTrue(removeSecurityProxy(branch).transitively_private)
+        self.assertTrue(branch.explicitly_private)
 
     def test_private_to_private(self):
         # Setting a private branch to be private is a no-op.
@@ -2299,6 +2307,8 @@
         self.assertTrue(branch.private)
         branch.setPrivate(True, branch.owner)
         self.assertTrue(branch.private)
+        self.assertTrue(removeSecurityProxy(branch).transitively_private)
+        self.assertTrue(branch.explicitly_private)
 
     def test_private_to_public_allowed(self):
         # If the namespace policy allows public branches, then changing from
@@ -2306,6 +2316,8 @@
         branch = self.factory.makeProductBranch(private=True)
         branch.setPrivate(False, branch.owner)
         self.assertFalse(branch.private)
+        self.assertFalse(removeSecurityProxy(branch).transitively_private)
+        self.assertFalse(branch.explicitly_private)
 
     def test_private_to_public_not_allowed(self):
         # If the namespace policy does not allow public branches, attempting

=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py	2011-08-30 19:25:53 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py	2011-09-14 11:51:42 +0000
@@ -529,8 +529,14 @@
         TestCaseWithFactory.setUp(self)
         remove_all_sample_data_branches()
         self.public_branch = self.factory.makeAnyBranch(name='public')
+        # We make private branch by stacking a public branch on top of a
+        # private one.
+        self.private_stacked_on_branch = self.factory.makeAnyBranch(
+            private=True)
+        self.public_stacked_on_branch = self.factory.makeAnyBranch(
+            stacked_on=self.private_stacked_on_branch)
         self.private_branch1 = self.factory.makeAnyBranch(
-            private=True, name='private1')
+            stacked_on=self.public_stacked_on_branch, name='private1')
         self.private_branch2 = self.factory.makeAnyBranch(
             private=True, name='private2')
         self.all_branches = getUtility(IAllBranches)
@@ -540,7 +546,8 @@
         # collection.
         self.assertEqual(
             sorted([self.public_branch, self.private_branch1,
-                 self.private_branch2]),
+                 self.private_branch2, self.public_stacked_on_branch,
+                 self.private_stacked_on_branch]),
             sorted(self.all_branches.getBranches()))
 
     def test_anonymous_sees_only_public(self):

=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
--- lib/lp/code/model/tests/test_branchlookup.py	2011-08-12 11:37:08 +0000
+++ lib/lp/code/model/tests/test_branchlookup.py	2011-09-14 11:51:42 +0000
@@ -141,6 +141,18 @@
         result = self.branch_set.getIdAndTrailingPath(path)
         self.assertEqual((None, None), result)
 
+    def test_branch_id_alias_transitive_private(self):
+        # Transitively private branches are not found at all
+        # (this is for anonymous access)
+        owner = self.factory.makePerson()
+        private_branch = self.factory.makeAnyBranch(
+            owner=owner, private=True)
+        branch = self.factory.makeAnyBranch(stacked_on=private_branch)
+        with person_logged_in(owner):
+            path = branch_id_alias(branch)
+        result = self.branch_set.getIdAndTrailingPath(path)
+        self.assertEqual((None, None), result)
+
     def test_branch_id_alias_public(self):
         # Public branches can be accessed.
         branch = self.factory.makeAnyBranch()
@@ -539,6 +551,15 @@
         self.assertRaises(
             NoSuchBranch, self.branch_lookup.getByLPPath, path)
 
+    def test_transitive_private_branch(self):
+        # If the unique name refers to an invisible branch, getByLPPath raises
+        # NoSuchBranch, just as if the branch weren't there at all.
+        private_branch = self.factory.makeAnyBranch(private=True)
+        branch = self.factory.makeAnyBranch(stacked_on=private_branch)
+        path = removeSecurityProxy(branch).unique_name
+        self.assertRaises(
+            NoSuchBranch, self.branch_lookup.getByLPPath, path)
+
     def test_resolve_product_branch_unique_name(self):
         # getByLPPath returns the branch, no trailing path and no series if
         # given the unique name of an existing product branch.
@@ -626,6 +647,17 @@
         self.assertRaises(
             NoLinkedBranch, self.branch_lookup.getByLPPath, product.name)
 
+    def test_transitive_private_linked_branch(self):
+        # If the given path refers to an object with an invisible linked
+        # branch, then getByLPPath raises `NoLinkedBranch`, as if the branch
+        # weren't there at all.
+        private_branch = self.factory.makeProductBranch(private=True)
+        branch = self.factory.makeProductBranch(stacked_on=private_branch)
+        product = removeSecurityProxy(branch).product
+        removeSecurityProxy(product).development_focus.branch = branch
+        self.assertRaises(
+            NoLinkedBranch, self.branch_lookup.getByLPPath, product.name)
+
     def test_no_official_branch(self):
         sourcepackage = self.factory.makeSourcePackage()
         exception = self.assertRaises(

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2011-08-29 07:55:48 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2011-09-14 11:51:42 +0000
@@ -1043,9 +1043,11 @@
         product = getUtility(IProductSet).getByName(product_name)
         if product is None:
             product = self.factory.makeProduct(name=product_name)
+        stacked_on_branch = self.factory.makeProductBranch(
+            product=product, owner=owner, registrant=registrant)
         branch = self.factory.makeProductBranch(
             product=product, owner=owner, registrant=registrant,
-            name=branch_name)
+            name=branch_name, stacked_on=stacked_on_branch)
         if registrant is None:
             registrant = owner
         bmp = branch.addLandingTarget(
@@ -1202,8 +1204,10 @@
 
         proposal = self._make_merge_proposal(
             'xray', 'november', 'work', registrant=albert)
-        # Mark the source branch private.
-        removeSecurityProxy(proposal.source_branch).explicitly_private = True
+        # Mark the source branch private by making it's stacked on branch
+        # private.
+        removeSecurityProxy(
+            proposal.source_branch.stacked_on).explicitly_private = True
 
         november = getUtility(IProductSet).getByName('november')
         # The proposal is visible to charles.

=== modified file 'lib/lp/code/model/tests/test_revision.py'
--- lib/lp/code/model/tests/test_revision.py	2011-08-29 07:55:48 +0000
+++ lib/lp/code/model/tests/test_revision.py	2011-09-14 11:51:42 +0000
@@ -821,6 +821,18 @@
         self.assertTrue(branch.private)
         self.assertTrue(cached.private)
 
+    def test_revisions_for_transitive_private_branch_marked_private(self):
+        # If the branch is stacked on a private branch, then the revisions in
+        # the cache will be marked private too.
+        private_branch = self.factory.makeAnyBranch(private=True)
+        branch = self.factory.makeAnyBranch(stacked_on=private_branch)
+        revision = self.factory.makeRevision()
+        branch.createBranchRevision(1, revision)
+        RevisionSet.updateRevisionCacheForBranch(branch)
+        [cached] = self._getRevisionCache()
+        self.assertTrue(branch.private)
+        self.assertTrue(cached.private)
+
     def test_product_branch_revisions(self):
         # The revision cache knows the product for revisions in product
         # branches.
@@ -897,6 +909,26 @@
         # But the privacy flags are different.
         self.assertNotEqual(rev1.private, rev2.private)
 
+    def test_existing_transitive_private_revisions_with_public_branch(self):
+        # If a revision is in both public and private branches, there is a
+        # revision cache row for both public and private. A branch is private
+        # if it is stacked on a private branch.
+        stacked_on_branch = self.factory.makeAnyBranch(private=True)
+        private_branch = self.factory.makeAnyBranch(
+            stacked_on=stacked_on_branch)
+        public_branch = self.factory.makeAnyBranch(private=False)
+        revision = self.factory.makeRevision()
+        private_branch.createBranchRevision(1, revision)
+        RevisionSet.updateRevisionCacheForBranch(private_branch)
+        public_branch.createBranchRevision(1, revision)
+        RevisionSet.updateRevisionCacheForBranch(public_branch)
+        [rev1, rev2] = self._getRevisionCache()
+        # Both revisions point to the same underlying revision.
+        self.assertEqual(rev1.revision, revision)
+        self.assertEqual(rev2.revision, revision)
+        # But the privacy flags are different.
+        self.assertNotEqual(rev1.private, rev2.private)
+
 
 class TestPruneRevisionCache(RevisionCacheTestCase):
     """Tests for RevisionSet.pruneRevisionCache."""

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2011-08-29 00:13:22 +0000
+++ lib/lp/registry/model/distribution.py	2011-09-14 11:51:42 +0000
@@ -655,7 +655,7 @@
                Branch.last_scanned_id,
                SPBDS.name AS distro_series_name,
                Branch.id,
-               Branch.private,
+               Branch.transitively_private,
                Branch.owner
         FROM Branch
         JOIN DistroSeries
@@ -675,7 +675,7 @@
             # Now we see just a touch of privacy concerns.
             # If the current user is anonymous, they cannot see any private
             # branches.
-            base_query += ('      AND NOT Branch.private\n')
+            base_query += ('      AND NOT Branch.transitively_private\n')
         # We want to order the results, in part for easier grouping at the
         # end.
         base_query += 'ORDER BY unique_name, last_scanned_id'

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2011-08-11 20:37:16 +0000
+++ lib/lp/registry/tests/test_person.py	2011-09-14 11:51:42 +0000
@@ -941,7 +941,7 @@
         duplicate, mergee = self._do_merge(duplicate, mergee)
         branches = [b.name for b in mergee.getBranches()]
         self.assertEqual(2, len(branches))
-        self.assertEqual([u'foo', u'foo-1'], branches)
+        self.assertContentEqual([u'foo', u'foo-1'], branches)
 
     def test_merge_moves_recipes(self):
         # When person/teams are merged, recipes owned by the from person are

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-09-13 22:43:21 +0000
+++ lib/lp/testing/factory.py	2011-09-14 11:51:42 +0000
@@ -1125,6 +1125,7 @@
             url=url, **optional_branch_args)
         if private:
             removeSecurityProxy(branch).explicitly_private = True
+            removeSecurityProxy(branch).transitively_private = True
         if stacked_on is not None:
             removeSecurityProxy(branch).stacked_on = stacked_on
         if reviewer is not None:


Follow ups