Merge lp:~mwhudson/launchpad/branching-ubuntu into lp:launchpad
- branching-ubuntu
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~mwhudson/launchpad/branching-ubuntu |
Merge into: | lp:launchpad |
Diff against target: |
1129 lines 4 files modified
database/schema/security.cfg (+16/-0) lib/lp/codehosting/branchdistro.py (+364/-0) lib/lp/codehosting/tests/test_branchdistro.py (+685/-0) scripts/branch-distro.py (+39/-0) |
To merge this branch: | bzr merge lp:~mwhudson/launchpad/branching-ubuntu |
Related bugs: | |
Related blueprints: |
Branching Ubuntu on release
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Lange (community) | Approve | ||
Review via email: mp+13269@code.launchpad.net |
Commit message
A script to open a new distribution for branch-based development -- see https:/
Description of the change
Michael Hudson-Doyle (mwhudson) wrote : | # |
Michael Hudson-Doyle (mwhudson) wrote : | # |
Oh, I guess I should say that we need to test this as much as we can on staging, I think this should be possible.
Jonathan Lange (jml) wrote : | # |
Hey Michael,
Not much to say here, given that we've already talked about it. I look forward to seeing this QA'd on staging.
I have a few comments, one or two of which must be fixed before landing. Other than that, looks good. Thanks for getting this done.
jml
> === modified file 'database/
> --- database/
> +++ database/
> @@ -641,6 +641,22 @@
> # Merge notifications
> public.
>
> +[branch-distro]
> +type=user
> +public.branch = SELECT, INSERT
> +public.
> +public.
> +public.
> +public.karma = SELECT, INSERT
> +public.karmaaction = SELECT
> +public.person = SELECT
> +public.product = SELECT
> +public.
> +public.
> +public.
> +public.
> +
> +
How did you determine these? Are you sure you've got everything?
> [targetnamecach
> type=user
> groups=script
>
> === added file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -0,0 +1,355 @@
> +# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Opening a new DistroSeries for branch based development.
> +
> +Intended to be run just after a new distro series has been completed, this
> +script will create an official package branch in the new series for every one
> +in the old. The old branch will become stacked on the new, to avoid a using
> +too much disk space whilst retaining best performance for the new branch.
> +"""
> +
> +import os
> +
> +from bzrlib.branch import Branch
> +from bzrlib.bzrdir import BzrDir
> +from bzrlib.errors import NotBranchError, NotStacked
> +
> +from lazr.uri import URI
> +
> +import transaction
> +
> +from zope.component import getUtility
> +
> +from canonical.
> +from canonical.config import config
> +
> +from lp.code.
> +from lp.code.
> +from lp.code.
> +from lp.code.
> + IFindOfficialBr
> +from lp.code.enums import BranchType
> +from lp.codehosting.vfs import branch_id_to_path
> +from lp.registry.
> +from lp.registry.
> +
> +
> +__metaclass__ = type
> +__all__ = []
> +
These should be above the imports & below the docstring, IIRC.
> +
> +def switch_
> + """Move bzr data from an old to a new branch, leaving old stacked on new.
> +
> + T...
Michael Hudson-Doyle (mwhudson) wrote : | # |
Jonathan Lange wrote:
> Review: Approve
> Hey Michael,
>
> Not much to say here, given that we've already talked about it. I look forward to seeing this QA'd on staging.
>
> I have a few comments, one or two of which must be fixed before landing. Other than that, looks good. Thanks for getting this done.
>
> jml
>
>> === modified file 'database/
>> --- database/
>> +++ database/
>> @@ -641,6 +641,22 @@
>> # Merge notifications
>> public.
>>
>> +[branch-distro]
>> +type=user
>> +public.branch = SELECT, INSERT
>> +public.
>> +public.
>> +public.
>> +public.karma = SELECT, INSERT
>> +public.karmaaction = SELECT
>> +public.person = SELECT
>> +public.product = SELECT
>> +public.
>> +public.
>> +public.
>> +public.
>> +
>> +
>
> How did you determine these?
The tests run as this user, so I ran make schema over and over and over
again whilst gradually increasing the permissions...
> Are you sure you've got everything?
Reasonably. As you noticed, I spent a long time writing tests...
>> [targetnamecach
>> type=user
>> groups=script
>>
>
>
>> === added file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -0,0 +1,355 @@
>> +# Copyright 2009 Canonical Ltd. This software is licensed under the
>> +# GNU Affero General Public License version 3 (see the file LICENSE).
>> +
>> +"""Opening a new DistroSeries for branch based development.
>> +
>> +Intended to be run just after a new distro series has been completed, this
>> +script will create an official package branch in the new series for every one
>> +in the old. The old branch will become stacked on the new, to avoid a using
>> +too much disk space whilst retaining best performance for the new branch.
>> +"""
>> +
>> +import os
>> +
>> +from bzrlib.branch import Branch
>> +from bzrlib.bzrdir import BzrDir
>> +from bzrlib.errors import NotBranchError, NotStacked
>> +
>> +from lazr.uri import URI
>> +
>> +import transaction
>> +
>> +from zope.component import getUtility
>> +
>> +from canonical.
>> +from canonical.config import config
>> +
>> +from lp.code.
>> +from lp.code.
>> +from lp.code.
>> +from lp.code.
>> + IFindOfficialBr
>> +from lp.code.enums import BranchType
>> +from lp.codehosting.vfs import branch_id_to_path
>> +from lp.registry.
>> +from lp.registry.
Preview Diff
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2009-10-05 15:07:10 +0000 |
3 | +++ database/schema/security.cfg 2009-10-14 00:31:13 +0000 |
4 | @@ -641,6 +641,22 @@ |
5 | # Merge notifications |
6 | public.codereviewvote = SELECT |
7 | |
8 | +[branch-distro] |
9 | +type=user |
10 | +public.branch = SELECT, INSERT |
11 | +public.branchsubscription = SELECT, INSERT |
12 | +public.distribution = SELECT |
13 | +public.distroseries = SELECT |
14 | +public.karma = SELECT, INSERT |
15 | +public.karmaaction = SELECT |
16 | +public.person = SELECT |
17 | +public.product = SELECT |
18 | +public.seriessourcepackagebranch = SELECT, INSERT, DELETE |
19 | +public.sourcepackagename = SELECT |
20 | +public.teamparticipation = SELECT |
21 | +public.validpersoncache = SELECT |
22 | + |
23 | + |
24 | [targetnamecacheupdater] |
25 | type=user |
26 | groups=script |
27 | |
28 | === added file 'lib/lp/codehosting/branchdistro.py' |
29 | --- lib/lp/codehosting/branchdistro.py 1970-01-01 00:00:00 +0000 |
30 | +++ lib/lp/codehosting/branchdistro.py 2009-10-14 00:31:13 +0000 |
31 | @@ -0,0 +1,364 @@ |
32 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
33 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
34 | + |
35 | +"""Opening a new DistroSeries for branch based development. |
36 | + |
37 | +Intended to be run just after a new distro series has been completed, this |
38 | +script will create an official package branch in the new series for every one |
39 | +in the old. The old branch will become stacked on the new, to avoid a using |
40 | +too much disk space whilst retaining best performance for the new branch. |
41 | +""" |
42 | + |
43 | +__metaclass__ = type |
44 | +__all__ = [ |
45 | + 'DistroBrancher', |
46 | + 'switch_branches', |
47 | + ] |
48 | + |
49 | +import os |
50 | + |
51 | +from bzrlib.branch import Branch |
52 | +from bzrlib.bzrdir import BzrDir |
53 | +from bzrlib.errors import NotBranchError, NotStacked |
54 | + |
55 | +from lazr.uri import URI |
56 | + |
57 | +import transaction |
58 | + |
59 | +from zope.component import getUtility |
60 | + |
61 | +from canonical.launchpad.interfaces import ILaunchpadCelebrities |
62 | +from canonical.config import config |
63 | + |
64 | +from lp.code.interfaces.branchcollection import IAllBranches |
65 | +from lp.code.interfaces.branch import BranchExists |
66 | +from lp.code.interfaces.branchnamespace import IBranchNamespaceSet |
67 | +from lp.code.interfaces.seriessourcepackagebranch import ( |
68 | + IFindOfficialBranchLinks) |
69 | +from lp.code.enums import BranchType |
70 | +from lp.codehosting.vfs import branch_id_to_path |
71 | +from lp.registry.interfaces.distribution import IDistributionSet |
72 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
73 | + |
74 | + |
75 | +def switch_branches(prefix, scheme, old_db_branch, new_db_branch): |
76 | + """Move bzr data from an old to a new branch, leaving old stacked on new. |
77 | + |
78 | + This function is intended to be used just after Ubuntu is released to |
79 | + create (at the bzr level) a new trunk branch for a source package for the |
80 | + next release of the distribution. We move the bzr data to the location |
81 | + for the new branch and replace the trunk branch for the just released |
82 | + version with a stacked branch pointing at the new branch. |
83 | + |
84 | + The procedure is to complicated to be carried out atomically, so if this |
85 | + function is interrupted things may be a little inconsistent (e.g. there |
86 | + might be a branch in the old location, but not stacked on the new location |
87 | + yet). There should be no data loss though. |
88 | + |
89 | + :param prefix: The non-branch id dependent part of the physical path to |
90 | + the branches on disk. |
91 | + :param scheme: The branches should be open-able at a URL of the form |
92 | + ``scheme + :/// + unique_name``. |
93 | + :param old_db_branch: The branch that currently has the trunk bzr data. |
94 | + :param old_db_branch: The new trunk branch. This should not have any |
95 | + presence on disk yet. |
96 | + """ |
97 | + # Move .bzr directory from old to new location, crashing through the |
98 | + # abstraction we usually hide our branch locations behind. |
99 | + old_underlying_path = os.path.join( |
100 | + prefix, branch_id_to_path(old_db_branch.id)) |
101 | + new_underlying_path = os.path.join( |
102 | + prefix, branch_id_to_path(new_db_branch.id)) |
103 | + os.makedirs(new_underlying_path) |
104 | + os.rename( |
105 | + os.path.join(old_underlying_path, '.bzr'), |
106 | + os.path.join(new_underlying_path, '.bzr')) |
107 | + |
108 | + # Create branch at old location -- we use the "clone('null:')" trick to |
109 | + # preserve the format. We have to open at the logical, unique_name-based, |
110 | + # location so that it works to set the stacked on url to '/' + a |
111 | + # unique_name. |
112 | + new_location_bzrdir = BzrDir.open( |
113 | + str(URI( |
114 | + scheme=scheme, host='', path='/' + new_db_branch.unique_name))) |
115 | + old_location_bzrdir = new_location_bzrdir.clone( |
116 | + str(URI( |
117 | + scheme=scheme, host='', path='/' + old_db_branch.unique_name)), |
118 | + revision_id='null:') |
119 | + |
120 | + # Set the stacked on url for old location. |
121 | + old_location_branch = old_location_bzrdir.open_branch() |
122 | + old_location_branch.set_stacked_on_url('/' + new_db_branch.unique_name) |
123 | + |
124 | + # Pull from new location to old -- this won't actually transfer any |
125 | + # revisions, just update the last revision pointer. |
126 | + old_location_branch.pull(new_location_bzrdir.open_branch()) |
127 | + |
128 | + |
129 | +class DistroBrancher: |
130 | + """Open a new distroseries for branch based development. |
131 | + |
132 | + `makeNewBranches` will create an official package branch in the new series |
133 | + for every one in the old. `checkNewBranches` will check that a previous |
134 | + run of this script completed successfully -- this is only likely to be |
135 | + really useful if a script run died halfway through or had to be killed. |
136 | + """ |
137 | + |
138 | + def __init__(self, logger, old_distroseries, new_distroseries): |
139 | + """Construct a `DistroBrancher`. |
140 | + |
141 | + The old and new distroseries must be from the same distribution, but |
142 | + not the same distroseries. |
143 | + |
144 | + :param logger: A Logger. Problems will be logged to this object at |
145 | + the WARNING level or higher; progress reports will be logged at |
146 | + the DEBUG level. |
147 | + :param old_distroseries: The distroseries that will be examined to |
148 | + find existing source package branches. |
149 | + :param new_distroseries: The distroseries that will have new official |
150 | + source branches made for it. |
151 | + """ |
152 | + self.logger = logger |
153 | + if old_distroseries.distribution != new_distroseries.distribution: |
154 | + raise AssertionError( |
155 | + "%s and %s are from different distributions!" % |
156 | + (old_distroseries, new_distroseries)) |
157 | + if old_distroseries == new_distroseries: |
158 | + raise AssertionError( |
159 | + "New and old distributions must be different!") |
160 | + self.old_distroseries = old_distroseries |
161 | + self.new_distroseries = new_distroseries |
162 | + |
163 | + @classmethod |
164 | + def fromNames(cls, logger, distribution_name, old_distroseries_name, |
165 | + new_distroseries_name): |
166 | + """Make a `DistroBrancher` from the names of a distro and two series. |
167 | + """ |
168 | + distribution = getUtility(IDistributionSet).getByName( |
169 | + distribution_name) |
170 | + new_distroseries = distribution.getSeries(new_distroseries_name) |
171 | + old_distroseries = distribution.getSeries(old_distroseries_name) |
172 | + return cls(logger, old_distroseries, new_distroseries) |
173 | + |
174 | + def _existingOfficialBranches(self): |
175 | + """Return the collection of official branches in the old distroseries. |
176 | + """ |
177 | + branches = getUtility(IAllBranches) |
178 | + distroseries_branches = branches.inDistroSeries(self.old_distroseries) |
179 | + return distroseries_branches.officialBranches().getBranches() |
180 | + |
181 | + def checkConsistentOfficialPackageBranch(self, db_branch): |
182 | + """Check that `db_branch` is a consistent official package branch. |
183 | + |
184 | + 'Consistent official package branch' means: |
185 | + |
186 | + * It's a package branch (rather than a personal or junk branch). |
187 | + * It's official for its SourcePackage and no other. |
188 | + |
189 | + This function simply returns True or False -- any problems will be |
190 | + logged to ``self.logger``. |
191 | + |
192 | + :param db_branch: The `IBranch` to check. |
193 | + :return: ``True`` if the branch is a consistent official package |
194 | + branch, ``False`` otherwise. |
195 | + """ |
196 | + if db_branch.product: |
197 | + self.logger.warning( |
198 | + "Encountered unexpected product branch %r", |
199 | + db_branch.unique_name) |
200 | + return False |
201 | + if not db_branch.distroseries: |
202 | + self.logger.warning( |
203 | + "Encountered unexpected personal branch %s", |
204 | + db_branch.unique_name) |
205 | + return False |
206 | + find_branch_links = getUtility(IFindOfficialBranchLinks) |
207 | + links = list(find_branch_links.findForBranch(db_branch)) |
208 | + if len(links) == 0: |
209 | + self.logger.warning( |
210 | + "%s is not an official branch", db_branch.unique_name) |
211 | + return False |
212 | + elif len(links) > 1: |
213 | + series_text = ', '.join([ |
214 | + link.sourcepackage.path for link in links]) |
215 | + self.logger.warning( |
216 | + "%s is official for multiple series: %s", |
217 | + db_branch.unique_name, series_text) |
218 | + return False |
219 | + elif links[0].sourcepackage != db_branch.sourcepackage: |
220 | + self.logger.warning( |
221 | + "%s is the official branch for %s but not its " |
222 | + "sourcepackage", db_branch.unique_name, |
223 | + links[0].sourcepackage.path) |
224 | + return False |
225 | + return True |
226 | + |
227 | + def makeNewBranches(self): |
228 | + """Make official branches in the new distroseries.""" |
229 | + for db_branch in self._existingOfficialBranches(): |
230 | + self.logger.debug("Processing %s" % db_branch.unique_name) |
231 | + try: |
232 | + self.makeOneNewBranch(db_branch) |
233 | + except BranchExists: |
234 | + pass |
235 | + |
236 | + def checkNewBranches(self): |
237 | + """Check the branches in the new distroseries are present and correct. |
238 | + |
239 | + This function checks that every official package branch in the old |
240 | + distroseries has a matching branch in the new distroseries and that |
241 | + stacking is set up as we expect in both the hosted and mirrored areas |
242 | + on disk. |
243 | + |
244 | + Every branch will be checked, even if some fail. |
245 | + |
246 | + This function simply returns True or False -- any problems will be |
247 | + logged to ``self.logger``. |
248 | + |
249 | + :return: ``True`` if every branch passes the check, ``False`` |
250 | + otherwise. |
251 | + """ |
252 | + ok = True |
253 | + for db_branch in self._existingOfficialBranches(): |
254 | + self.logger.debug("Checking %s" % db_branch.unique_name) |
255 | + try: |
256 | + if not self.checkOneBranch(db_branch): |
257 | + ok = False |
258 | + except: |
259 | + ok = False |
260 | + self.logger.exception( |
261 | + "Unexpected error checking %s!", db_branch) |
262 | + return ok |
263 | + |
264 | + def checkOneBranch(self, old_db_branch): |
265 | + """Check a branch in the old distroseries has been copied to the new. |
266 | + |
267 | + This function checks that `old_db_branch` has a matching branch in the |
268 | + new distroseries and that stacking is set up as we expect in both the |
269 | + hosted and mirrored areas on disk. |
270 | + |
271 | + This function simply returns True or False -- any problems will be |
272 | + logged to ``self.logger``. |
273 | + |
274 | + :param old_db_branch: The branch to check. |
275 | + :return: ``True`` if the branch passes the check, ``False`` otherwise. |
276 | + """ |
277 | + ok = self.checkConsistentOfficialPackageBranch(old_db_branch) |
278 | + if not ok: |
279 | + return ok |
280 | + new_sourcepackage = self.new_distroseries.getSourcePackage( |
281 | + old_db_branch.sourcepackagename) |
282 | + new_db_branch = new_sourcepackage.getBranch( |
283 | + PackagePublishingPocket.RELEASE) |
284 | + if new_db_branch is None: |
285 | + self.logger.warning( |
286 | + "No official branch found for %s", |
287 | + new_sourcepackage.path) |
288 | + return False |
289 | + ok = self.checkConsistentOfficialPackageBranch(new_db_branch) |
290 | + if not ok: |
291 | + return ok |
292 | + # for both mirrored and hosted areas: |
293 | + for scheme in 'lp-mirrored', 'lp-hosted': |
294 | + # the branch in the new distroseries is unstacked |
295 | + new_location = str(URI( |
296 | + scheme=scheme, host='', path='/' + new_db_branch.unique_name)) |
297 | + try: |
298 | + new_bzr_branch = Branch.open(new_location) |
299 | + except NotBranchError: |
300 | + self.logger.warning( |
301 | + "No bzr branch at new location %s", new_location) |
302 | + ok = False |
303 | + else: |
304 | + try: |
305 | + new_stacked_on_url = new_bzr_branch.get_stacked_on_url() |
306 | + ok = False |
307 | + self.logger.warning( |
308 | + "New branch at %s is stacked on %s, should be " |
309 | + "unstacked.", new_location, new_stacked_on_url) |
310 | + except NotStacked: |
311 | + pass |
312 | + # The branch in the old distroseries is stacked on that in the |
313 | + # new. |
314 | + old_location = str(URI( |
315 | + scheme=scheme, host='', path='/' + old_db_branch.unique_name)) |
316 | + try: |
317 | + old_bzr_branch = Branch.open(old_location) |
318 | + except NotBranchError: |
319 | + self.logger.warning( |
320 | + "No bzr branch at old location %s", old_location) |
321 | + ok = False |
322 | + else: |
323 | + try: |
324 | + old_stacked_on_url = old_bzr_branch.get_stacked_on_url() |
325 | + if old_stacked_on_url != '/' + new_db_branch.unique_name: |
326 | + self.logger.warning( |
327 | + "Old branch at %s is stacked on %s, should be " |
328 | + "stacked on %s", old_location, old_stacked_on_url, |
329 | + '/' + new_db_branch.unique_name) |
330 | + ok = False |
331 | + except NotStacked: |
332 | + self.logger.warning( |
333 | + "Old branch at %s is not stacked, should be stacked " |
334 | + "on %s", old_location, |
335 | + '/' + new_db_branch.unique_name) |
336 | + ok = False |
337 | + # The branch in the old distroseries has no revisions in its |
338 | + # repository. We open the repository independently of the |
339 | + # branch because the branch's repository has had its fallback |
340 | + # location activated. Note that this check might fail if new |
341 | + # revisions get pushed to the branch in the old distroseries, |
342 | + # which shouldn't happen but isn't totally impossible. |
343 | + old_repo = BzrDir.open(old_location).open_repository() |
344 | + if len(old_repo.all_revision_ids()) > 0: |
345 | + self.logger.warning( |
346 | + "Repository at %s has %s revisions.", |
347 | + old_location, len(old_repo.all_revision_ids())) |
348 | + ok = False |
349 | + # The branch in the old distroseries has at least some |
350 | + # history. (We can't check that the tips are the same because |
351 | + # the branch in the new distroseries might have new revisons). |
352 | + if old_bzr_branch.last_revision() == 'null:': |
353 | + self.logger.warning( |
354 | + "Old branch at %s has null tip revision.", |
355 | + old_location) |
356 | + ok = False |
357 | + return ok |
358 | + |
359 | + def makeOneNewBranch(self, old_db_branch): |
360 | + """Copy a branch to the new distroseries. |
361 | + |
362 | + This function makes a new database branch for the same source package |
363 | + as old_db_branch but in the new distroseries and then uses |
364 | + `switch_branches` to move the underlying bzr branch to the new series |
365 | + and replace the old branch with a branch stacked on the new series' |
366 | + branch. |
367 | + |
368 | + :param old_db_branch: The branch to copy into the new distroseries. |
369 | + :raises BranchExists: This will be raised if old_db_branch has already |
370 | + been copied to the new distroseries (in the database, at least). |
371 | + """ |
372 | + if not self.checkConsistentOfficialPackageBranch(old_db_branch): |
373 | + self.logger.warning("Skipping branch") |
374 | + return |
375 | + new_namespace = getUtility(IBranchNamespaceSet).get( |
376 | + person=old_db_branch.owner, product=None, |
377 | + distroseries=self.new_distroseries, |
378 | + sourcepackagename=old_db_branch.sourcepackagename) |
379 | + new_db_branch = new_namespace.createBranch( |
380 | + BranchType.HOSTED, old_db_branch.name, old_db_branch.registrant) |
381 | + new_db_branch.sourcepackage.setBranch( |
382 | + PackagePublishingPocket.RELEASE, new_db_branch, |
383 | + getUtility(ILaunchpadCelebrities).ubuntu_branches.teamowner) |
384 | + # switch_branches *moves* the data to locations dependent on the |
385 | + # new_branch's id, so if the transaction was rolled back we wouldn't |
386 | + # know the branch id and thus wouldn't be able to find the branch data |
387 | + # again. So commit before doing that. |
388 | + transaction.commit() |
389 | + switch_branches( |
390 | + config.codehosting.hosted_branches_root, |
391 | + 'lp-hosted', old_db_branch, new_db_branch) |
392 | + switch_branches( |
393 | + config.codehosting.mirrored_branches_root, |
394 | + 'lp-mirrored', old_db_branch, new_db_branch) |
395 | + return new_db_branch |
396 | |
397 | === added file 'lib/lp/codehosting/tests/test_branchdistro.py' |
398 | --- lib/lp/codehosting/tests/test_branchdistro.py 1970-01-01 00:00:00 +0000 |
399 | +++ lib/lp/codehosting/tests/test_branchdistro.py 2009-10-14 00:31:13 +0000 |
400 | @@ -0,0 +1,685 @@ |
401 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
402 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
403 | + |
404 | +"""Tests for making new source package branches just after a distro release. |
405 | +""" |
406 | + |
407 | +__metaclass__ = type |
408 | + |
409 | +import os |
410 | +import re |
411 | +from StringIO import StringIO |
412 | +from subprocess import PIPE, Popen, STDOUT |
413 | +import textwrap |
414 | +import unittest |
415 | + |
416 | +from bzrlib.branch import Branch |
417 | +from bzrlib.bzrdir import BzrDir |
418 | +from bzrlib.errors import NotStacked |
419 | +from bzrlib.tests import TestCaseWithTransport |
420 | +from bzrlib.transport import get_transport |
421 | +from bzrlib.transport.chroot import ChrootServer |
422 | + |
423 | +from lazr.uri import URI |
424 | + |
425 | +import transaction |
426 | + |
427 | +from canonical.config import config |
428 | +from canonical.testing.layers import ZopelessAppServerLayer |
429 | +from canonical.launchpad.scripts.logger import FakeLogger, QuietFakeLogger |
430 | + |
431 | +from lp.codehosting.branchdistro import DistroBrancher, switch_branches |
432 | +from lp.codehosting.vfs import branch_id_to_path |
433 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
434 | +from lp.testing import TestCaseWithFactory |
435 | + |
436 | + |
437 | +# We say "RELEASE" often enough to not want to say "PackagePublishingPocket." |
438 | +# each time. |
439 | +RELEASE = PackagePublishingPocket.RELEASE |
440 | + |
441 | + |
442 | +class FakeBranch: |
443 | + """Just enough of a Branch to pass `test_switch_branches`.""" |
444 | + |
445 | + def __init__(self, id): |
446 | + self.id = id |
447 | + |
448 | + @property |
449 | + def unique_name(self): |
450 | + return branch_id_to_path(self.id) |
451 | + |
452 | + |
453 | +class TestSwitchBranches(TestCaseWithTransport): |
454 | + """Tests for `switch_branches`.""" |
455 | + |
456 | + def test_switch_branches(self): |
457 | + # switch_branches moves a branch to the new location and places a |
458 | + # branch (with no revisions) stacked on the new branch in the old |
459 | + # location. |
460 | + |
461 | + chroot_server = ChrootServer(self.get_transport()) |
462 | + chroot_server.setUp() |
463 | + self.addCleanup(chroot_server.tearDown) |
464 | + scheme = chroot_server.get_url().rstrip('/:') |
465 | + |
466 | + old_branch = FakeBranch(1) |
467 | + self.get_transport(old_branch.unique_name).create_prefix() |
468 | + tree = self.make_branch_and_tree(old_branch.unique_name) |
469 | + tree.commit(message='.') |
470 | + |
471 | + new_branch = FakeBranch(2) |
472 | + |
473 | + switch_branches('.', scheme, old_branch, new_branch) |
474 | + |
475 | + # Post conditions: |
476 | + # 1. unstacked branch in new_branch's location |
477 | + # 2. stacked branch with no revisions in repo at old_branch |
478 | + # 3. last_revision() the same for two branches |
479 | + |
480 | + old_location_bzrdir = BzrDir.open(str(URI( |
481 | + scheme=scheme, host='', path='/' + old_branch.unique_name))) |
482 | + new_location_bzrdir = BzrDir.open(str(URI( |
483 | + scheme=scheme, host='', path='/' + new_branch.unique_name))) |
484 | + |
485 | + old_location_branch = old_location_bzrdir.open_branch() |
486 | + new_location_branch = new_location_bzrdir.open_branch() |
487 | + |
488 | + # 1. unstacked branch in new_branch's location |
489 | + self.assertRaises(NotStacked, new_location_branch.get_stacked_on_url) |
490 | + |
491 | + # 2. stacked branch with no revisions in repo at old_branch |
492 | + self.assertEqual( |
493 | + '/' + new_branch.unique_name, |
494 | + old_location_branch.get_stacked_on_url()) |
495 | + self.assertEqual( |
496 | + [], old_location_bzrdir.open_repository().all_revision_ids()) |
497 | + |
498 | + # 3. last_revision() the same for two branches |
499 | + self.assertEqual( |
500 | + old_location_branch.last_revision(), |
501 | + new_location_branch.last_revision()) |
502 | + |
503 | + |
504 | +class TestDistroBrancher(TestCaseWithFactory): |
505 | + """Tests for `DistroBrancher`.""" |
506 | + |
507 | + layer = ZopelessAppServerLayer |
508 | + |
509 | + def setUp(self): |
510 | + TestCaseWithFactory.setUp(self) |
511 | + self.useBzrBranches(real_server=True) |
512 | + |
513 | + def makeOfficialPackageBranch(self, distroseries=None): |
514 | + """Make an official package branch with an underlying bzr branch.""" |
515 | + db_branch = self.factory.makePackageBranch(distroseries=distroseries) |
516 | + db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner) |
517 | + |
518 | + transaction.commit() |
519 | + |
520 | + _, tree = self.create_branch_and_tree( |
521 | + tree_location=self.factory.getUniqueString(), db_branch=db_branch, |
522 | + hosted=True) |
523 | + tree.commit('') |
524 | + mirrored_branch = BzrDir.create_branch_convenience( |
525 | + db_branch.warehouse_url) |
526 | + mirrored_branch.pull(tree.branch) |
527 | + |
528 | + return db_branch |
529 | + |
530 | + def makeNewSeriesAndBrancher(self, distroseries=None): |
531 | + """Make a DistroBrancher. |
532 | + |
533 | + Any messages logged by this DistroBrancher can be checked by calling |
534 | + `assertLogMessages` below. |
535 | + """ |
536 | + if distroseries is None: |
537 | + distroseries = self.factory.makeDistroRelease() |
538 | + self._log_file = StringIO() |
539 | + new_distroseries = self.factory.makeDistroRelease( |
540 | + distribution=distroseries.distribution, name='new') |
541 | + transaction.commit() |
542 | + self.layer.switchDbUser('branch-distro') |
543 | + return DistroBrancher( |
544 | + FakeLogger(self._log_file), distroseries, new_distroseries) |
545 | + |
546 | + def clearLogMessages(self): |
547 | + """Forget about all logged messages seen so far.""" |
548 | + self._log_file.seek(0, 0) |
549 | + self._log_file.truncate() |
550 | + |
551 | + def assertLogMessages(self, patterns): |
552 | + """Assert that the messages logged meet expectations. |
553 | + |
554 | + :param patterns: A list of regular expressions. The length must match |
555 | + the number of messages logged, and then each pattern must match |
556 | + the messages logged in order. |
557 | + """ |
558 | + log_messages = self._log_file.getvalue().splitlines() |
559 | + if len(log_messages) > len(patterns): |
560 | + self.fail( |
561 | + "More log messages (%s) than expected (%s)" % |
562 | + (log_messages, patterns)) |
563 | + elif len(log_messages) < len(patterns): |
564 | + self.fail( |
565 | + "Fewer log messages (%s) than expected (%s)" % |
566 | + (log_messages, patterns)) |
567 | + for pattern, message in zip(patterns, log_messages): |
568 | + if not re.match(pattern, message): |
569 | + self.fail("%r does not match %r" % (pattern, message)) |
570 | + |
571 | + def test_DistroBrancher_same_distro_check(self): |
572 | + # DistroBrancher.__init__ raises AssertionError if the two |
573 | + # distroseries passed are not from the same distribution. |
574 | + self.assertRaises( |
575 | + AssertionError, DistroBrancher, None, |
576 | + self.factory.makeDistroRelease(), |
577 | + self.factory.makeDistroRelease()) |
578 | + |
579 | + def test_DistroBrancher_same_distroseries_check(self): |
580 | + # DistroBrancher.__init__ raises AssertionError if passed the same |
581 | + # distroseries twice. |
582 | + distroseries = self.factory.makeDistroRelease() |
583 | + self.assertRaises( |
584 | + AssertionError, DistroBrancher, None, distroseries, distroseries) |
585 | + |
586 | + def test_fromNames(self): |
587 | + # DistroBrancher.fromNames constructs a DistroBrancher from the names |
588 | + # of a distribution and two distroseries within it. |
589 | + distribution = self.factory.makeDistribution() |
590 | + distroseries1 = self.factory.makeDistroRelease( |
591 | + distribution=distribution) |
592 | + distroseries2 = self.factory.makeDistroRelease( |
593 | + distribution=distribution) |
594 | + brancher = DistroBrancher.fromNames( |
595 | + None, distribution.name, distroseries1.name, distroseries2.name) |
596 | + self.assertEqual( |
597 | + [distroseries1, distroseries2], |
598 | + [brancher.old_distroseries, brancher.new_distroseries]) |
599 | + |
600 | + # A word on testing strategy: we don't directly test the post conditions |
601 | + # of makeOneNewBranch, but we do test that it satisfies checkOneBranch and |
602 | + # the tests for checkOneBranch verify that this function rejects various |
603 | + # ways in which makeOneNewBranch could conceivably fail. |
604 | + |
605 | + def test_makeOneNewBranch(self): |
606 | + # makeOneNewBranch creates an official package branch in the new |
607 | + # distroseries. |
608 | + db_branch = self.makeOfficialPackageBranch() |
609 | + |
610 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
611 | + brancher.makeOneNewBranch(db_branch) |
612 | + |
613 | + new_branch = brancher.new_distroseries.getSourcePackage( |
614 | + db_branch.sourcepackage.name).getBranch(RELEASE) |
615 | + |
616 | + self.assertIsNot(None, new_branch) |
617 | + # The branch owner/name/target is the same, apart from the |
618 | + # distroseries. |
619 | + self.assertEqual( |
620 | + [db_branch.owner, db_branch.distribution, |
621 | + db_branch.sourcepackagename, db_branch.name], |
622 | + [new_branch.owner, new_branch.distribution, |
623 | + new_branch.sourcepackagename, new_branch.name]) |
624 | + |
625 | + def test_makeOneNewBranch_inconsistent_branch(self): |
626 | + # makeOneNewBranch skips over an inconsistent official package branch |
627 | + # (see `checkConsistentOfficialPackageBranch` for precisely what an |
628 | + # "inconsistent official package branch" is). |
629 | + unofficial_branch = self.factory.makePackageBranch() |
630 | + brancher = self.makeNewSeriesAndBrancher( |
631 | + unofficial_branch.distroseries) |
632 | + brancher.makeOneNewBranch(unofficial_branch) |
633 | + |
634 | + new_branch = brancher.new_distroseries.getSourcePackage( |
635 | + unofficial_branch.sourcepackage.name).getBranch(RELEASE) |
636 | + self.assertIs(None, new_branch) |
637 | + self.assertLogMessages( |
638 | + ['^WARNING .* is not an official branch$', |
639 | + '^WARNING Skipping branch$']) |
640 | + |
641 | + def test_makeNewBranches(self): |
642 | + # makeNewBranches calls makeOneNewBranch for each official branch in |
643 | + # the old distroseries. |
644 | + db_branch = self.makeOfficialPackageBranch() |
645 | + db_branch2 = self.makeOfficialPackageBranch( |
646 | + distroseries=db_branch.distroseries) |
647 | + |
648 | + new_distroseries = self.factory.makeDistroRelease( |
649 | + distribution=db_branch.distribution) |
650 | + |
651 | + brancher = DistroBrancher( |
652 | + QuietFakeLogger(), db_branch.distroseries, new_distroseries) |
653 | + |
654 | + brancher.makeNewBranches() |
655 | + |
656 | + new_sourcepackage = new_distroseries.getSourcePackage( |
657 | + db_branch.sourcepackage.name) |
658 | + new_branch = new_sourcepackage.getBranch(RELEASE) |
659 | + new_sourcepackage2 = new_distroseries.getSourcePackage( |
660 | + db_branch2.sourcepackage.name) |
661 | + new_branch2 = new_sourcepackage2.getBranch(RELEASE) |
662 | + |
663 | + self.assertIsNot(None, new_branch) |
664 | + self.assertIsNot(None, new_branch2) |
665 | + |
666 | + def test_makeNewBranches_idempotent(self): |
667 | + # makeNewBranches is idempotent in the sense that if a branch in the |
668 | + # old distroseries already has a counterpart in the new distroseries, |
669 | + # it is silently ignored. |
670 | + db_branch = self.makeOfficialPackageBranch() |
671 | + |
672 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
673 | + brancher.makeNewBranches() |
674 | + brancher.makeNewBranches() |
675 | + |
676 | + new_branch = brancher.new_distroseries.getSourcePackage( |
677 | + db_branch.sourcepackage.name).getBranch(RELEASE) |
678 | + |
679 | + self.assertIsNot(new_branch, None) |
680 | + |
681 | + def test_makeOneNewBranch_checks_ok(self): |
682 | + # After calling makeOneNewBranch for a branch, calling checkOneBranch |
683 | + # returns True for that branch. |
684 | + db_branch = self.makeOfficialPackageBranch() |
685 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
686 | + brancher.makeOneNewBranch(db_branch) |
687 | + self.clearLogMessages() |
688 | + ok = brancher.checkOneBranch(db_branch) |
689 | + self.assertLogMessages([]) |
690 | + self.assertTrue(ok) |
691 | + |
692 | + def test_checkConsistentOfficialPackageBranch_product_branch(self): |
693 | + # checkConsistentOfficialPackageBranch returns False when passed a |
694 | + # product branch. |
695 | + db_branch = self.factory.makeProductBranch() |
696 | + brancher = self.makeNewSeriesAndBrancher() |
697 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
698 | + self.assertLogMessages([ |
699 | + '^WARNING Encountered unexpected product branch .*/.*/.*$']) |
700 | + self.assertFalse(ok) |
701 | + |
702 | + def test_checkConsistentOfficialPackageBranch_personal_branch(self): |
703 | + # checkConsistentOfficialPackageBranch returns False when passed a |
704 | + # personal branch. |
705 | + db_branch = self.factory.makePersonalBranch() |
706 | + brancher = self.makeNewSeriesAndBrancher() |
707 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
708 | + self.assertLogMessages([ |
709 | + '^WARNING Encountered unexpected personal branch .*/.*/.*$']) |
710 | + self.assertFalse(ok) |
711 | + |
712 | + def test_checkConsistentOfficialPackageBranch_no_official_branch(self): |
713 | + # checkConsistentOfficialPackageBranch returns False when passed a |
714 | + # branch which is not official for any package. |
715 | + db_branch = self.factory.makePackageBranch() |
716 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
717 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
718 | + self.assertLogMessages( |
719 | + ['^WARNING .*/.*/.* is not an official branch$']) |
720 | + self.assertFalse(ok) |
721 | + |
722 | + def test_checkConsistentOfficialPackageBranch_official_elsewhere(self): |
723 | + # checkConsistentOfficialPackageBranch returns False when passed a |
724 | + # branch which is official for a sourcepackage that it is not a branch |
725 | + # for. |
726 | + db_branch = self.factory.makePackageBranch() |
727 | + self.factory.makeSourcePackage().setBranch( |
728 | + RELEASE, db_branch, db_branch.owner) |
729 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
730 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
731 | + self.assertLogMessages( |
732 | + ['^WARNING .*/.*/.* is the official branch for .*/.*/.* but not ' |
733 | + 'its sourcepackage$']) |
734 | + self.assertFalse(ok) |
735 | + |
736 | + def test_checkConsistentOfficialPackageBranch_official_twice(self): |
737 | + # checkConsistentOfficialPackageBranch returns False when passed a |
738 | + # branch that is official for two sourcepackages. |
739 | + db_branch = self.factory.makePackageBranch() |
740 | + db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner) |
741 | + self.factory.makeSourcePackage().setBranch( |
742 | + RELEASE, db_branch, db_branch.owner) |
743 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
744 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
745 | + self.assertLogMessages([ |
746 | + '^WARNING .*/.*/.* is official for multiple series: .*/.*/.*, ' |
747 | + '.*/.*/.*$']) |
748 | + self.assertFalse(ok) |
749 | + |
750 | + def test_checkConsistentOfficialPackageBranch_ok(self): |
751 | + # checkConsistentOfficialPackageBranch returns True when passed a |
752 | + # branch that is official for its sourcepackage and no other. |
753 | + db_branch = self.factory.makePackageBranch() |
754 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
755 | + db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner) |
756 | + ok = brancher.checkConsistentOfficialPackageBranch(db_branch) |
757 | + self.assertLogMessages([]) |
758 | + self.assertTrue(ok) |
759 | + |
760 | + def test_checkOneBranch_inconsistent_old_package_branch(self): |
761 | + # checkOneBranch returns False when passed a branch that is not a |
762 | + # consistent official package branch. |
763 | + db_branch = self.factory.makePackageBranch() |
764 | + brancher = self.makeNewSeriesAndBrancher() |
765 | + ok = brancher.checkOneBranch(db_branch) |
766 | + self.assertFalse(ok) |
767 | + self.assertLogMessages( |
768 | + ['^WARNING .*/.*/.* is not an official branch$']) |
769 | + |
770 | + def test_checkOneBranch_no_new_official_branch(self): |
771 | + # checkOneBranch returns False when there is no corresponding official |
772 | + # package branch in the new distroseries. |
773 | + db_branch = self.makeOfficialPackageBranch() |
774 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
775 | + ok = brancher.checkOneBranch(db_branch) |
776 | + self.assertFalse(ok) |
777 | + self.assertLogMessages( |
778 | + ['^WARNING No official branch found for .*/.*/.*$']) |
779 | + |
780 | + def test_checkOneBranch_inconsistent_new_package_branch(self): |
781 | + # checkOneBranch returns False when the corresponding official package |
782 | + # branch in the new distroseries is not consistent. |
783 | + db_branch = self.makeOfficialPackageBranch() |
784 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
785 | + new_db_branch = brancher.makeOneNewBranch(db_branch) |
786 | + self.layer.switchDbUser('launchpad') |
787 | + new_db_branch.setTarget( |
788 | + new_db_branch.owner, |
789 | + source_package=self.factory.makeSourcePackage()) |
790 | + transaction.commit() |
791 | + self.layer.switchDbUser('branch-distro') |
792 | + ok = brancher.checkOneBranch(new_db_branch) |
793 | + self.assertFalse(ok) |
794 | + self.assertLogMessages( |
795 | + ['^WARNING .*/.*/.* is the official branch for .*/.*/.* but not ' |
796 | + 'its sourcepackage$']) |
797 | + |
798 | + def checkOneBranch_new_branch_missing(self, branch_type): |
799 | + # checkOneBranch returns False when there is no bzr branch for the |
800 | + # database branch in the new distroseries. |
801 | + assert branch_type in ('hosted', 'mirrored') |
802 | + db_branch = self.makeOfficialPackageBranch() |
803 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
804 | + new_db_branch = brancher.makeOneNewBranch(db_branch) |
805 | + if branch_type == 'hosted': |
806 | + url = new_db_branch.getPullURL() |
807 | + else: |
808 | + url = new_db_branch.warehouse_url |
809 | + get_transport(url).delete_tree('.bzr') |
810 | + ok = brancher.checkOneBranch(db_branch) |
811 | + self.assertFalse(ok) |
812 | + # Deleting the new branch will break the old branch, as that's stacked |
813 | + # on the new one. |
814 | + self.assertLogMessages([ |
815 | + '^WARNING No bzr branch at new location lp-%s:///.*/.*/.*/.*$' |
816 | + % branch_type, |
817 | + '^WARNING No bzr branch at old location lp-%s:///.*/.*/.*/.*$' |
818 | + % branch_type, |
819 | + ]) |
820 | + |
821 | + def test_checkOneBranch_new_hosted_branch_missing(self): |
822 | + # checkOneBranch returns False when there is no bzr branch in the |
823 | + # hosted area for the database branch in the new distroseries. |
824 | + self.checkOneBranch_new_branch_missing('hosted') |
825 | + |
826 | + def test_checkOneBranch_new_mirrored_branch_missing(self): |
827 | + # checkOneBranch returns False when there is no bzr branch in the |
828 | + # mirrored area for the database branch in the new distroseries. |
829 | + self.checkOneBranch_new_branch_missing('mirrored') |
830 | + |
831 | + def checkOneBranch_old_branch_missing(self, branch_type): |
832 | + # checkOneBranch returns False when there is no bzr branchfor the |
833 | + # database branch in old distroseries. |
834 | + assert branch_type in ('hosted', 'mirrored') |
835 | + db_branch = self.makeOfficialPackageBranch() |
836 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
837 | + brancher.makeOneNewBranch(db_branch) |
838 | + if branch_type == 'hosted': |
839 | + url = db_branch.getPullURL() |
840 | + else: |
841 | + url = db_branch.warehouse_url |
842 | + get_transport(url).delete_tree('.bzr') |
843 | + ok = brancher.checkOneBranch(db_branch) |
844 | + self.assertFalse(ok) |
845 | + self.assertLogMessages([ |
846 | + '^WARNING No bzr branch at old location lp-%s:///.*/.*/.*/.*$' |
847 | + % branch_type, |
848 | + ]) |
849 | + |
850 | + def test_checkOneBranch_old_hosted_branch_missing(self): |
851 | + # checkOneBranch returns False when there is no bzr branch in the |
852 | + # hosted area for the database branch in old distroseries. |
853 | + self.checkOneBranch_old_branch_missing('hosted') |
854 | + |
855 | + def test_checkOneBranch_old_mirrored_branch_missing(self): |
856 | + # checkOneBranch returns False when there is no bzr branch in the |
857 | + # mirrored area for the database branch in old distroseries. |
858 | + self.checkOneBranch_old_branch_missing('mirrored') |
859 | + |
860 | + def checkOneBranch_new_stacked(self, branch_type): |
861 | + # checkOneBranch returns False when the bzr branch for the database |
862 | + # branch in new distroseries is stacked. |
863 | + assert branch_type in ('hosted', 'mirrored') |
864 | + db_branch = self.makeOfficialPackageBranch() |
865 | + b, _ = self.create_branch_and_tree( |
866 | + self.factory.getUniqueString(), hosted=(branch_type == 'hosted')) |
867 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
868 | + new_db_branch = brancher.makeOneNewBranch(db_branch) |
869 | + if branch_type == 'hosted': |
870 | + url = new_db_branch.getPullURL() |
871 | + else: |
872 | + url = new_db_branch.warehouse_url |
873 | + Branch.open(url).set_stacked_on_url('/' + b.unique_name) |
874 | + ok = brancher.checkOneBranch(db_branch) |
875 | + self.assertFalse(ok) |
876 | + self.assertLogMessages([ |
877 | + '^WARNING New branch at lp-%s:///.*/.*/.*/.* is stacked on ' |
878 | + '/.*/.*/.*, should be unstacked.$' % branch_type, |
879 | + ]) |
880 | + |
881 | + def test_checkOneBranch_new_hosted_stacked(self): |
882 | + # checkOneBranch returns False when the bzr branch in the hosted area |
883 | + # for the database branch in new distroseries is stacked. |
884 | + self.checkOneBranch_new_stacked('hosted') |
885 | + |
886 | + def test_checkOneBranch_new_mirrored_stacked(self): |
887 | + # checkOneBranch returns False when the bzr branch in the mirrored |
888 | + # area for the database branch in new distroseries is stacked. |
889 | + self.checkOneBranch_new_stacked('mirrored') |
890 | + |
891 | + def checkOneBranch_old_unstacked(self, branch_type): |
892 | + # checkOneBranch returns False when the bzr branch for the database |
893 | + # branch in old distroseries is not stacked. |
894 | + assert branch_type in ('hosted', 'mirrored') |
895 | + db_branch = self.makeOfficialPackageBranch() |
896 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
897 | + brancher.makeOneNewBranch(db_branch) |
898 | + if branch_type == 'hosted': |
899 | + url = db_branch.getPullURL() |
900 | + else: |
901 | + url = db_branch.warehouse_url |
902 | + old_bzr_branch = Branch.open(url) |
903 | + old_bzr_branch.set_stacked_on_url(None) |
904 | + ok = brancher.checkOneBranch(db_branch) |
905 | + self.assertLogMessages([ |
906 | + '^WARNING Old branch at lp-%s:///.*/.*/.*/.* is not stacked, ' |
907 | + 'should be stacked on /.*/.*/.*.$' % branch_type, |
908 | + '^.*has .* revisions.*$', |
909 | + ]) |
910 | + self.assertFalse(ok) |
911 | + |
912 | + def test_checkOneBranch_old_hosted_unstacked(self): |
913 | + # checkOneBranch returns False when the bzr branch in the hosted area |
914 | + # for the database branch in old distroseries is not stacked. |
915 | + self.checkOneBranch_old_unstacked('hosted') |
916 | + |
917 | + def test_checkOneBranch_old_mirrored_unstacked(self): |
918 | + # checkOneBranch returns False when the bzr branch in the mirrored |
919 | + # area for the database branch in old distroseries is not stacked. |
920 | + self.checkOneBranch_old_unstacked('mirrored') |
921 | + |
922 | + def checkOneBranch_old_misstacked(self, branch_type): |
923 | + # checkOneBranch returns False when the bzr branch for the database |
924 | + # branch in old distroseries stacked on some other branch than the |
925 | + # branch in the new distroseries. |
926 | + assert branch_type in ('hosted', 'mirrored') |
927 | + db_branch = self.makeOfficialPackageBranch() |
928 | + b, _ = self.create_branch_and_tree( |
929 | + self.factory.getUniqueString(), hosted=(branch_type == 'hosted')) |
930 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
931 | + brancher.makeOneNewBranch(db_branch) |
932 | + if branch_type == 'hosted': |
933 | + url = db_branch.getPullURL() |
934 | + else: |
935 | + url = db_branch.warehouse_url |
936 | + Branch.open(url).set_stacked_on_url('/' + b.unique_name) |
937 | + ok = brancher.checkOneBranch(db_branch) |
938 | + self.assertLogMessages([ |
939 | + '^WARNING Old branch at lp-%s:///.*/.*/.*/.* is stacked on ' |
940 | + '/.*/.*/.*, should be stacked on /.*/.*/.*.$' % branch_type, |
941 | + ]) |
942 | + self.assertFalse(ok) |
943 | + |
944 | + def test_checkOneBranch_old_hosted_misstacked(self): |
945 | + # checkOneBranch returns False when the bzr branch in the hosted area |
946 | + # for the database branch in old distroseries stacked on some other |
947 | + # branch than the branch in the new distroseries. |
948 | + self.checkOneBranch_old_misstacked('hosted') |
949 | + |
950 | + def test_checkOneBranch_old_mirrored_misstacked(self): |
951 | + # checkOneBranch returns False when the bzr branch in the mirrored |
952 | + # area for the database branch in old distroseries stacked on some |
953 | + # other branch than the branch in the new distroseries. |
954 | + self.checkOneBranch_old_misstacked('mirrored') |
955 | + |
956 | + def checkOneBranch_old_has_revisions(self, branch_type): |
957 | + # checkOneBranch returns False when the bzr branch for the database |
958 | + # branch in old distroseries has a repository that contains revisions. |
959 | + assert branch_type in ('hosted', 'mirrored') |
960 | + db_branch = self.makeOfficialPackageBranch() |
961 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
962 | + brancher.makeOneNewBranch(db_branch) |
963 | + if branch_type == 'hosted': |
964 | + url = db_branch.getPullURL() |
965 | + else: |
966 | + url = db_branch.warehouse_url |
967 | + old_bzr_branch = Branch.open(url) |
968 | + old_bzr_branch.create_checkout( |
969 | + self.factory.getUniqueString()).commit('') |
970 | + ok = brancher.checkOneBranch(db_branch) |
971 | + self.assertLogMessages([ |
972 | + '^WARNING Repository at lp-%s:///.*/.*/.*/.* has 1 revisions.' |
973 | + % branch_type |
974 | + ]) |
975 | + self.assertFalse(ok) |
976 | + |
977 | + def test_checkOneBranch_old_hosted_has_revisions(self): |
978 | + # checkOneBranch returns False when the bzr branch in the hosted area |
979 | + # for the database branch in old distroseries has a repository that |
980 | + # contains revisions. |
981 | + self.checkOneBranch_old_has_revisions('hosted') |
982 | + |
983 | + def test_checkOneBranch_old_mirrored_has_revisions(self): |
984 | + # checkOneBranch returns False when the bzr branch in the mirrored |
985 | + # area for the database branch in old distroseries has a repository |
986 | + # that contains revisions. |
987 | + self.checkOneBranch_old_has_revisions('mirrored') |
988 | + |
989 | + def checkOneBranch_old_has_null_tip(self, branch_type): |
990 | + # checkOneBranch returns False when the bzr branch for the database |
991 | + # branch in old distroseries has tip revision of 'null:'. |
992 | + assert branch_type in ('hosted', 'mirrored') |
993 | + db_branch = self.makeOfficialPackageBranch() |
994 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
995 | + brancher.makeOneNewBranch(db_branch) |
996 | + if branch_type == 'hosted': |
997 | + url = db_branch.getPullURL() |
998 | + else: |
999 | + url = db_branch.warehouse_url |
1000 | + old_bzr_branch = Branch.open(url) |
1001 | + old_bzr_branch.set_last_revision_info(0, 'null:') |
1002 | + ok = brancher.checkOneBranch(db_branch) |
1003 | + self.assertLogMessages([ |
1004 | + '^WARNING Old branch at lp-%s:///.*/.*/.*/.* has null tip ' |
1005 | + 'revision.' % branch_type |
1006 | + ]) |
1007 | + self.assertFalse(ok) |
1008 | + |
1009 | + def test_checkOneBranch_old_hosted_has_null_tip(self): |
1010 | + # checkOneBranch returns False when the bzr branch in the hosted area |
1011 | + # for the database branch in old distroseries has tip revision of |
1012 | + # 'null:'. |
1013 | + self.checkOneBranch_old_has_null_tip('hosted') |
1014 | + |
1015 | + def test_checkOneBranch_old_mirrored_has_null_tip(self): |
1016 | + # checkOneBranch returns False when the bzr branch in the mirrored |
1017 | + # area for the database branch in old distroseries has tip revision of |
1018 | + # 'null:'. |
1019 | + self.checkOneBranch_old_has_null_tip('mirrored') |
1020 | + |
1021 | + def runBranchDistroScript(self, args): |
1022 | + """Run the branch-distro.py script with the given arguments. |
1023 | + |
1024 | + ;param args: The arguments to pass to the branch-distro.py script. |
1025 | + :return: A tuple (returncode, output). stderr and stdout are both |
1026 | + contained in the output. |
1027 | + """ |
1028 | + script_path = os.path.join(config.root, 'scripts', 'branch-distro.py') |
1029 | + process = Popen([script_path] + args, stdout=PIPE, stderr=STDOUT) |
1030 | + output, error = process.communicate() |
1031 | + return process.returncode, output |
1032 | + |
1033 | + def test_makeNewBranches_script(self): |
1034 | + # Running the script with the arguments 'distro old-series new-series' |
1035 | + # makes new branches in the new series. |
1036 | + db_branch = self.makeOfficialPackageBranch() |
1037 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
1038 | + returncode, output = self.runBranchDistroScript( |
1039 | + ['-v', db_branch.distribution.name, |
1040 | + brancher.old_distroseries.name, brancher.new_distroseries.name]) |
1041 | + self.assertEqual(0, returncode) |
1042 | + self.assertEqual( |
1043 | + 'DEBUG Processing ' + db_branch.unique_name + '\n', output) |
1044 | + brancher.checkOneBranch(db_branch) |
1045 | + |
1046 | + def test_checkNewBranches_script_success(self): |
1047 | + # Running the script with the arguments '--check distro old-series |
1048 | + # new-series' checks that the branches in the new series are as |
1049 | + # expected. |
1050 | + db_branch = self.makeOfficialPackageBranch() |
1051 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
1052 | + brancher.makeNewBranches() |
1053 | + returncode, output = self.runBranchDistroScript( |
1054 | + ['-v', '--check', db_branch.distribution.name, |
1055 | + brancher.old_distroseries.name, brancher.new_distroseries.name]) |
1056 | + self.assertEqual(0, returncode) |
1057 | + self.assertEqual( |
1058 | + 'DEBUG Checking ' + db_branch.unique_name + '\n', output) |
1059 | + brancher.checkOneBranch(db_branch) |
1060 | + |
1061 | + def test_checkNewBranches_script_failure(self): |
1062 | + # Running the script with the arguments '--check distro old-series |
1063 | + # new-series' checks that the branches in the new series are as |
1064 | + # expected and logs warnings and exits with code 1 is things are not |
1065 | + # as expected. |
1066 | + db_branch = self.makeOfficialPackageBranch() |
1067 | + brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries) |
1068 | + returncode, output = self.runBranchDistroScript( |
1069 | + ['-v', '--check', db_branch.distribution.name, |
1070 | + brancher.old_distroseries.name, brancher.new_distroseries.name]) |
1071 | + sp_path = brancher.new_distroseries.getSourcePackage( |
1072 | + db_branch.sourcepackagename).path |
1073 | + expected = '''\ |
1074 | + DEBUG Checking %(branch_name)s |
1075 | + WARNING No official branch found for %(sp_path)s |
1076 | + ERROR Check failed |
1077 | + ''' % {'branch_name': db_branch.unique_name, 'sp_path': sp_path} |
1078 | + self.assertEqual( |
1079 | + textwrap.dedent(expected), output) |
1080 | + self.assertEqual(1, returncode) |
1081 | + |
1082 | + |
1083 | +def test_suite(): |
1084 | + return unittest.TestLoader().loadTestsFromName(__name__) |
1085 | + |
1086 | |
1087 | === added file 'scripts/branch-distro.py' |
1088 | --- scripts/branch-distro.py 1970-01-01 00:00:00 +0000 |
1089 | +++ scripts/branch-distro.py 2009-10-14 00:31:13 +0000 |
1090 | @@ -0,0 +1,39 @@ |
1091 | +#!/usr/bin/python2.4 |
1092 | +# |
1093 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
1094 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1095 | + |
1096 | +import _pythonpath |
1097 | + |
1098 | +from lp.codehosting.branchdistro import DistroBrancher |
1099 | +from lp.codehosting.vfs import get_multi_server |
1100 | +from lp.services.scripts.base import LaunchpadScript, LaunchpadScriptFailure |
1101 | + |
1102 | + |
1103 | +class BranchDistroScript(LaunchpadScript): |
1104 | + |
1105 | + usage = "%prog distro old-series new-series" |
1106 | + |
1107 | + def add_my_options(self): |
1108 | + self.parser.add_option( |
1109 | + '--check', dest="check", action="store_true", default=False, |
1110 | + help=("Check that the new distro series has its official " |
1111 | + "branches set up correctly.")) |
1112 | + |
1113 | + def main(self): |
1114 | + if len(self.args) != 3: |
1115 | + self.parser.error("Wrong number of arguments.") |
1116 | + brancher = DistroBrancher.fromNames(self.logger, *self.args) |
1117 | + server = get_multi_server(write_mirrored=True, write_hosted=True) |
1118 | + server.setUp() |
1119 | + try: |
1120 | + if self.options.check: |
1121 | + if not brancher.checkNewBranches(): |
1122 | + raise LaunchpadScriptFailure("Check failed") |
1123 | + else: |
1124 | + brancher.makeNewBranches() |
1125 | + finally: |
1126 | + server.tearDown() |
1127 | + |
1128 | +if __name__ == '__main__': |
1129 | + BranchDistroScript("branch-distro", dbuser='branch-distro').run() |
Hey Jono,
Finally I think this beast of a branch is ready (actually it's "only" 1123 lines, not too far over the limit :-).
It adds a script that carries out the procedure on https:/ /dev.launchpad. net/BranchingUb untu. It also adds a --check flag that verifies that a previous run of the script went ok. This was the only way I could convince myself to stop worrying about what would happen if the script crashed or was interrupted -- the idea is that if the script is interrupted we'll run the script again until it completes, then run branch-distro --check to find any damage that was left behind by the interrupted run.
I hope it makes sense, we've talked about this enough!
Cheers,
mwh