Merge lp:~cjwatson/launchpad/series-alias into lp:launchpad
- series-alias
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 16747 | ||||
Proposed branch: | lp:~cjwatson/launchpad/series-alias | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1011 lines (+383/-98) 22 files modified
lib/lp/archivepublisher/publishing.py (+106/-11) lib/lp/archivepublisher/tests/test_generate_extra_overrides.py (+9/-8) lib/lp/archivepublisher/tests/test_publisher.py (+43/-2) lib/lp/archivepublisher/tests/test_repositoryindexfile.py (+4/-4) lib/lp/archivepublisher/utils.py (+7/-6) lib/lp/archiveuploader/tests/test_uploadpolicy.py (+16/-1) lib/lp/archiveuploader/uploadpolicy.py (+3/-3) lib/lp/archiveuploader/uploadprocessor.py (+2/-2) lib/lp/registry/browser/distribution.py (+7/-0) lib/lp/registry/configure.zcml (+1/-0) lib/lp/registry/doc/distroseries.txt (+11/-0) lib/lp/registry/interfaces/distribution.py (+19/-3) lib/lp/registry/interfaces/distroseries.py (+4/-2) lib/lp/registry/model/distribution.py (+26/-9) lib/lp/registry/model/distroseries.py (+11/-2) lib/lp/registry/stories/distribution/xx-distribution-overview.txt (+60/-32) lib/lp/registry/stories/webservice/xx-distribution.txt (+5/-2) lib/lp/registry/tests/test_distribution.py (+17/-7) lib/lp/soyuz/interfaces/archive.py (+24/-0) lib/lp/soyuz/model/archive.py (+3/-1) lib/lp/soyuz/scripts/publishdistro.py (+3/-1) lib/lp/soyuz/scripts/tests/test_publishdistro.py (+2/-2) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/series-alias | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+178103@code.launchpad.net |
Commit message
Implement Distribution.
Description of the change
== Summary ==
We want to be able to set an alias for the development series of Ubuntu, and have that be automatically resolved for the purpose of downloads (i.e. symlinks in the archive) and uploads. Bug 1198279.
== Proposed fix ==
We have a new Distribution.
== Pre-implementation notes ==
This was rather more complex than initially supposed, mainly due to difficulties with PPAs, where the distribution's current series doesn't necessarily exist at all. We want people to be able to assume that the alias exists, but don't want to have to copy packages forward in PPAs or forcibly republish all PPAs any time the series changes. After some debate with William, we ended up with the compromise that we'll make the alias point to the latest series with any active publications, which should generally correspond to where most work is happening.
Otherwise, it's largely straightforward, although William dissuaded me from making the relevant internal APIs follow aliases in all situations. Instead, I've added a follow_aliases keyword argument in a few places so that those callers that explicitly want alias resolution can ask for it. Distribution.
== LOC Rationale ==
+253. I claim credit and will try to find something to offset this later ...
== Tests ==
Touches lots of registry code. Probably best to run the lot. I ran registry, soyuz, archiveuploader, and archivepublisher.
== Demo and Q/A ==
We should QA at least the following paths:
* Publish primary archive, check symlinks
* Publish PPAs with various series/publication combinations, check symlinks
* Upload to devel
* Visit /ubuntu/devel and some URLs underneath it in a web browser
Colin Watson (cjwatson) wrote : | # |
On Wed, Aug 28, 2013 at 05:47:36AM -0000, William Grant wrote:
> 20 + def _allIndexFiles(
> 21 + """Return all index files on disk for a distroseries."""
>
> This method seems like it should mostly be elsewhere. It's not reasonably practical to factor out the commonalities somehow?
I refactored this method a little bit internally to make it less
repetitive, but I'm not sure what you mean in general. The Publisher
class knows about paths to the various index files, so it seems to make
sense for a method that returns them all to live there.
Do you just mean that _allIndexFiles should be split up or folded in
somewhere else? I could have written it inline in
_latestNonEmpty
seemed nicer to use a generator than to build up a list.
> 57 + def createSeriesAli
>
> This has the slightly weird behavior that a single missing current suite will cause just that alias to point to the old series, while the other suites point to the new one. This probably can't happen in production, because primary archive indices are always all generated for all suites on series creation, even if empty.
The only alternatives I can think of here are:
1) this behaviour
2) sometimes have aliases which are dangling symlinks
3) hold all the aliases back to the older series
4) don't create any aliases at all on mismatch
I don't think 2) should be allowed. 3) has the problem that if we're
considering cases that ought not to happen in production then it's
entirely possible that there's no consistent alias to select. So the
alternatives appear to be 1) or 4). 4) would be confusing in a
different way ("why are there no aliases?") - what do you think?
> 732 distribution = self.distribution
> 733 if to_series is not None:
> 734 result = getUtility(
> 735 - distribution, to_series)
> 736 + distribution, to_series, follow_
> 737 if result is None:
> 738 raise NoSuchDistroSer
> 739 series = result
>
> This is probably fairly confusing for PPAs. You can copy to devel and it'll actually be different from the current devel symlink in the PPA, with no way to tell beforehand. But we probably can't avoid it given the current fairly braindead method signature.
Yes. As discussed at the releng sprint, I think we do want this
behaviour as it has the right properties of encouraging developers to
stick with the current development series, but it's true that it can be
confusing.
I've added a set of comments to the interfaces, which is probably the
best I can do.
William Grant (wgrant) wrote : | # |
On 29/08/13 02:42, Colin Watson wrote:
> On Wed, Aug 28, 2013 at 05:47:36AM -0000, William Grant wrote:
>> 20 + def _allIndexFiles(
>> 21 + """Return all index files on disk for a distroseries."""
>>
>> This method seems like it should mostly be elsewhere. It's not reasonably practical to factor out the commonalities somehow?
>
> I refactored this method a little bit internally to make it less
> repetitive, but I'm not sure what you mean in general. The Publisher
> class knows about paths to the various index files, so it seems to make
> sense for a method that returns them all to live there.
>
> Do you just mean that _allIndexFiles should be split up or folded in
> somewhere else? I could have written it inline in
> _latestNonEmpty
> seemed nicer to use a generator than to build up a list.
Other parts of the publisher know how to generate the paths to all those
indices, but I guess it's probably impractical to factor it out.
>> 57 + def createSeriesAli
>>
>> This has the slightly weird behavior that a single missing current suite will cause just that alias to point to the old series, while the other suites point to the new one. This probably can't happen in production, because primary archive indices are always all generated for all suites on series creation, even if empty.
>
> The only alternatives I can think of here are:
>
> 1) this behaviour
> 2) sometimes have aliases which are dangling symlinks
> 3) hold all the aliases back to the older series
> 4) don't create any aliases at all on mismatch
>
> I don't think 2) should be allowed. 3) has the problem that if we're
> considering cases that ought not to happen in production then it's
> entirely possible that there's no consistent alias to select. So the
> alternatives appear to be 1) or 4). 4) would be confusing in a
> different way ("why are there no aliases?") - what do you think?
I would probably have done 4, but 1 is fine.
>> 732 distribution = self.distribution
>> 733 if to_series is not None:
>> 734 result = getUtility(
>> 735 - distribution, to_series)
>> 736 + distribution, to_series, follow_
>> 737 if result is None:
>> 738 raise NoSuchDistroSer
>> 739 series = result
>>
>> This is probably fairly confusing for PPAs. You can copy to devel and it'll actually be different from the current devel symlink in the PPA, with no way to tell beforehand. But we probably can't avoid it given the current fairly braindead method signature.
>
> Yes. As discussed at the releng sprint, I think we do want this
> behaviour as it has the right properties of encouraging developers to
> stick with the current development series, but it's true that it can be
> confusing.
>
> I've added a set of comments to the interfaces, which is probably the
> best I can do.
Thanks, looks good.
Preview Diff
1 | === modified file 'lib/lp/archivepublisher/publishing.py' |
2 | --- lib/lp/archivepublisher/publishing.py 2013-07-05 15:01:08 +0000 |
3 | +++ lib/lp/archivepublisher/publishing.py 2013-08-29 10:30:37 +0000 |
4 | @@ -41,7 +41,10 @@ |
5 | get_ppa_reference, |
6 | RepositoryIndexFile, |
7 | ) |
8 | -from lp.registry.interfaces.pocket import PackagePublishingPocket |
9 | +from lp.registry.interfaces.pocket import ( |
10 | + PackagePublishingPocket, |
11 | + pocketsuffix, |
12 | + ) |
13 | from lp.registry.interfaces.series import SeriesStatus |
14 | from lp.services.database.constants import UTC_NOW |
15 | from lp.services.database.sqlbase import sqlvalues |
16 | @@ -158,6 +161,22 @@ |
17 | return Publisher(log, pubconf, disk_pool, archive, allowed_suites) |
18 | |
19 | |
20 | +def get_sources_path(config, suite_name, component): |
21 | + """Return path to Sources file for the given arguments.""" |
22 | + return os.path.join( |
23 | + config.distsroot, suite_name, component.name, "source", "Sources") |
24 | + |
25 | + |
26 | +def get_packages_path(config, suite_name, component, arch, subcomp=None): |
27 | + """Return path to Packages file for the given arguments.""" |
28 | + component_root = os.path.join(config.distsroot, suite_name, component.name) |
29 | + arch_path = "binary-%s" % arch.architecturetag |
30 | + if subcomp is None: |
31 | + return os.path.join(component_root, arch_path, "Packages") |
32 | + else: |
33 | + return os.path.join(component_root, subcomp, arch_path, "Packages") |
34 | + |
35 | + |
36 | class I18nIndex(_multivalued): |
37 | """Represents an i18n/Index file.""" |
38 | _multivalued_fields = { |
39 | @@ -417,6 +436,85 @@ |
40 | self.checkDirtySuiteBeforePublishing(distroseries, pocket) |
41 | self._writeSuite(distroseries, pocket) |
42 | |
43 | + def _allIndexFiles(self, distroseries): |
44 | + """Return all index files on disk for a distroseries.""" |
45 | + components = self.archive.getComponentsForSeries(distroseries) |
46 | + for pocket in self.archive.getPockets(): |
47 | + suite_name = distroseries.getSuite(pocket) |
48 | + for component in components: |
49 | + yield get_sources_path(self._config, suite_name, component) |
50 | + for arch in distroseries.architectures: |
51 | + if not arch.enabled: |
52 | + continue |
53 | + arch_path = "binary-%s" % arch.architecturetag |
54 | + yield get_packages_path( |
55 | + self._config, suite_name, component, arch) |
56 | + for subcomp in self.subcomponents: |
57 | + yield get_packages_path( |
58 | + self._config, suite_name, component, arch, subcomp) |
59 | + |
60 | + def _latestNonEmptySeries(self): |
61 | + """Find the latest non-empty series in an archive. |
62 | + |
63 | + Doing this properly (series with highest version and any active |
64 | + publications) is expensive. However, we just went to the effort of |
65 | + publishing everything; so a quick-and-dirty approach is to look |
66 | + through what we published on disk. |
67 | + """ |
68 | + for distroseries in self.distro: |
69 | + for index in self._allIndexFiles(distroseries): |
70 | + try: |
71 | + if os.path.getsize(index) > 0: |
72 | + return distroseries |
73 | + except OSError: |
74 | + pass |
75 | + |
76 | + def createSeriesAliases(self): |
77 | + """Ensure that any series aliases exist. |
78 | + |
79 | + The natural implementation would be to point the alias at |
80 | + self.distro.currentseries, but that works poorly for PPAs, where |
81 | + it's possible that no packages have been published for the current |
82 | + series. We also don't want to have to go through and republish all |
83 | + PPAs when we create a new series. Thus, we instead do the best we |
84 | + can by pointing the alias at the latest series with any publications |
85 | + in the archive, which is the best approximation to a development |
86 | + series for that PPA. |
87 | + |
88 | + This does mean that the published alias might point to an older |
89 | + series, then you upload something to the alias and find that the |
90 | + alias has now moved to a newer series. What can I say? The |
91 | + requirements are not entirely coherent for PPAs given that packages |
92 | + are not automatically copied forward. |
93 | + """ |
94 | + alias = self.distro.development_series_alias |
95 | + if alias is not None: |
96 | + current = self._latestNonEmptySeries() |
97 | + if current is None: |
98 | + return |
99 | + for pocket in self.archive.getPockets(): |
100 | + alias_suite = "%s%s" % (alias, pocketsuffix[pocket]) |
101 | + current_suite = current.getSuite(pocket) |
102 | + current_suite_path = os.path.join( |
103 | + self._config.distsroot, current_suite) |
104 | + if not os.path.isdir(current_suite_path): |
105 | + continue |
106 | + alias_suite_path = os.path.join( |
107 | + self._config.distsroot, alias_suite) |
108 | + if os.path.islink(alias_suite_path): |
109 | + if os.readlink(alias_suite_path) == current_suite: |
110 | + continue |
111 | + elif os.path.isdir(alias_suite_path): |
112 | + # Perhaps somebody did something misguided ... |
113 | + self.log.warning( |
114 | + "Alias suite path %s is a directory!" % alias_suite) |
115 | + continue |
116 | + try: |
117 | + os.unlink(alias_suite_path) |
118 | + except OSError: |
119 | + pass |
120 | + os.symlink(current_suite, alias_suite_path) |
121 | + |
122 | def _writeComponentIndexes(self, distroseries, pocket, component): |
123 | """Write Index files for single distroseries + pocket + component. |
124 | |
125 | @@ -431,10 +529,9 @@ |
126 | |
127 | self.log.debug("Generating Sources") |
128 | |
129 | - source_index_root = os.path.join( |
130 | - self._config.distsroot, suite_name, component.name, 'source') |
131 | source_index = RepositoryIndexFile( |
132 | - source_index_root, self._config.temproot, 'Sources') |
133 | + get_sources_path(self._config, suite_name, component), |
134 | + self._config.temproot) |
135 | |
136 | for spp in distroseries.getSourcePackagePublishing( |
137 | pocket, component, self.archive): |
138 | @@ -452,17 +549,15 @@ |
139 | self.log.debug("Generating Packages for %s" % arch_path) |
140 | |
141 | indices = {} |
142 | - package_index_root = os.path.join( |
143 | - self._config.distsroot, suite_name, component.name, arch_path) |
144 | indices[None] = RepositoryIndexFile( |
145 | - package_index_root, self._config.temproot, 'Packages') |
146 | + get_packages_path(self._config, suite_name, component, arch), |
147 | + self._config.temproot) |
148 | |
149 | for subcomp in self.subcomponents: |
150 | - sub_index_root = os.path.join( |
151 | - self._config.distsroot, suite_name, component.name, |
152 | - subcomp, arch_path) |
153 | indices[subcomp] = RepositoryIndexFile( |
154 | - sub_index_root, self._config.temproot, 'Packages') |
155 | + get_packages_path( |
156 | + self._config, suite_name, component, arch, subcomp), |
157 | + self._config.temproot) |
158 | |
159 | for bpp in distroseries.getBinaryPackagePublishing( |
160 | arch.architecturetag, pocket, component, self.archive): |
161 | |
162 | === modified file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py' |
163 | --- lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2013-05-23 07:06:42 +0000 |
164 | +++ lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2013-08-29 10:30:37 +0000 |
165 | @@ -18,6 +18,10 @@ |
166 | ) |
167 | import transaction |
168 | |
169 | +from lp.archivepublisher.publishing import ( |
170 | + get_packages_path, |
171 | + get_sources_path, |
172 | + ) |
173 | from lp.archivepublisher.scripts.generate_extra_overrides import ( |
174 | AtomicFile, |
175 | GenerateExtraOverrides, |
176 | @@ -167,12 +171,9 @@ |
177 | ensure_directory_exists(script.config.temproot) |
178 | |
179 | for component in distroseries.components: |
180 | - index_root = os.path.join( |
181 | - script.config.distsroot, distroseries.name, component.name) |
182 | - |
183 | - source_index_root = os.path.join(index_root, "source") |
184 | source_index = RepositoryIndexFile( |
185 | - source_index_root, script.config.temproot, "Sources") |
186 | + get_sources_path(script.config, distroseries.name, component), |
187 | + script.config.temproot) |
188 | for spp in distroseries.getSourcePackagePublishing( |
189 | PackagePublishingPocket.RELEASE, component, |
190 | distroseries.main_archive): |
191 | @@ -181,10 +182,10 @@ |
192 | source_index.close() |
193 | |
194 | for arch in distroseries.architectures: |
195 | - package_index_root = os.path.join( |
196 | - index_root, "binary-%s" % arch.architecturetag) |
197 | package_index = RepositoryIndexFile( |
198 | - package_index_root, script.config.temproot, "Packages") |
199 | + get_packages_path( |
200 | + script.config, distroseries.name, component, arch), |
201 | + script.config.temproot) |
202 | for bpp in distroseries.getBinaryPackagePublishing( |
203 | arch.architecturetag, PackagePublishingPocket.RELEASE, |
204 | component, distroseries.main_archive): |
205 | |
206 | === modified file 'lib/lp/archivepublisher/tests/test_publisher.py' |
207 | --- lib/lp/archivepublisher/tests/test_publisher.py 2013-06-18 07:17:20 +0000 |
208 | +++ lib/lp/archivepublisher/tests/test_publisher.py 2013-08-29 10:30:37 +0000 |
209 | @@ -1,4 +1,4 @@ |
210 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
211 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
212 | # GNU Affero General Public License version 3 (see the file LICENSE). |
213 | |
214 | """Tests for publisher class.""" |
215 | @@ -1173,6 +1173,47 @@ |
216 | self.assertReleaseContentsMatch( |
217 | release, 'main/i18n/Index', i18n_index_file.read()) |
218 | |
219 | + def testCreateSeriesAliasesNoAlias(self): |
220 | + """createSeriesAliases has nothing to do by default.""" |
221 | + publisher = Publisher( |
222 | + self.logger, self.config, self.disk_pool, |
223 | + self.ubuntutest.main_archive) |
224 | + publisher.createSeriesAliases() |
225 | + self.assertEqual([], os.listdir(self.config.distsroot)) |
226 | + |
227 | + def _assertPublishesSeriesAlias(self, publisher, expected): |
228 | + publisher.A_publish(False) |
229 | + publisher.C_writeIndexes(False) |
230 | + publisher.createSeriesAliases() |
231 | + self.assertTrue(os.path.exists(os.path.join( |
232 | + self.config.distsroot, expected))) |
233 | + for pocket, suffix in pocketsuffix.items(): |
234 | + path = os.path.join(self.config.distsroot, "devel%s" % suffix) |
235 | + expected_path = os.path.join( |
236 | + self.config.distsroot, expected + suffix) |
237 | + # A symlink for the RELEASE pocket exists. Symlinks for other |
238 | + # pockets only exist if the respective targets exist. |
239 | + if not suffix or os.path.exists(expected_path): |
240 | + self.assertTrue(os.path.islink(path)) |
241 | + self.assertEqual(expected + suffix, os.readlink(path)) |
242 | + else: |
243 | + self.assertFalse(os.path.islink(path)) |
244 | + |
245 | + def testCreateSeriesAliasesChangesAlias(self): |
246 | + """createSeriesAliases tracks the latest published series.""" |
247 | + publisher = Publisher( |
248 | + self.logger, self.config, self.disk_pool, |
249 | + self.ubuntutest.main_archive) |
250 | + self.ubuntutest.development_series_alias = "devel" |
251 | + # Oddly, hoary-test has a higher version than breezy-autotest. |
252 | + self.getPubSource(distroseries=self.ubuntutest["breezy-autotest"]) |
253 | + self._assertPublishesSeriesAlias(publisher, "breezy-autotest") |
254 | + hoary_pub = self.getPubSource( |
255 | + distroseries=self.ubuntutest["hoary-test"]) |
256 | + self._assertPublishesSeriesAlias(publisher, "hoary-test") |
257 | + hoary_pub.requestDeletion(self.ubuntutest.owner) |
258 | + self._assertPublishesSeriesAlias(publisher, "breezy-autotest") |
259 | + |
260 | def testHtaccessForPrivatePPA(self): |
261 | # A htaccess file is created for new private PPA's. |
262 | |
263 | @@ -1222,7 +1263,7 @@ |
264 | # Write a zero-length Translation-en file and compressed versions of |
265 | # it. |
266 | translation_en_index = RepositoryIndexFile( |
267 | - i18n_root, self.config.temproot, 'Translation-en') |
268 | + os.path.join(i18n_root, 'Translation-en'), self.config.temproot) |
269 | translation_en_index.close() |
270 | |
271 | all_files = set() |
272 | |
273 | === modified file 'lib/lp/archivepublisher/tests/test_repositoryindexfile.py' |
274 | --- lib/lp/archivepublisher/tests/test_repositoryindexfile.py 2010-07-18 00:24:06 +0000 |
275 | +++ lib/lp/archivepublisher/tests/test_repositoryindexfile.py 2013-08-29 10:30:37 +0000 |
276 | @@ -39,7 +39,7 @@ |
277 | 'temp_root'. |
278 | """ |
279 | return RepositoryIndexFile( |
280 | - self.root, self.temp_root, filename) |
281 | + os.path.join(self.root, filename), self.temp_root) |
282 | |
283 | def testWorkflow(self): |
284 | """`RepositoryIndexFile` workflow. |
285 | @@ -116,7 +116,7 @@ |
286 | """`RepositoryIndexFile` creates given 'root' path if necessary.""" |
287 | missing_root = os.path.join(self.root, 'donotexist') |
288 | repo_file = RepositoryIndexFile( |
289 | - missing_root, self.temp_root, 'boing') |
290 | + os.path.join(missing_root, 'boing'), self.temp_root) |
291 | |
292 | self.assertFalse(os.path.exists(missing_root)) |
293 | |
294 | @@ -130,5 +130,5 @@ |
295 | """`RepositoryIndexFile` cannot be given a missing 'temp_root'.""" |
296 | missing_temp_root = os.path.join(self.temp_root, 'donotexist') |
297 | self.assertRaises( |
298 | - AssertionError, |
299 | - RepositoryIndexFile, self.root, missing_temp_root, 'boing') |
300 | + AssertionError, RepositoryIndexFile, |
301 | + os.path.join(self.root, 'boing'), missing_temp_root) |
302 | |
303 | === modified file 'lib/lp/archivepublisher/utils.py' |
304 | --- lib/lp/archivepublisher/utils.py 2012-08-17 11:13:39 +0000 |
305 | +++ lib/lp/archivepublisher/utils.py 2013-08-29 10:30:37 +0000 |
306 | @@ -1,7 +1,7 @@ |
307 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
308 | # GNU Affero General Public License version 3 (see the file LICENSE). |
309 | |
310 | -"""Miscelaneous functions for publisher.""" |
311 | +"""Miscellaneous functions for publisher.""" |
312 | |
313 | __metaclass__ = type |
314 | |
315 | @@ -101,16 +101,17 @@ |
316 | (plain, gzip and bzip2) transparently and atomically. |
317 | """ |
318 | |
319 | - def __init__(self, root, temp_root, filename): |
320 | + def __init__(self, path, temp_root): |
321 | """Store repositories destinations and filename. |
322 | |
323 | - The given 'temp_root' needs to exist, on the other hand, 'root' |
324 | - will be created on `close` if it doesn't exist. |
325 | + The given 'temp_root' needs to exist; on the other hand, the |
326 | + directory containing 'path' will be created on `close` if it doesn't |
327 | + exist. |
328 | |
329 | - Additionally creates the needs temporary files in the given |
330 | + Additionally creates the needed temporary files in the given |
331 | 'temp_root'. |
332 | """ |
333 | - self.root = root |
334 | + self.root, filename = os.path.split(path) |
335 | assert os.path.exists(temp_root), 'Temporary root does not exist.' |
336 | |
337 | self.index_files = ( |
338 | |
339 | === modified file 'lib/lp/archiveuploader/tests/test_uploadpolicy.py' |
340 | --- lib/lp/archiveuploader/tests/test_uploadpolicy.py 2012-10-25 09:02:11 +0000 |
341 | +++ lib/lp/archiveuploader/tests/test_uploadpolicy.py 2013-08-29 10:30:37 +0000 |
342 | @@ -1,6 +1,6 @@ |
343 | #!/usr/bin/python |
344 | # |
345 | -# Copyright 2010-2012 Canonical Ltd. This software is licensed under the |
346 | +# Copyright 2010-2013 Canonical Ltd. This software is licensed under the |
347 | # GNU Affero General Public License version 3 (see the file LICENSE). |
348 | |
349 | from zope.component import getUtility |
350 | @@ -21,6 +21,7 @@ |
351 | from lp.services.database.sqlbase import flush_database_updates |
352 | from lp.testing import ( |
353 | celebrity_logged_in, |
354 | + person_logged_in, |
355 | TestCase, |
356 | TestCaseWithFactory, |
357 | ) |
358 | @@ -194,6 +195,20 @@ |
359 | NotFoundError, policy.setDistroSeriesAndPocket, |
360 | 'nonexistent_security') |
361 | |
362 | + def test_setDistroSeriesAndPocket_honours_aliases(self): |
363 | + # setDistroSeriesAndPocket honours uploads to the development series |
364 | + # alias, if set. |
365 | + policy = AbstractUploadPolicy() |
366 | + policy.distro = self.factory.makeDistribution() |
367 | + series = self.factory.makeDistroSeries( |
368 | + distribution=policy.distro, status=SeriesStatus.DEVELOPMENT) |
369 | + self.assertRaises( |
370 | + NotFoundError, policy.setDistroSeriesAndPocket, "devel") |
371 | + with person_logged_in(policy.distro.owner): |
372 | + policy.distro.development_series_alias = "devel" |
373 | + policy.setDistroSeriesAndPocket("devel") |
374 | + self.assertEqual(series, policy.distroseries) |
375 | + |
376 | def test_redirect_release_uploads_primary(self): |
377 | # With the insecure policy, the |
378 | # Distribution.redirect_release_uploads flag causes uploads to the |
379 | |
380 | === modified file 'lib/lp/archiveuploader/uploadpolicy.py' |
381 | --- lib/lp/archiveuploader/uploadpolicy.py 2012-10-25 09:02:11 +0000 |
382 | +++ lib/lp/archiveuploader/uploadpolicy.py 2013-08-29 10:30:37 +0000 |
383 | @@ -1,4 +1,4 @@ |
384 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
385 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
386 | # GNU Affero General Public License version 3 (see the file LICENSE). |
387 | |
388 | """Policy management for the upload handler.""" |
389 | @@ -145,8 +145,8 @@ |
390 | return |
391 | |
392 | self.distroseriesname = dr_name |
393 | - (self.distroseries, |
394 | - self.pocket) = self.distro.getDistroSeriesAndPocket(dr_name) |
395 | + self.distroseries, self.pocket = self.distro.getDistroSeriesAndPocket( |
396 | + dr_name, follow_aliases=True) |
397 | |
398 | if self.archive is None: |
399 | self.archive = self.distroseries.main_archive |
400 | |
401 | === modified file 'lib/lp/archiveuploader/uploadprocessor.py' |
402 | --- lib/lp/archiveuploader/uploadprocessor.py 2013-06-26 06:02:00 +0000 |
403 | +++ lib/lp/archiveuploader/uploadprocessor.py 2013-08-29 10:30:37 +0000 |
404 | @@ -1,4 +1,4 @@ |
405 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
406 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
407 | # GNU Affero General Public License version 3 (see the file LICENSE). |
408 | |
409 | """Code for 'processing' 'uploads'. Also see nascentupload.py. |
410 | @@ -743,7 +743,7 @@ |
411 | |
412 | suite_name = parts[1] |
413 | try: |
414 | - distribution.getDistroSeriesAndPocket(suite_name) |
415 | + distribution.getDistroSeriesAndPocket(suite_name, follow_aliases=True) |
416 | except NotFoundError: |
417 | raise exc_type("Could not find suite '%s'." % suite_name) |
418 | |
419 | |
420 | === modified file 'lib/lp/registry/browser/distribution.py' |
421 | --- lib/lp/registry/browser/distribution.py 2013-04-10 08:35:47 +0000 |
422 | +++ lib/lp/registry/browser/distribution.py 2013-08-29 10:30:37 +0000 |
423 | @@ -174,6 +174,13 @@ |
424 | def traverse_archive(self, name): |
425 | return self.context.getArchive(name) |
426 | |
427 | + def traverse(self, name): |
428 | + try: |
429 | + return super(DistributionNavigation, self).traverse(name) |
430 | + except NotFoundError: |
431 | + resolved = self.context.resolveSeriesAlias(name) |
432 | + return self.redirectSubTree(canonical_url(resolved), status=303) |
433 | + |
434 | |
435 | class DistributionSetNavigation(Navigation): |
436 | |
437 | |
438 | === modified file 'lib/lp/registry/configure.zcml' |
439 | --- lib/lp/registry/configure.zcml 2013-08-19 06:43:04 +0000 |
440 | +++ lib/lp/registry/configure.zcml 2013-08-29 10:30:37 +0000 |
441 | @@ -1714,6 +1714,7 @@ |
442 | bug_reported_acknowledgement |
443 | bug_reporting_guidelines |
444 | description |
445 | + development_series_alias |
446 | displayname |
447 | driver |
448 | enable_bug_expiration |
449 | |
450 | === modified file 'lib/lp/registry/doc/distroseries.txt' |
451 | --- lib/lp/registry/doc/distroseries.txt 2013-05-01 21:23:16 +0000 |
452 | +++ lib/lp/registry/doc/distroseries.txt 2013-08-29 10:30:37 +0000 |
453 | @@ -56,6 +56,17 @@ |
454 | >>> print distroseriesset.queryByVersion(ubuntu, "5.05") |
455 | None |
456 | |
457 | +queryByName works on series aliases too if follow_aliases is True. |
458 | + |
459 | + >>> ignored = login_person(ubuntu.owner.activemembers[0]) |
460 | + >>> ubuntu.development_series_alias = "devel" |
461 | + >>> login(ANONYMOUS) |
462 | + >>> print distroseriesset.queryByName(ubuntu, "devel") |
463 | + None |
464 | + >>> print distroseriesset.queryByName( |
465 | + ... ubuntu, "devel", follow_aliases=True).name |
466 | + hoary |
467 | + |
468 | We verify that a distroseries does in fact fully provide IDistroSeries: |
469 | |
470 | >>> verifyObject(IDistroSeries, warty) |
471 | |
472 | === modified file 'lib/lp/registry/interfaces/distribution.py' |
473 | --- lib/lp/registry/interfaces/distribution.py 2012-12-18 02:24:43 +0000 |
474 | +++ lib/lp/registry/interfaces/distribution.py 2013-08-29 10:30:37 +0000 |
475 | @@ -1,4 +1,4 @@ |
476 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
477 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
478 | # GNU Affero General Public License version 3 (see the file LICENSE). |
479 | |
480 | """Interfaces including and related to IDistribution.""" |
481 | @@ -76,6 +76,7 @@ |
482 | ) |
483 | from lp.registry.interfaces.announcement import IMakesAnnouncements |
484 | from lp.registry.interfaces.distributionmirror import IDistributionMirror |
485 | +from lp.registry.interfaces.distroseries import DistroSeriesNameField |
486 | from lp.registry.interfaces.karma import IKarmaContext |
487 | from lp.registry.interfaces.milestone import ( |
488 | ICanGetMilestonesDirectly, |
489 | @@ -362,6 +363,13 @@ |
490 | description=_("Redirect release pocket uploads to proposed pocket"), |
491 | readonly=False, required=True)) |
492 | |
493 | + development_series_alias = exported(DistroSeriesNameField( |
494 | + title=_("Alias for development series"), |
495 | + description=_( |
496 | + "If set, an alias for the current development series in this " |
497 | + "distribution."), |
498 | + constraint=name_validator, readonly=False, required=False)) |
499 | + |
500 | def getArchiveIDList(archive=None): |
501 | """Return a list of archive IDs suitable for sqlvalues() or quote(). |
502 | |
503 | @@ -396,12 +404,20 @@ |
504 | def getDevelopmentSeries(): |
505 | """Return the DistroSeries which are marked as in development.""" |
506 | |
507 | + def resolveSeriesAlias(name): |
508 | + """Resolve a series alias. |
509 | + |
510 | + :param name: The name to resolve. |
511 | + :raises NoSuchDistroSeries: If there is no match. |
512 | + """ |
513 | + |
514 | @operation_parameters( |
515 | name_or_version=TextLine(title=_("Name or version"), required=True)) |
516 | # Really IDistroSeries, see _schema_circular_imports.py. |
517 | @operation_returns_entry(Interface) |
518 | + @call_with(follow_aliases=True) |
519 | @export_read_operation() |
520 | - def getSeries(name_or_version): |
521 | + def getSeries(name_or_version, follow_aliases=False): |
522 | """Return the series with the name or version given. |
523 | |
524 | :param name_or_version: The `IDistroSeries.name` or |
525 | @@ -494,7 +510,7 @@ |
526 | and the value is a `IDistributionSourcePackageRelease`. |
527 | """ |
528 | |
529 | - def getDistroSeriesAndPocket(distroseriesname): |
530 | + def getDistroSeriesAndPocket(distroseriesname, follow_aliases=False): |
531 | """Return a (distroseries,pocket) tuple which is the given textual |
532 | distroseriesname in this distribution.""" |
533 | |
534 | |
535 | === modified file 'lib/lp/registry/interfaces/distroseries.py' |
536 | --- lib/lp/registry/interfaces/distroseries.py 2013-05-01 18:13:17 +0000 |
537 | +++ lib/lp/registry/interfaces/distroseries.py 2013-08-29 10:30:37 +0000 |
538 | @@ -1,4 +1,4 @@ |
539 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
540 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
541 | # GNU Affero General Public License version 3 (see the file LICENSE). |
542 | |
543 | """Interfaces including and related to IDistroSeries.""" |
544 | @@ -7,6 +7,7 @@ |
545 | |
546 | __all__ = [ |
547 | 'DerivationError', |
548 | + 'DistroSeriesNameField', |
549 | 'IDistroSeries', |
550 | 'IDistroSeriesEditRestricted', |
551 | 'IDistroSeriesPublic', |
552 | @@ -997,11 +998,12 @@ |
553 | """Return a set of distroseriess that can be translated in |
554 | rosetta.""" |
555 | |
556 | - def queryByName(distribution, name): |
557 | + def queryByName(distribution, name, follow_aliases=False): |
558 | """Query a DistroSeries by name. |
559 | |
560 | :distribution: An IDistribution. |
561 | :name: A string. |
562 | + :follow_aliases: If True, follow series aliases. |
563 | |
564 | Returns the matching DistroSeries, or None if not found. |
565 | """ |
566 | |
567 | === modified file 'lib/lp/registry/model/distribution.py' |
568 | --- lib/lp/registry/model/distribution.py 2013-08-19 06:43:04 +0000 |
569 | +++ lib/lp/registry/model/distribution.py 2013-08-29 10:30:37 +0000 |
570 | @@ -248,6 +248,7 @@ |
571 | active = True |
572 | package_derivatives_email = StringCol(notNull=False, default=None) |
573 | redirect_release_uploads = BoolCol(notNull=True, default=False) |
574 | + development_series_alias = StringCol(notNull=False, default=None) |
575 | |
576 | def __repr__(self): |
577 | displayname = self.displayname.encode('ASCII', 'backslashreplace') |
578 | @@ -823,15 +824,25 @@ |
579 | return getUtility( |
580 | IArchiveSet).getByDistroAndName(self, name) |
581 | |
582 | - def getSeries(self, name_or_version): |
583 | + def resolveSeriesAlias(self, name): |
584 | + """See `IDistribution`.""" |
585 | + if self.development_series_alias == name: |
586 | + currentseries = self.currentseries |
587 | + if currentseries is not None: |
588 | + return currentseries |
589 | + raise NoSuchDistroSeries(name) |
590 | + |
591 | + def getSeries(self, name_or_version, follow_aliases=False): |
592 | """See `IDistribution`.""" |
593 | distroseries = Store.of(self).find(DistroSeries, |
594 | Or(DistroSeries.name == name_or_version, |
595 | DistroSeries.version == name_or_version), |
596 | DistroSeries.distribution == self).one() |
597 | - if not distroseries: |
598 | - raise NoSuchDistroSeries(name_or_version) |
599 | - return distroseries |
600 | + if distroseries: |
601 | + return distroseries |
602 | + if follow_aliases: |
603 | + return self.resolveSeriesAlias(name_or_version) |
604 | + raise NoSuchDistroSeries(name_or_version) |
605 | |
606 | def getDevelopmentSeries(self): |
607 | """See `IDistribution`.""" |
608 | @@ -957,7 +968,8 @@ |
609 | search_text=search_text, owner=owner, sort=sort, |
610 | distribution=self).getResults() |
611 | |
612 | - def getDistroSeriesAndPocket(self, distroseries_name): |
613 | + def getDistroSeriesAndPocket(self, distroseries_name, |
614 | + follow_aliases=False): |
615 | """See `IDistribution`.""" |
616 | # Get the list of suffixes. |
617 | suffixes = [suffix for suffix, ignored in suffixpocket.items()] |
618 | @@ -966,13 +978,18 @@ |
619 | |
620 | for suffix in suffixes: |
621 | if distroseries_name.endswith(suffix): |
622 | + left_size = len(distroseries_name) - len(suffix) |
623 | + left = distroseries_name[:left_size] |
624 | try: |
625 | - left_size = len(distroseries_name) - len(suffix) |
626 | - return (self[distroseries_name[:left_size]], |
627 | - suffixpocket[suffix]) |
628 | + return self[left], suffixpocket[suffix] |
629 | except KeyError: |
630 | + if follow_aliases: |
631 | + try: |
632 | + resolved = self.resolveSeriesAlias(left) |
633 | + return resolved, suffixpocket[suffix] |
634 | + except NoSuchDistroSeries: |
635 | + pass |
636 | # Swallow KeyError to continue round the loop. |
637 | - pass |
638 | |
639 | raise NotFoundError(distroseries_name) |
640 | |
641 | |
642 | === modified file 'lib/lp/registry/model/distroseries.py' |
643 | --- lib/lp/registry/model/distroseries.py 2013-08-19 06:43:04 +0000 |
644 | +++ lib/lp/registry/model/distroseries.py 2013-08-29 10:30:37 +0000 |
645 | @@ -55,6 +55,7 @@ |
646 | from lp.bugs.model.structuralsubscription import ( |
647 | StructuralSubscriptionTargetMixin, |
648 | ) |
649 | +from lp.registry.errors import NoSuchDistroSeries |
650 | from lp.registry.interfaces.distroseries import ( |
651 | DerivationError, |
652 | IDistroSeries, |
653 | @@ -1490,9 +1491,17 @@ |
654 | DistroSeries.hide_all_translations == False, |
655 | DistroSeries.id == POTemplate.distroseriesID).config(distinct=True) |
656 | |
657 | - def queryByName(self, distribution, name): |
658 | + def queryByName(self, distribution, name, follow_aliases=False): |
659 | """See `IDistroSeriesSet`.""" |
660 | - return DistroSeries.selectOneBy(distribution=distribution, name=name) |
661 | + series = DistroSeries.selectOneBy(distribution=distribution, name=name) |
662 | + if series is not None: |
663 | + return series |
664 | + if follow_aliases: |
665 | + try: |
666 | + return distribution.resolveSeriesAlias(name) |
667 | + except NoSuchDistroSeries: |
668 | + pass |
669 | + return None |
670 | |
671 | def queryByVersion(self, distribution, version): |
672 | """See `IDistroSeriesSet`.""" |
673 | |
674 | === modified file 'lib/lp/registry/stories/distribution/xx-distribution-overview.txt' |
675 | --- lib/lp/registry/stories/distribution/xx-distribution-overview.txt 2012-07-30 12:11:31 +0000 |
676 | +++ lib/lp/registry/stories/distribution/xx-distribution-overview.txt 2013-08-29 10:30:37 +0000 |
677 | @@ -1,4 +1,6 @@ |
678 | -= Distributions = |
679 | +============= |
680 | +Distributions |
681 | +============= |
682 | |
683 | Launchpad can be used by both upstream projects and distributions - the |
684 | system understands the intrinsic differences between those kinds of |
685 | @@ -6,16 +8,18 @@ |
686 | system. |
687 | |
688 | |
689 | -== Distribution listings == |
690 | +Distribution listings |
691 | +===================== |
692 | |
693 | -There is a listing of all distributions at /distributions/: |
694 | +There is a listing of all distributions at /distros/: |
695 | |
696 | >>> anon_browser.open('http://launchpad.dev/distros/') |
697 | >>> print anon_browser.title |
698 | Distributions registered in Launchpad |
699 | |
700 | |
701 | -== Distribution home pages == |
702 | +Distribution home pages |
703 | +======================= |
704 | |
705 | Each distribution has a home page in the system. The page will show a |
706 | consolidated view of active series, milestones and derivatives of that |
707 | @@ -79,23 +83,23 @@ |
708 | The 5 latest derivatives are displayed on the home page |
709 | along with a link to list all of them. |
710 | |
711 | - >>> anon_browser.open('http://launchpad.dev/ubuntu') |
712 | - >>> print extract_text(find_tag_by_id(anon_browser.contents, |
713 | - ... 'derivatives')) |
714 | - Latest derivatives |
715 | - 9.9.9 |
716 | - “Hoary Mock” series |
717 | - (from Warty) |
718 | - 8.06 |
719 | - “Krunch” series |
720 | - (from Hoary) |
721 | - 6.6.6 |
722 | - “Breezy Badger Autotest” series |
723 | - (from Warty) |
724 | - 2005 |
725 | - “Guada2005” series |
726 | - (from Hoary) |
727 | - All derivatives |
728 | + >>> anon_browser.open('http://launchpad.dev/ubuntu') |
729 | + >>> print extract_text( |
730 | + ... find_tag_by_id(anon_browser.contents, 'derivatives')) |
731 | + Latest derivatives |
732 | + 9.9.9 |
733 | + “Hoary Mock” series |
734 | + (from Warty) |
735 | + 8.06 |
736 | + “Krunch” series |
737 | + (from Hoary) |
738 | + 6.6.6 |
739 | + “Breezy Badger Autotest” series |
740 | + (from Warty) |
741 | + 2005 |
742 | + “Guada2005” series |
743 | + (from Hoary) |
744 | + All derivatives |
745 | |
746 | |
747 | The "All derivatives" link takes you to the derivatives page. |
748 | @@ -106,16 +110,39 @@ |
749 | If there are no derivatives, the link to the derivatives page is |
750 | not there. |
751 | |
752 | - >>> anon_browser.open('http://launchpad.dev/ubuntutest') |
753 | - >>> print extract_text(find_tag_by_id(anon_browser.contents, |
754 | - ... 'derivatives')) |
755 | - Latest derivatives |
756 | - No derivatives. |
757 | - |
758 | - |
759 | - == Registration information == |
760 | - |
761 | -The distroseries pages presents the registeration information. |
762 | + >>> anon_browser.open('http://launchpad.dev/ubuntutest') |
763 | + >>> print extract_text( |
764 | + ... find_tag_by_id(anon_browser.contents, 'derivatives')) |
765 | + Latest derivatives |
766 | + No derivatives. |
767 | + |
768 | + |
769 | +If there is a development series alias, it becomes a redirect. |
770 | + |
771 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
772 | + >>> from lp.testing import celebrity_logged_in |
773 | + >>> from zope.component import getUtility |
774 | + |
775 | + >>> anon_browser.open("http://launchpad.dev/ubuntu/devel") |
776 | + Traceback (most recent call last): |
777 | + ... |
778 | + NotFound: Object: <Distribution ...>, name: u'devel' |
779 | + |
780 | + >>> with celebrity_logged_in("admin"): |
781 | + ... ubuntu = getUtility(IDistributionSet).getByName(u"ubuntu") |
782 | + ... ubuntu.development_series_alias = "devel" |
783 | + >>> anon_browser.open("http://launchpad.dev/ubuntu/devel") |
784 | + >>> print anon_browser.url |
785 | + http://launchpad.dev/ubuntu/hoary |
786 | + >>> anon_browser.open("http://launchpad.dev/ubuntu/devel/+builds") |
787 | + >>> print anon_browser.url |
788 | + http://launchpad.dev/ubuntu/hoary/+builds |
789 | + |
790 | + |
791 | +Registration information |
792 | +======================== |
793 | + |
794 | +The distroseries pages presents the registration information. |
795 | |
796 | >>> anon_browser.open('http://launchpad.dev/ubuntu') |
797 | |
798 | @@ -129,7 +156,8 @@ |
799 | http://launchpad.dev/~ubuntu-team |
800 | |
801 | |
802 | -== Redirection for webservice URLs == |
803 | +Redirection for webservice URLs |
804 | +=============================== |
805 | |
806 | The webservice exposes a URL for the archive associated with the distribution. |
807 | Displaying the page for that URL is nonsensical (it looks like the PPA |
808 | |
809 | === modified file 'lib/lp/registry/stories/webservice/xx-distribution.txt' |
810 | --- lib/lp/registry/stories/webservice/xx-distribution.txt 2012-10-25 11:47:43 +0000 |
811 | +++ lib/lp/registry/stories/webservice/xx-distribution.txt 2013-08-29 10:30:37 +0000 |
812 | @@ -32,6 +32,7 @@ |
813 | date_created: u'2006-10-16T18:31:43.415195+00:00' |
814 | derivatives_collection_link: u'http://.../ubuntu/derivatives' |
815 | description: u'Ubuntu is a new approach...' |
816 | + development_series_alias: None |
817 | display_name: u'Ubuntu' |
818 | domain_name: u'ubuntulinux.org' |
819 | driver_link: None |
820 | @@ -165,7 +166,8 @@ |
821 | "getCountryMirror" returns the country DNS mirror for a given country; |
822 | returning None if there isn't one. |
823 | |
824 | - >>> # Prepare stuff. |
825 | +Prepare stuff. |
826 | + |
827 | >>> from zope.component import getUtility |
828 | >>> from lp.testing.pages import webservice_for_person |
829 | >>> from lp.services.webapp.interfaces import OAuthPermission |
830 | @@ -187,7 +189,8 @@ |
831 | ... permission=OAuthPermission.WRITE_PUBLIC) |
832 | >>> logout() |
833 | |
834 | - >>> # Mark new mirror as official and a country mirror. |
835 | +Mark new mirror as official and a country mirror. |
836 | + |
837 | >>> patch = { |
838 | ... u'status': 'Official', |
839 | ... u'country_dns_mirror': True |
840 | |
841 | === modified file 'lib/lp/registry/tests/test_distribution.py' |
842 | --- lib/lp/registry/tests/test_distribution.py 2012-09-18 19:41:02 +0000 |
843 | +++ lib/lp/registry/tests/test_distribution.py 2013-08-29 10:30:37 +0000 |
844 | @@ -1,4 +1,4 @@ |
845 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
846 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
847 | # GNU Affero General Public License version 3 (see the file LICENSE). |
848 | |
849 | """Tests for Distribution.""" |
850 | @@ -404,8 +404,20 @@ |
851 | name="dappere", version="42.6") |
852 | self.assertEquals(series, distro.getSeries("42.6")) |
853 | |
854 | - |
855 | -class SeriesTests(TestCaseWithFactory): |
856 | + def test_development_series_alias(self): |
857 | + distro = self.factory.makeDistribution() |
858 | + with person_logged_in(distro.owner): |
859 | + distro.development_series_alias = "devel" |
860 | + self.assertRaises( |
861 | + NoSuchDistroSeries, distro.getSeries, "devel", follow_aliases=True) |
862 | + series = self.factory.makeDistroSeries( |
863 | + distribution=distro, status=SeriesStatus.DEVELOPMENT) |
864 | + self.assertRaises(NoSuchDistroSeries, distro.getSeries, "devel") |
865 | + self.assertEqual( |
866 | + series, distro.getSeries("devel", follow_aliases=True)) |
867 | + |
868 | + |
869 | +class DerivativesTests(TestCaseWithFactory): |
870 | """Test IDistribution.derivatives. |
871 | """ |
872 | |
873 | @@ -416,10 +428,8 @@ |
874 | distro2 = self.factory.makeDistribution() |
875 | previous_series = self.factory.makeDistroSeries(distribution=distro1) |
876 | series = self.factory.makeDistroSeries( |
877 | - distribution=distro2, |
878 | - previous_series=previous_series) |
879 | - self.assertContentEqual( |
880 | - [series], distro1.derivatives) |
881 | + distribution=distro2, previous_series=previous_series) |
882 | + self.assertContentEqual([series], distro1.derivatives) |
883 | |
884 | |
885 | class DistroSnapshotTestCase(TestCaseWithFactory): |
886 | |
887 | === modified file 'lib/lp/soyuz/interfaces/archive.py' |
888 | --- lib/lp/soyuz/interfaces/archive.py 2013-06-20 17:24:46 +0000 |
889 | +++ lib/lp/soyuz/interfaces/archive.py 2013-08-29 10:30:37 +0000 |
890 | @@ -1377,6 +1377,12 @@ |
891 | immediately if the copy passes basic security checks and the copy |
892 | will happen sometime later with full checking. |
893 | |
894 | + If the source or target distribution has a development series alias, |
895 | + then it may be used as the source or target distroseries name |
896 | + respectively; but note that this will always be resolved to the true |
897 | + development series of that distribution, which may not match the |
898 | + alias in the respective published archives. |
899 | + |
900 | :param source_name: a string name of the package to copy. |
901 | :param version: the version of the package to copy. |
902 | :param from_archive: the source archive from which to copy. |
903 | @@ -1456,6 +1462,12 @@ |
904 | Partial changes of the destination archive can happen because each |
905 | source is copied in its own transaction. |
906 | |
907 | + If the source or target distribution has a development series alias, |
908 | + then it may be used as the source or target distroseries name |
909 | + respectively; but note that this will always be resolved to the true |
910 | + development series of that distribution, which may not match the |
911 | + alias in the respective published archives. |
912 | + |
913 | :param source_names: a list of string names of packages to copy. |
914 | :param from_archive: the source archive from which to copy. |
915 | :param to_pocket: the target pocket (as a string). |
916 | @@ -1522,6 +1534,12 @@ |
917 | copies cannot be performed, the whole operation will fail. There |
918 | will be no partial changes of the destination archive. |
919 | |
920 | + If the source or target distribution has a development series alias, |
921 | + then it may be used as the source or target distroseries name |
922 | + respectively; but note that this will always be resolved to the true |
923 | + development series of that distribution, which may not match the |
924 | + alias in the respective published archives. |
925 | + |
926 | :param source_names: a list of string names of packages to copy. |
927 | :param from_archive: the source archive from which to copy. |
928 | :param to_pocket: the target pocket (as a string). |
929 | @@ -1564,6 +1582,12 @@ |
930 | Copy a specific version of a named source to the destination |
931 | archive if necessary. |
932 | |
933 | + If the source distribution has a development series alias, then it |
934 | + may be used as the source distroseries name; but note that this will |
935 | + always be resolved to the true development series of that |
936 | + distribution, which may not match the alias in the published source |
937 | + archive. |
938 | + |
939 | :param source_name: a string name of the package to copy. |
940 | :param version: the version of the package to copy. |
941 | :param from_archive: the source archive from which to copy. |
942 | |
943 | === modified file 'lib/lp/soyuz/model/archive.py' |
944 | --- lib/lp/soyuz/model/archive.py 2013-07-22 09:38:22 +0000 |
945 | +++ lib/lp/soyuz/model/archive.py 2013-08-29 10:30:37 +0000 |
946 | @@ -628,9 +628,11 @@ |
947 | SourcePackagePublishingHistory, *clauses).order_by( |
948 | SourcePackageName.name, Desc(SourcePackageRelease.version), |
949 | Desc(SourcePackagePublishingHistory.id)) |
950 | + |
951 | def eager_load(rows): |
952 | load_related( |
953 | SourcePackageRelease, rows, ['sourcepackagereleaseID']) |
954 | + |
955 | return DecoratedResultSet(sources, pre_iter_hook=eager_load) |
956 | |
957 | @property |
958 | @@ -1749,7 +1751,7 @@ |
959 | distribution = self.distribution |
960 | if to_series is not None: |
961 | result = getUtility(IDistroSeriesSet).queryByName( |
962 | - distribution, to_series) |
963 | + distribution, to_series, follow_aliases=True) |
964 | if result is None: |
965 | raise NoSuchDistroSeries(to_series) |
966 | series = result |
967 | |
968 | === modified file 'lib/lp/soyuz/scripts/publishdistro.py' |
969 | --- lib/lp/soyuz/scripts/publishdistro.py 2013-05-01 18:39:38 +0000 |
970 | +++ lib/lp/soyuz/scripts/publishdistro.py 2013-08-29 10:30:37 +0000 |
971 | @@ -1,4 +1,4 @@ |
972 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
973 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
974 | # GNU Affero General Public License version 3 (see the file LICENSE). |
975 | |
976 | """Publisher script class.""" |
977 | @@ -305,6 +305,8 @@ |
978 | publisher.D_writeReleaseFiles(careful_indexing) |
979 | # The caller will commit this last step. |
980 | |
981 | + publisher.createSeriesAliases() |
982 | + |
983 | def main(self): |
984 | """See `LaunchpadScript`.""" |
985 | self.validateOptions() |
986 | |
987 | === modified file 'lib/lp/soyuz/scripts/tests/test_publishdistro.py' |
988 | --- lib/lp/soyuz/scripts/tests/test_publishdistro.py 2013-05-02 00:14:02 +0000 |
989 | +++ lib/lp/soyuz/scripts/tests/test_publishdistro.py 2013-08-29 10:30:37 +0000 |
990 | @@ -1,4 +1,4 @@ |
991 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
992 | +# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
993 | # GNU Affero General Public License version 3 (see the file LICENSE). |
994 | |
995 | """Functional tests for publish-distro.py script.""" |
996 | @@ -30,7 +30,6 @@ |
997 | from lp.soyuz.enums import ( |
998 | ArchivePurpose, |
999 | ArchiveStatus, |
1000 | - BinaryPackageFormat, |
1001 | PackagePublishingStatus, |
1002 | ) |
1003 | from lp.soyuz.interfaces.archive import IArchiveSet |
1004 | @@ -373,6 +372,7 @@ |
1005 | self.C_doFTPArchive = FakeMethod() |
1006 | self.C_writeIndexes = FakeMethod() |
1007 | self.D_writeReleaseFiles = FakeMethod() |
1008 | + self.createSeriesAliases = FakeMethod() |
1009 | |
1010 | |
1011 | class TestPublishDistroMethods(TestCaseWithFactory): |
20 + def _allIndexFiles( self, distroseries):
21 + """Return all index files on disk for a distroseries."""
This method seems like it should mostly be elsewhere. It's not reasonably practical to factor out the commonalities somehow?
81 + if pocket == PackagePublishi ngPocket. RELEASE: pocket] )
82 + alias_suite = alias
83 + else:
84 + alias_suite = "%s%s" % (alias, pocketsuffix[
This need not be special-cased; pocketsuffix[ RELEASE] == "".
57 + def createSeriesAli ases(self) :
This has the slightly weird behavior that a single missing current suite will cause just that alias to point to the old series, while the other suites point to the new one. This probably can't happen in production, because primary archive indices are always all generated for all suites on series creation, even if empty.
732 distribution = self.distribution IDistroSeriesSe t).queryByName( aliases= True) ies(to_ series)
733 if to_series is not None:
734 result = getUtility(
735 - distribution, to_series)
736 + distribution, to_series, follow_
737 if result is None:
738 raise NoSuchDistroSer
739 series = result
This is probably fairly confusing for PPAs. You can copy to devel and it'll actually be different from the current devel symlink in the PPA, with no way to tell beforehand. But we probably can't avoid it given the current fairly braindead method signature.