Merge lp:~sinzui/launchpad/merge-and-private-branches into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15638
Proposed branch: lp:~sinzui/launchpad/merge-and-private-branches
Merge into: lp:launchpad
Diff against target: 199 lines (+59/-8)
6 files modified
lib/lp/code/interfaces/branchcollection.py (+3/-0)
lib/lp/code/model/branchcollection.py (+9/-1)
lib/lp/code/model/tests/test_branchcollection.py (+15/-1)
lib/lp/registry/browser/peoplemerge.py (+7/-0)
lib/lp/registry/browser/tests/test_peoplemerge.py (+18/-5)
lib/lp/registry/model/person.py (+7/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/merge-and-private-branches
Reviewer Review Type Date Requested Status
Richard Harding (community) Approve
Review via email: mp+115229@code.launchpad.net

Commit message

Do not merge or delete teams with private branches.

Description of the change

I tried to delete a team that had private branches in projects with
branch visibility rules. Merge transfers artefacts that must have
owners, such as as branches to ~registry. Merge oopses because ~registry
does not have a policy for the project.

While this bug will disappear when BVP is removed. We do not want
~registry to own private branches. The delete page must explain that the
team cannot be deleted until the team's private branches are deleted or
the ownership is transferred to another person.

--------------------------------------------------------------------

RULES

    Pre-implementation: no one
    * Find or write a method that can lookup a person's private branches
      without being constrained by viability.
    * Update delete and merge page to explain that Launchpad does not know
      what to do with the team's private branches. Someone must either change
      the owner of each private branch or delete it so that confidential
      information is neither lost or disclosed.
      * ValidatingMergeView.validate() must look for private branches.
    * Add an assert to check for private branches in PersonSet.Merge()
      as is done for other conditions conditions where we want the views
      to validate first.

QA

    * Visit https://qastaging.launchpad.net/~canonical-mq/+delete
    * Verify the page states that the team cannot be deleted
    * Delete or change the owners of the private branches
    * Visit https://qastaging.launchpad.net/~canonical-mq/+delete
    * Verify the Delete action available; use it
    * Verify the team is deleted after 10 minutes.
      * Otherwise check for an email reporting an error.

LINT

    lib/lp/code/interfaces/branchcollection.py
    lib/lp/code/model/branchcollection.py
    lib/lp/code/model/tests/test_branchcollection.py
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/browser/tests/test_peoplemerge.py
    lib/lp/registry/model/person.py

TEST

    ./bin/test -vvc lp.code.model.tests.test_branchcollection
    ./bin/test -vvc lp.registry.browser.tests.test_peoplemerge
    ./bin/test -vvc lp.registry.tests.test_personset

IMPLEMENTATION

Added isPrivate() to the IALlBranches collection that will be used
in conjunction with ownedBy() to get all private branches a person owns
without being constrained by the normal visibility checks.
    lib/lp/code/interfaces/branchcollection.py
    lib/lp/code/model/branchcollection.py
    lib/lp/code/model/tests/test_branchcollection.py

Check for private branches and advise the user to delete or transfer
them.
    lib/lp/registry/browser/tests/test_peoplemerge.py
    lib/lp/registry/model/person.py

Add an assert to ensure callsites check for private branches before
calling merge.
    lib/lp/registry/browser/peoplemerge.py

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/interfaces/branchcollection.py'
2--- lib/lp/code/interfaces/branchcollection.py 2012-07-14 07:50:13 +0000
3+++ lib/lp/code/interfaces/branchcollection.py 2012-07-16 21:17:23 +0000
4@@ -157,6 +157,9 @@
5 with a sourcepackage.
6 """
7
8+ def isPrivate():
9+ """Restrict the collection to private branches."""
10+
11 def ownedBy(person):
12 """Restrict the collection to branches owned by 'person'."""
13
14
15=== modified file 'lib/lp/code/model/branchcollection.py'
16--- lib/lp/code/model/branchcollection.py 2012-07-14 07:50:13 +0000
17+++ lib/lp/code/model/branchcollection.py 2012-07-16 21:17:23 +0000
18@@ -61,7 +61,10 @@
19 from lp.code.model.codereviewvote import CodeReviewVoteReference
20 from lp.code.model.revision import Revision
21 from lp.code.model.seriessourcepackagebranch import SeriesSourcePackageBranch
22-from lp.registry.enums import PUBLIC_INFORMATION_TYPES
23+from lp.registry.enums import (
24+ PRIVATE_INFORMATION_TYPES,
25+ PUBLIC_INFORMATION_TYPES,
26+ )
27 from lp.registry.model.distribution import Distribution
28 from lp.registry.model.distroseries import DistroSeries
29 from lp.registry.model.person import (
30@@ -607,6 +610,11 @@
31 Branch.product == None,
32 Branch.sourcepackagename == None])
33
34+ def isPrivate(self):
35+ """See `IBranchCollection`."""
36+ return self._filterBy([
37+ Branch.information_type.is_in(PRIVATE_INFORMATION_TYPES)])
38+
39 def ownedBy(self, person):
40 """See `IBranchCollection`."""
41 return self._filterBy([Branch.owner == person], symmetric=False)
42
43=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
44--- lib/lp/code/model/tests/test_branchcollection.py 2012-07-14 07:50:13 +0000
45+++ lib/lp/code/model/tests/test_branchcollection.py 2012-07-16 21:17:23 +0000
46@@ -303,11 +303,25 @@
47 self.factory.makeAnyBranch(owner=person)
48 self.factory.makeProductBranch(product=product)
49 collection = self.all_branches.inProduct(product).ownedBy(person)
50- self.all_branches.inProduct(product).ownedBy(person)
51 self.assertEqual([branch], list(collection.getBranches()))
52 collection = self.all_branches.ownedBy(person).inProduct(product)
53 self.assertEqual([branch], list(collection.getBranches()))
54
55+ def test_ownedBy_and_isPrivate(self):
56+ # 'ownedBy' and 'isPrivate' can combine to form a collection that is
57+ # restricted to private branches owned by a particular person.
58+ person = self.factory.makePerson()
59+ product = self.factory.makeProduct()
60+ branch = self.factory.makeProductBranch(
61+ product=product, owner=person,
62+ information_type=InformationType.USERDATA)
63+ self.factory.makeAnyBranch(owner=person)
64+ self.factory.makeProductBranch(product=product)
65+ collection = self.all_branches.isPrivate().ownedBy(person)
66+ self.assertEqual([branch], list(collection.getBranches()))
67+ collection = self.all_branches.ownedBy(person).isPrivate()
68+ self.assertEqual([branch], list(collection.getBranches()))
69+
70 def test_ownedByTeamMember_and_inProduct(self):
71 # 'ownedBy' and 'inProduct' can combine to form a collection that is
72 # restricted to branches of a particular product owned by a particular
73
74=== modified file 'lib/lp/registry/browser/peoplemerge.py'
75--- lib/lp/registry/browser/peoplemerge.py 2012-07-07 19:30:24 +0000
76+++ lib/lp/registry/browser/peoplemerge.py 2012-07-16 21:17:23 +0000
77@@ -24,6 +24,7 @@
78 LaunchpadFormView,
79 )
80 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
81+from lp.code.interfaces.branchcollection import IAllBranches
82 from lp.registry.interfaces.mailinglist import (
83 MailingListStatus,
84 PURGE_STATES,
85@@ -73,6 +74,12 @@
86 "can be merged. It may take ten minutes to remove the "
87 "deleted PPA's files.",
88 mapping=dict(name=dupe_person.name)))
89+ all_branches = getUtility(IAllBranches)
90+ if all_branches.ownedBy(dupe_person).isPrivate().count() != 0:
91+ self.addError(
92+ _("${name} owns private branches that must be "
93+ "deleted or transferred to another owner first.",
94+ mapping=dict(name=dupe_person.name)))
95 if dupe_person.isMergePending():
96 self.addError(_("${name} is already queued for merging.",
97 mapping=dict(name=dupe_person.name)))
98
99=== modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
100--- lib/lp/registry/browser/tests/test_peoplemerge.py 2012-01-01 02:58:52 +0000
101+++ lib/lp/registry/browser/tests/test_peoplemerge.py 2012-07-16 21:17:23 +0000
102@@ -6,6 +6,7 @@
103
104 from zope.component import getUtility
105
106+from lp.registry.enums import InformationType
107 from lp.registry.interfaces.person import (
108 IPersonSet,
109 TeamSubscriptionPolicy,
110@@ -46,7 +47,7 @@
111 def test_cannot_merge_person_with_ppas(self):
112 # A team with a PPA cannot be merged.
113 login_celebrity('admin')
114- archive = self.dupe.createPPA()
115+ self.dupe.createPPA()
116 login_celebrity('registry_experts')
117 view = create_initialized_view(
118 self.person_set, '+requestmerge', form=self.getForm())
119@@ -56,6 +57,18 @@
120 "files."],
121 view.errors)
122
123+ def test_cannot_merge_person_with_private_branches(self):
124+ # A team or user with a private branches cannot be merged.
125+ self.factory.makeBranch(
126+ owner=self.dupe, information_type=InformationType.USERDATA)
127+ login_celebrity('registry_experts')
128+ view = create_initialized_view(
129+ self.person_set, '+requestmerge', form=self.getForm())
130+ self.assertEqual(
131+ [u"dupe owns private branches that must be deleted or "
132+ "transferred to another owner first."],
133+ view.errors)
134+
135 def test_cannot_merge_person_with_itself(self):
136 # A IPerson cannot be merged with itself.
137 login_person(self.target)
138@@ -69,7 +82,7 @@
139 # A merge cannot be requested for an IPerson if it there is a job
140 # queued to merge it into another IPerson.
141 job_source = getUtility(IPersonMergeJobSource)
142- duplicate_job = job_source.create(
143+ job_source.create(
144 from_person=self.dupe, to_person=self.target)
145 login_person(self.target)
146 view = create_initialized_view(
147@@ -81,7 +94,7 @@
148 # A merge cannot be requested for an IPerson if it there is a job
149 # queued to merge it into another IPerson.
150 job_source = getUtility(IPersonMergeJobSource)
151- duplicate_job = job_source.create(
152+ job_source.create(
153 from_person=self.target, to_person=self.dupe)
154 login_person(self.target)
155 view = create_initialized_view(
156@@ -173,7 +186,7 @@
157 # A team with a PPA cannot be merged.
158 login_celebrity('admin')
159 self.dupe_team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
160- archive = self.dupe_team.createPPA()
161+ self.dupe_team.createPPA()
162 login_celebrity('registry_experts')
163 view = self.getView()
164 self.assertEqual(
165@@ -209,7 +222,7 @@
166 def test_cannot_merge_person_with_ppa(self):
167 # A person with a PPA cannot be merged.
168 login_celebrity('admin')
169- archive = self.dupe_person.createPPA()
170+ self.dupe_person.createPPA()
171 view = self.getView()
172 self.assertEqual(
173 [u"dupe-person has a PPA that must be deleted before it can be "
174
175=== modified file 'lib/lp/registry/model/person.py'
176--- lib/lp/registry/model/person.py 2012-07-07 19:30:24 +0000
177+++ lib/lp/registry/model/person.py 2012-07-16 21:17:23 +0000
178@@ -141,7 +141,10 @@
179 from lp.bugs.model.bugtarget import HasBugsBase
180 from lp.bugs.model.bugtask import get_related_bugtasks_search_params
181 from lp.bugs.model.structuralsubscription import StructuralSubscription
182-from lp.code.interfaces.branchcollection import IBranchCollection
183+from lp.code.interfaces.branchcollection import (
184+ IAllBranches,
185+ IBranchCollection,
186+ )
187 from lp.code.model.hasbranches import (
188 HasBranchesMixin,
189 HasMergeProposalsMixin,
190@@ -4186,6 +4189,9 @@
191 ArchiveStatus.DELETING]) is not None:
192 raise AssertionError(
193 'from_person has a ppa in ACTIVE or DELETING status')
194+ from_person_branches = getUtility(IAllBranches).ownedBy(from_person)
195+ if from_person_branches.isPrivate().count() != 0:
196+ raise AssertionError('from_person has private branches.')
197 if from_person.is_team:
198 self._purgeUnmergableTeamArtifacts(
199 from_person, to_person, reviewer)