Merge lp:~cjwatson/launchpad/git-recipe-find into lp:launchpad

Proposed by Colin Watson on 2016-01-12
Status: Merged
Merged at revision: 17894
Proposed branch: lp:~cjwatson/launchpad/git-recipe-find
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-recipe-model
Diff against target: 332 lines (+138/-24)
8 files modified
lib/lp/code/interfaces/gitref.py (+3/-2)
lib/lp/code/interfaces/gitrepository.py (+3/-2)
lib/lp/code/model/gitref.py (+13/-1)
lib/lp/code/model/gitrepository.py (+25/-1)
lib/lp/code/model/sourcepackagerecipedata.py (+34/-4)
lib/lp/code/model/tests/test_hasrecipes.py (+38/-5)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+9/-5)
lib/lp/registry/model/product.py (+13/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-recipe-find
Reviewer Review Type Date Requested Status
William Grant code 2016-01-12 Approve on 2016-01-14
Review via email: mp+282255@code.launchpad.net

This proposal supersedes a proposal from 2016-01-12.

Commit message

Add IHasRecipes implementations and other methods for finding Git recipes.

Description of the change

Add IHasRecipes implementations and other methods for finding Git recipes.

The changes to Product.recipes aren't specifically tested here, mainly because the existing tests for that rely on having browser code in place. I have test changes for that which I'll propose in a later branch.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/interfaces/gitref.py'
2--- lib/lp/code/interfaces/gitref.py 2015-11-23 11:34:15 +0000
3+++ lib/lp/code/interfaces/gitref.py 2016-01-14 17:24:45 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Git reference ("ref") interfaces."""
10@@ -47,11 +47,12 @@
11 GitObjectType,
12 )
13 from lp.code.interfaces.hasbranches import IHasMergeProposals
14+from lp.code.interfaces.hasrecipes import IHasRecipes
15 from lp.registry.interfaces.person import IPerson
16 from lp.services.webapp.interfaces import ITableBatchNavigator
17
18
19-class IGitRef(IHasMergeProposals, IPrivacy, IInformationType):
20+class IGitRef(IHasMergeProposals, IHasRecipes, IPrivacy, IInformationType):
21 """A reference in a Git repository."""
22
23 # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
24
25=== modified file 'lib/lp/code/interfaces/gitrepository.py'
26--- lib/lp/code/interfaces/gitrepository.py 2015-11-02 15:31:39 +0000
27+++ lib/lp/code/interfaces/gitrepository.py 2016-01-14 17:24:45 +0000
28@@ -1,4 +1,4 @@
29-# Copyright 2015 Canonical Ltd. This software is licensed under the
30+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
31 # GNU Affero General Public License version 3 (see the file LICENSE).
32
33 """Git repository interfaces."""
34@@ -64,6 +64,7 @@
35 )
36 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
37 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
38+from lp.code.interfaces.hasrecipes import IHasRecipes
39 from lp.registry.interfaces.distributionsourcepackage import (
40 IDistributionSourcePackage,
41 )
42@@ -118,7 +119,7 @@
43 return True
44
45
46-class IGitRepositoryView(Interface):
47+class IGitRepositoryView(IHasRecipes):
48 """IGitRepository attributes that require launchpad.View permission."""
49
50 id = Int(title=_("ID"), readonly=True, required=True)
51
52=== modified file 'lib/lp/code/model/gitref.py'
53--- lib/lp/code/model/gitref.py 2015-11-23 11:34:15 +0000
54+++ lib/lp/code/model/gitref.py 2016-01-14 17:24:45 +0000
55@@ -1,4 +1,4 @@
56-# Copyright 2015 Canonical Ltd. This software is licensed under the
57+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
58 # GNU Affero General Public License version 3 (see the file LICENSE).
59
60 __metaclass__ = type
61@@ -242,6 +242,18 @@
62 """See `IGitRef`."""
63 return self.repository.pending_writes
64
65+ @property
66+ def recipes(self):
67+ """See `IHasRecipes`."""
68+ from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
69+ from lp.code.model.sourcepackagerecipedata import (
70+ SourcePackageRecipeData,
71+ )
72+ recipes = SourcePackageRecipeData.findRecipes(
73+ self.repository, revspecs=list(set([self.path, self.name])))
74+ hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
75+ return DecoratedResultSet(recipes, pre_iter_hook=hook)
76+
77
78 @implementer(IGitRef)
79 class GitRef(StormBase, GitRefMixin):
80
81=== modified file 'lib/lp/code/model/gitrepository.py'
82--- lib/lp/code/model/gitrepository.py 2015-12-10 00:05:41 +0000
83+++ lib/lp/code/model/gitrepository.py 2016-01-14 17:24:45 +0000
84@@ -1,4 +1,4 @@
85-# Copyright 2015 Canonical Ltd. This software is licensed under the
86+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
87 # GNU Affero General Public License version 3 (see the file LICENSE).
88
89 __metaclass__ = type
90@@ -130,6 +130,7 @@
91 DEFAULT,
92 UTC_NOW,
93 )
94+from lp.services.database.decoratedresultset import DecoratedResultSet
95 from lp.services.database.enumcol import EnumCol
96 from lp.services.database.interfaces import IStore
97 from lp.services.database.stormbase import StormBase
98@@ -953,6 +954,29 @@
99 jobs.append(UpdatePreviewDiffJob.create(merge_proposal))
100 return jobs
101
102+ def _getRecipes(self, paths=None):
103+ """Undecorated version of recipes for use by `markRecipesStale`."""
104+ from lp.code.model.sourcepackagerecipedata import (
105+ SourcePackageRecipeData,
106+ )
107+ if paths is not None:
108+ revspecs = set()
109+ for path in paths:
110+ revspecs.add(path)
111+ if path.startswith("refs/heads/"):
112+ revspecs.add(path[len("refs/heads/"):])
113+ revspecs = list(revspecs)
114+ else:
115+ revspecs = None
116+ return SourcePackageRecipeData.findRecipes(self, revspecs=revspecs)
117+
118+ @property
119+ def recipes(self):
120+ """See `IHasRecipes`."""
121+ from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
122+ hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
123+ return DecoratedResultSet(self._getRecipes(), pre_iter_hook=hook)
124+
125 def _markProposalMerged(self, proposal, merged_revision_id, logger=None):
126 if logger is not None:
127 logger.info(
128
129=== modified file 'lib/lp/code/model/sourcepackagerecipedata.py'
130--- lib/lp/code/model/sourcepackagerecipedata.py 2016-01-14 17:24:45 +0000
131+++ lib/lp/code/model/sourcepackagerecipedata.py 2016-01-14 17:24:45 +0000
132@@ -30,6 +30,7 @@
133 from storm.expr import Union
134 from storm.locals import (
135 And,
136+ In,
137 Int,
138 Reference,
139 ReferenceSet,
140@@ -244,21 +245,50 @@
141 return parser.parse(permitted_instructions=SAFE_INSTRUCTIONS)
142
143 @staticmethod
144- def findRecipes(branch):
145+ def findRecipes(branch_or_repository, revspecs=None):
146+ """Find recipes for a given branch or repository.
147+
148+ :param branch_or_repository: The branch or repository to search for.
149+ :param revspecs: If not None, return only recipes whose `revspec` is
150+ in this sequence.
151+ :return: a collection of `ISourcePackageRecipe`s.
152+ """
153 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
154- store = Store.of(branch)
155+ store = Store.of(branch_or_repository)
156+ if IGitRepository.providedBy(branch_or_repository):
157+ data_clause = (
158+ SourcePackageRecipeData.base_git_repository ==
159+ branch_or_repository)
160+ insn_clause = (
161+ _SourcePackageRecipeDataInstruction.git_repository ==
162+ branch_or_repository)
163+ elif IBranch.providedBy(branch_or_repository):
164+ data_clause = (
165+ SourcePackageRecipeData.base_branch == branch_or_repository)
166+ insn_clause = (
167+ _SourcePackageRecipeDataInstruction.branch ==
168+ branch_or_repository)
169+ else:
170+ raise AssertionError(
171+ "Unsupported source: %r" % (branch_or_repository,))
172+ if revspecs is not None:
173+ data_clause = And(
174+ data_clause, In(SourcePackageRecipeData.revspec, revspecs))
175+ insn_clause = And(
176+ insn_clause,
177+ In(_SourcePackageRecipeDataInstruction.revspec, revspecs))
178 return store.find(
179 SourcePackageRecipe,
180 SourcePackageRecipe.id.is_in(Union(
181 Select(
182 SourcePackageRecipeData.sourcepackage_recipe_id,
183- SourcePackageRecipeData.base_branch == branch),
184+ data_clause),
185 Select(
186 SourcePackageRecipeData.sourcepackage_recipe_id,
187 And(
188 _SourcePackageRecipeDataInstruction.recipe_data_id ==
189 SourcePackageRecipeData.id,
190- _SourcePackageRecipeDataInstruction.branch == branch)
191+ insn_clause)
192 )
193 ))
194 )
195
196=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
197--- lib/lp/code/model/tests/test_hasrecipes.py 2015-09-16 13:26:12 +0000
198+++ lib/lp/code/model/tests/test_hasrecipes.py 2016-01-14 17:24:45 +0000
199@@ -1,4 +1,4 @@
200-# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
201+# Copyright 2010-2016 Canonical Ltd. This software is licensed under the
202 # GNU Affero General Public License version 3 (see the file LICENSE).
203
204 """Tests for classes that implement IHasRecipes."""
205@@ -39,6 +39,33 @@
206 self.factory.makeSourcePackageRecipe()
207 self.assertEqual(recipe, nonbase_branch.recipes.one())
208
209+ def test_git_repository_implements_hasrecipes(self):
210+ # Git repositories should implement IHasRecipes.
211+ repository = self.factory.makeGitRepository()
212+ self.assertProvides(repository, IHasRecipes)
213+
214+ def test_git_repository_recipes(self):
215+ # IGitRepository.recipes should provide all the SourcePackageRecipes
216+ # attached to that repository.
217+ base_ref1, base_ref2 = self.factory.makeGitRefs(
218+ paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
219+ [other_ref] = self.factory.makeGitRefs()
220+ self.factory.makeSourcePackageRecipe(branches=[base_ref1])
221+ self.factory.makeSourcePackageRecipe(branches=[base_ref2])
222+ self.factory.makeSourcePackageRecipe(branches=[other_ref])
223+ self.assertEqual(2, base_ref1.repository.recipes.count())
224+
225+ def test_git_repository_recipes_nonbase(self):
226+ # IGitRepository.recipes should provide all the SourcePackageRecipes
227+ # that refer to the repository, even as a non-base branch.
228+ [base_ref] = self.factory.makeGitRefs()
229+ [nonbase_ref] = self.factory.makeGitRefs()
230+ [other_ref] = self.factory.makeGitRefs()
231+ recipe = self.factory.makeSourcePackageRecipe(
232+ branches=[base_ref, nonbase_ref])
233+ self.factory.makeSourcePackageRecipe(branches=[other_ref])
234+ self.assertEqual(recipe, nonbase_ref.repository.recipes.one())
235+
236 def test_person_implements_hasrecipes(self):
237 # Person should implement IHasRecipes.
238 person = self.factory.makePerson()
239@@ -60,10 +87,16 @@
240
241 def test_product_recipes(self):
242 # IProduct.recipes should provide all the SourcePackageRecipes
243- # attached to that product's branches.
244+ # attached to that product's branches and Git repositories.
245 product = self.factory.makeProduct()
246 branch = self.factory.makeBranch(product=product)
247- self.factory.makeSourcePackageRecipe(branches=[branch])
248- self.factory.makeSourcePackageRecipe(branches=[branch])
249+ [ref] = self.factory.makeGitRefs(target=product)
250+ recipe1 = self.factory.makeSourcePackageRecipe(branches=[branch])
251+ recipe2 = self.factory.makeSourcePackageRecipe(branches=[branch])
252 self.factory.makeSourcePackageRecipe()
253- self.assertEqual(2, product.recipes.count())
254+ recipe3 = self.factory.makeSourcePackageRecipe(branches=[ref])
255+ recipe4 = self.factory.makeSourcePackageRecipe(branches=[ref])
256+ self.factory.makeSourcePackageRecipe(
257+ branches=self.factory.makeGitRefs())
258+ self.assertContentEqual(
259+ [recipe1, recipe2, recipe3, recipe4], product.recipes)
260
261=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
262--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-14 17:24:45 +0000
263+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-14 17:24:45 +0000
264@@ -1098,14 +1098,10 @@
265 self.recipe, 'date_last_modified', UTC_NOW)
266
267
268-class TestWebservice(TestCaseWithFactory):
269+class TestWebserviceMixin:
270
271 layer = AppServerLayer
272
273- def makeRecipeText(self):
274- branch = self.factory.makeBranch()
275- return MINIMAL_RECIPE_TEXT_BZR % branch.bzr_identity
276-
277 def makeRecipe(self, user=None, owner=None, recipe_text=None,
278 version='devel'):
279 # rockstar 21 Jul 2010 - This function does more commits than I'd
280@@ -1279,3 +1275,11 @@
281 with StormStatementRecorder() as recorder:
282 webservice.get(url)
283 self.assertThat(recorder, HasQueryCount(Equals(23)))
284+
285+
286+class TestWebserviceBzr(TestWebserviceMixin, BzrMixin, TestCaseWithFactory):
287+ pass
288+
289+
290+class TestWebserviceGit(TestWebserviceMixin, GitMixin, TestCaseWithFactory):
291+ pass
292
293=== modified file 'lib/lp/registry/model/product.py'
294--- lib/lp/registry/model/product.py 2015-10-01 17:32:41 +0000
295+++ lib/lp/registry/model/product.py 2016-01-14 17:24:45 +0000
296@@ -1,4 +1,4 @@
297-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
298+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
299 # GNU Affero General Public License version 3 (see the file LICENSE).
300
301 """Database classes including and related to Product."""
302@@ -122,6 +122,7 @@
303 from lp.code.interfaces.gitrepository import IGitRepositorySet
304 from lp.code.model.branch import Branch
305 from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
306+from lp.code.model.gitrepository import GitRepository
307 from lp.code.model.hasbranches import (
308 HasBranchesMixin,
309 HasCodeImportsMixin,
310@@ -1574,12 +1575,20 @@
311 @property
312 def recipes(self):
313 """See `IHasRecipes`."""
314- recipes = Store.of(self).find(
315+ tables = [
316+ SourcePackageRecipe,
317+ SourcePackageRecipeData,
318+ LeftJoin(Branch, SourcePackageRecipeData.base_branch == Branch.id),
319+ LeftJoin(
320+ GitRepository,
321+ SourcePackageRecipeData.base_git_repository ==
322+ GitRepository.id),
323+ ]
324+ recipes = Store.of(self).using(*tables).find(
325 SourcePackageRecipe,
326 SourcePackageRecipe.id ==
327 SourcePackageRecipeData.sourcepackage_recipe_id,
328- SourcePackageRecipeData.base_branch == Branch.id,
329- Branch.product == self)
330+ Or(Branch.product == self, GitRepository.project == self))
331 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
332 return DecoratedResultSet(recipes, pre_iter_hook=hook)
333