Added a repr for Bug
[be.git] / libbe / bugdir.py
1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
3 #
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 import os
18 import os.path
19 import cmdutil
20 import errno
21 import names
22 import mapfile
23 import time
24 import utility
25 from rcs import rcs_by_name
26
27 class NoBugDir(Exception):
28     def __init__(self, path):
29         msg = "The directory \"%s\" has no bug directory." % path
30         Exception.__init__(self, msg)
31         self.path = path
32
33  
34 def iter_parent_dirs(cur_dir):
35     cur_dir = os.path.realpath(cur_dir)
36     old_dir = None
37     while True:
38         yield cur_dir
39         old_dir = cur_dir
40         cur_dir = os.path.normpath(os.path.join(cur_dir, '..'))
41         if old_dir == cur_dir:
42             break;
43
44
45 def tree_root(dir, old_version=False):
46     for rootdir in iter_parent_dirs(dir):
47         versionfile=os.path.join(rootdir, ".be", "version")
48         if os.path.exists(versionfile):
49             if not old_version:
50                 test_version(versionfile)
51             return BugDir(os.path.join(rootdir, ".be"))
52         elif not os.path.exists(rootdir):
53             raise NoRootEntry(rootdir)
54         old_rootdir = rootdir
55         rootdir=os.path.join('..', rootdir)
56     
57     raise NoBugDir(dir)
58
59 class BadTreeVersion(Exception):
60     def __init__(self, version):
61         Exception.__init__(self, "Unsupported tree version: %s" % version)
62         self.version = version
63
64 def test_version(path):
65     tree_version = file(path, "rb").read()
66     if tree_version != TREE_VERSION_STRING:
67         raise BadTreeVersion(tree_version)
68
69 def set_version(path, rcs):
70     rcs.set_file_contents(os.path.join(path, "version"), TREE_VERSION_STRING)
71     
72
73 TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n"
74
75 class NoRootEntry(Exception):
76     def __init__(self, path):
77         self.path = path
78         Exception.__init__(self, "Specified root does not exist: %s" % path)
79
80 class AlreadyInitialized(Exception):
81     def __init__(self, path):
82         self.path = path
83         Exception.__init__(self, 
84                            "Specified root is already initialized: %s" % path)
85
86 def create_bug_dir(path, rcs):
87     """
88     >>> import no_rcs, tests
89     >>> create_bug_dir('/highly-unlikely-to-exist', no_rcs)
90     Traceback (most recent call last):
91     NoRootEntry: Specified root does not exist: /highly-unlikely-to-exist
92     >>> test_dir = os.path.dirname(tests.bug_arch_dir().dir)
93     >>> try:
94     ...     create_bug_dir(test_dir, no_rcs)
95     ... except AlreadyInitialized, e:
96     ...     print "Already Initialized"
97     Already Initialized
98     """
99     root = os.path.join(path, ".be")
100     try:
101         rcs.mkdir(root, paranoid=True)
102     except OSError, e:
103         if e.errno == errno.ENOENT:
104             raise NoRootEntry(path)
105         elif e.errno == errno.EEXIST:
106             raise AlreadyInitialized(path)
107         else:
108             raise
109     rcs.mkdir(os.path.join(root, "bugs"))
110     set_version(root, rcs)
111     map_save(rcs, os.path.join(root, "settings"), {"rcs_name": rcs.name})
112     return BugDir(os.path.join(path, ".be"))
113
114
115 def setting_property(name, valid=None):
116     def getter(self):
117         value = self.settings.get(name) 
118         if valid is not None:
119             if value not in valid:
120                 raise InvalidValue(name, value)
121         return value
122
123     def setter(self, value):
124         if valid is not None:
125             if value not in valid and value is not None:
126                 raise InvalidValue(name, value)
127         if value is None:
128             del self.settings[name]
129         else:
130             self.settings[name] = value
131         self.save_settings()
132     return property(getter, setter)
133
134
135 class BugDir:
136     def __init__(self, dir):
137         self.dir = dir
138         self.bugs_path = os.path.join(self.dir, "bugs")
139         try:
140             self.settings = map_load(os.path.join(self.dir, "settings"))
141         except NoSuchFile:
142             self.settings = {"rcs_name": "None"}
143
144     rcs_name = setting_property("rcs_name", ("None", "bzr", "Arch"))
145     _rcs = None
146
147     target = setting_property("target")
148
149     def save_settings(self):
150         map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings)
151
152     def get_rcs(self):
153         if self._rcs is not None and self.rcs_name == self._rcs.name:
154             return self._rcs
155         self._rcs = rcs_by_name(self.rcs_name)
156         return self._rcs
157
158     rcs = property(get_rcs)
159
160     def get_reference_bugdir(self, spec):
161         return BugDir(self.rcs.path_in_reference(self.dir, spec))
162
163     def list(self):
164         for uuid in self.list_uuids():
165             yield self.get_bug(uuid)
166
167     def bug_map(self):
168         bugs = {}
169         for bug in self.list():
170             bugs[bug.uuid] = bug
171         return bugs
172
173     def get_bug(self, uuid):
174         return Bug(self.bugs_path, uuid, self.rcs_name)
175
176     def list_uuids(self):
177         for uuid in os.listdir(self.bugs_path):
178             if (uuid.startswith('.')):
179                 continue
180             yield uuid
181
182     def new_bug(self, uuid=None):
183         if uuid is None:
184             uuid = names.uuid()
185         path = os.path.join(self.bugs_path, uuid)
186         self.rcs.mkdir(path)
187         bug = Bug(self.bugs_path, None, self.rcs_name)
188         bug.uuid = uuid
189         return bug
190
191 class InvalidValue(Exception):
192     def __init__(self, name, value):
193         msg = "Cannot assign value %s to %s" % (value, name)
194         Exception.__init__(self, msg)
195         self.name = name
196         self.value = value
197
198
199 def checked_property(name, valid):
200     def getter(self):
201         value = self.__getattribute__("_"+name)
202         if value not in valid:
203             raise InvalidValue(name, value)
204         return value
205
206     def setter(self, value):
207         if value not in valid:
208             raise InvalidValue(name, value)
209         return self.__setattr__("_"+name, value)
210     return property(getter, setter)
211
212 severity_levels = ("wishlist", "minor", "serious", "critical", "fatal")
213 active_status = ("open", "in-progress", "waiting", "new", "verified")
214 inactive_status = ("closed", "disabled", "fixed", "wontfix", "waiting")
215
216 severity_value = {}
217 for i in range(len(severity_levels)):
218     severity_value[severity_levels[i]] = i
219
220 class Bug(object):
221     status = checked_property("status", (None,)+active_status+inactive_status)
222     severity = checked_property("severity", (None, "wishlist", "minor",
223                                              "serious", "critical", "fatal"))
224
225     def __init__(self, path, uuid, rcs_name):
226         self.path = path
227         self.uuid = uuid
228         if uuid is not None:
229             dict = map_load(self.get_path("values"))
230         else:
231             dict = {}
232
233         self.rcs_name = rcs_name
234
235         self.summary = dict.get("summary")
236         self.creator = dict.get("creator")
237         self.target = dict.get("target")
238         self.status = dict.get("status")
239         self.severity = dict.get("severity")
240         self.assigned = dict.get("assigned")
241         self.time = dict.get("time")
242         if self.time is not None:
243             self.time = utility.str_to_time(self.time)
244
245     def __repr__(self):
246         return "Bug(uuid=%r)" % self.uuid
247
248     def get_path(self, file):
249         return os.path.join(self.path, self.uuid, file)
250
251     def _get_active(self):
252         return self.status in active_status
253
254     active = property(_get_active)
255
256     def add_attr(self, map, name):
257         value = self.__getattribute__(name)
258         if value is not None:
259             map[name] = value
260
261     def save(self):
262         map = {}
263         self.add_attr(map, "assigned")
264         self.add_attr(map, "summary")
265         self.add_attr(map, "creator")
266         self.add_attr(map, "target")
267         self.add_attr(map, "status")
268         self.add_attr(map, "severity")
269         if self.time is not None:
270             map["time"] = utility.time_to_str(self.time)
271         path = self.get_path("values")
272         map_save(rcs_by_name(self.rcs_name), path, map)
273
274     def _get_rcs(self):
275         return rcs_by_name(self.rcs_name)
276
277     rcs = property(_get_rcs)
278
279     def new_comment(self):
280         if not os.path.exists(self.get_path("comments")):
281             self.rcs.mkdir(self.get_path("comments"))
282         comm = Comment(None, self)
283         comm.uuid = names.uuid()
284         return comm
285
286     def get_comment(self, uuid):
287         return Comment(uuid, self)
288
289     def iter_comment_ids(self):
290         try:
291             for uuid in os.listdir(self.get_path("comments")):
292                 if (uuid.startswith('.')):
293                     continue
294                 yield uuid
295         except OSError, e:
296             if e.errno != errno.ENOENT:
297                 raise
298             return
299
300     def list_comments(self):
301         comments = [Comment(id, self) for id in self.iter_comment_ids()]
302         comments.sort(cmp_date)
303         return comments
304
305 def cmp_date(comm1, comm2):
306     return cmp(comm1.date, comm2.date)
307
308 def new_bug(dir, uuid=None):
309     bug = dir.new_bug(uuid)
310     bug.creator = names.creator()
311     bug.severity = "minor"
312     bug.status = "open"
313     bug.time = time.time()
314     return bug
315
316 def new_comment(bug, body=None):
317     comm = bug.new_comment()
318     comm.From = names.creator()
319     comm.date = time.time()
320     comm.body = body
321     return comm
322
323 def add_headers(obj, map, names):
324     map_names = {}
325     for name in names:
326         map_names[name] = pyname_to_header(name)
327     add_attrs(obj, map, names, map_names)
328
329 def add_attrs(obj, map, names, map_names=None):
330     if map_names is None:
331         map_names = {}
332         for name in names:
333             map_names[name] = name 
334         
335     for name in names:
336         value = obj.__getattribute__(name)
337         if value is not None:
338             map[map_names[name]] = value
339
340
341 class Comment(object):
342     def __init__(self, uuid, bug):
343         object.__init__(self)
344         self.uuid = uuid 
345         self.bug = bug
346         if self.uuid is not None and self.bug is not None:
347             mapfile = map_load(self.get_path("values"))
348             self.date = utility.str_to_time(mapfile["Date"])
349             self.From = mapfile["From"]
350             self.in_reply_to = mapfile.get("In-reply-to")
351             self.body = file(self.get_path("body")).read().decode("utf-8")
352         else:
353             self.date = None
354             self.From = None
355             self.in_reply_to = None
356             self.body = None
357
358     def save(self):
359         map_file = {"Date": utility.time_to_str(self.date)}
360         add_headers(self, map_file, ("From", "in_reply_to"))
361         if not os.path.exists(self.get_path(None)):
362             self.bug.rcs.mkdir(self.get_path(None))
363         map_save(self.bug.rcs, self.get_path("values"), map_file)
364         self.bug.rcs.set_file_contents(self.get_path("body"), 
365                                        self.body.encode('utf-8'))
366             
367
368     def get_path(self, name):
369         my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
370         if name is None:
371             return my_dir
372         return os.path.join(my_dir, name)
373         
374 def pyname_to_header(name):
375     return name.capitalize().replace('_', '-')
376     
377     
378 def map_save(rcs, path, map):
379     """Save the map as a mapfile to the specified path"""
380     add = not os.path.exists(path)
381     output = file(path, "wb")
382     mapfile.generate(output, map)
383     if add:
384         rcs.add_id(path)
385
386 class NoSuchFile(Exception):
387     def __init__(self, pathname):
388         Exception.__init__(self, "No such file: %s" % pathname)
389
390
391 def map_load(path):
392     try:
393         return mapfile.parse(file(path, "rb"))
394     except IOError, e:
395         if e.errno != errno.ENOENT:
396             raise e
397         raise NoSuchFile(path)
398
399
400 class MockBug:
401     def __init__(self, severity):
402         self.severity = severity
403
404 def cmp_severity(bug_1, bug_2):
405     """
406     Compare the severity levels of two bugs, with more sever bugs comparing
407     as less.
408
409     >>> cmp_severity(MockBug(None), MockBug(None))
410     0
411     >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0
412     True
413     >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0
414     True
415     >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0
416     True
417     """
418     val_1 = severity_value.get(bug_1.severity)
419     val_2 = severity_value.get(bug_2.severity)
420     return -cmp(val_1, val_2)