1
# Copyright 2009 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Opening a new DistroSeries for branch based development.
6
Intended to be run just after a new distro series has been completed, this
7
script will create an official package branch in the new series for every one
8
in the old. The old branch will become stacked on the new, to avoid a using
9
too much disk space whilst retaining best performance for the new branch.
20
from bzrlib.branch import Branch
21
from bzrlib.bzrdir import BzrDir
22
from bzrlib.errors import (
26
from bzrlib.revision import NULL_REVISION
28
from zope.component import getUtility
30
from canonical.config import config
31
from lp.services.database.lpstorm import IMasterStore
32
from lp.code.enums import (
33
BranchLifecycleStatus,
36
from lp.code.errors import BranchExists
37
from lp.code.interfaces.branchcollection import IAllBranches
38
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
39
from lp.code.interfaces.seriessourcepackagebranch import (
40
IFindOfficialBranchLinks,
42
from lp.code.model.branchrevision import BranchRevision
43
from lp.codehosting.vfs import branch_id_to_path
44
from lp.registry.interfaces.distribution import IDistributionSet
45
from lp.registry.interfaces.pocket import PackagePublishingPocket
48
def switch_branches(prefix, scheme, old_db_branch, new_db_branch):
49
"""Move bzr data from an old to a new branch, leaving old stacked on new.
51
This function is intended to be used just after Ubuntu is released to
52
create (at the bzr level) a new trunk branch for a source package for the
53
next release of the distribution. We move the bzr data to the location
54
for the new branch and replace the trunk branch for the just released
55
version with a stacked branch pointing at the new branch.
57
The procedure is to complicated to be carried out atomically, so if this
58
function is interrupted things may be a little inconsistent (e.g. there
59
might be a branch in the old location, but not stacked on the new location
60
yet). There should be no data loss though.
62
:param prefix: The non-branch id dependent part of the physical path to
64
:param scheme: The branches should be open-able at a URL of the form
65
``scheme + :/// + unique_name``.
66
:param old_db_branch: The branch that currently has the trunk bzr data.
67
:param old_db_branch: The new trunk branch. This should not have any
70
# Move .bzr directory from old to new location, crashing through the
71
# abstraction we usually hide our branch locations behind.
72
old_underlying_path = os.path.join(
73
prefix, branch_id_to_path(old_db_branch.id))
74
new_underlying_path = os.path.join(
75
prefix, branch_id_to_path(new_db_branch.id))
76
os.makedirs(new_underlying_path)
78
os.path.join(old_underlying_path, '.bzr'),
79
os.path.join(new_underlying_path, '.bzr'))
81
# Create branch at old location -- we use the "clone('null:')" trick to
82
# preserve the format. We have to open at the logical, unique_name-based,
83
# location so that it works to set the stacked on url to '/' + a
85
new_location_bzrdir = BzrDir.open(
86
scheme + ':///' + new_db_branch.unique_name)
87
old_location_bzrdir = new_location_bzrdir.clone(
88
scheme + ':///' + old_db_branch.unique_name, revision_id='null:')
90
# Set the stacked on url for old location.
91
old_location_branch = old_location_bzrdir.open_branch()
92
old_location_branch.set_stacked_on_url('/' + new_db_branch.unique_name)
94
# Pull from new location to old -- this won't actually transfer any
95
# revisions, just update the last revision pointer.
96
old_location_branch.pull(new_location_bzrdir.open_branch())
100
"""Open a new distroseries for branch based development.
102
`makeNewBranches` will create an official package branch in the new series
103
for every one in the old. `checkNewBranches` will check that a previous
104
run of this script completed successfully -- this is only likely to be
105
really useful if a script run died halfway through or had to be killed.
108
def __init__(self, logger, old_distroseries, new_distroseries):
109
"""Construct a `DistroBrancher`.
111
The old and new distroseries must be from the same distribution, but
112
not the same distroseries.
114
:param logger: A Logger. Problems will be logged to this object at
115
the WARNING level or higher; progress reports will be logged at
117
:param old_distroseries: The distroseries that will be examined to
118
find existing source package branches.
119
:param new_distroseries: The distroseries that will have new official
120
source branches made for it.
123
if old_distroseries.distribution != new_distroseries.distribution:
124
raise AssertionError(
125
"%s and %s are from different distributions!" %
126
(old_distroseries, new_distroseries))
127
if old_distroseries == new_distroseries:
128
raise AssertionError(
129
"New and old distributions must be different!")
130
self.old_distroseries = old_distroseries
131
self.new_distroseries = new_distroseries
134
def fromNames(cls, logger, distribution_name, old_distroseries_name,
135
new_distroseries_name):
136
"""Make a `DistroBrancher` from the names of a distro and two series.
138
distribution = getUtility(IDistributionSet).getByName(
140
new_distroseries = distribution.getSeries(new_distroseries_name)
141
old_distroseries = distribution.getSeries(old_distroseries_name)
142
return cls(logger, old_distroseries, new_distroseries)
144
def _existingOfficialBranches(self):
145
"""Return the collection of official branches in the old distroseries.
147
branches = getUtility(IAllBranches)
148
distroseries_branches = branches.inDistroSeries(self.old_distroseries)
149
return distroseries_branches.officialBranches().getBranches(
152
def checkConsistentOfficialPackageBranch(self, db_branch):
153
"""Check that `db_branch` is a consistent official package branch.
155
'Consistent official package branch' means:
157
* It's a package branch (rather than a personal or junk branch).
158
* It's official for its SourcePackage and no other.
160
This function simply returns True or False -- any problems will be
161
logged to ``self.logger``.
163
:param db_branch: The `IBranch` to check.
164
:return: ``True`` if the branch is a consistent official package
165
branch, ``False`` otherwise.
167
if db_branch.product:
169
"Encountered unexpected product branch %r",
170
db_branch.unique_name)
172
if not db_branch.distroseries:
174
"Encountered unexpected personal branch %s",
175
db_branch.unique_name)
177
find_branch_links = getUtility(IFindOfficialBranchLinks)
178
links = list(find_branch_links.findForBranch(db_branch))
181
"%s is not an official branch", db_branch.unique_name)
184
series_text = ', '.join([
185
link.sourcepackage.path for link in links])
187
"%s is official for multiple series: %s",
188
db_branch.unique_name, series_text)
190
elif links[0].sourcepackage != db_branch.sourcepackage:
192
"%s is the official branch for %s but not its "
193
"sourcepackage", db_branch.unique_name,
194
links[0].sourcepackage.path)
198
def makeNewBranches(self):
199
"""Make official branches in the new distroseries."""
200
for db_branch in self._existingOfficialBranches():
201
self.logger.debug("Processing %s" % db_branch.unique_name)
203
self.makeOneNewBranch(db_branch)
207
def checkNewBranches(self):
208
"""Check the branches in the new distroseries are present and correct.
210
This function checks that every official package branch in the old
211
distroseries has a matching branch in the new distroseries and that
212
stacking is set up as we expect on disk.
214
Every branch will be checked, even if some fail.
216
This function simply returns True or False -- any problems will be
217
logged to ``self.logger``.
219
:return: ``True`` if every branch passes the check, ``False``
223
for db_branch in self._existingOfficialBranches():
224
self.logger.debug("Checking %s" % db_branch.unique_name)
226
if not self.checkOneBranch(db_branch):
230
self.logger.exception(
231
"Unexpected error checking %s!", db_branch)
234
def checkOneBranch(self, old_db_branch):
235
"""Check a branch in the old distroseries has been copied to the new.
237
This function checks that `old_db_branch` has a matching branch in the
238
new distroseries and that stacking is set up as we expect on disk.
240
This function simply returns True or False -- any problems will be
241
logged to ``self.logger``.
243
:param old_db_branch: The branch to check.
244
:return: ``True`` if the branch passes the check, ``False`` otherwise.
246
ok = self.checkConsistentOfficialPackageBranch(old_db_branch)
249
new_sourcepackage = self.new_distroseries.getSourcePackage(
250
old_db_branch.sourcepackagename)
251
new_db_branch = new_sourcepackage.getBranch(
252
PackagePublishingPocket.RELEASE)
253
if new_db_branch is None:
255
"No official branch found for %s",
256
new_sourcepackage.path)
258
ok = self.checkConsistentOfficialPackageBranch(new_db_branch)
261
# the branch in the new distroseries is unstacked
262
new_location = 'lp-internal:///' + new_db_branch.unique_name
264
new_bzr_branch = Branch.open(new_location)
265
except NotBranchError:
267
"No bzr branch at new location %s", new_location)
271
new_stacked_on_url = new_bzr_branch.get_stacked_on_url()
274
"New branch at %s is stacked on %s, should be "
275
"unstacked.", new_location, new_stacked_on_url)
278
# The branch in the old distroseries is stacked on that in the
280
old_location = 'lp-internal:///' + old_db_branch.unique_name
282
old_bzr_branch = Branch.open(old_location)
283
except NotBranchError:
285
"No bzr branch at old location %s", old_location)
289
old_stacked_on_url = old_bzr_branch.get_stacked_on_url()
290
if old_stacked_on_url != '/' + new_db_branch.unique_name:
292
"Old branch at %s is stacked on %s, should be "
293
"stacked on %s", old_location, old_stacked_on_url,
294
'/' + new_db_branch.unique_name)
298
"Old branch at %s is not stacked, should be stacked "
299
"on %s", old_location,
300
'/' + new_db_branch.unique_name)
302
# The branch in the old distroseries has no revisions in its
303
# repository. We open the repository independently of the
304
# branch because the branch's repository has had its fallback
305
# location activated. Note that this check might fail if new
306
# revisions get pushed to the branch in the old distroseries,
307
# which shouldn't happen but isn't totally impossible.
308
old_repo = BzrDir.open(old_location).open_repository()
309
if len(old_repo.all_revision_ids()) > 0:
311
"Repository at %s has %s revisions.",
312
old_location, len(old_repo.all_revision_ids()))
314
# The branch in the old distroseries has at least some
315
# history. (We can't check that the tips are the same because
316
# the branch in the new distroseries might have new revisons).
317
if old_bzr_branch.last_revision() == 'null:':
319
"Old branch at %s has null tip revision.",
324
def makeOneNewBranch(self, old_db_branch):
325
"""Copy a branch to the new distroseries.
327
This function makes a new database branch for the same source package
328
as old_db_branch but in the new distroseries and then uses
329
`switch_branches` to move the underlying bzr branch to the new series
330
and replace the old branch with a branch stacked on the new series'
333
:param old_db_branch: The branch to copy into the new distroseries.
334
:raises BranchExists: This will be raised if old_db_branch has already
335
been copied to the new distroseries (in the database, at least).
337
if not self.checkConsistentOfficialPackageBranch(old_db_branch):
338
self.logger.warning("Skipping branch")
340
new_namespace = getUtility(IBranchNamespaceSet).get(
341
person=old_db_branch.owner, product=None,
342
distroseries=self.new_distroseries,
343
sourcepackagename=old_db_branch.sourcepackagename)
344
new_db_branch = new_namespace.createBranch(
345
BranchType.HOSTED, self.new_distroseries.name,
346
old_db_branch.registrant)
347
new_db_branch.sourcepackage.setBranch(
348
PackagePublishingPocket.RELEASE, new_db_branch,
350
old_db_branch.lifecycle_status = BranchLifecycleStatus.MATURE
351
# switch_branches *moves* the data to locations dependent on the
352
# new_branch's id, so if the transaction was rolled back we wouldn't
353
# know the branch id and thus wouldn't be able to find the branch data
354
# again. So commit before doing that.
357
config.codehosting.mirrored_branches_root,
358
'lp-internal', old_db_branch, new_db_branch)
359
# Directly copy the branch revisions from the old branch to the new
361
store = IMasterStore(BranchRevision)
364
INSERT INTO BranchRevision (branch, revision, sequence)
365
SELECT %s, BranchRevision.revision, BranchRevision.sequence
368
""" % (new_db_branch.id, old_db_branch.id))
370
# Update the scanned details first, that way when hooking into
371
# branchChanged, it won't try to create a new scan job.
372
tip_revision = old_db_branch.getTipRevision()
373
new_db_branch.updateScannedDetails(
374
tip_revision, old_db_branch.revision_count)
376
tip_revision.revision_id if tip_revision is not None else
378
new_db_branch.branchChanged(
380
old_db_branch.control_format,
381
old_db_branch.branch_format,
382
old_db_branch.repository_format)
383
old_db_branch.stacked_on = new_db_branch