~launchpad-pqm/launchpad/devel

9636.4.2 by Michael Hudson
add some boilerplate
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
9636.4.29 by Michael Hudson
some docstrings
4
"""Opening a new DistroSeries for branch based development.
5
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.
10
"""
9636.4.2 by Michael Hudson
add some boilerplate
11
9636.4.41 by Michael Hudson
address review comments
12
__metaclass__ = type
13
__all__ = [
14
    'DistroBrancher',
15
    'switch_branches',
16
    ]
17
9636.4.5 by Michael Hudson
more guesswork
18
import os
19
9636.4.14 by Michael Hudson
yet more stuff! i need to get organized soon!
20
from bzrlib.branch import Branch
9636.4.5 by Michael Hudson
more guesswork
21
from bzrlib.bzrdir import BzrDir
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
from bzrlib.errors import (
23
    NotBranchError,
24
    NotStacked,
25
    )
13046.1.1 by Aaron Bentley
branch-distro.py works with empty branches.
26
from bzrlib.revision import NULL_REVISION
9636.4.8 by Michael Hudson
happy case test passes
27
import transaction
9636.4.3 by Michael Hudson
stuff
28
from zope.component import getUtility
29
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
30
from canonical.config import config
14560.2.29 by Curtis Hovey
Restored lpstorm module name because it lp engineers know that name.
31
from lp.services.database.lpstorm import IMasterStore
11869.4.1 by Curtis Hovey
Removed all the imports from canonical.launchpad.interfaces from lp.*
32
from lp.code.enums import (
33
    BranchLifecycleStatus,
34
    BranchType,
35
    )
11270.2.9 by Tim Penhey
Missed another import.
36
from lp.code.errors import BranchExists
9636.4.9 by Michael Hudson
meditations
37
from lp.code.interfaces.branchcollection import IAllBranches
9636.4.3 by Michael Hudson
stuff
38
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
39
from lp.code.interfaces.seriessourcepackagebranch import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
40
    IFindOfficialBranchLinks,
41
    )
11573.7.3 by Tim Penhey
Avoid the scan.
42
from lp.code.model.branchrevision import BranchRevision
9636.4.5 by Michael Hudson
more guesswork
43
from lp.codehosting.vfs import branch_id_to_path
9636.4.32 by Michael Hudson
XXX reduction
44
from lp.registry.interfaces.distribution import IDistributionSet
9636.4.5 by Michael Hudson
more guesswork
45
from lp.registry.interfaces.pocket import PackagePublishingPocket
9636.4.29 by Michael Hudson
some docstrings
46
9636.4.3 by Michael Hudson
stuff
47
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
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.
50
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.
56
9636.4.41 by Michael Hudson
address review comments
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.
61
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
62
    :param prefix: The non-branch id dependent part of the physical path to
63
        the branches on disk.
9636.4.29 by Michael Hudson
some docstrings
64
    :param scheme: The branches should be open-able at a URL of the form
65
        ``scheme + :/// + unique_name``.
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
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
68
        presence on disk yet.
69
    """
9636.4.23 by Michael Hudson
fix two tests
70
    # Move .bzr directory from old to new location, crashing through the
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
71
    # abstraction we usually hide our branch locations behind.
9636.4.9 by Michael Hudson
meditations
72
    old_underlying_path = os.path.join(
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
73
        prefix, branch_id_to_path(old_db_branch.id))
9636.4.9 by Michael Hudson
meditations
74
    new_underlying_path = os.path.join(
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
75
        prefix, branch_id_to_path(new_db_branch.id))
9636.4.9 by Michael Hudson
meditations
76
    os.makedirs(new_underlying_path)
9636.4.5 by Michael Hudson
more guesswork
77
    os.rename(
9636.4.9 by Michael Hudson
meditations
78
        os.path.join(old_underlying_path, '.bzr'),
79
        os.path.join(new_underlying_path, '.bzr'))
9636.4.5 by Michael Hudson
more guesswork
80
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
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
84
    # unique_name.
9636.4.28 by Michael Hudson
address some comments from jml
85
    new_location_bzrdir = BzrDir.open(
9590.1.83 by Michael Hudson
indentation screwup
86
        scheme + ':///' + new_db_branch.unique_name)
9636.4.5 by Michael Hudson
more guesswork
87
    old_location_bzrdir = new_location_bzrdir.clone(
9590.1.83 by Michael Hudson
indentation screwup
88
        scheme + ':///' + old_db_branch.unique_name, revision_id='null:')
9636.4.5 by Michael Hudson
more guesswork
89
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
90
    # Set the stacked on url for old location.
9636.4.5 by Michael Hudson
more guesswork
91
    old_location_branch = old_location_bzrdir.open_branch()
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
92
    old_location_branch.set_stacked_on_url('/' + new_db_branch.unique_name)
9636.4.5 by Michael Hudson
more guesswork
93
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
94
    # Pull from new location to old -- this won't actually transfer any
95
    # revisions, just update the last revision pointer.
9636.4.5 by Michael Hudson
more guesswork
96
    old_location_branch.pull(new_location_bzrdir.open_branch())
97
98
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
99
class DistroBrancher:
9636.4.29 by Michael Hudson
some docstrings
100
    """Open a new distroseries for branch based development.
101
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.
106
    """
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
107
108
    def __init__(self, logger, old_distroseries, new_distroseries):
9636.4.29 by Michael Hudson
some docstrings
109
        """Construct a `DistroBrancher`.
110
111
        The old and new distroseries must be from the same distribution, but
112
        not the same distroseries.
113
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
116
            the DEBUG level.
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.
121
        """
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
122
        self.logger = logger
123
        if old_distroseries.distribution != new_distroseries.distribution:
124
            raise AssertionError(
125
                "%s and %s are from different distributions!" %
126
                (old_distroseries, new_distroseries))
9636.4.28 by Michael Hudson
address some comments from jml
127
        if old_distroseries == new_distroseries:
9636.4.32 by Michael Hudson
XXX reduction
128
            raise AssertionError(
129
                "New and old distributions must be different!")
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
130
        self.old_distroseries = old_distroseries
131
        self.new_distroseries = new_distroseries
132
9636.4.32 by Michael Hudson
XXX reduction
133
    @classmethod
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.
137
        """
138
        distribution = getUtility(IDistributionSet).getByName(
139
            distribution_name)
140
        new_distroseries = distribution.getSeries(new_distroseries_name)
141
        old_distroseries = distribution.getSeries(old_distroseries_name)
9636.4.36 by Michael Hudson
er, hooray unit tests
142
        return cls(logger, old_distroseries, new_distroseries)
9636.4.32 by Michael Hudson
XXX reduction
143
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
144
    def _existingOfficialBranches(self):
9636.4.29 by Michael Hudson
some docstrings
145
        """Return the collection of official branches in the old distroseries.
146
        """
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
147
        branches = getUtility(IAllBranches)
9636.4.16 by Michael Hudson
update existing tests to new structure. need to write a boatload more though
148
        distroseries_branches = branches.inDistroSeries(self.old_distroseries)
12504.1.3 by Robert Collins
Reject reversion of this branch on trunk.
149
        return distroseries_branches.officialBranches().getBranches(
150
            eager_load=False)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
151
152
    def checkConsistentOfficialPackageBranch(self, db_branch):
9636.4.29 by Michael Hudson
some docstrings
153
        """Check that `db_branch` is a consistent official package branch.
154
155
        'Consistent official package branch' means:
156
157
         * It's a package branch (rather than a personal or junk branch).
158
         * It's official for its SourcePackage and no other.
159
160
        This function simply returns True or False -- any problems will be
161
        logged to ``self.logger``.
162
163
        :param db_branch: The `IBranch` to check.
164
        :return: ``True`` if the branch is a consistent official package
165
            branch, ``False`` otherwise.
166
        """
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
167
        if db_branch.product:
168
            self.logger.warning(
169
                "Encountered unexpected product branch %r",
170
                db_branch.unique_name)
171
            return False
172
        if not db_branch.distroseries:
173
            self.logger.warning(
9636.4.24 by Michael Hudson
tests for checkConsistentOfficialPackageBranch
174
                "Encountered unexpected personal branch %s",
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
175
                db_branch.unique_name)
176
            return False
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
177
        find_branch_links = getUtility(IFindOfficialBranchLinks)
178
        links = list(find_branch_links.findForBranch(db_branch))
179
        if len(links) == 0:
180
            self.logger.warning(
9636.4.26 by Michael Hudson
i think all tests finally pass
181
                "%s is not an official branch", db_branch.unique_name)
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
182
            return False
183
        elif len(links) > 1:
9636.4.29 by Michael Hudson
some docstrings
184
            series_text = ', '.join([
185
                link.sourcepackage.path for link in links])
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
186
            self.logger.warning(
9636.4.31 by Michael Hudson
thrumpty million test comments
187
                "%s is official for multiple series: %s",
9636.4.29 by Michael Hudson
some docstrings
188
                db_branch.unique_name, series_text)
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
189
            return False
9636.4.26 by Michael Hudson
i think all tests finally pass
190
        elif links[0].sourcepackage != db_branch.sourcepackage:
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
191
            self.logger.warning(
9636.4.28 by Michael Hudson
address some comments from jml
192
                "%s is the official branch for %s but not its "
9636.4.26 by Michael Hudson
i think all tests finally pass
193
                "sourcepackage", db_branch.unique_name,
9636.4.28 by Michael Hudson
address some comments from jml
194
                links[0].sourcepackage.path)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
195
            return False
196
        return True
197
9636.4.29 by Michael Hudson
some docstrings
198
    def makeNewBranches(self):
199
        """Make official branches in the new distroseries."""
200
        for db_branch in self._existingOfficialBranches():
9636.4.37 by Michael Hudson
dear <deity> please let this be it
201
            self.logger.debug("Processing %s" % db_branch.unique_name)
9636.4.29 by Michael Hudson
some docstrings
202
            try:
203
                self.makeOneNewBranch(db_branch)
204
            except BranchExists:
205
                pass
206
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
207
    def checkNewBranches(self):
9636.4.29 by Michael Hudson
some docstrings
208
        """Check the branches in the new distroseries are present and correct.
209
210
        This function checks that every official package branch in the old
211
        distroseries has a matching branch in the new distroseries and that
9590.1.85 by Michael Hudson
fix some docstrings
212
        stacking is set up as we expect on disk.
9636.4.29 by Michael Hudson
some docstrings
213
9636.4.41 by Michael Hudson
address review comments
214
        Every branch will be checked, even if some fail.
215
9636.4.29 by Michael Hudson
some docstrings
216
        This function simply returns True or False -- any problems will be
217
        logged to ``self.logger``.
218
219
        :return: ``True`` if every branch passes the check, ``False``
220
            otherwise.
221
        """
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
222
        ok = True
223
        for db_branch in self._existingOfficialBranches():
9636.4.37 by Michael Hudson
dear <deity> please let this be it
224
            self.logger.debug("Checking %s" % db_branch.unique_name)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
225
            try:
226
                if not self.checkOneBranch(db_branch):
227
                    ok = False
228
            except:
229
                ok = False
9636.4.40 by Michael Hudson
lint
230
                self.logger.exception(
231
                    "Unexpected error checking %s!", db_branch)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
232
        return ok
233
234
    def checkOneBranch(self, old_db_branch):
9636.4.29 by Michael Hudson
some docstrings
235
        """Check a branch in the old distroseries has been copied to the new.
236
237
        This function checks that `old_db_branch` has a matching branch in the
9590.1.85 by Michael Hudson
fix some docstrings
238
        new distroseries and that stacking is set up as we expect on disk.
9636.4.29 by Michael Hudson
some docstrings
239
240
        This function simply returns True or False -- any problems will be
241
        logged to ``self.logger``.
242
243
        :param old_db_branch: The branch to check.
244
        :return: ``True`` if the branch passes the check, ``False`` otherwise.
245
        """
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
246
        ok = self.checkConsistentOfficialPackageBranch(old_db_branch)
247
        if not ok:
248
            return ok
9636.4.28 by Michael Hudson
address some comments from jml
249
        new_sourcepackage = self.new_distroseries.getSourcePackage(
250
            old_db_branch.sourcepackagename)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
251
        new_db_branch = new_sourcepackage.getBranch(
252
            PackagePublishingPocket.RELEASE)
253
        if new_db_branch is None:
254
            self.logger.warning(
9636.4.38 by Michael Hudson
no of course it wasn't
255
                "No official branch found for %s",
256
                new_sourcepackage.path)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
257
            return False
258
        ok = self.checkConsistentOfficialPackageBranch(new_db_branch)
9636.4.25 by Michael Hudson
more tests, including for branches official multiple times
259
        if not ok:
260
            return ok
9590.1.82 by Michael Hudson
start fixing the branch-distro tests
261
        # the branch in the new distroseries is unstacked
9590.1.83 by Michael Hudson
indentation screwup
262
        new_location = 'lp-internal:///' + new_db_branch.unique_name
9590.1.82 by Michael Hudson
start fixing the branch-distro tests
263
        try:
264
            new_bzr_branch = Branch.open(new_location)
265
        except NotBranchError:
266
            self.logger.warning(
267
                "No bzr branch at new location %s", new_location)
268
            ok = False
269
        else:
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
270
            try:
9590.1.82 by Michael Hudson
start fixing the branch-distro tests
271
                new_stacked_on_url = new_bzr_branch.get_stacked_on_url()
272
                ok = False
9636.4.19 by Michael Hudson
moar tests
273
                self.logger.warning(
9590.1.82 by Michael Hudson
start fixing the branch-distro tests
274
                    "New branch at %s is stacked on %s, should be "
275
                    "unstacked.", new_location, new_stacked_on_url)
276
            except NotStacked:
277
                pass
9590.1.83 by Michael Hudson
indentation screwup
278
        # The branch in the old distroseries is stacked on that in the
279
        # new.
280
        old_location = 'lp-internal:///' + old_db_branch.unique_name
281
        try:
282
            old_bzr_branch = Branch.open(old_location)
283
        except NotBranchError:
284
            self.logger.warning(
285
                "No bzr branch at old location %s", old_location)
286
            ok = False
287
        else:
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
288
            try:
9590.1.83 by Michael Hudson
indentation screwup
289
                old_stacked_on_url = old_bzr_branch.get_stacked_on_url()
290
                if old_stacked_on_url != '/' + new_db_branch.unique_name:
9636.4.21 by Michael Hudson
more tests and fixes
291
                    self.logger.warning(
9590.1.83 by Michael Hudson
indentation screwup
292
                        "Old branch at %s is stacked on %s, should be "
293
                        "stacked on %s", old_location, old_stacked_on_url,
9636.4.21 by Michael Hudson
more tests and fixes
294
                        '/' + new_db_branch.unique_name)
295
                    ok = False
9590.1.83 by Michael Hudson
indentation screwup
296
            except NotStacked:
297
                self.logger.warning(
298
                    "Old branch at %s is not stacked, should be stacked "
299
                    "on %s", old_location,
300
                    '/' + new_db_branch.unique_name)
301
                ok = False
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:
310
                self.logger.warning(
311
                    "Repository at %s has %s revisions.",
312
                    old_location, len(old_repo.all_revision_ids()))
313
                ok = False
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:':
318
                self.logger.warning(
319
                    "Old branch at %s has null tip revision.",
320
                    old_location)
321
                ok = False
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
322
        return ok
323
324
    def makeOneNewBranch(self, old_db_branch):
9636.4.29 by Michael Hudson
some docstrings
325
        """Copy a branch to the new distroseries.
326
9636.4.32 by Michael Hudson
XXX reduction
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'
331
        branch.
9636.4.29 by Michael Hudson
some docstrings
332
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).
336
        """
9636.4.26 by Michael Hudson
i think all tests finally pass
337
        if not self.checkConsistentOfficialPackageBranch(old_db_branch):
9636.4.28 by Michael Hudson
address some comments from jml
338
            self.logger.warning("Skipping branch")
9636.4.26 by Michael Hudson
i think all tests finally pass
339
            return
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
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(
9772.5.1 by Michael Hudson
test in fix
345
            BranchType.HOSTED, self.new_distroseries.name,
346
            old_db_branch.registrant)
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
347
        new_db_branch.sourcepackage.setBranch(
348
            PackagePublishingPocket.RELEASE, new_db_branch,
13139.3.8 by Francis J. Lacoste
More ubuntu_branches removal.
349
            new_db_branch.owner)
11573.5.2 by Tim Penhey
Make the old branches mature.
350
        old_db_branch.lifecycle_status = BranchLifecycleStatus.MATURE
9636.4.41 by Michael Hudson
address review comments
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.
9636.4.15 by Michael Hudson
new structure, this one is a winner i hope
355
        transaction.commit()
356
        switch_branches(
357
            config.codehosting.mirrored_branches_root,
9590.1.82 by Michael Hudson
start fixing the branch-distro tests
358
            'lp-internal', old_db_branch, new_db_branch)
13046.1.2 by Aaron Bentley
Fix lint.
359
        # Directly copy the branch revisions from the old branch to the new
360
        # branch.
11573.7.3 by Tim Penhey
Avoid the scan.
361
        store = IMasterStore(BranchRevision)
362
        store.execute(
363
            """
364
            INSERT INTO BranchRevision (branch, revision, sequence)
365
            SELECT %s, BranchRevision.revision, BranchRevision.sequence
366
            FROM BranchRevision
367
            WHERE branch = %s
368
            """ % (new_db_branch.id, old_db_branch.id))
369
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)
13046.1.1 by Aaron Bentley
branch-distro.py works with empty branches.
375
        tip_revision_id = (
376
            tip_revision.revision_id if tip_revision is not None else
377
            NULL_REVISION)
11573.7.3 by Tim Penhey
Avoid the scan.
378
        new_db_branch.branchChanged(
13046.1.1 by Aaron Bentley
branch-distro.py works with empty branches.
379
            '', tip_revision_id,
11573.7.3 by Tim Penhey
Avoid the scan.
380
            old_db_branch.control_format,
381
            old_db_branch.branch_format,
382
            old_db_branch.repository_format)
11573.7.8 by Tim Penhey
Update the stacked_on location for the older branch.
383
        old_db_branch.stacked_on = new_db_branch
11573.7.3 by Tim Penhey
Avoid the scan.
384
        transaction.commit()
9636.4.19 by Michael Hudson
moar tests
385
        return new_db_branch