~launchpad-pqm/launchpad/devel

14538.2.49 by Curtis Hovey
Updated copyright.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
3
4
__metaclass__ = type
5
__all__ = [
6
    'PackageDiff',
7
    'PackageDiffSet',
8
    ]
9
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
10
import gzip
12599.4.2 by Leonard Richardson
Merge from trunk.
11
import itertools
6159.7.2 by Muharem Hrnjadovic
simplified code
12
import os
13
import shutil
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
14
import subprocess
6159.7.2 by Muharem Hrnjadovic
simplified code
15
import tempfile
16
6615.1.4 by James Henstridge
Add back support for getting package diffs in one query.
17
from sqlobject import ForeignKey
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
18
from storm.expr import Desc
6620.4.1 by James Henstridge
Fix distro package pages for packages with no published releases by
19
from storm.store import EmptyResultSet
6159.7.2 by Muharem Hrnjadovic
simplified code
20
from zope.component import getUtility
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
21
from zope.interface import implements
6422.2.1 by Muharem Hrnjadovic
bug fixed
22
12599.4.2 by Leonard Richardson
Merge from trunk.
23
from lp.services.database.bulk import load
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
24
from lp.services.database.constants import UTC_NOW
25
from lp.services.database.datetimecol import UtcDateTimeCol
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
26
from lp.services.database.decoratedresultset import DecoratedResultSet
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
27
from lp.services.database.enumcol import EnumCol
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
28
from lp.services.database.lpstorm import IStore
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
29
from lp.services.database.sqlbase import (
30
    SQLBase,
31
    sqlvalues,
32
    )
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
33
from lp.services.librarian.interfaces import ILibraryFileAliasSet
34
from lp.services.librarian.model import (
35
    LibraryFileAlias,
36
    LibraryFileContent,
37
    )
14606.2.2 by William Grant
Move canonical.librarian.{client,utils} to lp.services.librarian.
38
from lp.services.librarian.utils import copy_and_close
39
from lp.services.webapp.interfaces import (
40
    DEFAULT_FLAVOR,
41
    IStoreSelector,
42
    MAIN_STORE,
43
    )
11411.6.8 by Julian Edwards
Move PackageDiffStatus
44
from lp.soyuz.enums import PackageDiffStatus
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
45
from lp.soyuz.interfaces.packagediff import (
46
    IPackageDiff,
47
    IPackageDiffSet,
48
    )
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
49
50
6159.7.7 by Muharem Hrnjadovic
review comment fixes
51
def perform_deb_diff(tmp_dir, out_filename, from_files, to_files):
6159.7.6 by Muharem Hrnjadovic
review comment fixes
52
    """Perform a (deb)diff on two packages.
53
54
    A debdiff will be invoked on the files associated with the
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
55
    two packages to be diff'ed. The resulting output will be a tuple
56
    containing the process return code and the STDERR output.
6159.7.6 by Muharem Hrnjadovic
review comment fixes
57
58
    :param tmp_dir: The temporary directory with the package files.
59
    :type tmp_dir: ``str``
60
    :param out_filename: The name of the file that will hold the
61
        resulting debdiff output.
62
    :type tmp_dir: ``str``
63
    :param from_files: A list with the names of the files associated
64
        with the first package.
65
    :type from_files: ``list``
66
    :param to_files: A list with the names of the files associated
67
        with the second package.
68
    :type to_files: ``list``
69
    """
70
    compressed_bytes = -1
71
    [from_dsc] = [name for name in from_files
72
                  if name.lower().endswith('.dsc')]
73
    [to_dsc] = [name for name in to_files
74
                if name.lower().endswith('.dsc')]
75
    args = ['debdiff', from_dsc, to_dsc]
76
77
    full_path = os.path.join(tmp_dir, out_filename)
78
    out_file = None
79
    try:
80
        out_file = open(full_path, 'w')
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
81
        process = subprocess.Popen(
82
            args, stdout=out_file, stderr=subprocess.PIPE, cwd=tmp_dir)
83
        stdout, stderr = process.communicate()
6159.7.6 by Muharem Hrnjadovic
review comment fixes
84
    finally:
85
        if out_file is not None:
86
            out_file.close()
87
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
88
    return process.returncode, stderr
6159.7.6 by Muharem Hrnjadovic
review comment fixes
89
7573.1.1 by Celso Providelo
Using PackageDiff.status column.
90
6159.7.7 by Muharem Hrnjadovic
review comment fixes
91
def download_file(destination_path, libraryfile):
6159.7.6 by Muharem Hrnjadovic
review comment fixes
92
    """Download a file from the librarian to the destination path.
93
94
    :param destination_path: Absolute destination path (where the
95
        file should be downloaded to).
96
    :type destination_path: ``str``
97
    :param libraryfile: The librarian file that is to be downloaded.
98
    :type libraryfile: ``LibraryFileAlias``
99
    """
6422.2.1 by Muharem Hrnjadovic
bug fixed
100
    libraryfile.open()
101
    destination_file = open(destination_path, 'w')
102
    copy_and_close(libraryfile, destination_file)
6159.7.6 by Muharem Hrnjadovic
review comment fixes
103
7573.1.1 by Celso Providelo
Using PackageDiff.status column.
104
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
105
class PackageDiff(SQLBase):
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
106
    """A Package Diff request."""
107
5568.2.7 by Celso Providelo
removing misleading IHasOnwer implementation from PackageDiff.
108
    implements(IPackageDiff)
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
109
110
    _defaultOrder = ['id']
111
112
    date_requested = UtcDateTimeCol(notNull=False, default=UTC_NOW)
113
114
    requester = ForeignKey(
115
        dbName='requester', foreignKey='Person', notNull=True)
116
117
    from_source = ForeignKey(
118
        dbName="from_source", foreignKey='SourcePackageRelease', notNull=True)
119
120
    to_source = ForeignKey(
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
121
        dbName="to_source", foreignKey='SourcePackageRelease', notNull=True)
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
122
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
123
    date_fulfilled = UtcDateTimeCol(notNull=False, default=None)
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
124
125
    diff_content = ForeignKey(
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
126
        dbName="diff_content", foreignKey='LibraryFileAlias',
127
        notNull=False, default=None)
128
7573.1.1 by Celso Providelo
Using PackageDiff.status column.
129
    status = EnumCol(
130
        dbName='status', notNull=True, schema=PackageDiffStatus,
131
        default=PackageDiffStatus.PENDING)
132
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
133
    @property
134
    def title(self):
135
        """See `IPackageDiff`."""
6392.1.2 by Celso Providelo
Shortening PackageDiff titles.
136
        ancestry_archive = self.from_source.upload_archive
137
        if ancestry_archive == self.to_source.upload_archive:
138
            ancestry_identifier = self.from_source.version
139
        else:
140
            ancestry_identifier = "%s (in %s)" % (
141
                self.from_source.version,
142
                ancestry_archive.distribution.name.capitalize())
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
143
        return 'diff from %s to %s' % (
144
            ancestry_identifier, self.to_source.version)
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
145
6557.1.1 by Celso Providelo
Fixing bug #235907 (store packagediffs in private librarian when required).
146
    @property
147
    def private(self):
148
        """See `IPackageDiff`."""
149
        return self.to_source.upload_archive.private
150
12189.1.3 by William Grant
Rename _countExpiredLFAs to _countDeletedLFAs.
151
    def _countDeletedLFAs(self):
152
        """How many files associated with either source package have been
153
        deleted from the librarian?"""
10269.1.2 by Muharem Hrnjadovic
added fix, now on to the tests.
154
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
155
        query = """
156
            SELECT COUNT(lfa.id)
157
            FROM
158
                SourcePackageRelease spr, SourcePackageReleaseFile sprf,
159
                LibraryFileAlias lfa
160
            WHERE
161
                spr.id IN %s
162
                AND sprf.SourcePackageRelease = spr.id
163
                AND sprf.libraryfile = lfa.id
164
                AND lfa.content IS NULL
10269.1.4 by Muharem Hrnjadovic
tests work.
165
            """ % sqlvalues((self.from_source.id, self.to_source.id))
10269.1.2 by Muharem Hrnjadovic
added fix, now on to the tests.
166
        result = store.execute(query).get_one()
10269.1.4 by Muharem Hrnjadovic
tests work.
167
        return (0 if result is None else result[0])
10269.1.2 by Muharem Hrnjadovic
added fix, now on to the tests.
168
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
169
    def performDiff(self):
6159.7.2 by Muharem Hrnjadovic
simplified code
170
        """See `IPackageDiff`.
171
172
        This involves creating a temporary directory, downloading the files
173
        from both SPRs involved from the librarian, running debdiff, storing
174
        the output in the librarian and updating the PackageDiff record.
175
        """
10269.1.2 by Muharem Hrnjadovic
added fix, now on to the tests.
176
        # Make sure the files associated with the two source packages are
177
        # still available in the librarian.
12189.1.3 by William Grant
Rename _countExpiredLFAs to _countDeletedLFAs.
178
        if self._countDeletedLFAs() > 0:
10269.1.2 by Muharem Hrnjadovic
added fix, now on to the tests.
179
            self.status = PackageDiffStatus.FAILED
180
            return
181
6159.7.2 by Muharem Hrnjadovic
simplified code
182
        # Create the temporary directory where the files will be
183
        # downloaded to and where the debdiff will be performed.
184
        tmp_dir = tempfile.mkdtemp()
185
186
        try:
187
            directions = ('from', 'to')
188
189
            # Keep track of the files belonging to the respective packages.
190
            downloaded = dict(zip(directions, ([], [])))
191
192
            # Please note that packages may have files in common.
193
            files_seen = []
194
195
            # Make it easy to iterate over packages.
196
            packages = dict(
197
                zip(directions, (self.from_source, self.to_source)))
198
199
            # Iterate over the packages to be diff'ed.
200
            for direction, package in packages.iteritems():
6473.2.2 by Celso Providelo
Extending the fix to catch filename conflicts with different contents.
201
                # Create distinct directory locations for
202
                # 'from' and 'to' files.
6473.2.3 by Celso Providelo
applying review comments, r=Edwin.
203
                absolute_path = os.path.join(tmp_dir, direction)
204
                os.makedirs(absolute_path)
6473.2.2 by Celso Providelo
Extending the fix to catch filename conflicts with different contents.
205
206
                # Download the files associated with each package in
207
                # their corresponding relative location.
6159.7.2 by Muharem Hrnjadovic
simplified code
208
                for file in package.files:
209
                    the_name = file.libraryfile.filename
6473.2.2 by Celso Providelo
Extending the fix to catch filename conflicts with different contents.
210
                    relative_location = os.path.join(direction, the_name)
211
                    downloaded[direction].append(relative_location)
6473.2.3 by Celso Providelo
applying review comments, r=Edwin.
212
                    destination_path = os.path.join(absolute_path, the_name)
6159.7.7 by Muharem Hrnjadovic
review comment fixes
213
                    download_file(destination_path, file.libraryfile)
6159.7.2 by Muharem Hrnjadovic
simplified code
214
215
            # All downloads are done. Construct the name of the resulting
216
            # diff file.
6382.5.1 by Celso Providelo
Package-Diff revolution. Inverting the 'natural' diff order, so the diffs will read better as from ANCESTRY to CURRENT. Modifying SPR.package_diffs join to return all diff *to* the context SPR and not *from* it. Finally making the diff title and filename less redundant.
217
            result_filename = '%s_%s_%s.diff' % (
6159.7.3 by Muharem Hrnjadovic
fixed result diff file name
218
                self.from_source.sourcepackagename.name,
219
                self.from_source.version,
220
                self.to_source.version)
6159.7.2 by Muharem Hrnjadovic
simplified code
221
222
            # Perform the actual diff operation.
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
223
            return_code, stderr = perform_deb_diff(
6159.7.2 by Muharem Hrnjadovic
simplified code
224
                tmp_dir, result_filename, downloaded['from'],
225
                downloaded['to'])
226
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
227
            # `debdiff` failed, mark the package diff request accordingly
9102.3.1 by William Grant
Accept 1 as an exit code from debdiff. Karmic's exits with 1 if they differ.
228
            # and return. 0 means no differences, 1 means they differ.
229
            # Note that pre-Karmic debdiff will return 0 even if they differ.
230
            if return_code not in (0, 1):
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
231
                self.status = PackageDiffStatus.FAILED
232
                return
233
234
            # Compress the generated diff.
235
            out_file = open(os.path.join(tmp_dir, result_filename))
236
            gzip_result_filename = result_filename + '.gz'
237
            gzip_file_path = os.path.join(tmp_dir, gzip_result_filename)
238
            gzip_file = gzip.GzipFile(gzip_file_path, mode='wb')
239
            copy_and_close(out_file, gzip_file)
240
241
            # Calculate the compressed size.
242
            gzip_size = os.path.getsize(gzip_file_path)
243
244
            # Upload the compressed diff to librarian and update
245
            # the package diff request.
246
            gzip_file = open(gzip_file_path)
6159.7.2 by Muharem Hrnjadovic
simplified code
247
            try:
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
248
                librarian_set = getUtility(ILibraryFileAliasSet)
249
                self.diff_content = librarian_set.create(
250
                    gzip_result_filename, gzip_size, gzip_file,
6557.1.1 by Celso Providelo
Fixing bug #235907 (store packagediffs in private librarian when required).
251
                    'application/gzipped-patch', restricted=self.private)
6159.7.2 by Muharem Hrnjadovic
simplified code
252
            finally:
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
253
                gzip_file.close()
6159.7.2 by Muharem Hrnjadovic
simplified code
254
7627.2.1 by Celso Providelo
Fixing #242387 (package-diff should cope with 'debdiff' failures).
255
            # Last but not least, mark the diff as COMPLETED.
6159.7.2 by Muharem Hrnjadovic
simplified code
256
            self.date_fulfilled = UTC_NOW
7573.1.1 by Celso Providelo
Using PackageDiff.status column.
257
            self.status = PackageDiffStatus.COMPLETED
6159.7.2 by Muharem Hrnjadovic
simplified code
258
        finally:
259
            shutil.rmtree(tmp_dir)
260
5568.2.6 by Celso Providelo
Finishing PackageDiff content classes, interfaces, zcml and minimal tests.
261
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
262
class PackageDiffSet:
263
    """This class is to deal with Distribution related stuff"""
264
265
    implements(IPackageDiffSet)
266
267
    def __iter__(self):
6286.3.1 by Celso Providelo
Entending PackageDiffSet to support processing-pending-packagediffs.py script
268
        """See `IPackageDiffSet`."""
269
        return iter(PackageDiff.select(orderBy=['-id']))
5568.2.4 by Celso Providelo
Adding PackageDiff content classes and utilities.
270
271
    def get(self, diff_id):
272
        """See `IPackageDiffSet`."""
273
        return PackageDiff.get(diff_id)
6286.3.1 by Celso Providelo
Entending PackageDiffSet to support processing-pending-packagediffs.py script
274
275
    def getPendingDiffs(self, limit=None):
7573.1.2 by Celso Providelo
Re-implement IPackageDiffSet.getPendingDIff() using storm and based on 'status' instead of 'date_fulfilled'.
276
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
277
        result = store.find(
278
            PackageDiff, PackageDiff.status == PackageDiffStatus.PENDING)
279
        result.order_by(PackageDiff.id)
7573.1.3 by Celso Providelo
applying review comments, r=salgado.
280
        return result.config(limit=limit)
6615.1.4 by James Henstridge
Add back support for getting package diffs in one query.
281
12599.4.2 by Leonard Richardson
Merge from trunk.
282
    def getDiffsToReleases(self, sprs, preload_for_display=False):
6615.1.4 by James Henstridge
Add back support for getting package diffs in one query.
283
        """See `IPackageDiffSet`."""
12599.4.2 by Leonard Richardson
Merge from trunk.
284
        from lp.registry.model.distribution import Distribution
285
        from lp.soyuz.model.archive import Archive
286
        from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
6620.4.1 by James Henstridge
Fix distro package pages for packages with no published releases by
287
        if len(sprs) == 0:
288
            return EmptyResultSet()
6555.7.9 by Stuart Bishop
Use new store selection api
289
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
6615.1.4 by James Henstridge
Add back support for getting package diffs in one query.
290
        spr_ids = [spr.id for spr in sprs]
7675.166.301 by Stuart Bishop
Replace In(col, i) with col.is_in(u) to work around Bug #670906 and delint
291
        result = store.find(
292
            PackageDiff, PackageDiff.to_sourceID.is_in(spr_ids))
6615.1.4 by James Henstridge
Add back support for getting package diffs in one query.
293
        result.order_by(PackageDiff.to_sourceID,
294
                        Desc(PackageDiff.date_requested))
12599.4.2 by Leonard Richardson
Merge from trunk.
295
296
        def preload_hook(rows):
297
            lfas = load(LibraryFileAlias, (pd.diff_contentID for pd in rows))
298
            lfcs = load(LibraryFileContent, (lfa.contentID for lfa in lfas))
299
            sprs = load(
300
                SourcePackageRelease,
301
                itertools.chain.from_iterable(
302
                    (pd.from_sourceID, pd.to_sourceID) for pd in rows))
303
            archives = load(Archive, (spr.upload_archiveID for spr in sprs))
304
            distros = load(Distribution, (a.distributionID for a in archives))
305
306
        if preload_for_display:
307
            return DecoratedResultSet(result, pre_iter_hook=preload_hook)
308
        else:
309
            return result
12828.6.3 by Steve Kowalik
Write IPackageDiffSet.getDiffBetweenReleases(), and use it.
310
12828.6.4 by Steve Kowalik
Change IPackageDiffSet.getDiffBetweenReleases() to take 2 arguments.
311
    def getDiffBetweenReleases(self, from_spr, to_spr):
12828.6.3 by Steve Kowalik
Write IPackageDiffSet.getDiffBetweenReleases(), and use it.
312
        """See `IPackageDiffSet`."""
12828.6.5 by Steve Kowalik
Remove silly comment, and fix wrapping for IPackageDiffSet calls.
313
        return IStore(PackageDiff).find(
12828.6.3 by Steve Kowalik
Write IPackageDiffSet.getDiffBetweenReleases(), and use it.
314
            PackageDiff,
12828.6.4 by Steve Kowalik
Change IPackageDiffSet.getDiffBetweenReleases() to take 2 arguments.
315
            from_sourceID=from_spr.id, to_sourceID=to_spr.id).first()