diff options
47 files changed, 799 insertions, 703 deletions
diff --git a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values b/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values deleted file mode 100644 index 68c357f..0000000 --- a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values +++ /dev/null @@ -1,35 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=disabled - - - - - - -summary=Oh, wait - - - - - - -time=Fri, 03 Feb 2006 21:35:52 +0000 - - - diff --git a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values b/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values deleted file mode 100644 index 33cacf2..0000000 --- a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values +++ /dev/null @@ -1,28 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=closed - - - - - - -summary= - - - diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body new file mode 100644 index 0000000..d09a4be --- /dev/null +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body @@ -0,0 +1 @@ +This seems to be taken care of. diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values new file mode 100644 index 0000000..6c7fb63 --- /dev/null +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Fri, 14 Nov 2008 05:00:43 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values index 3b96b7b..cf41641 100644 --- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values +++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values index b528771..02f718a 100644 --- a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values +++ b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values index 2971ab4..08c3ae4 100644 --- a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values +++ b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values b/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values deleted file mode 100644 index 33cacf2..0000000 --- a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values +++ /dev/null @@ -1,28 +0,0 @@ - - - -creator=abentley - - - - - - -severity=minor - - - - - - -status=closed - - - - - - -summary= - - - diff --git a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values index 5a7b54e..4d1cded 100644 --- a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values +++ b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed 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 diff --git a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values index 625495f..4622bc6 100644 --- a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values +++ b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values @@ -15,7 +15,7 @@ severity=serious -status=closed +status=fixed diff --git a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values index 93689fb..921528e 100644 --- a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values +++ b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values index ff8a30a..2fa1905 100644 --- a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values +++ b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values @@ -22,7 +22,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values index e854f0e..7e7f554 100644 --- a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values +++ b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body new file mode 100644 index 0000000..d7a57d9 --- /dev/null +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body @@ -0,0 +1 @@ +I dunno, bugs everywhere is such a great mental image... ;) diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values new file mode 100644 index 0000000..cb5a094 --- /dev/null +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 15 Nov 2008 23:56:51 +0000 + + + + + + +From=wking + + + diff --git a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values index 8024d04..39b0fd7 100644 --- a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values +++ b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values @@ -22,7 +22,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values index 87a5ca5..ef82d6f 100644 --- a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values +++ b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values @@ -8,7 +8,7 @@ severity=fatal -status=closed +status=fixed diff --git a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values index 233e336..bcf47f4 100644 --- a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values +++ b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed @@ -22,37 +22,16 @@ from libbe.bugdir import tree_root, create_bug_dir from libbe import names, plugin, cmdutil import sys import os -import becommands.severity -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 - set-root: assign the root directory for bug tracking - new: Create a new bug - list: list bugs - show: show a particular bug - close: close a bug - open: re-open a bug - severity: %s - -Unimplemented becommands - comment: append a comment to a bug -""" % becommands.severity.__desc__ - +import becommands +__doc__ == cmdutil.help() if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'): - cmdutil.print_command_list() + print cmdutil.help() else: try: try: - sys.exit(execute(sys.argv[1], sys.argv[2:])) + sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:])) except KeyError, e: raise UserError("Unknown command \"%s\"" % e.args[0]) except cmdutil.GetHelp: diff --git a/becommands/__init__.py b/becommands/__init__.py index e69de29..6b07378 100644 --- a/becommands/__init__.py +++ b/becommands/__init__.py @@ -0,0 +1,15 @@ +"Command plugins for the BugsEverywhere be script." + +__all__ = ["set_root", "set", "new", "remove", "list", "show", "close", "open", + "assign", "severity", "status", "target", "comment", "diff", + "upgrade", "help"] + +def import_all(): + for i in __all__: + name = __name__ + "." + i + try: + __import__(name, globals(), locals(), []) + except ImportError: + print "Import of %s failed!" % (name,) + +import_all() 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/close.py b/becommands/close.py index 52ab735..8e62b90 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -16,6 +16,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Close a bug""" from libbe import cmdutil +__desc__ = __doc__ + def execute(args): """ >>> from libbe import tests diff --git a/becommands/comment.py b/becommands/comment.py index e3a1d93..5939490 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -15,8 +15,11 @@ # 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 +__desc__ = __doc__ + def execute(args): """ >>> from libbe import tests, names @@ -29,7 +32,7 @@ def execute(args): u'This is a comment about a\\n' >>> comment.From == names.creator() True - >>> comment.date <= int(time.time()) + >>> comment.time <= int(time.time()) True >>> comment.in_reply_to is None True @@ -62,7 +65,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/diff.py b/becommands/diff.py index 82ebb2c..5a3a7cf 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -18,6 +18,8 @@ """Compare bug reports with older tree""" from libbe import bugdir, diff, cmdutil import os +__desc__ = __doc__ + def execute(args): options, args = get_parser().parse_args(args) if len(args) == 0: diff --git a/becommands/help.py b/becommands/help.py index aa4aa64..f6cfe3f 100644 --- a/becommands/help.py +++ b/becommands/help.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 """Print help for given subcommand""" -from libbe import bugdir, cmdutil, names, utility +from libbe import cmdutil, names, utility +__desc__ = __doc__ def execute(args): """ @@ -25,7 +26,7 @@ def execute(args): if len(args) > 1: raise cmdutil.UserError("Too many arguments.") if len(args) == 0: - cmdutil.print_command_list() + print cmdutil.help() else: try: print cmdutil.help(args[0]) diff --git a/becommands/list.py b/becommands/list.py index d745702..59eb8ad 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -15,30 +15,86 @@ # 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_full, severity_values, status_values, \ + active_status_values, inactive_status_values import os +__desc__ = __doc__ + def execute(args): options, args = get_parser().parse_args(args) if len(args) > 0: raise cmdutil.UsageError - active = True - severity = ("minor", "serious", "critical", "fatal") - if options.wishlist: - severity = ("wishlist",) - if options.closed: - active = False tree = cmdutil.bug_tree() - current_id = names.creator() + # select status + if options.status != None: + if options.status == "all": + status = status_values + else: + status = options.status.split(',') + else: + status = [] + if options.active == True: + status.extend(list(active_status_values)) + if options.unconfirmed == True: + status.append("unconfirmed") + if options.open == True: + status.append("opened") + if options.test == True: + status.append("test") + if status == []: # set the default value + status = active_status_values + # select severity + if options.severity != None: + if options.severity == "all": + severity = severity_values + else: + severity = options.severity.split(',') + else: + severity = [] + if options.wishlist == True: + severity.extend("wishlist") + if options.important == True: + serious = severity_values.index("serious") + severity.append(list(severity_values[serious:])) + if severity == []: # set the default value + severity = severity_values + # select assigned + if options.assigned != None: + if options.assigned == "all": + assigned = "all" + else: + assigned = options.assigned.split(',') + else: + assigned = [] + if options.mine == True: + assigned.extend('-') + if assigned == []: # set the default value + assigned = "all" + for i in range(len(assigned)): + if assigned[i] == '-': + assigned[i] = names.creator() + # select target + if options.target != None: + if options.target == "all": + target = "all" + else: + target = options.target.split(',') + else: + target = [] + if options.cur_target == True: + target.append(tree.target) + if target == []: # set the default value + target = "all" + def filter(bug): - if options.mine and bug.assigned != current_id: + if status != "all" and not bug.status in status: + return False + if severity != "all" and not bug.severity in severity: return False - if options.cur_target: - if tree.target is None or bug.target != tree.target: - return False - if active is not None: - if bug.active != active: - return False - if bug.severity not in severity: + if assigned != "all" and not bug.assigned in assigned: + return False + if target != "all" and not bug.target in target: return False return True @@ -47,74 +103,67 @@ def execute(args): if len(bugs) == 0: print "No matching bugs found" - my_target_bugs = [] - other_target_bugs = [] - unassigned_target_bugs = [] - my_bugs = [] - other_bugs = [] - unassigned_bugs = [] - if tree.target is not None: - for bug in bugs: - if bug.target != tree.target: - continue - if bug.assigned == current_id: - my_target_bugs.append(bug) - elif bug.assigned is None: - unassigned_target_bugs.append(bug) - else: - other_target_bugs.append(bug) - - for bug in bugs: - if tree.target is not None and bug.target == tree.target: - continue - if bug.assigned == current_id: - my_bugs.append(bug) - elif bug.assigned is None: - unassigned_bugs.append(bug) - else: - 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) + def list_bugs(cur_bugs, title=None, no_target=False): + cur_bugs.sort(cmp_full) if len(cur_bugs) > 0: - print cmdutil.underlined(title) + if title != None: + print cmdutil.underlined(title) for bug in cur_bugs: - print cmdutil.bug_summary(bug, all_bugs, no_target=no_target, - shortlist=True) + print bug.string(all_bugs, shortlist=True), - list_bugs(my_target_bugs, - "Bugs assigned to you for target %s" % tree.target, - no_target=True) - list_bugs(unassigned_target_bugs, - "Unassigned bugs for target %s" % tree.target, no_target=True) - list_bugs(other_target_bugs, - "Bugs assigned to others for target %s" % tree.target, - no_target=True) - list_bugs(my_bugs, "Bugs assigned to you") - list_bugs(unassigned_bugs, "Unassigned bugs") - list_bugs(other_bugs, "Bugs assigned to others") - + list_bugs(bugs, no_target=False) def get_parser(): parser = cmdutil.CmdOptionParser("be list [options]") - parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist", - help="List bugs with 'wishlist' severity") - parser.add_option("-c", "--closed", action="store_true", dest="closed", - help="List closed bugs") - parser.add_option("-m", "--mine", action="store_true", dest="mine", - help="List only bugs assigned to you") - parser.add_option("-t", "--cur-target", action="store_true", - dest="cur_target", - help="List only bugs for the current target") + parser.add_option("-s", "--status", metavar="STATUS", dest="status", + help="List options matching STATUS", default=None) + parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity", + help="List options matching SEVERITY", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="List options matching ASSIGNED", default=None) + parser.add_option("-t", "--target", metavar="TARGET", dest="target", + help="List options matching TARGET", default=None) + # boolean shortucts. All of these are special cases of long forms + bools = (("w", "wishlist", "List bugs with 'wishlist' severity"), + ("i", "important", "List bugs with >= 'serious' severity"), + ("A", "active", "List all active bugs"), + ("u", "unconfirmed", "List unconfirmed bugs"), + ("o", "open", "List open bugs"), + ("T", "test", "List bugs in testing"), + ("m", "mine", "List bugs assigned to you"), + ("c", "cur-target", "List bugs for the current target")) + for s in bools: + attr = s[1].replace('-','_') + short = "-%c" % s[0] + long = "--%s" % s[1] + help = s[2] + parser.add_option(short, long, action="store_true", dest=attr, help=help) return parser longhelp=""" -This command lists bugs. Options are cumulative, so that -mc will list only -closed bugs assigned to you. -""" +This command lists bugs. There are several criteria that you can +search by: + * status + * severity + * assigned (who the bug is assigned to) + * target (bugfix deadline) +Allowed values for each criterion may be given in a comma seperated +list. The special string "all" may be used with any of these options +to match all values of the criterion. + +status + %s +severity + %s +assigned + free form, with the string '-' being a shortcut for yourself. +target + free form + +In addition, there are some shortcut options that set boolean flags. +The boolean options are ignored if the matching string option is used. +""" % (','.join(status_values), + ','.join(severity_values)) def help(): return get_parser().help_str() + longhelp diff --git a/becommands/new.py b/becommands/new.py index 7bd2382..40ab3f5 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -15,7 +15,10 @@ # 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 +__desc__ = __doc__ + def execute(args): """ >>> import os, time @@ -41,11 +44,11 @@ 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()) - print "Created bug with ID %s" % cmdutil.unique_name(bug, bugs) + print "Created bug with ID %s" % names.unique_name(bug, bugs) def get_parser(): parser = cmdutil.CmdOptionParser("be new SUMMARY") diff --git a/becommands/open.py b/becommands/open.py index f7c23c1..654a1f5 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -16,6 +16,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Re-open a bug""" from libbe import cmdutil +__desc__ = __doc__ + def execute(args): """ >>> from libbe import tests diff --git a/becommands/set.py b/becommands/set.py index e359df1..8a76133 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -16,6 +16,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Change tree settings""" from libbe import cmdutil +__desc__ = __doc__ + def execute(args): """ >>> from libbe import tests diff --git a/becommands/set_root.py b/becommands/set_root.py index 2ae7e1a..cc21c31 100644 --- a/becommands/set_root.py +++ b/becommands/set_root.py @@ -16,6 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Assign the root directory for bug tracking""" from libbe import bugdir, cmdutil, rcs +__desc__ = __doc__ def execute(args): """ diff --git a/becommands/severity.py b/becommands/severity.py index af99bf7..6845875 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -15,8 +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 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 +46,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 +56,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..669a81d 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -15,8 +15,10 @@ # 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, names, utility +from libbe.bug import thread_comments import os +__desc__ = __doc__ def execute(args): options, args = get_parser().parse_args(args) @@ -24,20 +26,14 @@ def execute(args): raise cmdutil.UserError("Please specify a bug id.") bug_dir = cmdutil.bug_tree() bug = cmdutil.get_bug(args[0], bug_dir) - print cmdutil.bug_summary(bug, list(bug_dir.list())).rstrip("\n") - if bug.time is None: - time_str = "(Unknown time)" - else: - time_str = "%s (%s)" % (utility.handy_time(bug.time), - utility.time_to_str(bug.time)) - print "Created: %s" % time_str - unique_name = cmdutil.unique_name(bug, bug_dir.list()) + print bug.string().rstrip("\n") + unique_name = names.unique_name(bug, bug_dir.list()) comments = [] name_map = {} 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/template b/becommands/template deleted file mode 100644 index 3c871e6..0000000 --- a/becommands/template +++ /dev/null @@ -1,21 +0,0 @@ -"""Short description""" -from libbe import bugdir, cmdutil, names -import os -def execute(args): - options, args = get_parser().parse_args(args) - if len(args) > 0: - raise cmdutil.UsageError - - -def get_parser(): - parser = cmdutil.CmdOptionParser("be list [options]") -# parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist", -# help="List bugs with 'wishlist' severity") - return parser - -longhelp=""" -This is for the longwinded description -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/upgrade.py b/becommands/upgrade.py index 3dcb4eb..7ed3630 100644 --- a/becommands/upgrade.py +++ b/becommands/upgrade.py @@ -18,6 +18,7 @@ import os.path import errno from libbe import bugdir, rcs, cmdutil +__desc__ = __doc__ 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 = root.get_bug(uuid) new_bug.uuid = old_bug.uuid new_bug.summary = old_bug.summary new_bug.creator = old_bug.creator diff --git a/libbe/arch.py b/libbe/arch.py index 038325a..001f852 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -14,28 +14,17 @@ # 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 -from subprocess import Popen, PIPE import os import config import errno + +from rcs import invoke + client = config.get_val("arch_client") if client is None: client = "tla" config.set_val("arch_client", client) -def invoke(args): - try : - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise Exception("Command failed: %s" % strerror) - output = q.stdout.read() - error = q.stderr.read() - status = q.wait() - if status >= 0: - return status, output, error - raise Exception("Command failed: %s" % error) - def invoke_client(*args, **kwargs): cl_args = [client] diff --git a/libbe/bug.py b/libbe/bug.py new file mode 100644 index 0000000..f973cf0 --- /dev/null +++ b/libbe/bug.py @@ -0,0 +1,389 @@ +# 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 shutil +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."), + ("disabled", "?")) + + +### 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, bugdir): + 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.bugdir = bugdir + + 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 string(self, bugs=None, shortlist=False): + if bugs == None: + bugs = list(self.bugdir.list()) + short_name = names.unique_name(self, bugs) + if shortlist == False: + if self.time == None: + timestring = "" + else: + htime = utility.handy_time(self.time) + ftime = utility.time_to_str(self.time) + timestring = "%s (%s)" % (htime, ftime) + info = [("ID", self.uuid), + ("Short name", short_name), + ("Severity", self.severity), + ("Status", self.status), + ("Assigned", self.assigned), + ("Target", self.target), + ("Creator", self.creator), + ("Created", timestring)] + newinfo = [] + for k,v in info: + if v == None: + newinfo.append((k,"")) + else: + newinfo.append((k,v)) + info = newinfo + longest_key_len = max([len(k) for k,v in info]) + infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info] + return "".join(infolines) + "%s\n" % self.summary + else: + statuschar = self.status[0] + severitychar = self.severity[0] + chars = "%c%c" % (statuschar, severitychar) + return "%s:%s: %s\n" % (short_name, chars, self.summary) + def __str__(self): + return self.string(shortlist=True) + def get_path(self, file=None): + if file == None: + return os.path.join(self.path, self.uuid) + else: + 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 remove(self): + path = self.get_path() + shutil.rmtree(path) + + 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.time = 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()): + self.bug.rcs.mkdir(self.get_path()) + 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=None): + 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 427ed38..f8f45b8 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: @@ -171,7 +174,7 @@ class BugDir: return bugs def get_bug(self, uuid): - return Bug(self.bugs_path, uuid, self.rcs_name) + return Bug(self.bugs_path, uuid, self.rcs_name, self) def list_uuids(self): for uuid in os.listdir(self.bugs_path): @@ -184,262 +187,13 @@ class BugDir: uuid = names.uuid() path = os.path.join(self.bugs_path, uuid) self.rcs.mkdir(path) - bug = Bug(self.bugs_path, None, self.rcs_name) + bug = Bug(self.bugs_path, None, self.rcs_name, self) 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 = self.__getattribute__("_"+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 self.__setattr__("_"+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 = self.__getattribute__(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 = obj.__getattribute__(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 079601e..ace2d81 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -23,17 +23,6 @@ from textwrap import TextWrapper from StringIO import StringIO import utility -def unique_name(bug, bugs): - chars = 1 - for some_bug in bugs: - if bug.uuid == some_bug.uuid: - continue - while (bug.uuid[:chars] == some_bug.uuid[:chars]): - chars+=1 - if chars < 3: - chars = 3 - return bug.uuid[:chars] - class UserError(Exception): def __init__(self, msg): Exception.__init__(self, msg) @@ -65,23 +54,6 @@ def get_bug(spec, bug_dir=None): raise UserError("No bug matches %s" % spec) return matches[0] -def bug_summary(bug, bugs, no_target=False, shortlist=False): - target = bug.target - if target is None or no_target: - target = "" - else: - target = " Target: %s" % target - if bug.assigned is None: - assigned = "" - else: - assigned = " Assigned: %s" % bug.assigned - if shortlist == False: - return " ID: %s\n Severity: %s\n%s%s\n Creator: %s \n%s\n" % \ - (unique_name(bug, bugs), bug.severity, assigned, target, - bug.creator, bug.summary) - else: - return "%4s: %s\n" % (unique_name(bug, bugs), bug.summary) - def iter_commands(): for name, module in plugin.iter_plugins("becommands"): yield name.replace("_", "-"), module @@ -104,9 +76,20 @@ def execute(cmd, args): encoding = locale.getpreferredencoding() or 'ascii' return get_command(cmd).execute([a.decode(encoding) for a in args]) -def help(cmd): - return get_command(cmd).help() - +def help(cmd=None): + if cmd != None: + return get_command(cmd).help() + else: + cmdlist = [] + for name, module in iter_commands(): + cmdlist.append((name, module.__desc__)) + longest_cmd_len = max([len(name) for name,desc in cmdlist]) + ret = ["Bugs Everywhere - Distributed bug tracking\n", + "Supported commands"] + for name, desc in cmdlist: + numExtraSpaces = longest_cmd_len-len(name) + ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc)) + return "\n".join(ret) class GetHelp(Exception): pass @@ -125,7 +108,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) @@ -187,7 +170,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) @@ -215,15 +198,6 @@ def bug_tree(dir=None): except bugdir.NoBugDir, e: raise UserErrorWrap(e) -def print_command_list(): - cmdlist = [] - print """Bugs Everywhere - Distributed bug tracking - -Supported commands""" - for name, module in iter_commands(): - cmdlist.append((name, module.__doc__)) - for name, desc in cmdlist: - print "be %s\n %s" % (name, desc) def _test(): import doctest diff --git a/libbe/diff.py b/libbe/diff.py index c1dc429..7a1dbcc 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -17,6 +17,7 @@ """Compare two bug trees""" from libbe import cmdutil, bugdir from libbe.utility import time_to_str +from libbe.bug import cmp_severity def diff(old_tree, new_tree): old_bug_map = old_tree.bug_map() @@ -44,16 +45,16 @@ def diff_report(diff_data, bug_dir): (removed, modified, added) = diff_data bugs = list(bug_dir.list()) def modified_cmp(left, right): - return bugdir.cmp_severity(left[1], right[1]) + return cmp_severity(left[1], right[1]) - added.sort(bugdir.cmp_severity) - removed.sort(bugdir.cmp_severity) + added.sort(cmp_severity) + removed.sort(cmp_severity) modified.sort(modified_cmp) if len(added) > 0: print "New bug reports:" for bug in added: - print cmdutil.bug_summary(bug, bugs, no_target=True) + print bug.string(shortlist=True) if len(modified) > 0: printed = False @@ -69,7 +70,7 @@ def diff_report(diff_data, bug_dir): if len(removed) > 0: print "Removed bug reports:" for bug in removed: - print cmdutil.bug_summary(bug, bugs, no_target=True) + print bug.string(bugs, shortlist=True) def change_lines(old, new, attributes): change_list = [] @@ -101,10 +102,10 @@ def bug_changes(old, new, bugs): if len(change_strings) == 0: return None - return "%s%s\n" % (cmdutil.bug_summary(new, bugs, shortlist=True), + return "%s%s\n" % (new.string(bugs, shortlist=True), "\n".join(change_strings)) def comment_summary(comment, status): return "%8s comment from %s on %s" % (status, comment.From, - time_to_str(comment.date)) + time_to_str(comment.time)) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 6a304fd..3f09edd 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,72 +102,23 @@ 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) -def split_diff3(this, other, f): - """Split a file or string with diff3 conflicts into two files. +class NoSuchFile(Exception): + def __init__(self, pathname): + Exception.__init__(self, "No such file: %s" % pathname) - :param this: The THIS file to write. May be a utility.FileString - :param other: The OTHER file to write. May be a utility.FileString - :param f: The file or string to split. - :return: True if there were conflicts - >>> split_diff3(utility.FileString(), utility.FileString(), - ... "a\\nb\\nc\\nd\\n") - False - >>> this = utility.FileString() - >>> other = utility.FileString() - >>> split_diff3(this, other, "<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n") - True - >>> this.str - 'status=closed\\n' - >>> other.str - 'status=closedd\\n' - """ - f = utility.get_file(f) - this_active = True - other_active = True - conflicts = False - for line in f: - if line.startswith("<<<<<<<"): - conflicts = True - this_active = True - other_active = False - elif line.startswith("======="): - this_active = False - other_active = True - elif line.startswith(">>>>>>>"): - this_active = True - other_active = True - else: - if this_active: - this.write(line) - if other_active: - other.write(line) - return conflicts - -def split_diff3_str(f): - """Split a file/string with diff3 conflicts into two strings. If there - were no conflicts, one string is returned. - - >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n") - >>> len(result) - 2 - >>> result[0] != result[1] - True - >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closed\\n>>>>>>> values2\\n") - >>> len(result) - 2 - >>> result[0] == result[1] - True - >>> result = split_diff3_str("a\\nb\\nc\\nd\\n") - >>> len(result) - 1 - >>> result[0] - 'a\\nb\\nc\\nd\\n' - """ - this = utility.FileString() - other = utility.FileString() - if split_diff3(this, other, f): - return (this.str, other.str) - else: - return (this.str,) +def map_load(path): + try: + return parse(file(path, "rb")) + except IOError, e: + if e.errno != errno.ENOENT: + raise e + raise NoSuchFile(path) diff --git a/libbe/names.py b/libbe/names.py index d2e077a..c86063d 100644 --- a/libbe/names.py +++ b/libbe/names.py @@ -35,3 +35,21 @@ def creator(): return os.environ["LOGNAME"] else: return os.environ["USERNAME"] + +def unique_name(bug, bugs): + """ + Generate short names from uuids. Picks the minimum number of + characters (>=3) from the beginning of the uuid such that the + short names are unique. + + Obviously, as the number of bugs in the database grows, these + short names will cease to be unique. The complete uuid should be + used for long term reference. + """ + chars = 3 + for some_bug in bugs: + if bug.uuid == some_bug.uuid: + continue + while (bug.uuid[:chars] == some_bug.uuid[:chars]): + chars+=1 + return bug.uuid[:chars] diff --git a/libbe/plugin.py b/libbe/plugin.py index 4016ca1..9254986 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -55,6 +55,7 @@ def get_plugin(prefix, name): plugin_path = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) if plugin_path not in sys.path: sys.path.append(plugin_path) + def _test(): import doctest doctest.testmod() diff --git a/libbe/template b/libbe/template deleted file mode 100644 index 467eee4..0000000 --- a/libbe/template +++ /dev/null @@ -1,48 +0,0 @@ -"""Compare two bug trees""" -from bugdir import cmdutil - -def diff(old_tree, new_tree): - old_bug_map = old_tree.bug_map() - new_bug_map = new_tree.bug_map() - added = [] - removed = [] - modified = [] - for old_bug in old_bug_map.itervalues(): - new_bug = new_bug_map.get(bug.uuid) - if new_bug is None : - removed.append(old_bug) - else: - if old_bug != new_bug: - modified.append((old_bug, new_bug)) - for new_bug in new_bug_map.itervalues(): - if not old_bug_map.haskey(new_bug.id): - added.append(new_bug) - return (removed, modified, added) - - -def reference_diff(bugdir, spec=None): - return diff(bugdir.reference_bugdir(), bugdir) - -def diff_report(diff_data, bugdir) - (removed, modified, added) = diff_data - def modified_cmp(left, right): - return cmp_severity(left[1], right[1]) - - added.sort(bugdir.cmp_severity) - removed.sort(bugdir.cmp_severity) - modified.sort(modified_cmp) - - print "New bug reports:" - for bug in added: - cmdutil.bug_summary(bug, bugdir, no_target=True) - - print "modified bug reports:" - for old_bug, new_bug in modified: - cmdutil.bug_summary(new_bug, bugdir, no_target=True) - - print "Removed bug reports:" - for bug in removed: - cmdutil.bug_summary(bug, bugdir, no_target=True) - - - 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() diff --git a/test_usage.sh b/test_usage.sh new file mode 100755 index 0000000..e214e75 --- /dev/null +++ b/test_usage.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Run through some simple usage cases. This both tests that important +# features work, and gives an example of suggested usage to get people +# started. +# +# usage: test_usage.sh + +set -e # exit imediately on failed command +set -o pipefail # pipes fail if any stage fails +set -v # verbose, echo commands to stdout + +exec 6>&2 # save stderr to file descriptor 6 +exec 2>&1 # fd 2 now writes to stdout + +ID=`bzr whoami` +echo "I am: $ID" + +TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX` +cd $TESTDIR +bzr init +be set-root . +OUT=`be new 'having too much fun'` +echo "$OUT" +BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'` +echo "Working with bug: $BUG" +be comment $BUG "This is an argument" +be comment $BUG:1 "No it isn't" # comment on the first comment +be show $BUG # show details on a given bug +be close $BUG # set bug status to 'closed' +be comment $BUG "It's closed, but I can still comment." +be open $BUG # set bug status to 'open' +be comment $BUG "Reopend, comment again" +be status $BUG fixed # set bug status to 'fixed' +be show $BUG # show bug details & comments +be list # list all open bugs +be list --status closed # list all closed bugs +be assign $BUG # assign the bug to yourself +be list -m # see bugs assigned to you +be assign $BUG 'Joe' # assign the bug to Joe +be list --assigned Joe # list the bugs assigned to Joe +be assign $BUG none # assign the bug to noone +be remove $BUG # decide that you don't like that bug after all +cd / +rm -rf $TESTDIR + +exec 2>&6 6>&- # restore stderr and close fd 6 |