~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/codehosting/branchdistro.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-12-22 04:55:30 UTC
  • mfrom: (14577.1.1 testfix)
  • Revision ID: launchpad@pqm.canonical.com-20111222045530-wki9iu6c0ysqqwkx
[r=wgrant][no-qa] Fix test_publisherconfig lpstorm import. Probably a
        silent conflict between megalint and apocalypse.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
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
"""
 
11
 
 
12
__metaclass__ = type
 
13
__all__ = [
 
14
    'DistroBrancher',
 
15
    'switch_branches',
 
16
    ]
 
17
 
 
18
import os
 
19
 
 
20
from bzrlib.branch import Branch
 
21
from bzrlib.bzrdir import BzrDir
 
22
from bzrlib.errors import (
 
23
    NotBranchError,
 
24
    NotStacked,
 
25
    )
 
26
from bzrlib.revision import NULL_REVISION
 
27
import transaction
 
28
from zope.component import getUtility
 
29
 
 
30
from canonical.config import config
 
31
from lp.services.database.lpstorm import IMasterStore
 
32
from lp.code.enums import (
 
33
    BranchLifecycleStatus,
 
34
    BranchType,
 
35
    )
 
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,
 
41
    )
 
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
 
46
 
 
47
 
 
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
 
 
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
 
 
62
    :param prefix: The non-branch id dependent part of the physical path to
 
63
        the branches on disk.
 
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
 
68
        presence on disk yet.
 
69
    """
 
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)
 
77
    os.rename(
 
78
        os.path.join(old_underlying_path, '.bzr'),
 
79
        os.path.join(new_underlying_path, '.bzr'))
 
80
 
 
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.
 
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:')
 
89
 
 
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)
 
93
 
 
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())
 
97
 
 
98
 
 
99
class DistroBrancher:
 
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
    """
 
107
 
 
108
    def __init__(self, logger, old_distroseries, new_distroseries):
 
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
        """
 
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))
 
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
 
132
 
 
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)
 
142
        return cls(logger, old_distroseries, new_distroseries)
 
143
 
 
144
    def _existingOfficialBranches(self):
 
145
        """Return the collection of official branches in the old distroseries.
 
146
        """
 
147
        branches = getUtility(IAllBranches)
 
148
        distroseries_branches = branches.inDistroSeries(self.old_distroseries)
 
149
        return distroseries_branches.officialBranches().getBranches(
 
150
            eager_load=False)
 
151
 
 
152
    def checkConsistentOfficialPackageBranch(self, db_branch):
 
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
        """
 
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(
 
174
                "Encountered unexpected personal branch %s",
 
175
                db_branch.unique_name)
 
176
            return False
 
177
        find_branch_links = getUtility(IFindOfficialBranchLinks)
 
178
        links = list(find_branch_links.findForBranch(db_branch))
 
179
        if len(links) == 0:
 
180
            self.logger.warning(
 
181
                "%s is not an official branch", db_branch.unique_name)
 
182
            return False
 
183
        elif len(links) > 1:
 
184
            series_text = ', '.join([
 
185
                link.sourcepackage.path for link in links])
 
186
            self.logger.warning(
 
187
                "%s is official for multiple series: %s",
 
188
                db_branch.unique_name, series_text)
 
189
            return False
 
190
        elif links[0].sourcepackage != db_branch.sourcepackage:
 
191
            self.logger.warning(
 
192
                "%s is the official branch for %s but not its "
 
193
                "sourcepackage", db_branch.unique_name,
 
194
                links[0].sourcepackage.path)
 
195
            return False
 
196
        return True
 
197
 
 
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)
 
202
            try:
 
203
                self.makeOneNewBranch(db_branch)
 
204
            except BranchExists:
 
205
                pass
 
206
 
 
207
    def checkNewBranches(self):
 
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
 
212
        stacking is set up as we expect on disk.
 
213
 
 
214
        Every branch will be checked, even if some fail.
 
215
 
 
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
        """
 
222
        ok = True
 
223
        for db_branch in self._existingOfficialBranches():
 
224
            self.logger.debug("Checking %s" % db_branch.unique_name)
 
225
            try:
 
226
                if not self.checkOneBranch(db_branch):
 
227
                    ok = False
 
228
            except:
 
229
                ok = False
 
230
                self.logger.exception(
 
231
                    "Unexpected error checking %s!", db_branch)
 
232
        return ok
 
233
 
 
234
    def checkOneBranch(self, old_db_branch):
 
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
 
238
        new distroseries and that stacking is set up as we expect on disk.
 
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
        """
 
246
        ok = self.checkConsistentOfficialPackageBranch(old_db_branch)
 
247
        if not ok:
 
248
            return ok
 
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:
 
254
            self.logger.warning(
 
255
                "No official branch found for %s",
 
256
                new_sourcepackage.path)
 
257
            return False
 
258
        ok = self.checkConsistentOfficialPackageBranch(new_db_branch)
 
259
        if not ok:
 
260
            return ok
 
261
        # the branch in the new distroseries is unstacked
 
262
        new_location = 'lp-internal:///' + new_db_branch.unique_name
 
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:
 
270
            try:
 
271
                new_stacked_on_url = new_bzr_branch.get_stacked_on_url()
 
272
                ok = False
 
273
                self.logger.warning(
 
274
                    "New branch at %s is stacked on %s, should be "
 
275
                    "unstacked.", new_location, new_stacked_on_url)
 
276
            except NotStacked:
 
277
                pass
 
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:
 
288
            try:
 
289
                old_stacked_on_url = old_bzr_branch.get_stacked_on_url()
 
290
                if old_stacked_on_url != '/' + new_db_branch.unique_name:
 
291
                    self.logger.warning(
 
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)
 
295
                    ok = False
 
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
 
322
        return ok
 
323
 
 
324
    def makeOneNewBranch(self, old_db_branch):
 
325
        """Copy a branch to the new distroseries.
 
326
 
 
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.
 
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
        """
 
337
        if not self.checkConsistentOfficialPackageBranch(old_db_branch):
 
338
            self.logger.warning("Skipping branch")
 
339
            return
 
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,
 
349
            new_db_branch.owner)
 
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.
 
355
        transaction.commit()
 
356
        switch_branches(
 
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
 
360
        # branch.
 
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)
 
375
        tip_revision_id = (
 
376
            tip_revision.revision_id if tip_revision is not None else
 
377
            NULL_REVISION)
 
378
        new_db_branch.branchChanged(
 
379
            '', tip_revision_id,
 
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
 
384
        transaction.commit()
 
385
        return new_db_branch