Merge lp:~abentley/launchpad/mp-by-revision-id into lp:launchpad

Proposed by Aaron Bentley on 2012-07-12
Status: Merged
Approved by: j.c.sackett on 2012-07-12
Approved revision: no longer in the source branch.
Merged at revision: 15624
Proposed branch: lp:~abentley/launchpad/mp-by-revision-id
Merge into: lp:launchpad
Diff against target: 278 lines (+134/-7)
6 files modified
lib/lp/_schema_circular_imports.py (+7/-1)
lib/lp/code/interfaces/branch.py (+14/-0)
lib/lp/code/model/branch.py (+5/-0)
lib/lp/code/model/branchcollection.py (+16/-4)
lib/lp/code/model/tests/test_branch.py (+84/-0)
lib/lp/testing/factory.py (+8/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/mp-by-revision-id
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-07-12 Approve on 2012-07-12
Review via email: mp+114693@code.launchpad.net

Commit Message

Find merge proposals by revision-id.

Description of the Change

= Summary =
Fix bug #1020961: API cannot find merge proposal without branch

== Proposed fix ==
Add merged_revision parameter to GenericBranchCollection.getMergeProposals, add BranchSet.getMergeProposals and export via API.

== Pre-implementation notes ==
None

== LOC Rationale ==
I have a LOC credit of 1983

== Implementation details ==
This approach uses BranchRevision to find the association between a revision_id and a merge proposal. Another option would be a schema change to associate the merge proposal with a Revision.id or revision-id directly, but I don't belive that this API has enough traffic to warrant that.

== Tests ==
bin/test -vt TestGetMergeProposals

== Demo and Q/A ==
Using launchpadlib (potentially via lp-shell), use getMergeProposals with revision-id '<email address hidden>'. The sole result should be https://code.qastaging.launchpad.net/~wallyworld/launchpad/sharing-picker-933823-2/+merge/94087

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/model/branchcollection.py
  lib/lp/code/interfaces/branch.py
  lib/lp/testing/factory.py
  lib/lp/code/model/branch.py
  lib/lp/_schema_circular_imports.py
  lib/lp/code/model/tests/test_branch.py

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

Looks good. Thanks, Aaron.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2012-06-19 16:09:38 +0000
3+++ lib/lp/_schema_circular_imports.py 2012-07-13 14:40:32 +0000
4@@ -59,7 +59,10 @@
5 )
6 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
7 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
8-from lp.code.interfaces.branch import IBranch
9+from lp.code.interfaces.branch import (
10+ IBranch,
11+ IBranchSet,
12+ )
13 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
14 from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
15 from lp.code.interfaces.branchsubscription import IBranchSubscription
16@@ -263,6 +266,9 @@
17 patch_collection_return_type(
18 IBranch, 'getMergeProposals', IBranchMergeProposal)
19
20+patch_collection_return_type(
21+ IBranchSet, 'getMergeProposals', IBranchMergeProposal)
22+
23 IBranchMergeProposal['getComment'].queryTaggedValue(
24 LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = ICodeReviewComment
25 IBranchMergeProposal['createComment'].queryTaggedValue(
26
27=== modified file 'lib/lp/code/interfaces/branch.py'
28--- lib/lp/code/interfaces/branch.py 2012-07-12 06:51:37 +0000
29+++ lib/lp/code/interfaces/branch.py 2012-07-13 14:40:32 +0000
30@@ -1393,6 +1393,20 @@
31 branches.
32 """
33
34+ @operation_returns_collection_of(Interface)
35+ @call_with(visible_by_user=REQUEST_USER)
36+ @operation_parameters(merged_revision=TextLine())
37+ @export_read_operation()
38+ @operation_for_version("devel")
39+ def getMergeProposals(merged_revision, visible_by_user=None):
40+ """Return the merge proposals that resulted in this revision.
41+
42+ :param merged_revision: The revision_id of the revision that resulted
43+ from this merge proposal.
44+ :param visible_by_user: The user to whom the proposals must be
45+ visible. If None, only public proposals will be returned.
46+ """
47+
48
49 class IBranchListingQueryOptimiser(Interface):
50 """Interface for a helper utility to do efficient queries for branches.
51
52=== modified file 'lib/lp/code/model/branch.py'
53--- lib/lp/code/model/branch.py 2012-07-13 04:55:30 +0000
54+++ lib/lp/code/model/branch.py 2012-07-13 14:40:32 +0000
55@@ -1541,6 +1541,11 @@
56 'person_name': person.displayname,
57 'visible_branches': visible_branches}
58
59+ def getMergeProposals(self, merged_revision, visible_by_user=None):
60+ """See IBranchSet."""
61+ collection = getUtility(IAllBranches).visibleByUser(visible_by_user)
62+ return collection.getMergeProposals(merged_revision=merged_revision)
63+
64
65 def update_trigger_modified_fields(branch):
66 """Make the trigger updated fields reload when next accessed."""
67
68=== modified file 'lib/lp/code/model/branchcollection.py'
69--- lib/lp/code/model/branchcollection.py 2012-07-13 00:06:59 +0000
70+++ lib/lp/code/model/branchcollection.py 2012-07-13 14:40:32 +0000
71@@ -54,10 +54,12 @@
72 get_branch_privacy_filter,
73 )
74 from lp.code.model.branchmergeproposal import BranchMergeProposal
75+from lp.code.model.branchrevision import BranchRevision
76 from lp.code.model.branchsubscription import BranchSubscription
77 from lp.code.model.codeimport import CodeImport
78 from lp.code.model.codereviewcomment import CodeReviewComment
79 from lp.code.model.codereviewvote import CodeReviewVoteReference
80+from lp.code.model.revision import Revision
81 from lp.code.model.seriessourcepackagebranch import SeriesSourcePackageBranch
82 from lp.registry.enums import PUBLIC_INFORMATION_TYPES
83 from lp.registry.model.distribution import Distribution
84@@ -322,7 +324,7 @@
85
86 def getMergeProposals(self, statuses=None, for_branches=None,
87 target_branch=None, merged_revnos=None,
88- eager_load=False):
89+ merged_revision=None, eager_load=False):
90 """See `IBranchCollection`."""
91 if for_branches is not None and not for_branches:
92 # We have an empty branches list, so we can shortcut.
93@@ -333,10 +335,11 @@
94 elif (self._asymmetric_filter_expressions or
95 for_branches is not None or
96 target_branch is not None or
97- merged_revnos is not None):
98+ merged_revnos is not None or
99+ merged_revision is not None):
100 return self._naiveGetMergeProposals(
101 statuses, for_branches, target_branch, merged_revnos,
102- eager_load=eager_load)
103+ merged_revision, eager_load=eager_load)
104 else:
105 # When examining merge proposals in a scope, this is a moderately
106 # effective set of constrained queries. It is not effective when
107@@ -346,7 +349,7 @@
108
109 def _naiveGetMergeProposals(self, statuses=None, for_branches=None,
110 target_branch=None, merged_revnos=None,
111- eager_load=False):
112+ merged_revision=None, eager_load=False):
113 Target = ClassAlias(Branch, "target")
114 extra_tables = list(set(
115 self._tables.values() + self._asymmetric_tables.values()))
116@@ -369,6 +372,15 @@
117 if merged_revnos is not None:
118 expressions.append(
119 BranchMergeProposal.merged_revno.is_in(merged_revnos))
120+ if merged_revision is not None:
121+ expressions.extend([
122+ BranchMergeProposal.merged_revno == BranchRevision.sequence,
123+ BranchRevision.revision_id == Revision.id,
124+ BranchRevision.branch_id ==
125+ BranchMergeProposal.target_branchID,
126+ Revision.revision_id == merged_revision
127+ ])
128+ tables.extend([BranchRevision, Revision])
129 if statuses is not None:
130 expressions.append(
131 BranchMergeProposal.queue_status.is_in(statuses))
132
133=== modified file 'lib/lp/code/model/tests/test_branch.py'
134--- lib/lp/code/model/tests/test_branch.py 2012-07-12 08:16:22 +0000
135+++ lib/lp/code/model/tests/test_branch.py 2012-07-13 14:40:32 +0000
136@@ -87,6 +87,7 @@
137 IFindOfficialBranchLinks,
138 )
139 from lp.code.model.branch import (
140+ BranchSet,
141 ClearDependentBranch,
142 ClearOfficialPackageBranch,
143 ClearSeriesBranch,
144@@ -153,6 +154,7 @@
145 TestCaseWithFactory,
146 time_counter,
147 ws_object,
148+ WebServiceTestCase,
149 )
150 from lp.testing.factory import LaunchpadObjectFactory
151 from lp.testing.layers import (
152@@ -2887,6 +2889,88 @@
153 new_product, get_policies_for_artifact(branch)[0].pillar)
154
155
156+def make_proposal_and_branch_revision(factory, revno, revision_id,
157+ userdata_target=False):
158+ if userdata_target:
159+ information_type = InformationType.USERDATA
160+ else:
161+ information_type = InformationType.PUBLIC
162+ target_branch = factory.makeAnyBranch(information_type=information_type)
163+ revision = factory.makeBranchRevision(revision_id=revision_id,
164+ branch=target_branch,
165+ sequence=revno)
166+ return factory.makeBranchMergeProposal(merged_revno=revno,
167+ target_branch=target_branch)
168+
169+
170+class TestGetMergeProposalsWS(WebServiceTestCase):
171+
172+ def test_getMergeProposals(self):
173+ """getMergeProposals works as expected over the API."""
174+ bmp = make_proposal_and_branch_revision(self.factory, 5, 'rev-id',
175+ userdata_target=True)
176+ transaction.commit()
177+ user = removeSecurityProxy(bmp).target_branch.owner
178+ service = self.factory.makeLaunchpadService(
179+ user, version=self.ws_version)
180+ result = service.branches.getMergeProposals(merged_revision='rev-id')
181+ self.assertEqual([self.wsObject(bmp, user)], list(result))
182+
183+
184+class TestGetMergeProposals(TestCaseWithFactory):
185+
186+ layer = DatabaseFunctionalLayer
187+
188+ def setUp(self):
189+ super(TestGetMergeProposals, self).setUp()
190+ self.branch_set = BranchSet()
191+
192+ def test_getMergeProposals_with_no_merged_revno(self):
193+ """Merge proposals with no merged revno are not found."""
194+ make_proposal_and_branch_revision(self.factory, None, 'rev-id')
195+ result = self.branch_set.getMergeProposals(merged_revision='rev-id')
196+ self.assertEqual([], list(result))
197+
198+ def test_getMergeProposals_with_any_merged_revno(self):
199+ """Any arbitrary revno will connect a revid to a proposal."""
200+ bmp = make_proposal_and_branch_revision(
201+ self.factory, self.factory.getUniqueInteger(), 'rev-id')
202+ result = self.branch_set.getMergeProposals(merged_revision='rev-id')
203+ self.assertEqual([bmp], list(result))
204+
205+ def test_getMergeProposals_correct_merged_revno(self):
206+ """Only proposals with the correct merged_revno match."""
207+ bmp1 = make_proposal_and_branch_revision(self.factory, 4, 'rev-id')
208+ bmp2 = make_proposal_and_branch_revision(self.factory, 5, 'other')
209+ result = self.branch_set.getMergeProposals(merged_revision='rev-id')
210+ self.assertEqual([bmp1], list(result))
211+ result = self.branch_set.getMergeProposals(merged_revision='other')
212+ self.assertEqual([bmp2], list(result))
213+
214+ def test_getMergeProposals_correct_branch(self):
215+ """Only proposals with the correct branch match."""
216+ bmp1 = make_proposal_and_branch_revision(self.factory, 5, 'rev-id')
217+ make_proposal_and_branch_revision(self.factory, 5, 'other')
218+ result = self.branch_set.getMergeProposals(merged_revision='rev-id')
219+ self.assertEqual([bmp1], list(result))
220+
221+ def test_getMergeProposals_skips_hidden(self):
222+ """Proposals not visible to the user are skipped."""
223+ make_proposal_and_branch_revision(
224+ self.factory, 5, 'rev-id', userdata_target=True)
225+ result = self.branch_set.getMergeProposals(merged_revision='rev-id',
226+ visible_by_user=self.factory.makePerson())
227+ self.assertEqual([], list(result))
228+
229+ def test_getMergeProposals_shows_visible_userdata(self):
230+ """Proposals visible to the user are listed."""
231+ bmp = make_proposal_and_branch_revision(
232+ self.factory, 5, 'rev-id', userdata_target=True)
233+ result = self.branch_set.getMergeProposals(merged_revision='rev-id',
234+ visible_by_user=bmp.target_branch.owner)
235+ self.assertEqual([bmp], list(result))
236+
237+
238 class TestScheduleDiffUpdates(TestCaseWithFactory):
239
240 layer = DatabaseFunctionalLayer
241
242=== modified file 'lib/lp/testing/factory.py'
243--- lib/lp/testing/factory.py 2012-07-03 10:29:53 +0000
244+++ lib/lp/testing/factory.py 2012-07-13 14:40:32 +0000
245@@ -1441,9 +1441,10 @@
246 product=None, initial_comment=None,
247 source_branch=None, preview_diff=None,
248 date_created=None, description=None,
249- reviewer=None):
250+ reviewer=None, merged_revno=None):
251 """Create a proposal to merge based on anonymous branches."""
252 if target_branch is not None:
253+ target_branch = removeSecurityProxy(target_branch)
254 target = target_branch.target
255 elif source_branch is not None:
256 target = source_branch.target
257@@ -1475,6 +1476,7 @@
258 date_created=date_created)
259
260 unsafe_proposal = removeSecurityProxy(proposal)
261+ unsafe_proposal.merged_revno = merged_revno
262 if preview_diff is not None:
263 unsafe_proposal.preview_diff = preview_diff
264 if (set_state is None or
265@@ -1638,8 +1640,12 @@
266 '', parent.revision_id, None, None, None)
267 branch.updateScannedDetails(parent, sequence)
268
269- def makeBranchRevision(self, branch, revision_id=None, sequence=None,
270+ def makeBranchRevision(self, branch=None, revision_id=None, sequence=None,
271 parent_ids=None, revision_date=None):
272+ if branch is None:
273+ branch = self.makeBranch()
274+ else:
275+ branch = removeSecurityProxy(branch)
276 revision = self.makeRevision(
277 rev_id=revision_id, parent_ids=parent_ids,
278 revision_date=revision_date)