Merge lp:~cjwatson/launchpad/git-recipe-browser-create into lp:launchpad
- git-recipe-browser-create
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17901 | ||||
Proposed branch: | lp:~cjwatson/launchpad/git-recipe-browser-create | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/git-recipe-browser-listing | ||||
Diff against target: |
1107 lines (+346/-152) 17 files modified
lib/lp/code/browser/configure.zcml (+12/-0) lib/lp/code/browser/gitref.py (+13/-1) lib/lp/code/browser/gitrepository.py (+10/-1) lib/lp/code/browser/sourcepackagerecipe.py (+29/-4) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+200/-139) lib/lp/code/browser/tests/test_tales.py (+1/-1) lib/lp/code/errors.py (+10/-0) lib/lp/code/help/related-recipes.html (+2/-2) lib/lp/code/interfaces/sourcepackagerecipe.py (+4/-0) lib/lp/code/model/sourcepackagerecipe.py (+12/-0) lib/lp/code/model/tests/test_gitrepository.py (+8/-0) lib/lp/code/model/tests/test_hasrecipes.py (+5/-0) lib/lp/code/model/tests/test_recipebuilder.py (+3/-0) lib/lp/code/model/tests/test_sourcepackagerecipe.py (+13/-0) lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt (+10/-4) lib/lp/code/templates/gitref-recipes.pt (+7/-0) lib/lp/code/templates/gitrepository-recipes.pt (+7/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-recipe-browser-create | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+282324@code.launchpad.net |
Commit message
Add views to create new Git recipes.
Description of the change
Add views to create new Git recipes.
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
1 | === modified file 'lib/lp/code/browser/configure.zcml' |
2 | --- lib/lp/code/browser/configure.zcml 2016-01-12 15:10:57 +0000 |
3 | +++ lib/lp/code/browser/configure.zcml 2016-01-20 12:22:22 +0000 |
4 | @@ -1219,6 +1219,18 @@ |
5 | name="+new-recipe" |
6 | template="../templates/sourcepackagerecipe-new.pt"/> |
7 | <browser:page |
8 | + for="lp.code.interfaces.gitrepository.IGitRepository" |
9 | + class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView" |
10 | + permission="launchpad.AnyPerson" |
11 | + name="+new-recipe" |
12 | + template="../templates/sourcepackagerecipe-new.pt"/> |
13 | + <browser:page |
14 | + for="lp.code.interfaces.gitref.IGitRef" |
15 | + class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeAddView" |
16 | + permission="launchpad.AnyPerson" |
17 | + name="+new-recipe" |
18 | + template="../templates/sourcepackagerecipe-new.pt"/> |
19 | + <browser:page |
20 | for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe" |
21 | class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeEditView" |
22 | permission="launchpad.Edit" |
23 | |
24 | === modified file 'lib/lp/code/browser/gitref.py' |
25 | --- lib/lp/code/browser/gitref.py 2016-01-12 15:10:57 +0000 |
26 | +++ lib/lp/code/browser/gitref.py 2016-01-20 12:22:22 +0000 |
27 | @@ -41,6 +41,8 @@ |
28 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
29 | from lp.code.interfaces.gitref import IGitRef |
30 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
31 | +from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG |
32 | +from lp.services.features import getFeatureFlag |
33 | from lp.services.helpers import english_list |
34 | from lp.services.propertycache import cachedproperty |
35 | from lp.services.webapp import ( |
36 | @@ -62,7 +64,9 @@ |
37 | |
38 | usedfor = IGitRef |
39 | facet = 'branches' |
40 | - links = ['create_snap', 'register_merge', 'source', 'view_recipes'] |
41 | + links = [ |
42 | + 'create_recipe', 'create_snap', 'register_merge', 'source', |
43 | + 'view_recipes'] |
44 | |
45 | def source(self): |
46 | """Return a link to the branch's browsing interface.""" |
47 | @@ -75,6 +79,14 @@ |
48 | enabled = self.context.namespace.supports_merge_proposals |
49 | return Link('+register-merge', text, icon='add', enabled=enabled) |
50 | |
51 | + def create_recipe(self): |
52 | + # You can't create a recipe for a reference in a private repository. |
53 | + enabled = ( |
54 | + not self.context.private and |
55 | + bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG))) |
56 | + text = "Create packaging recipe" |
57 | + return Link("+new-recipe", text, enabled=enabled, icon="add") |
58 | + |
59 | |
60 | class GitRefView(LaunchpadView, HasSnapsViewMixin): |
61 | |
62 | |
63 | === modified file 'lib/lp/code/browser/gitrepository.py' |
64 | --- lib/lp/code/browser/gitrepository.py 2016-01-12 15:10:57 +0000 |
65 | +++ lib/lp/code/browser/gitrepository.py 2016-01-20 12:22:22 +0000 |
66 | @@ -70,6 +70,7 @@ |
67 | IPerson, |
68 | IPersonSet, |
69 | ) |
70 | +from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG |
71 | from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary |
72 | from lp.services.config import config |
73 | from lp.services.database.constants import UTC_NOW |
74 | @@ -210,7 +211,7 @@ |
75 | usedfor = IGitRepository |
76 | facet = "branches" |
77 | links = [ |
78 | - "add_subscriber", "source", "subscription", |
79 | + "add_subscriber", "create_recipe", "source", "subscription", |
80 | "view_recipes", "visibility"] |
81 | |
82 | @enabled_with_permission("launchpad.AnyPerson") |
83 | @@ -242,6 +243,14 @@ |
84 | text = "Change information type" |
85 | return Link("+edit-information-type", text) |
86 | |
87 | + def create_recipe(self): |
88 | + # You can't create a recipe for a private repository. |
89 | + enabled = ( |
90 | + not self.context.private and |
91 | + bool(getFeatureFlag(GIT_RECIPES_FEATURE_FLAG))) |
92 | + text = "Create packaging recipe" |
93 | + return Link("+new-recipe", text, enabled=enabled, icon="add") |
94 | + |
95 | |
96 | @implementer(IGitRefBatchNavigator) |
97 | class GitRefBatchNavigator(TableBatchNavigator): |
98 | |
99 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' |
100 | --- lib/lp/code/browser/sourcepackagerecipe.py 2016-01-15 12:42:28 +0000 |
101 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000 |
102 | @@ -92,11 +92,14 @@ |
103 | ) |
104 | from lp.code.interfaces.branch import IBranch |
105 | from lp.code.interfaces.branchtarget import IBranchTarget |
106 | +from lp.code.interfaces.gitref import IGitRef |
107 | +from lp.code.interfaces.gitrepository import IGitRepository |
108 | from lp.code.interfaces.sourcepackagerecipe import ( |
109 | IRecipeBranchSource, |
110 | ISourcePackageRecipe, |
111 | ISourcePackageRecipeSource, |
112 | MINIMAL_RECIPE_TEXT_BZR, |
113 | + MINIMAL_RECIPE_TEXT_GIT, |
114 | ) |
115 | from lp.code.vocabularies.sourcepackagerecipe import BuildableDistroSeries |
116 | from lp.registry.interfaces.series import SeriesStatus |
117 | @@ -737,14 +740,18 @@ |
118 | self.form_fields['distroseries'].for_input = True |
119 | |
120 | def getBranch(self): |
121 | - """The branch on which the recipe is built.""" |
122 | + """The branch or repository on which the recipe is built.""" |
123 | return self.context |
124 | |
125 | def _recipe_names(self): |
126 | """A generator of recipe names.""" |
127 | # +junk-daily doesn't make a very good recipe name, so use the |
128 | - # branch name in that case. |
129 | - if self.context.target.allow_recipe_name_from_target: |
130 | + # branch name in that case; similarly for personal Git repositories. |
131 | + if ((IBranch.providedBy(self.context) and |
132 | + self.context.target.allow_recipe_name_from_target) or |
133 | + ((IGitRepository.providedBy(self.context) or |
134 | + IGitRef.providedBy(self.context)) and |
135 | + self.context.namespace.allow_recipe_name_from_target)): |
136 | branch_target_name = self.context.target.name.split('/')[-1] |
137 | else: |
138 | branch_target_name = self.context.name |
139 | @@ -765,9 +772,27 @@ |
140 | distroseries = BuildableDistroSeries.findSeries(self.user) |
141 | series = [series for series in distroseries if series.status in ( |
142 | SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)] |
143 | + if IBranch.providedBy(self.context): |
144 | + recipe_text = MINIMAL_RECIPE_TEXT_BZR % self.context.identity |
145 | + elif IGitRepository.providedBy(self.context): |
146 | + default_ref = None |
147 | + if self.context.default_branch is not None: |
148 | + default_ref = self.context.getRefByPath( |
149 | + self.context.default_branch) |
150 | + if default_ref is not None: |
151 | + branch_name = default_ref.name |
152 | + else: |
153 | + branch_name = "ENTER-BRANCH-NAME" |
154 | + recipe_text = MINIMAL_RECIPE_TEXT_GIT % ( |
155 | + self.context.identity, branch_name) |
156 | + elif IGitRef.providedBy(self.context): |
157 | + recipe_text = MINIMAL_RECIPE_TEXT_GIT % ( |
158 | + self.context.repository.identity, self.context.name) |
159 | + else: |
160 | + raise AssertionError("Unsupported context: %r" % (self.context,)) |
161 | return { |
162 | 'name': self._find_unused_name(self.user), |
163 | - 'recipe_text': MINIMAL_RECIPE_TEXT_BZR % self.context.bzr_identity, |
164 | + 'recipe_text': recipe_text, |
165 | 'owner': self.user, |
166 | 'distroseries': series, |
167 | 'build_daily': True, |
168 | |
169 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' |
170 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-12 12:28:09 +0000 |
171 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000 |
172 | @@ -36,6 +36,7 @@ |
173 | SourcePackageRecipeBuildView, |
174 | ) |
175 | from lp.code.interfaces.sourcepackagerecipe import ( |
176 | + GIT_RECIPES_FEATURE_FLAG, |
177 | MINIMAL_RECIPE_TEXT_BZR, |
178 | MINIMAL_RECIPE_TEXT_GIT, |
179 | ) |
180 | @@ -45,6 +46,7 @@ |
181 | from lp.registry.interfaces.series import SeriesStatus |
182 | from lp.registry.interfaces.teammembership import TeamMembershipStatus |
183 | from lp.services.database.constants import UTC_NOW |
184 | +from lp.services.features.testing import FeatureFixture |
185 | from lp.services.propertycache import clear_property_cache |
186 | from lp.services.webapp import canonical_url |
187 | from lp.services.webapp.escaping import html_escape |
188 | @@ -204,6 +206,10 @@ |
189 | branch_type = "repository" |
190 | no_such_object_message = "is not a Git repository on Launchpad." |
191 | |
192 | + def setUp(self): |
193 | + super(GitMixin, self).setUp() |
194 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
195 | + |
196 | def makeBranch(self, **kwargs): |
197 | return self.factory.makeGitRefs(**kwargs)[0] |
198 | |
199 | @@ -265,36 +271,10 @@ |
200 | daily_build_archive=self.ppa, **kwargs) |
201 | |
202 | |
203 | -class TestSourcePackageRecipeAddViewInitalValues(TestCaseWithFactory): |
204 | +class TestSourcePackageRecipeAddViewInitialValuesMixin: |
205 | |
206 | layer = DatabaseFunctionalLayer |
207 | |
208 | - def test_project_branch_initial_name(self): |
209 | - # When a project branch is used, the initial name is the name of the |
210 | - # project followed by "-daily" |
211 | - widget = self.factory.makeProduct(name='widget') |
212 | - branch = self.factory.makeProductBranch(widget) |
213 | - with person_logged_in(branch.owner): |
214 | - view = create_initialized_view(branch, '+new-recipe') |
215 | - self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
216 | - |
217 | - def test_package_branch_initial_name(self): |
218 | - # When a package branch is used, the initial name is the name of the |
219 | - # source package followed by "-daily" |
220 | - branch = self.factory.makePackageBranch(sourcepackagename='widget') |
221 | - with person_logged_in(branch.owner): |
222 | - view = create_initialized_view(branch, '+new-recipe') |
223 | - self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
224 | - |
225 | - def test_personal_branch_initial_name(self): |
226 | - # When a personal branch is used, the initial name is the name of the |
227 | - # branch followed by "-daily". +junk-daily is not valid nor |
228 | - # helpful. |
229 | - branch = self.factory.makePersonalBranch(name='widget') |
230 | - with person_logged_in(branch.owner): |
231 | - view = create_initialized_view(branch, '+new-recipe') |
232 | - self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
233 | - |
234 | def test_initial_name_exists(self): |
235 | # If the initial name exists, a generator is used to find an unused |
236 | # name by appending a numbered suffix on the end. |
237 | @@ -302,7 +282,7 @@ |
238 | self.factory.makeSourcePackageRecipe( |
239 | owner=owner, name=u'widget-daily') |
240 | widget = self.factory.makeProduct(name='widget') |
241 | - branch = self.factory.makeProductBranch(widget) |
242 | + branch = self.makeBranch(target=widget) |
243 | with person_logged_in(owner): |
244 | view = create_initialized_view(branch, '+new-recipe') |
245 | self.assertThat('widget-daily-1', Equals(view.initial_values['name'])) |
246 | @@ -331,7 +311,7 @@ |
247 | future = self.factory.makeDistroSeries( |
248 | distribution=archive.distribution, |
249 | status=SeriesStatus.FUTURE) |
250 | - branch = self.factory.makeAnyBranch() |
251 | + branch = self.makeBranch() |
252 | with person_logged_in(archive.owner): |
253 | view = create_initialized_view(branch, '+new-recipe') |
254 | series = set(view.initial_values['distroseries']) |
255 | @@ -342,30 +322,88 @@ |
256 | self.assertEqual(set(), series.intersection(other_series)) |
257 | |
258 | |
259 | -class TestSourcePackageRecipeAddView(BzrMixin, TestCaseForRecipe): |
260 | +class TestSourcePackageRecipeAddViewInitialValuesBzr( |
261 | + TestSourcePackageRecipeAddViewInitialValuesMixin, BzrMixin, |
262 | + TestCaseWithFactory): |
263 | + |
264 | + def test_project_branch_initial_name(self): |
265 | + # When a project branch is used, the initial name is the name of the |
266 | + # project followed by "-daily". |
267 | + widget = self.factory.makeProduct(name='widget') |
268 | + branch = self.factory.makeProductBranch(widget) |
269 | + with person_logged_in(branch.owner): |
270 | + view = create_initialized_view(branch, '+new-recipe') |
271 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
272 | + |
273 | + def test_package_branch_initial_name(self): |
274 | + # When a package branch is used, the initial name is the name of the |
275 | + # source package followed by "-daily". |
276 | + branch = self.factory.makePackageBranch(sourcepackagename='widget') |
277 | + with person_logged_in(branch.owner): |
278 | + view = create_initialized_view(branch, '+new-recipe') |
279 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
280 | + |
281 | + def test_personal_branch_initial_name(self): |
282 | + # When a personal branch is used, the initial name is the name of |
283 | + # the branch followed by "-daily". +junk-daily is neither valid nor |
284 | + # helpful. |
285 | + branch = self.factory.makePersonalBranch(name='widget') |
286 | + with person_logged_in(branch.owner): |
287 | + view = create_initialized_view(branch, '+new-recipe') |
288 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
289 | + |
290 | + |
291 | +class TestSourcePackageRecipeAddViewInitialValuesGit( |
292 | + TestSourcePackageRecipeAddViewInitialValuesMixin, GitMixin, |
293 | + TestCaseWithFactory): |
294 | + |
295 | + def test_project_repository_initial_name(self): |
296 | + # When a project repository is used, the initial name is the name of |
297 | + # the project followed by "-daily". |
298 | + widget = self.factory.makeProduct(name='widget') |
299 | + repository = self.factory.makeGitRepository(target=widget) |
300 | + with person_logged_in(repository.owner): |
301 | + view = create_initialized_view(repository, '+new-recipe') |
302 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
303 | + |
304 | + def test_package_repository_initial_name(self): |
305 | + # When a package repository is used, the initial name is the name of |
306 | + # the source package followed by "-daily". |
307 | + dsp = self.factory.makeDistributionSourcePackage( |
308 | + sourcepackagename='widget') |
309 | + repository = self.factory.makeGitRepository(target=dsp) |
310 | + with person_logged_in(repository.owner): |
311 | + view = create_initialized_view(repository, '+new-recipe') |
312 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
313 | + |
314 | + def test_personal_repository_initial_name(self): |
315 | + # When a personal repository is used, the initial name is the name |
316 | + # of the repository followed by "-daily". <person-name>-daily is |
317 | + # not helpful. |
318 | + owner = self.factory.makePerson() |
319 | + repository = self.factory.makeGitRepository( |
320 | + owner=owner, target=owner, name=u'widget') |
321 | + with person_logged_in(repository.owner): |
322 | + view = create_initialized_view(repository, '+new-recipe') |
323 | + self.assertThat('widget-daily', Equals(view.initial_values['name'])) |
324 | + |
325 | + |
326 | +class TestSourcePackageRecipeAddViewMixin: |
327 | |
328 | layer = DatabaseFunctionalLayer |
329 | |
330 | - def makeBranch(self): |
331 | - product = self.factory.makeProduct( |
332 | - name='ratatouille', displayname='Ratatouille') |
333 | - branch = self.factory.makeBranch( |
334 | - owner=self.chef, product=product, name='veggies') |
335 | - self.factory.makeSourcePackage(sourcepackagename='ratatouille') |
336 | - return branch |
337 | - |
338 | def test_create_new_recipe_not_logged_in(self): |
339 | product = self.factory.makeProduct( |
340 | name='ratatouille', displayname='Ratatouille') |
341 | - branch = self.factory.makeBranch( |
342 | - owner=self.chef, product=product, name='veggies') |
343 | + branch = self.makeBranch( |
344 | + owner=self.chef, target=product, name=u'veggies') |
345 | |
346 | browser = self.getViewBrowser(branch, no_login=True) |
347 | self.assertRaises( |
348 | Unauthorized, browser.getLink('Create packaging recipe').click) |
349 | |
350 | def test_create_new_recipe(self): |
351 | - branch = self.makeBranch() |
352 | + branch = self.makeBranchAndPackage() |
353 | # A new recipe can be created from the branch page. |
354 | browser = self.getUserBrowser(canonical_url(branch), user=self.chef) |
355 | browser.getLink('Create packaging recipe').click() |
356 | @@ -388,7 +426,7 @@ |
357 | def test_create_new_recipe_private_branch(self): |
358 | # Recipes can't be created on private branches. |
359 | with person_logged_in(self.chef): |
360 | - branch = self.factory.makeBranch( |
361 | + branch = self.makeBranch( |
362 | owner=self.chef, information_type=InformationType.USERDATA) |
363 | branch_url = canonical_url(branch) |
364 | |
365 | @@ -403,7 +441,7 @@ |
366 | self.factory.makeTeam( |
367 | name='good-chefs', displayname='Good Chefs', members=[self.chef]) |
368 | browser = self.getViewBrowser( |
369 | - self.makeBranch(), '+new-recipe', user=self.chef) |
370 | + self.makeBranchAndPackage(), '+new-recipe', user=self.chef) |
371 | # The options for the owner include the Good Chefs team. |
372 | options = browser.getControl(name='field.owner.owner').displayOptions |
373 | self.assertEquals( |
374 | @@ -415,7 +453,7 @@ |
375 | team = self.factory.makeTeam( |
376 | name='good-chefs', displayname='Good Chefs', members=[self.chef]) |
377 | browser = self.getViewBrowser( |
378 | - self.makeBranch(), '+new-recipe', user=self.chef) |
379 | + self.makeBranchAndPackage(), '+new-recipe', user=self.chef) |
380 | browser.getControl(name='field.name').value = 'daily' |
381 | browser.getControl('Description').value = 'Make some food!' |
382 | browser.getControl('Other').click() |
383 | @@ -430,7 +468,7 @@ |
384 | |
385 | def test_create_new_recipe_suggests_user(self): |
386 | """The current user is suggested as a recipe owner, once.""" |
387 | - branch = self.factory.makeBranch(owner=self.chef) |
388 | + branch = self.makeBranch(owner=self.chef) |
389 | text = self.getMainText(branch, '+new-recipe') |
390 | self.assertTextMatchesExpressionIgnoreWhitespace( |
391 | r'Owner: Master Chef \(chef\) Other:', text) |
392 | @@ -440,7 +478,7 @@ |
393 | team = self.factory.makeTeam( |
394 | name='branch-team', displayname='Branch Team', |
395 | members=[self.chef]) |
396 | - branch = self.factory.makeBranch(owner=team) |
397 | + branch = self.makeBranch(owner=team) |
398 | text = self.getMainText(branch, '+new-recipe') |
399 | self.assertTextMatchesExpressionIgnoreWhitespace( |
400 | r'Owner: Master Chef \(chef\)' |
401 | @@ -450,7 +488,7 @@ |
402 | """If current user isn't a member of branch owner, it is ignored.""" |
403 | team = self.factory.makeTeam( |
404 | name='branch-team', displayname='Branch Team') |
405 | - branch = self.factory.makeBranch(owner=team) |
406 | + branch = self.makeBranch(owner=team) |
407 | text = self.getMainText(branch, '+new-recipe') |
408 | self.assertTextMatchesExpressionIgnoreWhitespace( |
409 | r'Owner: Master Chef \(chef\) Other:', text) |
410 | @@ -460,8 +498,8 @@ |
411 | # is communicated to the user properly. |
412 | product = self.factory.makeProduct( |
413 | name='ratatouille', displayname='Ratatouille') |
414 | - branch = self.factory.makeBranch( |
415 | - owner=self.chef, product=product, name='veggies') |
416 | + branch = self.makeBranch( |
417 | + owner=self.chef, target=product, name=u'veggies') |
418 | browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef) |
419 | browser.getControl('Description').value = 'Make some food!' |
420 | browser.getControl('Recipe text').value = ( |
421 | @@ -475,8 +513,8 @@ |
422 | if branch is None: |
423 | product = self.factory.makeProduct( |
424 | name='ratatouille', displayname='Ratatouille') |
425 | - branch = self.factory.makeBranch( |
426 | - owner=self.chef, product=product, name='veggies') |
427 | + branch = self.makeBranch( |
428 | + owner=self.chef, target=product, name=u'veggies') |
429 | browser = self.getViewBrowser(branch, '+new-recipe', user=self.chef) |
430 | browser.getControl(name='field.name').value = 'daily' |
431 | browser.getControl('Description').value = 'Make some food!' |
432 | @@ -487,18 +525,11 @@ |
433 | def test_create_recipe_usage(self): |
434 | # The error for a recipe with invalid instruction parameters should |
435 | # include instruction usage. |
436 | - branch = self.factory.makeBranch(name='veggies') |
437 | - self.factory.makeBranch(name='packaging') |
438 | + branch = self.makeBranch(name=u'veggies') |
439 | + self.makeBranch(name=u'packaging') |
440 | |
441 | browser = self.createRecipe( |
442 | - dedent('''\ |
443 | - # bzr-builder format 0.2 deb-version 0+{revno} |
444 | - %(branch)s |
445 | - merge |
446 | - ''' % { |
447 | - 'branch': branch.bzr_identity, |
448 | - }), |
449 | - branch=branch) |
450 | + self.getMinimalRecipeText(branch) + "merge\n", branch=branch) |
451 | self.assertEqual( |
452 | 'Error parsing recipe:3:6: ' |
453 | 'End of line while looking for the branch id.\n' |
454 | @@ -506,7 +537,8 @@ |
455 | get_feedback_messages(browser.contents)[1]) |
456 | |
457 | def test_create_recipe_no_distroseries(self): |
458 | - browser = self.getViewBrowser(self.makeBranch(), '+new-recipe') |
459 | + browser = self.getViewBrowser( |
460 | + self.makeBranchAndPackage(), '+new-recipe') |
461 | browser.getControl(name='field.name').value = 'daily' |
462 | browser.getControl('Description').value = 'Make some food!' |
463 | browser.getControl(name='field.distroseries').value = [] |
464 | @@ -518,7 +550,11 @@ |
465 | def test_create_recipe_bad_base_branch(self): |
466 | # If a user tries to create source package recipe with a bad base |
467 | # branch location, they should get an error. |
468 | - browser = self.createRecipe(MINIMAL_RECIPE_TEXT_BZR % 'foo') |
469 | + browser = self.createRecipe( |
470 | + self.minimal_recipe_text.splitlines()[0] + '\nfoo\n') |
471 | + # This page doesn't know whether the user was aiming for a Bazaar |
472 | + # branch or a Git repository; the error message always says |
473 | + # "branch". |
474 | self.assertEqual( |
475 | get_feedback_messages(browser.contents)[1], |
476 | 'foo is not a branch on Launchpad.') |
477 | @@ -528,28 +564,27 @@ |
478 | # instruction branch location, they should get an error. |
479 | product = self.factory.makeProduct( |
480 | name='ratatouille', displayname='Ratatouille') |
481 | - branch = self.factory.makeBranch( |
482 | - owner=self.chef, product=product, name='veggies') |
483 | - recipe = MINIMAL_RECIPE_TEXT_BZR % branch.bzr_identity |
484 | + branch = self.makeBranch( |
485 | + owner=self.chef, target=product, name=u'veggies') |
486 | + recipe = self.getMinimalRecipeText(branch) |
487 | recipe += 'nest packaging foo debian' |
488 | browser = self.createRecipe(recipe, branch) |
489 | self.assertEqual( |
490 | get_feedback_messages(browser.contents)[1], |
491 | - 'foo is not a branch on Launchpad.') |
492 | + 'foo %s' % self.no_such_object_message) |
493 | |
494 | def test_create_recipe_format_too_new(self): |
495 | # If the recipe's format version is too new, we should notify the |
496 | # user. |
497 | product = self.factory.makeProduct( |
498 | name='ratatouille', displayname='Ratatouille') |
499 | - branch = self.factory.makeBranch( |
500 | - owner=self.chef, product=product, name='veggies') |
501 | + branch = self.makeBranch( |
502 | + owner=self.chef, target=product, name=u'veggies') |
503 | |
504 | with recipe_parser_newest_version(145.115): |
505 | - recipe = dedent(u'''\ |
506 | - # bzr-builder format 145.115 deb-version {debupstream}-0~{revno} |
507 | - %s |
508 | - ''') % branch.bzr_identity |
509 | + recipe = re.sub( |
510 | + 'format [^ ]*', 'format 145.115', |
511 | + self.getMinimalRecipeText(branch)) |
512 | browser = self.createRecipe(recipe, branch) |
513 | self.assertEqual( |
514 | get_feedback_messages(browser.contents)[1], |
515 | @@ -564,8 +599,8 @@ |
516 | |
517 | product = self.factory.makeProduct( |
518 | name='ratatouille', displayname='Ratatouille') |
519 | - branch = self.factory.makeBranch( |
520 | - owner=self.chef, product=product, name='veggies') |
521 | + branch = self.makeBranch( |
522 | + owner=self.chef, target=product, name=u'veggies') |
523 | |
524 | # A new recipe can be created from the branch page. |
525 | browser = self.getUserBrowser(canonical_url(branch), user=self.chef) |
526 | @@ -583,15 +618,16 @@ |
527 | def test_create_recipe_private_branch(self): |
528 | # If a user tries to create source package recipe with a private |
529 | # base branch, they should get an error. |
530 | - branch = self.factory.makeAnyBranch( |
531 | + branch = self.makeBranch( |
532 | owner=self.user, information_type=InformationType.USERDATA) |
533 | with person_logged_in(self.user): |
534 | - bzr_identity = branch.bzr_identity |
535 | - recipe_text = MINIMAL_RECIPE_TEXT_BZR % bzr_identity |
536 | + identity = self.getRepository(branch).identity |
537 | + recipe_text = self.getMinimalRecipeText(branch) |
538 | browser = self.createRecipe(recipe_text) |
539 | self.assertEqual( |
540 | get_feedback_messages(browser.contents)[1], |
541 | - 'Recipe may not refer to private branch: %s' % bzr_identity) |
542 | + 'Recipe may not refer to private %s: %s' % ( |
543 | + self.branch_type, identity)) |
544 | |
545 | def _test_new_recipe_with_no_related_branches(self, branch): |
546 | # The Related Branches section should not appear if there are no |
547 | @@ -607,72 +643,20 @@ |
548 | |
549 | def test_new_product_branch_with_no_related_branches_recipe(self): |
550 | # We can create a new recipe off a product branch. |
551 | - branch = self.factory.makeBranch() |
552 | + branch = self.makeBranch() |
553 | self._test_new_recipe_with_no_related_branches(branch) |
554 | |
555 | def test_new_package_branch_with_no_linked_branches_recipe(self): |
556 | # We can create a new recipe off a sourcepackage branch where the |
557 | # sourcepackage has no linked branches. |
558 | - branch = self.factory.makePackageBranch() |
559 | + branch = self.makePackageBranch() |
560 | self._test_new_recipe_with_no_related_branches(branch) |
561 | |
562 | - def test_new_recipe_with_package_branches(self): |
563 | - # The series branches table should not appear if there are none. |
564 | - (branch, related_series_branch_info, related_package_branches) = ( |
565 | - self.factory.makeRelatedBranches(with_series_branches=False)) |
566 | - browser = self.getUserBrowser( |
567 | - canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
568 | - soup = BeautifulSoup(browser.contents) |
569 | - related_branches = soup.find('fieldset', {'id': 'related-branches'}) |
570 | - self.assertIsNot(related_branches, None) |
571 | - related_branches = soup.find( |
572 | - 'div', {'id': 'related-package-branches'}) |
573 | - self.assertIsNot(related_branches, None) |
574 | - related_branches = soup.find( |
575 | - 'div', {'id': 'related-series-branches'}) |
576 | - self.assertIs(related_branches, None) |
577 | - |
578 | - def test_new_recipe_with_series_branches(self): |
579 | - # The package branches table should not appear if there are none. |
580 | - (branch, related_series_branch_info, related_package_branches) = ( |
581 | - self.factory.makeRelatedBranches(with_package_branches=False)) |
582 | - browser = self.getUserBrowser( |
583 | - canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
584 | - soup = BeautifulSoup(browser.contents) |
585 | - related_branches = soup.find('fieldset', {'id': 'related-branches'}) |
586 | - self.assertIsNot(related_branches, None) |
587 | - related_branches = soup.find( |
588 | - 'div', {'id': 'related-series-branches'}) |
589 | - self.assertIsNot(related_branches, None) |
590 | - related_branches = soup.find( |
591 | - 'div', {'id': 'related-package-branches'}) |
592 | - self.assertIs(related_branches, None) |
593 | - |
594 | - def test_new_product_branch_recipe_with_related_branches(self): |
595 | - # The related branches should be rendered correctly on the page. |
596 | - (branch, related_series_branch_info, |
597 | - related_package_branch_info) = self.factory.makeRelatedBranches() |
598 | - browser = self.getUserBrowser( |
599 | - canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
600 | - self.checkRelatedBranches( |
601 | - related_series_branch_info, related_package_branch_info, |
602 | - browser.contents) |
603 | - |
604 | - def test_new_sourcepackage_branch_recipe_with_related_branches(self): |
605 | - # The related branches should be rendered correctly on the page. |
606 | - reference_branch = self.factory.makePackageBranch() |
607 | - (branch, ignore, related_package_branch_info) = ( |
608 | - self.factory.makeRelatedBranches(reference_branch)) |
609 | - browser = self.getUserBrowser( |
610 | - canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
611 | - self.checkRelatedBranches( |
612 | - set(), related_package_branch_info, browser.contents) |
613 | - |
614 | def test_ppa_selector_not_shown_if_user_has_no_ppas(self): |
615 | # If the user creating a recipe has no existing PPAs, the selector |
616 | # isn't shown, but the field to enter a new PPA name is. |
617 | self.user = self.factory.makePerson() |
618 | - branch = self.factory.makeAnyBranch() |
619 | + branch = self.makeBranch() |
620 | with person_logged_in(self.user): |
621 | content = self.getMainContent(branch, '+new-recipe') |
622 | ppa_name = content.find(attrs={'id': 'field.ppa_name'}) |
623 | @@ -692,7 +676,7 @@ |
624 | # If the user creating a recipe has existing PPAs, the selector is |
625 | # shown, along with radio buttons to decide whether to use an existing |
626 | # ppa or to create a new one. |
627 | - branch = self.factory.makeAnyBranch() |
628 | + branch = self.makeBranch() |
629 | with person_logged_in(self.user): |
630 | content = self.getMainContent(branch, '+new-recipe') |
631 | ppa_name = content.find(attrs={'id': 'field.ppa_name'}) |
632 | @@ -712,7 +696,7 @@ |
633 | def test_create_new_ppa(self): |
634 | # If the user doesn't have any PPAs, a new once can be created. |
635 | self.user = self.factory.makePerson(name='eric') |
636 | - branch = self.factory.makeAnyBranch() |
637 | + branch = self.makeBranch() |
638 | |
639 | # A new recipe can be created from the branch page. |
640 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) |
641 | @@ -737,7 +721,7 @@ |
642 | # Make a PPA called 'ppa' using the default. |
643 | with person_logged_in(self.user): |
644 | self.user.createPPA(name='foo') |
645 | - branch = self.factory.makeAnyBranch() |
646 | + branch = self.makeBranch() |
647 | |
648 | # A new recipe can be created from the branch page. |
649 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) |
650 | @@ -756,7 +740,7 @@ |
651 | # If a new PPA is being created, and the user has not specified a |
652 | # name, an error is shown. |
653 | self.user = self.factory.makePerson(name='eric') |
654 | - branch = self.factory.makeAnyBranch() |
655 | + branch = self.makeBranch() |
656 | |
657 | # A new recipe can be created from the branch page. |
658 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) |
659 | @@ -779,7 +763,7 @@ |
660 | with person_logged_in(team.teamowner): |
661 | team.setMembershipData( |
662 | self.user, TeamMembershipStatus.ADMIN, team.teamowner) |
663 | - branch = self.factory.makeAnyBranch(owner=team) |
664 | + branch = self.makeBranch(owner=team) |
665 | |
666 | # A new recipe can be created from the branch page. |
667 | browser = self.getUserBrowser(canonical_url(branch), user=self.user) |
668 | @@ -799,6 +783,83 @@ |
669 | self.assertIsNot(None, new_ppa) |
670 | |
671 | |
672 | +class TestSourcePackageRecipeAddViewBzr( |
673 | + TestSourcePackageRecipeAddViewMixin, BzrMixin, TestCaseForRecipe): |
674 | + |
675 | + def makeBranchAndPackage(self): |
676 | + product = self.factory.makeProduct( |
677 | + name='ratatouille', displayname='Ratatouille') |
678 | + branch = self.factory.makeBranch( |
679 | + owner=self.chef, product=product, name='veggies') |
680 | + self.factory.makeSourcePackage(sourcepackagename='ratatouille') |
681 | + return branch |
682 | + |
683 | + def test_new_recipe_with_package_branches(self): |
684 | + # The series branches table should not appear if there are none. |
685 | + branch, related_series_branch_info, related_package_branches = ( |
686 | + self.makeRelatedBranches(with_series_branches=False)) |
687 | + browser = self.getUserBrowser( |
688 | + canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
689 | + soup = BeautifulSoup(browser.contents) |
690 | + related_branches = soup.find('fieldset', {'id': 'related-branches'}) |
691 | + self.assertIsNot(related_branches, None) |
692 | + related_branches = soup.find( |
693 | + 'div', {'id': 'related-package-branches'}) |
694 | + self.assertIsNot(related_branches, None) |
695 | + related_branches = soup.find( |
696 | + 'div', {'id': 'related-series-branches'}) |
697 | + self.assertIs(related_branches, None) |
698 | + |
699 | + def test_new_recipe_with_series_branches(self): |
700 | + # The package branches table should not appear if there are none. |
701 | + branch, related_series_branch_info, related_package_branches = ( |
702 | + self.makeRelatedBranches(with_package_branches=False)) |
703 | + browser = self.getUserBrowser( |
704 | + canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
705 | + soup = BeautifulSoup(browser.contents) |
706 | + related_branches = soup.find('fieldset', {'id': 'related-branches'}) |
707 | + self.assertIsNot(related_branches, None) |
708 | + related_branches = soup.find( |
709 | + 'div', {'id': 'related-series-branches'}) |
710 | + self.assertIsNot(related_branches, None) |
711 | + related_branches = soup.find( |
712 | + 'div', {'id': 'related-package-branches'}) |
713 | + self.assertIs(related_branches, None) |
714 | + |
715 | + def test_new_product_branch_recipe_with_related_branches(self): |
716 | + # The related branches should be rendered correctly on the page. |
717 | + branch, related_series_branch_info, related_package_branch_info = ( |
718 | + self.makeRelatedBranches()) |
719 | + browser = self.getUserBrowser( |
720 | + canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
721 | + self.checkRelatedBranches( |
722 | + related_series_branch_info, related_package_branch_info, |
723 | + browser.contents) |
724 | + |
725 | + def test_new_sourcepackage_branch_recipe_with_related_branches(self): |
726 | + # The related branches should be rendered correctly on the page. |
727 | + reference_branch = self.makePackageBranch() |
728 | + branch, _, related_package_branch_info = ( |
729 | + self.makeRelatedBranches(reference_branch)) |
730 | + browser = self.getUserBrowser( |
731 | + canonical_url(branch, view_name='+new-recipe'), user=self.chef) |
732 | + self.checkRelatedBranches( |
733 | + set(), related_package_branch_info, browser.contents) |
734 | + |
735 | + |
736 | +class TestSourcePackageRecipeAddViewGit( |
737 | + TestSourcePackageRecipeAddViewMixin, GitMixin, TestCaseForRecipe): |
738 | + |
739 | + def makeBranchAndPackage(self): |
740 | + product = self.factory.makeProduct( |
741 | + name='ratatouille', displayname='Ratatouille') |
742 | + repository = self.factory.makeGitRepository( |
743 | + owner=self.chef, target=product, name=u'veggies') |
744 | + self.factory.makeDistributionSourcePackage( |
745 | + sourcepackagename='ratatouille') |
746 | + return repository |
747 | + |
748 | + |
749 | class TestSourcePackageRecipeEditViewMixin: |
750 | """Test the editing behaviour of a source package recipe.""" |
751 | |
752 | |
753 | === modified file 'lib/lp/code/browser/tests/test_tales.py' |
754 | --- lib/lp/code/browser/tests/test_tales.py 2015-09-11 06:04:36 +0000 |
755 | +++ lib/lp/code/browser/tests/test_tales.py 2016-01-20 12:22:22 +0000 |
756 | @@ -235,7 +235,7 @@ |
757 | '<a href="%s">%s recipe build in ubuntu shiny</a> ' |
758 | '[~eric/ubuntu/ppa]' |
759 | % (canonical_url(build, path_only_if_possible=True), |
760 | - build.recipe.base_branch.unique_name))) |
761 | + build.recipe.base.unique_name))) |
762 | |
763 | def test_link_no_recipe(self): |
764 | eric = self.factory.makePerson(name='eric') |
765 | |
766 | === modified file 'lib/lp/code/errors.py' |
767 | --- lib/lp/code/errors.py 2016-01-12 01:24:06 +0000 |
768 | +++ lib/lp/code/errors.py 2016-01-20 12:22:22 +0000 |
769 | @@ -30,6 +30,7 @@ |
770 | 'ClaimReviewFailed', |
771 | 'DiffNotFound', |
772 | 'GitDefaultConflict', |
773 | + 'GitRecipesFeatureDisabled', |
774 | 'GitRepositoryCreationException', |
775 | 'GitRepositoryCreationFault', |
776 | 'GitRepositoryCreationForbidden', |
777 | @@ -485,6 +486,15 @@ |
778 | self.newest_supported = newest_supported |
779 | |
780 | |
781 | +@error_status(httplib.UNAUTHORIZED) |
782 | +class GitRecipesFeatureDisabled(Exception): |
783 | + """Only certain users can create new Git recipes.""" |
784 | + |
785 | + def __init__(self): |
786 | + message = "You do not have permission to create Git recipes." |
787 | + Exception.__init__(self, message) |
788 | + |
789 | + |
790 | @error_status(httplib.BAD_REQUEST) |
791 | class RecipeBuildException(Exception): |
792 | |
793 | |
794 | === modified file 'lib/lp/code/help/related-recipes.html' |
795 | --- lib/lp/code/help/related-recipes.html 2010-11-04 22:50:52 +0000 |
796 | +++ lib/lp/code/help/related-recipes.html 2016-01-20 12:22:22 +0000 |
797 | @@ -18,8 +18,8 @@ |
798 | </p> |
799 | |
800 | <p>A "recipe" is a description of the steps Launchpad's package builder |
801 | - should take to construct a source package from a set of Bazaar branches |
802 | - that you specify. |
803 | + should take to construct a source package from a set of Bazaar or Git |
804 | + branches that you specify. |
805 | </p> |
806 | |
807 | <p> |
808 | |
809 | === modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py' |
810 | --- lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000 |
811 | +++ lib/lp/code/interfaces/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000 |
812 | @@ -8,6 +8,7 @@ |
813 | |
814 | |
815 | __all__ = [ |
816 | + 'GIT_RECIPES_FEATURE_FLAG', |
817 | 'IRecipeBranchSource', |
818 | 'ISourcePackageRecipe', |
819 | 'ISourcePackageRecipeData', |
820 | @@ -68,6 +69,9 @@ |
821 | from lp.soyuz.interfaces.archive import IArchive |
822 | |
823 | |
824 | +GIT_RECIPES_FEATURE_FLAG = u'code.git.recipes.enabled' |
825 | + |
826 | + |
827 | MINIMAL_RECIPE_TEXT_BZR = dedent(u'''\ |
828 | # bzr-builder format 0.3 deb-version {debupstream}-0~{revno} |
829 | %s |
830 | |
831 | === modified file 'lib/lp/code/model/sourcepackagerecipe.py' |
832 | --- lib/lp/code/model/sourcepackagerecipe.py 2016-01-15 11:50:57 +0000 |
833 | +++ lib/lp/code/model/sourcepackagerecipe.py 2016-01-20 12:22:22 +0000 |
834 | @@ -41,8 +41,10 @@ |
835 | from lp.code.errors import ( |
836 | BuildAlreadyPending, |
837 | BuildNotAllowedForDistro, |
838 | + GitRecipesFeatureDisabled, |
839 | ) |
840 | from lp.code.interfaces.sourcepackagerecipe import ( |
841 | + GIT_RECIPES_FEATURE_FLAG, |
842 | IRecipeBranchSource, |
843 | ISourcePackageRecipe, |
844 | ISourcePackageRecipeData, |
845 | @@ -68,6 +70,7 @@ |
846 | IStore, |
847 | ) |
848 | from lp.services.database.stormexpr import Greatest |
849 | +from lp.services.features import getFeatureFlag |
850 | from lp.services.propertycache import ( |
851 | cachedproperty, |
852 | get_property_cache, |
853 | @@ -227,6 +230,15 @@ |
854 | sprecipe.date_created = date_created |
855 | sprecipe.date_last_modified = date_created |
856 | store.add(sprecipe) |
857 | + # We can only do this feature flag check at the end, because we need |
858 | + # to have constructed the SourcePackageRecipeData in order to know |
859 | + # whether it refers to a Git repository and then we need the |
860 | + # SourcePackageRecipe to respect DB constraints before doing |
861 | + # anything else. The transaction will be aborted either way if the |
862 | + # check fails. |
863 | + if (sprecipe.base_git_repository is not None and |
864 | + not getFeatureFlag(GIT_RECIPES_FEATURE_FLAG)): |
865 | + raise GitRecipesFeatureDisabled |
866 | return sprecipe |
867 | |
868 | @staticmethod |
869 | |
870 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
871 | --- lib/lp/code/model/tests/test_gitrepository.py 2016-01-12 04:34:09 +0000 |
872 | +++ lib/lp/code/model/tests/test_gitrepository.py 2016-01-20 12:22:22 +0000 |
873 | @@ -72,6 +72,7 @@ |
874 | IGitRepositoryView, |
875 | ) |
876 | from lp.code.interfaces.revision import IRevisionSet |
877 | +from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG |
878 | from lp.code.model.branchmergeproposal import BranchMergeProposal |
879 | from lp.code.model.branchmergeproposaljob import ( |
880 | BranchMergeProposalJob, |
881 | @@ -454,12 +455,14 @@ |
882 | |
883 | def test_destroySelf_with_SourcePackageRecipe(self): |
884 | # If repository is a base_git_repository in a recipe, it is deleted. |
885 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
886 | recipe = self.factory.makeSourcePackageRecipe( |
887 | branches=self.factory.makeGitRefs(owner=self.user)) |
888 | recipe.base_git_repository.destroySelf(break_references=True) |
889 | |
890 | def test_destroySelf_with_SourcePackageRecipe_as_non_base(self): |
891 | # If repository is referred to by a recipe, it is deleted. |
892 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
893 | [ref1] = self.factory.makeGitRefs(owner=self.user) |
894 | [ref2] = self.factory.makeGitRefs(owner=self.user) |
895 | self.factory.makeSourcePackageRecipe(branches=[ref1, ref2]) |
896 | @@ -698,6 +701,7 @@ |
897 | |
898 | def test_deletionRequirements_with_SourcePackageRecipe(self): |
899 | # Recipes are listed as deletion requirements. |
900 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
901 | recipe = self.factory.makeSourcePackageRecipe( |
902 | branches=self.factory.makeGitRefs()) |
903 | self.assertEqual( |
904 | @@ -1845,6 +1849,10 @@ |
905 | |
906 | layer = ZopelessDatabaseLayer |
907 | |
908 | + def setUp(self): |
909 | + super(TestGitRepositoryMarkRecipesStale, self).setUp() |
910 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
911 | + |
912 | def test_base_repository_recipe(self): |
913 | # On ref changes, recipes where this ref is the base become stale. |
914 | [ref] = self.factory.makeGitRefs() |
915 | |
916 | === modified file 'lib/lp/code/model/tests/test_hasrecipes.py' |
917 | --- lib/lp/code/model/tests/test_hasrecipes.py 2016-01-14 17:24:14 +0000 |
918 | +++ lib/lp/code/model/tests/test_hasrecipes.py 2016-01-20 12:22:22 +0000 |
919 | @@ -6,6 +6,8 @@ |
920 | __metaclass__ = type |
921 | |
922 | from lp.code.interfaces.hasrecipes import IHasRecipes |
923 | +from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG |
924 | +from lp.services.features.testing import FeatureFixture |
925 | from lp.testing import TestCaseWithFactory |
926 | from lp.testing.layers import DatabaseFunctionalLayer |
927 | |
928 | @@ -47,6 +49,7 @@ |
929 | def test_git_repository_recipes(self): |
930 | # IGitRepository.recipes should provide all the SourcePackageRecipes |
931 | # attached to that repository. |
932 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
933 | base_ref1, base_ref2 = self.factory.makeGitRefs( |
934 | paths=[u"refs/heads/ref1", u"refs/heads/ref2"]) |
935 | [other_ref] = self.factory.makeGitRefs() |
936 | @@ -58,6 +61,7 @@ |
937 | def test_git_repository_recipes_nonbase(self): |
938 | # IGitRepository.recipes should provide all the SourcePackageRecipes |
939 | # that refer to the repository, even as a non-base branch. |
940 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
941 | [base_ref] = self.factory.makeGitRefs() |
942 | [nonbase_ref] = self.factory.makeGitRefs() |
943 | [other_ref] = self.factory.makeGitRefs() |
944 | @@ -88,6 +92,7 @@ |
945 | def test_product_recipes(self): |
946 | # IProduct.recipes should provide all the SourcePackageRecipes |
947 | # attached to that product's branches and Git repositories. |
948 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
949 | product = self.factory.makeProduct() |
950 | branch = self.factory.makeBranch(product=product) |
951 | [ref] = self.factory.makeGitRefs(target=product) |
952 | |
953 | === modified file 'lib/lp/code/model/tests/test_recipebuilder.py' |
954 | --- lib/lp/code/model/tests/test_recipebuilder.py 2016-01-15 11:50:57 +0000 |
955 | +++ lib/lp/code/model/tests/test_recipebuilder.py 2016-01-20 12:22:22 +0000 |
956 | @@ -29,11 +29,13 @@ |
957 | TestHandleStatusMixin, |
958 | TestVerifySuccessfulBuildMixin, |
959 | ) |
960 | +from lp.code.interfaces.sourcepackagerecipe import GIT_RECIPES_FEATURE_FLAG |
961 | from lp.code.model.recipebuilder import RecipeBuildBehaviour |
962 | from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild |
963 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
964 | from lp.registry.interfaces.series import SeriesStatus |
965 | from lp.services.config import config |
966 | +from lp.services.features.testing import FeatureFixture |
967 | from lp.services.log.logger import BufferLogger |
968 | from lp.soyuz.adapters.archivedependencies import ( |
969 | get_sources_list_for_building, |
970 | @@ -258,6 +260,7 @@ |
971 | self.assertEqual(args["archives"], expected_archives) |
972 | |
973 | def test_extraBuildArgs_git(self): |
974 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
975 | job = self.makeJob(git=True) |
976 | distroarchseries = job.build.distroseries.architectures[0] |
977 | expected_archives = get_sources_list_for_building( |
978 | |
979 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py' |
980 | --- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-12 03:54:38 +0000 |
981 | +++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2016-01-20 12:22:22 +0000 |
982 | @@ -31,11 +31,13 @@ |
983 | from lp.buildmaster.model.buildqueue import BuildQueue |
984 | from lp.code.errors import ( |
985 | BuildAlreadyPending, |
986 | + GitRecipesFeatureDisabled, |
987 | PrivateBranchRecipe, |
988 | PrivateGitRepositoryRecipe, |
989 | TooNewRecipeFormat, |
990 | ) |
991 | from lp.code.interfaces.sourcepackagerecipe import ( |
992 | + GIT_RECIPES_FEATURE_FLAG, |
993 | ISourcePackageRecipe, |
994 | ISourcePackageRecipeSource, |
995 | ISourcePackageRecipeView, |
996 | @@ -56,6 +58,7 @@ |
997 | from lp.registry.interfaces.series import SeriesStatus |
998 | from lp.services.database.bulk import load_referencing |
999 | from lp.services.database.constants import UTC_NOW |
1000 | +from lp.services.features.testing import FeatureFixture |
1001 | from lp.services.propertycache import clear_property_cache |
1002 | from lp.services.webapp.authorization import check_permission |
1003 | from lp.services.webapp.publisher import canonical_url |
1004 | @@ -122,6 +125,10 @@ |
1005 | branch_type = "repository" |
1006 | recipe_id = "git-build-recipe" |
1007 | |
1008 | + def setUp(self): |
1009 | + super(GitMixin, self).setUp() |
1010 | + self.useFixture(FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"})) |
1011 | + |
1012 | def makeBranch(self, **kwargs): |
1013 | return self.factory.makeGitRefs(**kwargs)[0] |
1014 | |
1015 | @@ -823,6 +830,12 @@ |
1016 | TestSourcePackageRecipeMixin, GitMixin, TestCaseWithFactory): |
1017 | """Test `SourcePackageRecipe` objects for Git.""" |
1018 | |
1019 | + def test_feature_flag_disabled(self): |
1020 | + # Without a feature flag, we will not create new Git recipes. |
1021 | + self.useFixture(FeatureFixture({})) |
1022 | + self.assertRaises( |
1023 | + GitRecipesFeatureDisabled, self.makeSourcePackageRecipe) |
1024 | + |
1025 | |
1026 | class TestRecipeBranchRoundTrippingMixin: |
1027 | |
1028 | |
1029 | === modified file 'lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt' |
1030 | --- lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-12 15:10:57 +0000 |
1031 | +++ lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt 2016-01-20 12:22:22 +0000 |
1032 | @@ -74,15 +74,21 @@ |
1033 | Create a new sample repository, some branches in it, and some source package |
1034 | recipes to go along with them. |
1035 | |
1036 | + >>> from lp.code.interfaces.sourcepackagerecipe import ( |
1037 | + ... GIT_RECIPES_FEATURE_FLAG, |
1038 | + ... ) |
1039 | + >>> from lp.services.features.testing import FeatureFixture |
1040 | + |
1041 | >>> login('foo.bar@canonical.com') |
1042 | >>> repository = factory.makeGitRepository() |
1043 | >>> ref1, ref2, ref3 = factory.makeGitRefs( |
1044 | ... repository=repository, |
1045 | ... paths=[u"refs/heads/a", u"refs/heads/b", u"refs/heads/c"]) |
1046 | - >>> recipe1a = factory.makeSourcePackageRecipe(branches=[ref1]) |
1047 | - >>> recipe1b = factory.makeSourcePackageRecipe(branches=[ref1]) |
1048 | - >>> recipe2 = factory.makeSourcePackageRecipe(branches=[ref2]) |
1049 | - >>> recipe3 = factory.makeSourcePackageRecipe(branches=[ref3]) |
1050 | + >>> with FeatureFixture({GIT_RECIPES_FEATURE_FLAG: u"on"}): |
1051 | + ... recipe1a = factory.makeSourcePackageRecipe(branches=[ref1]) |
1052 | + ... recipe1b = factory.makeSourcePackageRecipe(branches=[ref1]) |
1053 | + ... recipe2 = factory.makeSourcePackageRecipe(branches=[ref2]) |
1054 | + ... recipe3 = factory.makeSourcePackageRecipe(branches=[ref3]) |
1055 | |
1056 | Keep these urls, including the target url. We'll use these later. |
1057 | |
1058 | |
1059 | === modified file 'lib/lp/code/templates/gitref-recipes.pt' |
1060 | --- lib/lp/code/templates/gitref-recipes.pt 2016-01-12 15:10:57 +0000 |
1061 | +++ lib/lp/code/templates/gitref-recipes.pt 2016-01-20 12:22:22 +0000 |
1062 | @@ -2,6 +2,7 @@ |
1063 | xmlns:tal="http://xml.zope.org/namespaces/tal" |
1064 | xmlns:metal="http://xml.zope.org/namespaces/metal" |
1065 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
1066 | + tal:define="context_menu view/context/menu:context" |
1067 | id="related-recipes"> |
1068 | |
1069 | <h3>Related source package recipes</h3> |
1070 | @@ -14,6 +15,12 @@ |
1071 | <a href="/+help-code/related-recipes.html" target="help" |
1072 | class="sprite maybe action-icon">(?)</a> |
1073 | </div> |
1074 | + |
1075 | + <span |
1076 | + tal:define="link context_menu/create_recipe" |
1077 | + tal:condition="link/enabled" |
1078 | + tal:replace="structure link/render" |
1079 | + /> |
1080 | </div> |
1081 | |
1082 | </div> |
1083 | |
1084 | === modified file 'lib/lp/code/templates/gitrepository-recipes.pt' |
1085 | --- lib/lp/code/templates/gitrepository-recipes.pt 2016-01-12 15:10:57 +0000 |
1086 | +++ lib/lp/code/templates/gitrepository-recipes.pt 2016-01-20 12:22:22 +0000 |
1087 | @@ -2,6 +2,7 @@ |
1088 | xmlns:tal="http://xml.zope.org/namespaces/tal" |
1089 | xmlns:metal="http://xml.zope.org/namespaces/metal" |
1090 | xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
1091 | + tal:define="context_menu view/context/menu:context" |
1092 | id="related-recipes"> |
1093 | |
1094 | <h3>Related source package recipes</h3> |
1095 | @@ -14,6 +15,12 @@ |
1096 | <a href="/+help-code/related-recipes.html" target="help" |
1097 | class="sprite maybe action-icon">(?)</a> |
1098 | </div> |
1099 | + |
1100 | + <span |
1101 | + tal:define="link context_menu/create_recipe" |
1102 | + tal:condition="link/enabled" |
1103 | + tal:replace="structure link/render" |
1104 | + /> |
1105 | </div> |
1106 | |
1107 | </div> |