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

Proposed by Colin Watson
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 Approve
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.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py 2015-11-23 11:34:15 +0000
+++ lib/lp/code/interfaces/gitref.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Git reference ("ref") interfaces."""4"""Git reference ("ref") interfaces."""
@@ -47,11 +47,12 @@
47 GitObjectType,47 GitObjectType,
48 )48 )
49from lp.code.interfaces.hasbranches import IHasMergeProposals49from lp.code.interfaces.hasbranches import IHasMergeProposals
50from lp.code.interfaces.hasrecipes import IHasRecipes
50from lp.registry.interfaces.person import IPerson51from lp.registry.interfaces.person import IPerson
51from lp.services.webapp.interfaces import ITableBatchNavigator52from lp.services.webapp.interfaces import ITableBatchNavigator
5253
5354
54class IGitRef(IHasMergeProposals, IPrivacy, IInformationType):55class IGitRef(IHasMergeProposals, IHasRecipes, IPrivacy, IInformationType):
55 """A reference in a Git repository."""56 """A reference in a Git repository."""
5657
57 # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL58 # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
5859
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-11-02 15:31:39 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Git repository interfaces."""4"""Git repository interfaces."""
@@ -64,6 +64,7 @@
64 )64 )
65from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository65from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
66from lp.code.interfaces.hasgitrepositories import IHasGitRepositories66from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
67from lp.code.interfaces.hasrecipes import IHasRecipes
67from lp.registry.interfaces.distributionsourcepackage import (68from lp.registry.interfaces.distributionsourcepackage import (
68 IDistributionSourcePackage,69 IDistributionSourcePackage,
69 )70 )
@@ -118,7 +119,7 @@
118 return True119 return True
119120
120121
121class IGitRepositoryView(Interface):122class IGitRepositoryView(IHasRecipes):
122 """IGitRepository attributes that require launchpad.View permission."""123 """IGitRepository attributes that require launchpad.View permission."""
123124
124 id = Int(title=_("ID"), readonly=True, required=True)125 id = Int(title=_("ID"), readonly=True, required=True)
125126
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2015-11-23 11:34:15 +0000
+++ lib/lp/code/model/gitref.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -242,6 +242,18 @@
242 """See `IGitRef`."""242 """See `IGitRef`."""
243 return self.repository.pending_writes243 return self.repository.pending_writes
244244
245 @property
246 def recipes(self):
247 """See `IHasRecipes`."""
248 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
249 from lp.code.model.sourcepackagerecipedata import (
250 SourcePackageRecipeData,
251 )
252 recipes = SourcePackageRecipeData.findRecipes(
253 self.repository, revspecs=list(set([self.path, self.name])))
254 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
255 return DecoratedResultSet(recipes, pre_iter_hook=hook)
256
245257
246@implementer(IGitRef)258@implementer(IGitRef)
247class GitRef(StormBase, GitRefMixin):259class GitRef(StormBase, GitRefMixin):
248260
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-12-10 00:05:41 +0000
+++ lib/lp/code/model/gitrepository.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -130,6 +130,7 @@
130 DEFAULT,130 DEFAULT,
131 UTC_NOW,131 UTC_NOW,
132 )132 )
133from lp.services.database.decoratedresultset import DecoratedResultSet
133from lp.services.database.enumcol import EnumCol134from lp.services.database.enumcol import EnumCol
134from lp.services.database.interfaces import IStore135from lp.services.database.interfaces import IStore
135from lp.services.database.stormbase import StormBase136from lp.services.database.stormbase import StormBase
@@ -953,6 +954,29 @@
953 jobs.append(UpdatePreviewDiffJob.create(merge_proposal))954 jobs.append(UpdatePreviewDiffJob.create(merge_proposal))
954 return jobs955 return jobs
955956
957 def _getRecipes(self, paths=None):
958 """Undecorated version of recipes for use by `markRecipesStale`."""
959 from lp.code.model.sourcepackagerecipedata import (
960 SourcePackageRecipeData,
961 )
962 if paths is not None:
963 revspecs = set()
964 for path in paths:
965 revspecs.add(path)
966 if path.startswith("refs/heads/"):
967 revspecs.add(path[len("refs/heads/"):])
968 revspecs = list(revspecs)
969 else:
970 revspecs = None
971 return SourcePackageRecipeData.findRecipes(self, revspecs=revspecs)
972
973 @property
974 def recipes(self):
975 """See `IHasRecipes`."""
976 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
977 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
978 return DecoratedResultSet(self._getRecipes(), pre_iter_hook=hook)
979
956 def _markProposalMerged(self, proposal, merged_revision_id, logger=None):980 def _markProposalMerged(self, proposal, merged_revision_id, logger=None):
957 if logger is not None:981 if logger is not None:
958 logger.info(982 logger.info(
959983
=== modified file 'lib/lp/code/model/sourcepackagerecipedata.py'
--- lib/lp/code/model/sourcepackagerecipedata.py 2016-01-14 17:24:45 +0000
+++ lib/lp/code/model/sourcepackagerecipedata.py 2016-01-14 17:24:45 +0000
@@ -30,6 +30,7 @@
30from storm.expr import Union30from storm.expr import Union
31from storm.locals import (31from storm.locals import (
32 And,32 And,
33 In,
33 Int,34 Int,
34 Reference,35 Reference,
35 ReferenceSet,36 ReferenceSet,
@@ -244,21 +245,50 @@
244 return parser.parse(permitted_instructions=SAFE_INSTRUCTIONS)245 return parser.parse(permitted_instructions=SAFE_INSTRUCTIONS)
245246
246 @staticmethod247 @staticmethod
247 def findRecipes(branch):248 def findRecipes(branch_or_repository, revspecs=None):
249 """Find recipes for a given branch or repository.
250
251 :param branch_or_repository: The branch or repository to search for.
252 :param revspecs: If not None, return only recipes whose `revspec` is
253 in this sequence.
254 :return: a collection of `ISourcePackageRecipe`s.
255 """
248 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe256 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
249 store = Store.of(branch)257 store = Store.of(branch_or_repository)
258 if IGitRepository.providedBy(branch_or_repository):
259 data_clause = (
260 SourcePackageRecipeData.base_git_repository ==
261 branch_or_repository)
262 insn_clause = (
263 _SourcePackageRecipeDataInstruction.git_repository ==
264 branch_or_repository)
265 elif IBranch.providedBy(branch_or_repository):
266 data_clause = (
267 SourcePackageRecipeData.base_branch == branch_or_repository)
268 insn_clause = (
269 _SourcePackageRecipeDataInstruction.branch ==
270 branch_or_repository)
271 else:
272 raise AssertionError(
273 "Unsupported source: %r" % (branch_or_repository,))
274 if revspecs is not None:
275 data_clause = And(
276 data_clause, In(SourcePackageRecipeData.revspec, revspecs))
277 insn_clause = And(
278 insn_clause,
279 In(_SourcePackageRecipeDataInstruction.revspec, revspecs))
250 return store.find(280 return store.find(
251 SourcePackageRecipe,281 SourcePackageRecipe,
252 SourcePackageRecipe.id.is_in(Union(282 SourcePackageRecipe.id.is_in(Union(
253 Select(283 Select(
254 SourcePackageRecipeData.sourcepackage_recipe_id,284 SourcePackageRecipeData.sourcepackage_recipe_id,
255 SourcePackageRecipeData.base_branch == branch),285 data_clause),
256 Select(286 Select(
257 SourcePackageRecipeData.sourcepackage_recipe_id,287 SourcePackageRecipeData.sourcepackage_recipe_id,
258 And(288 And(
259 _SourcePackageRecipeDataInstruction.recipe_data_id ==289 _SourcePackageRecipeDataInstruction.recipe_data_id ==
260 SourcePackageRecipeData.id,290 SourcePackageRecipeData.id,
261 _SourcePackageRecipeDataInstruction.branch == branch)291 insn_clause)
262 )292 )
263 ))293 ))
264 )294 )
265295
=== modified file 'lib/lp/code/model/tests/test_hasrecipes.py'
--- lib/lp/code/model/tests/test_hasrecipes.py 2015-09-16 13:26:12 +0000
+++ lib/lp/code/model/tests/test_hasrecipes.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2015 Canonical Ltd. This software is licensed under the1# Copyright 2010-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for classes that implement IHasRecipes."""4"""Tests for classes that implement IHasRecipes."""
@@ -39,6 +39,33 @@
39 self.factory.makeSourcePackageRecipe()39 self.factory.makeSourcePackageRecipe()
40 self.assertEqual(recipe, nonbase_branch.recipes.one())40 self.assertEqual(recipe, nonbase_branch.recipes.one())
4141
42 def test_git_repository_implements_hasrecipes(self):
43 # Git repositories should implement IHasRecipes.
44 repository = self.factory.makeGitRepository()
45 self.assertProvides(repository, IHasRecipes)
46
47 def test_git_repository_recipes(self):
48 # IGitRepository.recipes should provide all the SourcePackageRecipes
49 # attached to that repository.
50 base_ref1, base_ref2 = self.factory.makeGitRefs(
51 paths=[u"refs/heads/ref1", u"refs/heads/ref2"])
52 [other_ref] = self.factory.makeGitRefs()
53 self.factory.makeSourcePackageRecipe(branches=[base_ref1])
54 self.factory.makeSourcePackageRecipe(branches=[base_ref2])
55 self.factory.makeSourcePackageRecipe(branches=[other_ref])
56 self.assertEqual(2, base_ref1.repository.recipes.count())
57
58 def test_git_repository_recipes_nonbase(self):
59 # IGitRepository.recipes should provide all the SourcePackageRecipes
60 # that refer to the repository, even as a non-base branch.
61 [base_ref] = self.factory.makeGitRefs()
62 [nonbase_ref] = self.factory.makeGitRefs()
63 [other_ref] = self.factory.makeGitRefs()
64 recipe = self.factory.makeSourcePackageRecipe(
65 branches=[base_ref, nonbase_ref])
66 self.factory.makeSourcePackageRecipe(branches=[other_ref])
67 self.assertEqual(recipe, nonbase_ref.repository.recipes.one())
68
42 def test_person_implements_hasrecipes(self):69 def test_person_implements_hasrecipes(self):
43 # Person should implement IHasRecipes.70 # Person should implement IHasRecipes.
44 person = self.factory.makePerson()71 person = self.factory.makePerson()
@@ -60,10 +87,16 @@
6087
61 def test_product_recipes(self):88 def test_product_recipes(self):
62 # IProduct.recipes should provide all the SourcePackageRecipes89 # IProduct.recipes should provide all the SourcePackageRecipes
63 # attached to that product's branches.90 # attached to that product's branches and Git repositories.
64 product = self.factory.makeProduct()91 product = self.factory.makeProduct()
65 branch = self.factory.makeBranch(product=product)92 branch = self.factory.makeBranch(product=product)
66 self.factory.makeSourcePackageRecipe(branches=[branch])93 [ref] = self.factory.makeGitRefs(target=product)
67 self.factory.makeSourcePackageRecipe(branches=[branch])94 recipe1 = self.factory.makeSourcePackageRecipe(branches=[branch])
95 recipe2 = self.factory.makeSourcePackageRecipe(branches=[branch])
68 self.factory.makeSourcePackageRecipe()96 self.factory.makeSourcePackageRecipe()
69 self.assertEqual(2, product.recipes.count())97 recipe3 = self.factory.makeSourcePackageRecipe(branches=[ref])
98 recipe4 = self.factory.makeSourcePackageRecipe(branches=[ref])
99 self.factory.makeSourcePackageRecipe(
100 branches=self.factory.makeGitRefs())
101 self.assertContentEqual(
102 [recipe1, recipe2, recipe3, recipe4], product.recipes)
70103
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-14 17:24:45 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-14 17:24:45 +0000
@@ -1098,14 +1098,10 @@
1098 self.recipe, 'date_last_modified', UTC_NOW)1098 self.recipe, 'date_last_modified', UTC_NOW)
10991099
11001100
1101class TestWebservice(TestCaseWithFactory):1101class TestWebserviceMixin:
11021102
1103 layer = AppServerLayer1103 layer = AppServerLayer
11041104
1105 def makeRecipeText(self):
1106 branch = self.factory.makeBranch()
1107 return MINIMAL_RECIPE_TEXT_BZR % branch.bzr_identity
1108
1109 def makeRecipe(self, user=None, owner=None, recipe_text=None,1105 def makeRecipe(self, user=None, owner=None, recipe_text=None,
1110 version='devel'):1106 version='devel'):
1111 # rockstar 21 Jul 2010 - This function does more commits than I'd1107 # rockstar 21 Jul 2010 - This function does more commits than I'd
@@ -1279,3 +1275,11 @@
1279 with StormStatementRecorder() as recorder:1275 with StormStatementRecorder() as recorder:
1280 webservice.get(url)1276 webservice.get(url)
1281 self.assertThat(recorder, HasQueryCount(Equals(23)))1277 self.assertThat(recorder, HasQueryCount(Equals(23)))
1278
1279
1280class TestWebserviceBzr(TestWebserviceMixin, BzrMixin, TestCaseWithFactory):
1281 pass
1282
1283
1284class TestWebserviceGit(TestWebserviceMixin, GitMixin, TestCaseWithFactory):
1285 pass
12821286
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2015-10-01 17:32:41 +0000
+++ lib/lp/registry/model/product.py 2016-01-14 17:24:45 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes including and related to Product."""4"""Database classes including and related to Product."""
@@ -122,6 +122,7 @@
122from lp.code.interfaces.gitrepository import IGitRepositorySet122from lp.code.interfaces.gitrepository import IGitRepositorySet
123from lp.code.model.branch import Branch123from lp.code.model.branch import Branch
124from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES124from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
125from lp.code.model.gitrepository import GitRepository
125from lp.code.model.hasbranches import (126from lp.code.model.hasbranches import (
126 HasBranchesMixin,127 HasBranchesMixin,
127 HasCodeImportsMixin,128 HasCodeImportsMixin,
@@ -1574,12 +1575,20 @@
1574 @property1575 @property
1575 def recipes(self):1576 def recipes(self):
1576 """See `IHasRecipes`."""1577 """See `IHasRecipes`."""
1577 recipes = Store.of(self).find(1578 tables = [
1579 SourcePackageRecipe,
1580 SourcePackageRecipeData,
1581 LeftJoin(Branch, SourcePackageRecipeData.base_branch == Branch.id),
1582 LeftJoin(
1583 GitRepository,
1584 SourcePackageRecipeData.base_git_repository ==
1585 GitRepository.id),
1586 ]
1587 recipes = Store.of(self).using(*tables).find(
1578 SourcePackageRecipe,1588 SourcePackageRecipe,
1579 SourcePackageRecipe.id ==1589 SourcePackageRecipe.id ==
1580 SourcePackageRecipeData.sourcepackage_recipe_id,1590 SourcePackageRecipeData.sourcepackage_recipe_id,
1581 SourcePackageRecipeData.base_branch == Branch.id,1591 Or(Branch.product == self, GitRepository.project == self))
1582 Branch.product == self)
1583 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes1592 hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
1584 return DecoratedResultSet(recipes, pre_iter_hook=hook)1593 return DecoratedResultSet(recipes, pre_iter_hook=hook)
15851594