diff options
-rw-r--r-- | .be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values | 2 | ||||
-rw-r--r-- | .be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values | 2 | ||||
-rwxr-xr-x | be | 6 | ||||
-rw-r--r-- | becommands/assign.py | 2 | ||||
-rw-r--r-- | becommands/comment.py | 5 | ||||
-rw-r--r-- | becommands/help.py | 2 | ||||
-rw-r--r-- | becommands/list.py | 9 | ||||
-rw-r--r-- | becommands/new.py | 6 | ||||
-rw-r--r-- | becommands/severity.py | 23 | ||||
-rw-r--r-- | becommands/show.py | 5 | ||||
-rw-r--r-- | becommands/status.py (renamed from becommands/inprogress.py) | 53 | ||||
-rw-r--r-- | becommands/upgrade.py | 3 | ||||
-rw-r--r-- | libbe/bug.py | 343 | ||||
-rw-r--r-- | libbe/bugdir.py | 262 | ||||
-rw-r--r-- | libbe/cmdutil.py | 4 | ||||
-rw-r--r-- | libbe/mapfile.py | 24 | ||||
-rw-r--r-- | libbe/tests.py | 6 |
17 files changed, 457 insertions, 300 deletions
diff --git a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values b/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values index 68c357f..1388ccb 100644 --- a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values +++ b/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values @@ -15,7 +15,7 @@ severity=minor -status=disabled +status=closed diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values index 480386b..823e2bc 100644 --- a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values @@ -15,7 +15,7 @@ severity=minor -status=disabled +status=closed @@ -23,13 +23,13 @@ from libbe import names, plugin, cmdutil import sys import os import becommands.severity +import becommands.status import becommands.list import becommands.show import becommands.set_root import becommands.new import becommands.close import becommands.open -import becommands.inprogress __doc__ = """Bugs Everywhere - Distributed bug tracking Supported becommands @@ -40,10 +40,12 @@ Supported becommands close: close a bug open: re-open a bug severity: %s + status: %s Unimplemented becommands comment: append a comment to a bug -""" % becommands.severity.__desc__ +""" % (becommands.severity.__desc__, + becommands.status.__desc__) diff --git a/becommands/assign.py b/becommands/assign.py index 38ece52..d595c1d 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -15,7 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Assign an individual or group to fix a bug""" -from libbe import bugdir, cmdutil, names +from libbe import cmdutil, names __desc__ = __doc__ def execute(args): diff --git a/becommands/comment.py b/becommands/comment.py index e3a1d93..d214a19 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -15,7 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Add a comment to a bug""" -from libbe import bugdir, cmdutil, names, utility +from libbe import cmdutil, names, utility +from libbe.bug import new_comment import os def execute(args): """ @@ -62,7 +63,7 @@ def execute(args): if not body.endswith('\n'): body+='\n' - comment = bugdir.new_comment(bug, body) + comment = new_comment(bug, body) if parent_comment is not None: comment.in_reply_to = parent_comment.uuid comment.save() diff --git a/becommands/help.py b/becommands/help.py index aa4aa64..45d35ca 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -15,7 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Print help for given subcommand""" -from libbe import bugdir, cmdutil, names, utility +from libbe import cmdutil, names, utility def execute(args): """ diff --git a/becommands/list.py b/becommands/list.py index cf06ccd..9351840 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -15,7 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """List bugs""" -from libbe import bugdir, cmdutil, names +from libbe import cmdutil, names +from libbe.bug import cmp_severity, cmp_time import os def execute(args): options, args = get_parser().parse_args(args) @@ -75,10 +76,8 @@ def execute(args): other_bugs.append(bug) def list_bugs(cur_bugs, title, no_target=False): - def cmp_date(bug1, bug2): - return -cmp(bug1.time, bug2.time) - cur_bugs.sort(cmp_date) - cur_bugs.sort(bugdir.cmp_severity) + cur_bugs.sort(cmp_time) + cur_bugs.sort(cmp_severity) if len(cur_bugs) > 0: print cmdutil.underlined(title) for bug in cur_bugs: diff --git a/becommands/new.py b/becommands/new.py index 7bd2382..b22dd0a 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -15,7 +15,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Create a new bug""" -from libbe import bugdir, cmdutil, names, utility +from libbe import cmdutil, names, utility +from libbe.bug import new_bug + def execute(args): """ >>> import os, time @@ -41,7 +43,7 @@ def execute(args): if len(args) != 1: raise cmdutil.UserError("Please supply a summary message") dir = cmdutil.bug_tree() - bug = bugdir.new_bug(dir) + bug = new_bug(dir) bug.summary = args[0] bug.save() bugs = (dir.list()) diff --git a/becommands/severity.py b/becommands/severity.py index af99bf7..1a68c31 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -15,8 +15,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show or change a bug's severity level""" -from libbe import bugdir from libbe import cmdutil +from libbe.bug import severity_values, severity_description + __desc__ = __doc__ def execute(args): @@ -46,7 +47,7 @@ def execute(args): elif len(args) == 2: try: bug.severity = args[1] - except bugdir.InvalidValue, e: + except ValueError, e: if e.name != "severity": raise raise cmdutil.UserError ("Invalid severity level: %s" % e.value) @@ -56,19 +57,21 @@ def get_parser(): parser = cmdutil.CmdOptionParser("be severity bug-id [severity]") return parser -longhelp=""" -Show or change a bug's severity level. +longhelp=[""" +Show or change a bug's severity level. If no severity is specified, the current value is printed. If a severity level is specified, it will be assigned to the bug. Severity levels are: -wishlist: A feature that could improve usefulness, but not a bug. - minor: The standard bug level. - serious: A bug that requires workarounds. -critical: A bug that prevents some features from working at all. - fatal: A bug that makes the package unusable. -""" +"""] +longest_severity_len = max([len(s) for s in severity_values]) +for severity in severity_values : + description = severity_description[severity] + s = "%*s : %s\n" % (longest_severity_len, severity, description) + longhelp.append(s) +longhelp = ''.join(longhelp) + def help(): return get_parser().help_str() + longhelp diff --git a/becommands/show.py b/becommands/show.py index 8e83a1f..e75c1ac 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -15,7 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show a particular bug""" -from libbe import bugdir, cmdutil, utility +from libbe import cmdutil, utility +from libbe.bug import thread_comments import os def execute(args): @@ -37,7 +38,7 @@ def execute(args): for c_name, comment in cmdutil.iter_comment_name(bug, unique_name): name_map[comment.uuid] = c_name comments.append(comment) - threaded = bugdir.thread_comments(comments) + threaded = thread_comments(comments) cmdutil.print_threaded_comments(threaded, name_map) def get_parser(): diff --git a/becommands/inprogress.py b/becommands/status.py index 05da971..b57db4e 100644 --- a/becommands/inprogress.py +++ b/becommands/status.py @@ -14,35 +14,62 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -"""Bug fixing in progress""" +"""Show or change a bug's status""" from libbe import cmdutil +from libbe.bug import status_values, status_description +__desc__ = __doc__ + def execute(args): """ >>> from libbe import tests >>> import os >>> dir = tests.simple_bug_dir() >>> os.chdir(dir.dir) - >>> dir.get_bug("a").status - u'open' >>> execute(["a"]) - >>> dir.get_bug("a").status - u'in-progress' + open + >>> execute(["a", "closed"]) + >>> execute(["a"]) + closed + >>> execute(["a", "none"]) + Traceback (most recent call last): + UserError: Invalid status: none >>> tests.clean_up() """ options, args = get_parser().parse_args(args) - if len(args) !=1: - raise cmdutil.UserError("Please specify a bug id.") + assert(len(args) in (0, 1, 2)) + if len(args) == 0: + print help() + return bug = cmdutil.get_bug(args[0]) - bug.status = "in-progress" - bug.save() + if len(args) == 1: + print bug.status + elif len(args) == 2: + try: + bug.status = args[1] + except ValueError, e: + if e.name != "status": + raise + raise cmdutil.UserError ("Invalid status: %s" % e.value) + bug.save() def get_parser(): - parser = cmdutil.CmdOptionParser("be inprogress BUG-ID") + parser = cmdutil.CmdOptionParser("be status bug-id [status]") return parser -longhelp=""" -Mark a bug as 'in-progress'. -""" +longhelp=[""" +Show or change a bug's severity level. + +If no severity is specified, the current value is printed. If a severity level +is specified, it will be assigned to the bug. + +Severity levels are: +"""] +longest_status_len = max([len(s) for s in status_values]) +for status in status_values : + description = status_description[status] + s = "%*s : %s\n" % (longest_status_len, status, description) + longhelp.append(s) +longhelp = ''.join(longhelp) def help(): return get_parser().help_str() + longhelp diff --git a/becommands/upgrade.py b/becommands/upgrade.py index 3dcb4eb..8f7c3a4 100644 --- a/becommands/upgrade.py +++ b/becommands/upgrade.py @@ -18,6 +18,7 @@ import os.path import errno from libbe import bugdir, rcs, cmdutil +from libbe.bug import Bug def execute(args): options, args = get_parser().parse_args(args) @@ -25,7 +26,7 @@ def execute(args): for uuid in root.list_uuids(): old_bug = OldBug(root.bugs_path, uuid) - new_bug = bugdir.Bug(root.bugs_path, None) + new_bug = Bug(root.bugs_path, None) new_bug.uuid = old_bug.uuid new_bug.summary = old_bug.summary new_bug.creator = old_bug.creator diff --git a/libbe/bug.py b/libbe/bug.py new file mode 100644 index 0000000..46dd521 --- /dev/null +++ b/libbe/bug.py @@ -0,0 +1,343 @@ +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# <abentley@panoramicfeedback.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import os +import os.path +import errno +import names +import mapfile +import time +import utility +from rcs import rcs_by_name + + +### Define and describe valid bug categories +# Use a tuple of (category, description) tuples since we don't have +# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ + +# in order of increasing severity +severity_level_def = ( + ("wishlist","A feature that could improve usefullness, but not a bug."), + ("minor","The standard bug level."), + ("serious","A bug that requires workarounds."), + ("critical","A bug that prevents some features from working at all."), + ("fatal","A bug that makes the package unusable.")) + +# in order of increasing resolution +# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html +active_status_def = ( + ("unconfirmed","A possible bug which lacks independent existance confirmation."), + ("open","A working bug that has not been assigned to a developer."), + ("assigned","A working bug that has been assigned to a developer."), + ("test","The code has been adjusted, but the fix is still being tested.")) +inactive_status_def = ( + ("closed", "The bug is no longer relevant."), + ("fixed", "The bug should no longer occur."), + ("wontfix","It's not a bug, it's a feature.")) + + +### Convert the description tuples to more useful formats + +severity_values = tuple([val for val,description in severity_level_def]) +severity_description = dict(severity_level_def) +severity_index = {} +for i in range(len(severity_values)): + severity_index[severity_values[i]] = i + +active_status_values = tuple(val for val,description in active_status_def) +inactive_status_values = tuple(val for val,description in inactive_status_def) +status_values = active_status_values + inactive_status_values +status_description = dict(active_status_def+inactive_status_def) +status_index = {} +for i in range(len(status_values)): + status_index[status_values[i]] = i + + +def checked_property(name, valid): + """ + Provide access to an attribute name, testing for valid values. + """ + def getter(self): + value = getattr(self, "_"+name) + if value not in valid: + raise InvalidValue(name, value) + return value + + def setter(self, value): + if value not in valid: + raise InvalidValue(name, value) + return setattr(self, "_"+name, value) + return property(getter, setter) + + +class Bug(object): + severity = checked_property("severity", severity_values) + status = checked_property("status", status_values) + + def __init__(self, path, uuid, rcs_name): + self.path = path + self.uuid = uuid + if uuid is not None: + dict = mapfile.map_load(self.get_path("values")) + else: + dict = {} + + self.rcs_name = rcs_name + + self.summary = dict.get("summary") + self.creator = dict.get("creator") + self.target = dict.get("target") + self.status = dict.get("status", "open") + self.severity = dict.get("severity", "minor") + self.assigned = dict.get("assigned") + self.time = dict.get("time") + if self.time is not None: + self.time = utility.str_to_time(self.time) + + def __repr__(self): + return "Bug(uuid=%r)" % self.uuid + + def get_path(self, file): + return os.path.join(self.path, self.uuid, file) + + def _get_active(self): + return self.status in active_status_values + + active = property(_get_active) + + def add_attr(self, map, name): + value = getattr(self, name) + if value is not None: + map[name] = value + + def save(self): + map = {} + self.add_attr(map, "assigned") + self.add_attr(map, "summary") + self.add_attr(map, "creator") + self.add_attr(map, "target") + self.add_attr(map, "status") + self.add_attr(map, "severity") + if self.time is not None: + map["time"] = utility.time_to_str(self.time) + path = self.get_path("values") + mapfile.map_save(rcs_by_name(self.rcs_name), path, map) + + def _get_rcs(self): + return rcs_by_name(self.rcs_name) + + rcs = property(_get_rcs) + + def new_comment(self): + if not os.path.exists(self.get_path("comments")): + self.rcs.mkdir(self.get_path("comments")) + comm = Comment(None, self) + comm.uuid = names.uuid() + return comm + + def get_comment(self, uuid): + return Comment(uuid, self) + + def iter_comment_ids(self): + path = self.get_path("comments") + if not os.path.isdir(path): + return + try: + for uuid in os.listdir(path): + if (uuid.startswith('.')): + continue + yield uuid + except OSError, e: + if e.errno != errno.ENOENT: + raise + return + + def list_comments(self): + comments = [Comment(id, self) for id in self.iter_comment_ids()] + comments.sort(cmp_time) + return comments + +def new_bug(dir, uuid=None): + bug = dir.new_bug(uuid) + bug.creator = names.creator() + bug.severity = "minor" + bug.status = "open" + bug.time = time.time() + return bug + +def new_comment(bug, body=None): + comm = bug.new_comment() + comm.From = names.creator() + comm.date = time.time() + comm.body = body + return comm + +def add_headers(obj, map, names): + map_names = {} + for name in names: + map_names[name] = pyname_to_header(name) + add_attrs(obj, map, names, map_names) + +def add_attrs(obj, map, names, map_names=None): + if map_names is None: + map_names = {} + for name in names: + map_names[name] = name + + for name in names: + value = getattr(obj, name) + if value is not None: + map[map_names[name]] = value + + +class Comment(object): + def __init__(self, uuid, bug): + object.__init__(self) + self.uuid = uuid + self.bug = bug + if self.uuid is not None and self.bug is not None: + map = mapfile.map_load(self.get_path("values")) + self.time = utility.str_to_time(map["Date"]) + self.From = map["From"] + self.in_reply_to = map.get("In-reply-to") + self.content_type = map.get("Content-type", "text/plain") + self.body = file(self.get_path("body")).read().decode("utf-8") + else: + self.time = None + self.From = None + self.in_reply_to = None + self.content_type = "text/plain" + self.body = None + + def save(self): + map_file = {"Date": utility.time_to_str(self.time)} + add_headers(self, map_file, ("From", "in_reply_to", "content_type")) + if not os.path.exists(self.get_path(None)): + self.bug.rcs.mkdir(self.get_path(None)) + mapfile.map_save(self.bug.rcs, self.get_path("values"), map_file) + self.bug.rcs.set_file_contents(self.get_path("body"), + self.body.encode('utf-8')) + + + def get_path(self, name): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + return os.path.join(my_dir, name) + + +def thread_comments(comments): + child_map = {} + top_comments = [] + for comment in comments: + child_map[comment.uuid] = [] + for comment in comments: + if comment.in_reply_to is None or comment.in_reply_to not in child_map: + top_comments.append(comment) + continue + child_map[comment.in_reply_to].append(comment) + + def recurse_children(comment): + child_list = [] + for child in child_map[comment.uuid]: + child_list.append(recurse_children(child)) + return (comment, child_list) + return [recurse_children(c) for c in top_comments] + +def pyname_to_header(name): + return name.capitalize().replace('_', '-') + + + +class MockBug: + def __init__(self, attr, value): + setattr(self, attr, value) + +# the general rule for bug sorting is that "more important" bugs are +# less than "less important" bugs. This way sorting a list of bugs +# will put the most important bugs first in the list. When relative +# importance is unclear, the sorting follows some arbitrary convention +# (i.e. dictionary order). + +def cmp_severity(bug_1, bug_2): + """ + Compare the severity levels of two bugs, with more severe bugs comparing + as less. + + >>> attr="severity" + >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"wishlist")) == 0 + True + >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"minor")) > 0 + True + >>> cmp_severity(MockBug(attr,"critical"), MockBug(attr,"wishlist")) < 0 + True + """ + return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity]) + +def cmp_status(bug_1, bug_2): + """ + Compare the status levels of two bugs, with more 'open' bugs + comparing as less. + + >>> attr="status" + >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"open")) == 0 + True + >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"closed")) < 0 + True + >>> cmp_status(MockBug(attr,"closed"), MockBug(attr,"open")) > 0 + True + """ + val_2 = status_index[bug_2.status] + return cmp(status_index[bug_1.status], status_index[bug_2.status]) + +def cmp_attr(bug_1, bug_2, attr, invert=False): + """ + Compare a general attribute between two bugs using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="severity" + >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=False) < 0 + True + >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=True) > 0 + True + >>> cmp_attr(MockBug(attr,1), MockBug(attr,1), attr) == 0 + True + """ + if invert == True : + return -cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + else : + return cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + +# alphabetical rankings (a < z) +cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator") +cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") +# chronological rankings (newer < older) +cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) + +def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned, + cmp_time,cmp_creator)): + for comparison in cmp_list : + val = comparison(bug_1, bug_2) + if val != 0 : + return val + return 0 + +class InvalidValue(ValueError): + def __init__(self, name, value): + msg = "Cannot assign value %s to %s" % (value, name) + Exception.__init__(self, msg) + self.name = name + self.value = value diff --git a/libbe/bugdir.py b/libbe/bugdir.py index bcc163c..7570bb3 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -23,6 +23,7 @@ import mapfile import time import utility from rcs import rcs_by_name +from bug import Bug class NoBugDir(Exception): def __init__(self, path): @@ -108,7 +109,8 @@ def create_bug_dir(path, rcs): raise rcs.mkdir(os.path.join(root, "bugs")) set_version(root, rcs) - map_save(rcs, os.path.join(root, "settings"), {"rcs_name": rcs.name}) + mapfile.map_save(rcs, + os.path.join(root, "settings"), {"rcs_name": rcs.name}) return BugDir(os.path.join(path, ".be")) @@ -137,8 +139,8 @@ class BugDir: self.dir = dir self.bugs_path = os.path.join(self.dir, "bugs") try: - self.settings = map_load(os.path.join(self.dir, "settings")) - except NoSuchFile: + self.settings = mapfile.map_load(os.path.join(self.dir, "settings")) + except mapfile.NoSuchFile: self.settings = {"rcs_name": "None"} rcs_name = setting_property("rcs_name", ("None", "bzr", "git", "Arch", "hg")) @@ -147,7 +149,8 @@ class BugDir: target = setting_property("target") def save_settings(self): - map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings) + mapfile.map_save(self.rcs, + os.path.join(self.dir, "settings"), self.settings) def get_rcs(self): if self._rcs is not None and self.rcs_name == self._rcs.name: @@ -188,258 +191,9 @@ class BugDir: bug.uuid = uuid return bug -class InvalidValue(Exception): +class InvalidValue(ValueError): def __init__(self, name, value): msg = "Cannot assign value %s to %s" % (value, name) Exception.__init__(self, msg) self.name = name self.value = value - - -def checked_property(name, valid): - def getter(self): - value = getattr(self, "_"+name) - if value not in valid: - raise InvalidValue(name, value) - return value - - def setter(self, value): - if value not in valid: - raise InvalidValue(name, value) - return setattr(self, "_"+name, value) - return property(getter, setter) - -severity_levels = ("wishlist", "minor", "serious", "critical", "fatal") -active_status = ("open", "in-progress", "waiting", "new", "verified") -inactive_status = ("closed", "disabled", "fixed", "wontfix", "waiting") - -severity_value = {} -for i in range(len(severity_levels)): - severity_value[severity_levels[i]] = i - -class Bug(object): - status = checked_property("status", (None,)+active_status+inactive_status) - severity = checked_property("severity", (None, "wishlist", "minor", - "serious", "critical", "fatal")) - - def __init__(self, path, uuid, rcs_name): - self.path = path - self.uuid = uuid - if uuid is not None: - dict = map_load(self.get_path("values")) - else: - dict = {} - - self.rcs_name = rcs_name - - self.summary = dict.get("summary") - self.creator = dict.get("creator") - self.target = dict.get("target") - self.status = dict.get("status") - self.severity = dict.get("severity") - self.assigned = dict.get("assigned") - self.time = dict.get("time") - if self.time is not None: - self.time = utility.str_to_time(self.time) - - def __repr__(self): - return "Bug(uuid=%r)" % self.uuid - - def get_path(self, file): - return os.path.join(self.path, self.uuid, file) - - def _get_active(self): - return self.status in active_status - - active = property(_get_active) - - def add_attr(self, map, name): - value = getattr(self, name) - if value is not None: - map[name] = value - - def save(self): - map = {} - self.add_attr(map, "assigned") - self.add_attr(map, "summary") - self.add_attr(map, "creator") - self.add_attr(map, "target") - self.add_attr(map, "status") - self.add_attr(map, "severity") - if self.time is not None: - map["time"] = utility.time_to_str(self.time) - path = self.get_path("values") - map_save(rcs_by_name(self.rcs_name), path, map) - - def _get_rcs(self): - return rcs_by_name(self.rcs_name) - - rcs = property(_get_rcs) - - def new_comment(self): - if not os.path.exists(self.get_path("comments")): - self.rcs.mkdir(self.get_path("comments")) - comm = Comment(None, self) - comm.uuid = names.uuid() - return comm - - def get_comment(self, uuid): - return Comment(uuid, self) - - def iter_comment_ids(self): - path = self.get_path("comments") - if not os.path.isdir(path): - return - try: - for uuid in os.listdir(path): - if (uuid.startswith('.')): - continue - yield uuid - except OSError, e: - if e.errno != errno.ENOENT: - raise - return - - def list_comments(self): - comments = [Comment(id, self) for id in self.iter_comment_ids()] - comments.sort(cmp_date) - return comments - -def cmp_date(comm1, comm2): - return cmp(comm1.date, comm2.date) - -def new_bug(dir, uuid=None): - bug = dir.new_bug(uuid) - bug.creator = names.creator() - bug.severity = "minor" - bug.status = "open" - bug.time = time.time() - return bug - -def new_comment(bug, body=None): - comm = bug.new_comment() - comm.From = names.creator() - comm.date = time.time() - comm.body = body - return comm - -def add_headers(obj, map, names): - map_names = {} - for name in names: - map_names[name] = pyname_to_header(name) - add_attrs(obj, map, names, map_names) - -def add_attrs(obj, map, names, map_names=None): - if map_names is None: - map_names = {} - for name in names: - map_names[name] = name - - for name in names: - value = getattr(obj, name) - if value is not None: - map[map_names[name]] = value - - -class Comment(object): - def __init__(self, uuid, bug): - object.__init__(self) - self.uuid = uuid - self.bug = bug - if self.uuid is not None and self.bug is not None: - mapfile = map_load(self.get_path("values")) - self.date = utility.str_to_time(mapfile["Date"]) - self.From = mapfile["From"] - self.in_reply_to = mapfile.get("In-reply-to") - self.content_type = mapfile.get("Content-type", "text/plain") - self.body = file(self.get_path("body")).read().decode("utf-8") - else: - self.date = None - self.From = None - self.in_reply_to = None - self.content_type = "text/plain" - self.body = None - - def save(self): - map_file = {"Date": utility.time_to_str(self.date)} - add_headers(self, map_file, ("From", "in_reply_to", "content_type")) - if not os.path.exists(self.get_path(None)): - self.bug.rcs.mkdir(self.get_path(None)) - map_save(self.bug.rcs, self.get_path("values"), map_file) - self.bug.rcs.set_file_contents(self.get_path("body"), - self.body.encode('utf-8')) - - - def get_path(self, name): - my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) - if name is None: - return my_dir - return os.path.join(my_dir, name) - - -def thread_comments(comments): - child_map = {} - top_comments = [] - for comment in comments: - child_map[comment.uuid] = [] - for comment in comments: - if comment.in_reply_to is None or comment.in_reply_to not in child_map: - top_comments.append(comment) - continue - child_map[comment.in_reply_to].append(comment) - - def recurse_children(comment): - child_list = [] - for child in child_map[comment.uuid]: - child_list.append(recurse_children(child)) - return (comment, child_list) - return [recurse_children(c) for c in top_comments] - - -def pyname_to_header(name): - return name.capitalize().replace('_', '-') - - -def map_save(rcs, path, map): - """Save the map as a mapfile to the specified path""" - add = not os.path.exists(path) - output = file(path, "wb") - mapfile.generate(output, map) - if add: - rcs.add_id(path) - -class NoSuchFile(Exception): - def __init__(self, pathname): - Exception.__init__(self, "No such file: %s" % pathname) - - -def map_load(path): - try: - return mapfile.parse(file(path, "rb")) - except IOError, e: - if e.errno != errno.ENOENT: - raise e - raise NoSuchFile(path) - - -class MockBug: - def __init__(self, severity): - self.severity = severity - -def cmp_severity(bug_1, bug_2): - """ - Compare the severity levels of two bugs, with more sever bugs comparing - as less. - - >>> cmp_severity(MockBug(None), MockBug(None)) - 0 - >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0 - True - >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0 - True - >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0 - True - """ - val_1 = severity_value.get(bug_1.severity) - val_2 = severity_value.get(bug_2.severity) - return -cmp(val_1, val_2) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 5c9f5b1..6fb915a 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -132,7 +132,7 @@ def iter_comment_name(bug, unique_name): (This is a user-friendly id, not the comment uuid) """ def key(comment): - return comment.date + return comment.time for num, comment in enumerate(sorted(bug.list_comments(), key=key)): yield ("%s:%d" % (unique_name, num+1), comment) @@ -194,7 +194,7 @@ def print_threaded_comments(comments, name_map, indent=""): print >> s, "--------- Comment ---------" print >> s, "Name: %s" % name_map[comment.uuid] print >> s, "From: %s" % comment.From - print >> s, "Date: %s\n" % utility.time_to_str(comment.date) + print >> s, "Date: %s\n" % utility.time_to_str(comment.time) print >> s, comment.body.rstrip('\n') s.seek(0) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 6a304fd..95c3169 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -14,7 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import os.path +import errno import utility + class IllegalKey(Exception): def __init__(self, key): Exception.__init__(self, 'Illegal key "%s"' % key) @@ -99,6 +102,27 @@ def parse(f): result[name] = value return result +def map_save(rcs, path, map): + """Save the map as a mapfile to the specified path""" + add = not os.path.exists(path) + output = file(path, "wb") + generate(output, map) + if add: + rcs.add_id(path) + +class NoSuchFile(Exception): + def __init__(self, pathname): + Exception.__init__(self, "No such file: %s" % pathname) + + +def map_load(path): + try: + return parse(file(path, "rb")) + except IOError, e: + if e.errno != errno.ENOENT: + raise e + raise NoSuchFile(path) + def split_diff3(this, other, f): """Split a file or string with diff3 conflicts into two files. diff --git a/libbe/tests.py b/libbe/tests.py index a7d925d..461e6e8 100644 --- a/libbe/tests.py +++ b/libbe/tests.py @@ -18,7 +18,7 @@ import tempfile import shutil import os import os.path -from libbe import bugdir, arch +from libbe import bugdir, bug, arch cleanable = [] def clean_up(): global cleanable @@ -47,8 +47,8 @@ def bug_arch_dir(): def simple_bug_dir(): dir = bug_arch_dir() - bug_a = bugdir.new_bug(dir, "a") - bug_b = bugdir.new_bug(dir, "b") + bug_a = bug.new_bug(dir, "a") + bug_b = bug.new_bug(dir, "b") bug_b.status = "closed" bug_a.save() bug_b.save() |