~launchpad-pqm/launchpad/devel

1149 by Canonical.com Patch Queue Manager
malone debbugs integration
1
import os
2
import re
3
from datetime import datetime
4
import email
5
import cStringIO
6
7
class Bug:
8
    def __init__(self, db, id, package=None, date=None, status=None,
9
                 originator=None, severity=None, tags=None, report=None):
10
        self.db = db
11
        self.id = id
12
        self._emails = []
13
14
        if package:
15
            self.package = package
16
        if date:
17
            self.date = date
18
        if status:
19
            self.status = status
20
        if originator:
21
            self.originator = originator
22
        if severity:
23
            self.severity = severity
24
        if tags:
25
            self.tags = tags
26
        if report:
27
            self.report = report
28
29
    def is_open(self):
30
        #return not self.done and 'fixed' not in self.tags
31
        return self.status != 'done' and 'fixed' not in self.tags
32
33
    def affects_unstable(self):
34
        return 'sid' in self.tags or ('woody' not in self.tags and
35
                                 'sarge' not in self.tags and
36
                                 'experimental' not in self.tags)
37
38
    def affects_package(self, packageset):
39
        for package in self.packagelist():
40
            if package in packageset:
41
                return True
42
        return False
43
44
    def is_release_critical(self):
45
        return self.severity in ('critical', 'grave', 'serious')
46
47
    def __str__(self):
48
        return 'Bug#%d' % self.id
49
50
    def __getattr__(self, name):
51
        # Lazy loading of non-indexed attributes
52
53
        if not self.db.load(self, name):
54
            raise AttributeError, name
55
56
        if not hasattr(self, name):
57
            raise InternalError, "Database.load did not provide attribute '%s'" % name
58
59
        return getattr(self, name)
60
61
    def packagelist(self):
62
        if self.package is None:
63
            return []
64
        if ',' in self.package:
65
            return self.package.split(',')
66
        return [self.package]
67
68
    def emails(self):
69
        if self._emails:
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
70
            return self._emails
71
        for comment in self.comments:
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
72
            message = email.message_from_string(comment)
73
            self._emails.append(message)
74
        return self._emails
75
76
class IndexParseError(Exception): pass
77
class StatusParseError(Exception): pass
78
class StatusMissing(Exception): pass
79
class SummaryMissing(Exception): pass
80
class SummaryParseError(Exception): pass
81
class SummaryVersionError(Exception): pass
82
class ReportMissing(Exception): pass
83
class ReportParseError(Exception): pass
84
class LogMissing(Exception): pass
85
class LogParseFailed(Exception): pass
86
class InternalError(Exception): pass
87
88
class Database:
89
    def __init__(self, root):
90
        self.root = root
91
92
    class bug_iterator:
93
        index_record = re.compile(r'^(?P<package>\S+) (?P<bugid>\d+) (?P<date>\d+) (?P<status>\w+) \[(?P<originator>.*)\] (?P<severity>\w+)(?: (?P<tags>.*))?$')
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
94
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
95
        def __init__(self, db, filter=None):
96
            self.db = db
97
            self.index = open(os.path.join(self.db.root, 'index/index.db'))
1194 by Canonical.com Patch Queue Manager
rosetta stats legend, fixes for hoary
98
            self.filter = filter
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
99
100
        def next(self):
101
            line = self.index.readline()
102
            if not line:
103
                raise StopIteration
104
105
            match = self.index_record.match(line)
106
            if not match:
107
                raise IndexParseError(line)
108
109
            return Bug(self.db,
110
                       int(match.group('bugid')),
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
111
                       match.group('package'),
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
112
                       datetime.fromtimestamp(int(match.group('date'))),
113
                       match.group('status'),
114
                       match.group('originator'),
115
                       match.group('severity'),
116
                       match.group('tags').split(' '))
117
                       
118
    def load(self, bug, name):
119
        if name in ('originator', 'date', 'subject', 'msgid', 'package',
120
                    'tags', 'done', 'forwarded', 'mergedwith', 'severity'):
121
            self.load_summary(bug)
122
        elif name == 'report':
123
            self.load_report(bug)
124
        elif name in ('comments',):
125
            self.load_log(bug)
126
        elif name == 'status':
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
127
            if bug.done is not None:
128
                bug.status = 'done'
129
            if bug.forwarded is not None:
130
                bug.status = 'forwarded'
131
            bug.status = 'open'
132
        else:
1149 by Canonical.com Patch Queue Manager
malone debbugs integration
133
            return False
134
135
        return True
136
137
    def load_summary(self, bug):
138
        summary = os.path.join(self.root, 'db-h', self._hash(bug), '%d.summary' % bug.id)
139
140
        try:
141
            fd = open(summary)
142
        except IOError, e:
143
            if e.errno == 2:
144
                raise SummaryMissing, summary
145
            raise
146
147
        try:
148
            message = email.message_from_file(fd)
149
        except Exception, e:
150
            raise SummaryParseError, '%s: %s' % (summary, str(e))
151
152
        version = message['format-version']
153
        if version is None:
154
            raise SummaryParseError, "%s: Missing Format-Version" % summary
155
        
156
        if version != '2':
157
            raise SummaryVersionError, "%s: I don't understand version %s" % (summary, version)
158
159
        bug.originator = message['submitter']
160
        bug.date = datetime.fromtimestamp(int(message['date']))
161
        bug.subject = message['subject']
162
        bug.msgid = message['message-id']
163
        bug.package = message['package']
164
        bug.done = message['done']
165
        bug.forwarded = message['forwarded-to']
166
        bug.severity = message['severity']
167
168
        if 'merged-with' in message:
169
            bug.mergedwith = map(int,message['merged-with'].split(' '))
170
        else:
171
            bug.mergedwith = []
172
173
        if 'tags' in message:
174
            bug.tags = message['tags'].split(' ')
175
        else:
176
            bug.tags = []
177
178
    def load_report(self, bug):
179
        report = os.path.join(self.root, 'db-h', self._hash(bug), '%d.report' % bug.id)
180
181
        try:
182
            fd = open(report)
183
        except IOError, e:
184
            if e.errno == 2:
185
                raise ReportMissing, report
186
            raise
187
188
        bug.report = fd.read()
189
        fd.close()
190
191
    def load_log(self, bug):
192
        log = os.path.join(self.root, 'db-h', self._hash(bug), '%d.log' % bug.id)
193
        comments = []
194
195
        try:
196
            logreader = os.popen('./debbugs-log.pl %s' % log, 'r')
197
            comment = cStringIO.StringIO()
198
            for line in logreader:
199
                if line == '.\n':
200
                    comments.append(comment.getvalue())
201
                    comment = cStringIO.StringIO()
202
                elif line.startswith('.'):
203
                    comment.write(line[1:])
204
                else:
205
                    comment.write(line)
206
            if comment.tell() != 0:
207
                raise LogParseFailed('Unterminated comment from debbugs-log.pl')
208
            exitcode = logreader.close()
209
            if exitcode is not None:
210
                raise LogParseFailed('debbugs-log.pl exited with code %d' % exitcode)
211
        except IOError, e:
212
            if e.errno == 2:
213
                raise LogMissing, log
214
            raise
215
216
        bug.comments = comments
217
218
    def _hash(self, bug):
219
        return '%02d' % (bug.id % 100)
220
221
    def __iter__(self):
222
        return self.bug_iterator(self, None)
223
224
    def __getitem__(self, bug):
225
        return Bug(self, bug)
226
227
if __name__ == '__main__':
228
    import sys
229
230
    for bug in Database('/srv/debzilla.no-name-yet.com/debbugs'):
231
        try:
232
            print bug, bug.subject
233
        except Exception, e:
234
            print >>sys.stderr, '%s: %s' % (e.__class__.__name__, str(e))
235
236
237