1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
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.
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.
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
25 from rcs import rcs_by_name
27 class NoBugDir(Exception):
28 def __init__(self, path):
29 msg = "The directory \"%s\" has no bug directory." % path
30 Exception.__init__(self, msg)
34 def iter_parent_dirs(cur_dir):
35 cur_dir = os.path.realpath(cur_dir)
40 cur_dir = os.path.normpath(os.path.join(cur_dir, '..'))
41 if old_dir == cur_dir:
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):
50 test_version(versionfile)
51 return BugDir(os.path.join(rootdir, ".be"))
52 elif not os.path.exists(rootdir):
53 raise NoRootEntry(rootdir)
55 rootdir=os.path.join('..', rootdir)
59 class BadTreeVersion(Exception):
60 def __init__(self, version):
61 Exception.__init__(self, "Unsupported tree version: %s" % version)
62 self.version = version
64 def test_version(path):
65 tree_version = file(path, "rb").read()
66 if tree_version != TREE_VERSION_STRING:
67 raise BadTreeVersion(tree_version)
69 def set_version(path, rcs):
70 rcs.set_file_contents(os.path.join(path, "version"), TREE_VERSION_STRING)
73 TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n"
75 class NoRootEntry(Exception):
76 def __init__(self, path):
78 Exception.__init__(self, "Specified root does not exist: %s" % path)
80 class AlreadyInitialized(Exception):
81 def __init__(self, path):
83 Exception.__init__(self,
84 "Specified root is already initialized: %s" % path)
86 def create_bug_dir(path, rcs):
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)
94 ... create_bug_dir(test_dir, no_rcs)
95 ... except AlreadyInitialized, e:
96 ... print "Already Initialized"
99 root = os.path.join(path, ".be")
101 rcs.mkdir(root, paranoid=True)
103 if e.errno == errno.ENOENT:
104 raise NoRootEntry(path)
105 elif e.errno == errno.EEXIST:
106 raise AlreadyInitialized(path)
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"))
115 def setting_property(name, valid=None):
117 value = self.settings.get(name)
118 if valid is not None:
119 if value not in valid:
120 raise InvalidValue(name, value)
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)
128 del self.settings[name]
130 self.settings[name] = value
132 return property(getter, setter)
136 def __init__(self, dir):
138 self.bugs_path = os.path.join(self.dir, "bugs")
140 self.settings = map_load(os.path.join(self.dir, "settings"))
142 self.settings = {"rcs_name": "None"}
144 rcs_name = setting_property("rcs_name", ("None", "bzr", "Arch"))
147 target = setting_property("target")
149 def save_settings(self):
150 map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings)
153 if self._rcs is not None and self.rcs_name == self._rcs.name:
155 self._rcs = rcs_by_name(self.rcs_name)
158 rcs = property(get_rcs)
160 def get_reference_bugdir(self, spec):
161 return BugDir(self.rcs.path_in_reference(self.dir, spec))
164 for uuid in self.list_uuids():
165 yield self.get_bug(uuid)
169 for bug in self.list():
173 def get_bug(self, uuid):
174 return Bug(self.bugs_path, uuid, self.rcs_name)
176 def list_uuids(self):
177 for uuid in os.listdir(self.bugs_path):
178 if (uuid.startswith('.')):
182 def new_bug(self, uuid=None):
185 path = os.path.join(self.bugs_path, uuid)
187 bug = Bug(self.bugs_path, None, self.rcs_name)
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)
199 def checked_property(name, valid):
201 value = self.__getattribute__("_"+name)
202 if value not in valid:
203 raise InvalidValue(name, value)
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)
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")
217 for i in range(len(severity_levels)):
218 severity_value[severity_levels[i]] = i
221 status = checked_property("status", (None,)+active_status+inactive_status)
222 severity = checked_property("severity", (None, "wishlist", "minor",
223 "serious", "critical", "fatal"))
225 def __init__(self, path, uuid, rcs_name):
229 dict = map_load(self.get_path("values"))
233 self.rcs_name = rcs_name
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)
246 return "Bug(uuid=%r)" % self.uuid
248 def get_path(self, file):
249 return os.path.join(self.path, self.uuid, file)
251 def _get_active(self):
252 return self.status in active_status
254 active = property(_get_active)
256 def add_attr(self, map, name):
257 value = self.__getattribute__(name)
258 if value is not None:
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)
275 return rcs_by_name(self.rcs_name)
277 rcs = property(_get_rcs)
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()
286 def get_comment(self, uuid):
287 return Comment(uuid, self)
289 def iter_comment_ids(self):
291 for uuid in os.listdir(self.get_path("comments")):
292 if (uuid.startswith('.')):
296 if e.errno != errno.ENOENT:
300 def list_comments(self):
301 comments = [Comment(id, self) for id in self.iter_comment_ids()]
302 comments.sort(cmp_date)
305 def cmp_date(comm1, comm2):
306 return cmp(comm1.date, comm2.date)
308 def new_bug(dir, uuid=None):
309 bug = dir.new_bug(uuid)
310 bug.creator = names.creator()
311 bug.severity = "minor"
313 bug.time = time.time()
316 def new_comment(bug, body=None):
317 comm = bug.new_comment()
318 comm.From = names.creator()
319 comm.date = time.time()
323 def add_headers(obj, map, names):
326 map_names[name] = pyname_to_header(name)
327 add_attrs(obj, map, names, map_names)
329 def add_attrs(obj, map, names, map_names=None):
330 if map_names is None:
333 map_names[name] = name
336 value = obj.__getattribute__(name)
337 if value is not None:
338 map[map_names[name]] = value
341 class Comment(object):
342 def __init__(self, uuid, bug):
343 object.__init__(self)
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.content_type = mapfile.get("Content-type", "text/plain")
352 self.body = file(self.get_path("body")).read().decode("utf-8")
356 self.in_reply_to = None
357 self.content_type = "text/plain"
361 map_file = {"Date": utility.time_to_str(self.date)}
362 add_headers(self, map_file, ("From", "in_reply_to", "content_type"))
363 if not os.path.exists(self.get_path(None)):
364 self.bug.rcs.mkdir(self.get_path(None))
365 map_save(self.bug.rcs, self.get_path("values"), map_file)
366 self.bug.rcs.set_file_contents(self.get_path("body"),
367 self.body.encode('utf-8'))
370 def get_path(self, name):
371 my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
374 return os.path.join(my_dir, name)
377 def thread_comments(comments):
380 for comment in comments:
381 child_map[comment.uuid] = []
382 for comment in comments:
383 if comment.in_reply_to is None or comment.in_reply_to not in child_map:
384 top_comments.append(comment)
386 child_map[comment.in_reply_to].append(comment)
388 def recurse_children(comment):
390 for child in child_map[comment.uuid]:
391 child_list.append(recurse_children(child))
392 return (comment, child_list)
393 return [recurse_children(c) for c in top_comments]
396 def pyname_to_header(name):
397 return name.capitalize().replace('_', '-')
400 def map_save(rcs, path, map):
401 """Save the map as a mapfile to the specified path"""
402 add = not os.path.exists(path)
403 output = file(path, "wb")
404 mapfile.generate(output, map)
408 class NoSuchFile(Exception):
409 def __init__(self, pathname):
410 Exception.__init__(self, "No such file: %s" % pathname)
415 return mapfile.parse(file(path, "rb"))
417 if e.errno != errno.ENOENT:
419 raise NoSuchFile(path)
423 def __init__(self, severity):
424 self.severity = severity
426 def cmp_severity(bug_1, bug_2):
428 Compare the severity levels of two bugs, with more sever bugs comparing
431 >>> cmp_severity(MockBug(None), MockBug(None))
433 >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0
435 >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0
437 >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0
440 val_1 = severity_value.get(bug_1.severity)
441 val_2 = severity_value.get(bug_2.severity)
442 return -cmp(val_1, val_2)