changes.py 6.92 KB
Newer Older
1
2
#   changes.py — .changes file handling class
#
Baptiste Beauplat's avatar
Baptiste Beauplat committed
3
4
#   This file is part of debexpo -
#   https://salsa.debian.org/mentors.debian.net-team/debexpo
5
#
Jonny Lamb's avatar
Jonny Lamb committed
6
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
7
#   Copyright © 2010 Jan Dittberner <jandd@debian.org>
8
#   Copyright © 2019 Baptiste Beauplat <lyknode@cilg.org>
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

31
32
33
34
"""
Holds *changes* file handling class.
"""

35
from debian import deb822
36
from debian.debian_support import NativeVersion
37
38
from os.path import dirname, abspath, basename, join
from os import replace, unlink
39

40
from debexpo.bugs.models import Bug
41
42
43
from debexpo.accounts.models import User
from debexpo.tools.files import GPGSignedFile
from debexpo.tools.debian.control import ControlFiles
44
45
from debexpo.tools.debian.dsc import Dsc
from debexpo.tools.debian.source import Source
46
from debexpo.tools.clients.ftp_master import ClientFTPMasterAPI
47

48

49
50
class ExceptionChanges(Exception):
    pass
Baptiste Beauplat's avatar
Baptiste Beauplat committed
51

52
53

class Changes(GPGSignedFile):
54
55
56
57
    """
    Helper class to parse *changes* files nicely.
    """

58
    def __init__(self, filename):
59
60
61
62
63
64
65
66
67
68
69
70
        """
        Object constructor. The object allows the user to specify **either**:

        #. a path to a *changes* file to parse

        ::

          a = Changes(filename='/tmp/packagename_version.changes')

        ``filename``
            Path to *changes* file to parse.
        """
71
        super().__init__(abspath(filename))
72

73
74
        with open(self.filename, 'rb') as fd:
            self._data = deb822.Changes(fd)
75

76
77
78
        if len(self._data) == 0:
            raise ExceptionChanges('Changes file {} could not be parsed'.format(
                self.filename))
79

80
        self._build_changes()
81

82
83
84
85
    def owns(self, filename):
        for item in self.files.files:
            if filename == str(item):
                return True
86

87
        return False
88

89
90
91
92
93
94
95
96
97
98
99
    def authenticate(self, gpg=True):
        if gpg:
            super().authenticate()
            self.uploader = User.objects.get(key=self.key)
        else:
            user = User.objects.lookup_user_from_address(self.uploader)

            if not user:
                raise ExceptionChanges(f'No user found for {self.uploader}')

            self.uploader = user
100

101
    def _build_changes(self):
102
        self.dsc = None
103
        self.bugs = None
Baptiste Beauplat's avatar
Baptiste Beauplat committed
104
        self.source_package = None
105
        self.maintainer = self._data.get('Maintainer')
106
        self.uploader = self._data.get('Changed-By', self.maintainer)
107
        self.source = self._data.get('Source')
108
        self.version = self._data.get('Version')
109
110
        self.distribution = self._data.get('Distribution')
        self.changes = self._data.get('Changes')
111
        self.files = ControlFiles(dirname(self.filename), self._data)
112
        self.closes = self._data.get('Closes', '')
113
        self.component = self.files.get_component()
114

115
116
117
118
119
120
121
122
        self._cleanup_changes()

    def _cleanup_changes(self):
        if self.changes:
            lines = self.changes.splitlines()
            if len(lines) > 1 and not lines[0]:
                self.changes = '\n'.join(lines[1:])

123
124
125
126
127
128
129
130
131
    def validate(self):
        # Per debian policy:
        # https://www.debian.org/doc/debian-policy/ch-controlfields.html#debian-changes-files-changes
        for key in ['Architecture', 'Changes', 'Checksums-Sha1',
                    'Checksums-Sha256', 'Date', 'Distribution', 'Files',
                    'Format', 'Maintainer', 'Source', 'Version']:
            if key not in self._data:
                raise ExceptionChanges('Changes file invalid. Missing key '
                                       '{}'.format(key))
132

133
134
135
136
137
138
139
140
141
142
143
    def get_bugs(self):
        if self.bugs is not None:
            return self.bugs

        if self.closes:
            self.bugs = Bug.objects.fetch_bugs(self.closes.split(' '))
        else:
            self.bugs = []

        return self.bugs

144
145
    def __str__(self):
        return basename(self.filename)
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162

    def move(self, destdir):
        self.files.move(destdir)

        dest = join(destdir, basename(self.filename))
        replace(self.filename, dest)
        self.filename = dest

    def remove(self):
        self.files.remove()
        self.files = None

        unlink(self.filename)
        self.filename = None

        self.cleanup_source()

Baptiste Beauplat's avatar
Baptiste Beauplat committed
163
164
165
166
167
168
    def get_source(self):
        if not self.source_package:
            self.source_package = Source(self.dsc)

        return self.source_package

169
170
    def cleanup_source(self):
        if self.dsc:
171
172
            self.dsc.files.remove()

Baptiste Beauplat's avatar
Baptiste Beauplat committed
173
174
        if self.source_package:
            self.source_package.remove()
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

    def parse_dsc(self):
        filename = self.files.find(r'\.dsc$')

        if not filename:
            raise ExceptionChanges(
                'dsc is missing from changes\n'
                'Make sure you include the full source'
                ' (if you are using sbuild make sure to use the'
                ' --source option or the equivalent configuration'
                ' item; if you are using dpkg-buildpackage directly'
                ' use the default flags or -S for a source only'
                ' upload)')

        try:
190
            self.dsc = Dsc(filename, self.component)
191
192
        except Exception as e:
            raise ExceptionChanges(e)
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209

    def assert_newer(self):
        client = ClientFTPMasterAPI()
        versions = client.get_existing_versions_for(self.source,
                                                    self.distribution)

        if versions:
            # There can be multiple version for a single distribution, let's
            # validate against the maximum version.
            version = max([NativeVersion(version) for version in versions])

            if NativeVersion(self.version) <= NativeVersion(version):
                raise ExceptionChanges(
                    f'{self.source} exists in the official Debian archive with '
                    f'the version {version}/{self.distribution}. You may not '
                    'upload a lower or equal version to this one.'
                )