diff options
Diffstat (limited to 'interfaces/email/interactive/becommands')
21 files changed, 3364 insertions, 0 deletions
diff --git a/interfaces/email/interactive/becommands/assign.py b/interfaces/email/interactive/becommands/assign.py new file mode 100644 index 0000000..794f028 --- /dev/null +++ b/interfaces/email/interactive/becommands/assign.py @@ -0,0 +1,87 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Assign an individual or group to fix a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bd.bug_from_shortname("a").assigned is None + True + + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned == bd.user_id + True + + >>> execute(["a", "someone"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").assigned + someone + + >>> execute(["a","none"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned is None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + assert(len(args) in (0, 1, 2)) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = bd.bug_from_shortname(args[0]) + if len(args) == 1: + bug.assigned = bd.user_id + elif len(args) == 2: + if args[1] == "none": + bug.assigned = None + else: + bug.assigned = args[1] + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be assign BUG-ID [ASSIGNEE]") + return parser + +longhelp = """ +Assign a person to fix a bug. + +By default, the bug is self-assigned. If an assignee is specified, the bug +will be assigned to that person. + +Assignees should be the person's Bugs Everywhere identity, the string that +appears in Creator fields. + +To un-assign a bug, specify "none" for the assignee. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/close.py b/interfaces/email/interactive/becommands/close.py new file mode 100644 index 0000000..0532ed2 --- /dev/null +++ b/interfaces/email/interactive/becommands/close.py @@ -0,0 +1,60 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Close a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import bugdir + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("a").status + open + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug.status = "closed" + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be close BUG-ID") + return parser + +longhelp=""" +Close the bug identified by BUG-ID. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/comment.py b/interfaces/email/interactive/becommands/comment.py new file mode 100644 index 0000000..9a614b2 --- /dev/null +++ b/interfaces/email/interactive/becommands/comment.py @@ -0,0 +1,228 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Add a comment to a bug""" +from libbe import cmdutil, bugdir, comment, editor +import os +import sys +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import time + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_shortname(bd, "a") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + <BLANKLINE> + >>> comment.author == bd.user_id + True + >>> comment.time <= int(time.time()) + True + >>> comment.in_reply_to is None + True + + >>> if 'EDITOR' in os.environ: + ... del os.environ["EDITOR"] + >>> execute(["b"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: No comment supplied, and EDITOR not specified. + + >>> os.environ["EDITOR"] = "echo 'I like cheese' > " + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_shortname(bd, "b") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + I like cheese + <BLANKLINE> + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug or comment id.") + if len(args) > 2: + raise cmdutil.UsageError("Too many arguments.") + + shortname = args[0] + if shortname.count(':') > 1: + raise cmdutil.UserError("Invalid id '%s'." % shortname) + elif shortname.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bugname = shortname.split(':')[0] + is_reply = True + else: + bugname = shortname + is_reply = False + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, bugname) + bug.load_comments(load_full=False) + if is_reply: + parent = bug.comment_root.comment_from_shortname(shortname, + bug_shortname=bugname) + else: + parent = bug.comment_root + + if len(args) == 1: # try to launch an editor for comment-body entry + try: + if parent == bug.comment_root: + parent_body = bug.summary+"\n" + else: + parent_body = parent.body + estr = "Please enter your comment above\n\n> %s\n" \ + % ("\n> ".join(parent_body.splitlines())) + body = editor.editor_string(estr) + except editor.CantFindEditor, e: + raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." + if body is None: + raise cmdutil.UserError("No comment entered.") + elif args[1] == '-': # read body from stdin + binary = not (options.content_type == None + or options.content_type.startswith("text/")) + if not binary: + body = sys.stdin.read() + if not body.endswith('\n'): + body+='\n' + else: # read-in without decoding + body = sys.__stdin__.read() + else: # body = arg[1] + body = args[1] + if not body.endswith('\n'): + body+='\n' + + if options.XML == False: + new = parent.new_reply(body=body) + if options.author != None: + new.author = options.author + if options.alt_id != None: + new.alt_id = options.alt_id + if options.content_type != None: + new.content_type = options.content_type + else: # import XML comment [list] + # read in the comments + str_body = body.encode("unicode_escape").replace(r'\n', '\n') + comment_list = ElementTree.XML(str_body) + if comment_list.tag not in ["bug", "comment-list"]: + raise comment.InvalidXML( + comment_list, "root element must be <bug> or <comment-list>") + new_comments = [] + ids = [] + for c in bug.comment_root.traverse(): + ids.append(c.uuid) + if c.alt_id != None: + ids.append(c.alt_id) + for child in comment_list.getchildren(): + if child.tag == "comment": + new = comment.Comment(bug) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + if new.alt_id in ids: + raise cmdutil.UserError( + "Clashing comment alt_id: %s" % new.alt_id) + ids.append(new.uuid) + if new.alt_id != None: + ids.append(new.alt_id) + if new.in_reply_to == None: + new.in_reply_to = parent.uuid + new_comments.append(new) + else: + print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + % (child.tag, comment_list.tag) + try: + comment.list_to_root(new_comments,bug,root=parent, # link new comments + ignore_missing_references=options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) + # Protect against programmer error causing data loss: + kids = [c.uuid for c in parent.traverse()] + for nc in new_comments: + assert nc.uuid in kids, "%s wasn't added to %s" % (nc.uuid, parent.uuid) + nc.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") + parser.add_option("-a", "--author", metavar="AUTHOR", dest="author", + help="Set the comment author", default=None) + parser.add_option("--alt-id", metavar="ID", dest="alt_id", + help="Set an alternate comment ID", default=None) + parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", + help="Set comment content-type (e.g. text/plain)", default=None) + parser.add_option("-x", "--xml", action="store_true", default=False, + dest='XML', help="Use COMMENT to specify an XML comment description rather than the comment body. The root XML element should be either <bug> or <comment-list> with one or more <comment> children. The syntax for the <comment> elements should match that generated by 'be show --xml COMMENT-ID'. Unrecognized tags are ignored. Missing tags are left at the default value. The comment UUIDs are always auto-generated, so if you set a <uuid> field, but no <alt-id> field, your <uuid> will be used as the comment's <alt-id>. An exception is raised if <alt-id> conflicts with an existing comment.") + parser.add_option("-i", "--ignore-missing-references", action="store_true", + dest="ignore_missing_references", + help="For XML import, if any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception).") + return parser + +longhelp=""" +To add a comment to a bug, use the bug ID as the argument. To reply +to another comment, specify the comment name (as shown in "be show" +output). COMMENT, if specified, should be either the text of your +comment or "-", in which case the text will be read from stdin. If +you do not specify a COMMENT, $EDITOR is used to launch an editor. If +COMMENT is unspecified and EDITOR is not set, no comment will be +created. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # fist positional argument is a bug or comment id + if len(args) >= 2: + partial = args[1].split(':')[0] # take only bugid portion + else: + partial = "" + ids = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bugs = [] + for uuid in bd.list_uuids(): + if uuid.startswith(partial): + bug = bd.bug_from_uuid(uuid) + if bug.active == True: + bugs.append(bug) + for bug in bugs: + shortname = bd.bug_shortname(bug) + ids.append(shortname) + bug.load_comments(load_full=False) + for id,comment in bug.comment_shortnames(shortname): + ids.append(id) + except bugdir.NoBugDir: + pass + raise cmdutil.GetCompletions(ids) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/commit.py b/interfaces/email/interactive/becommands/commit.py new file mode 100644 index 0000000..dc70e7e --- /dev/null +++ b/interfaces/email/interactive/becommands/commit.py @@ -0,0 +1,78 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Commit the currently pending changes to the repository""" +from libbe import cmdutil, bugdir, editor, vcs +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> full_path = "testfile" + >>> test_contents = "A test file" + >>> bd.vcs.set_file_contents(full_path, test_contents) + >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS + Committed ... + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) != 1: + raise cmdutil.UsageError("Please supply a commit message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if args[0] == '-': # read summary from stdin + assert options.body != "EDITOR", \ + "Cannot spawn and editor when the summary is using stdin." + summary = sys.stdin.readline() + else: + summary = args[0] + if options.body == None: + body = None + elif options.body == "EDITOR": + body = editor.editor_string("Please enter your commit message above") + else: + body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True) + try: + revision = bd.vcs.commit(summary, body=body, + allow_empty=options.allow_empty) + except vcs.EmptyCommit, e: + print e + return 1 + else: + print "Committed %s" % revision + +def get_parser(): + parser = cmdutil.CmdOptionParser("be commit COMMENT") + parser.add_option("-b", "--body", metavar="FILE", dest="body", + help='Provide a detailed body for the commit message. In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', default=None) + parser.add_option("-a", "--allow-empty", dest="allow_empty", + help="Allow empty commits", + default=False, action="store_true") + return parser + +longhelp=""" +Commit the current repository status. The summary specified on the +commandline is a string (only one line) that describes the commit +briefly or "-", in which case the string will be read from stdin. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/depend.py b/interfaces/email/interactive/becommands/depend.py new file mode 100644 index 0000000..f72b8ba --- /dev/null +++ b/interfaces/email/interactive/becommands/depend.py @@ -0,0 +1,339 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Add/remove bug dependencies""" +from libbe import cmdutil, bugdir, tree +import os, copy +__desc__ = __doc__ + +BLOCKS_TAG="BLOCKS:" +BLOCKED_BY_TAG="BLOCKED-BY:" + +class BrokenLink (Exception): + def __init__(self, blocked_bug, blocking_bug, blocks=True): + if blocks == True: + msg = "Missing link: %s blocks %s" \ + % (blocking_bug.uuid, blocked_bug.uuid) + else: + msg = "Missing link: %s blocked by %s" \ + % (blocked_bug.uuid, blocking_bug.uuid) + Exception.__init__(self, msg) + self.blocked_bug = blocked_bug + self.blocking_bug = blocking_bug + + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["a"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + >>> execute(["b", "a"], manipulate_encodings=False) + b blocked by: + a + b blocks: + a + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + a blocks: + b closed + >>> execute(["-r", "b", "a"], manipulate_encodings=False) + b blocks: + a + >>> execute(["-r", "a", "b"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if options.repair == True: + if len(args) > 0: + raise cmdutil.UsageError("No arguments with --repair calls.") + elif len(args) < 1: + raise cmdutil.UsageError("Please a bug id.") + elif len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + elif len(args) == 2 and options.tree_depth != None: + raise cmdutil.UsageError("Only one bug id used in tree mode.") + + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.repair == True: + good,fixed,broken = check_dependencies(bd, repair_broken_links=True) + assert len(broken) == 0, broken + if len(fixed) > 0: + print "Fixed the following links:" + print "\n".join(["%s |-- %s" % (blockee.uuid, blocker.uuid) + for blockee,blocker in fixed]) + return 0 + + bugA = cmdutil.bug_from_shortname(bd, args[0]) + + if options.tree_depth != None: + dtree = DependencyTree(bd, bugA, options.tree_depth) + if len(dtree.blocked_by_tree()) > 0: + print "%s blocked by:" % bugA.uuid + for depth,node in dtree.blocked_by_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + if len(dtree.blocks_tree()) > 0: + print "%s blocks:" % bugA.uuid + for depth,node in dtree.blocks_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + return 0 + + if len(args) == 2: + bugB = cmdutil.bug_from_shortname(bd, args[1]) + if options.remove == True: + remove_block(bugA, bugB) + else: # add the dependency + add_block(bugA, bugB) + + blocked_by = get_blocked_by(bd, bugA) + if len(blocked_by) > 0: + print "%s blocked by:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocked_by]) + else: + print '\n'.join([bug.uuid for bug in blocked_by]) + blocks = get_blocks(bd, bugA) + if len(blocks) > 0: + print "%s blocks:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) + for bug in blocks]) + else: + print '\n'.join([bug.uuid for bug in blocks]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") + parser.add_option("-r", "--remove", action="store_true", + dest="remove", default=False, + help="Remove dependency (instead of adding it)") + parser.add_option("-s", "--show-status", action="store_true", + dest="show_status", default=False, + help="Show status of blocking bugs") + parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, + type="int", dest="tree_depth", + help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") + parser.add_option("--repair", action="store_true", + dest="repair", default=False, + help="Check for and repair one-way links") + return parser + +longhelp=""" +Set a dependency with the second bug (B) blocking the first bug (A). +If bug B is not specified, just print a list of bugs blocking (A). + +To search for bugs blocked by a particular bug, try + $ be list --extra-strings BLOCKED-BY:<your-bug-uuid> + +In repair mode, add the missing direction to any one-way links. + +The "|--" symbol in the repair-mode output is inspired by the +"negative feedback" arrow common in biochemistry. See, for example + http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_blocks_string(blocked_bug): + return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) + +def _generate_blocked_by_string(blocking_bug): + return "%s%s" % (BLOCKED_BY_TAG, blocking_bug.uuid) + +def _parse_blocks_string(string): + assert string.startswith(BLOCKS_TAG) + return string[len(BLOCKS_TAG):] + +def _parse_blocked_by_string(string): + assert string.startswith(BLOCKED_BY_TAG) + return string[len(BLOCKED_BY_TAG):] + +def _add_remove_extra_string(bug, string, add): + estrs = bug.extra_strings + if add == True: + estrs.append(string) + else: # remove the string + estrs.remove(string) + bug.extra_strings = estrs # reassign to notice change + +def _get_blocks(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKS_TAG): + uuids.append(_parse_blocks_string(line)) + return uuids + +def _get_blocked_by(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKED_BY_TAG): + uuids.append(_parse_blocked_by_string(line)) + return uuids + +def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None): + if blocks == True: # add blocks link + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + else: # add blocked by link + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + +# functions exposed to other modules + +def add_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + +def remove_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=False) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=False) + +def get_blocks(bugdir, bug): + """ + Return a list of bugs that the given bug blocks. + """ + blocks = [] + for uuid in _get_blocks(bug): + blocks.append(bugdir.bug_from_uuid(uuid)) + return blocks + +def get_blocked_by(bugdir, bug): + """ + Return a list of bugs blocking the given bug blocks. + """ + blocked_by = [] + for uuid in _get_blocked_by(bug): + blocked_by.append(bugdir.bug_from_uuid(uuid)) + return blocked_by + +def check_dependencies(bugdir, repair_broken_links=False): + """ + Check that links are bi-directional for all bugs in bugdir. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_uuid("a") + >>> b = bd.bug_from_uuid("b") + >>> blocked_by_string = _generate_blocked_by_string(b) + >>> _add_remove_extra_string(a, blocked_by_string, add=True) + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False) + >>> good + [] + >>> repaired + [] + >>> broken + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> _get_blocks(b) + [] + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True) + >>> _get_blocks(b) + ['a'] + >>> good + [] + >>> repaired + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> broken + [] + """ + if bugdir.sync_with_disk == True: + bugdir.load_all_bugs() + good_links = [] + fixed_links = [] + broken_links = [] + for bug in bugdir: + for blocker in get_blocked_by(bugdir, bug): + blocks = get_blocks(bugdir, blocker) + if (bug, blocks) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocks: + if repair_broken_links == True: + _repair_one_way_link(bug, blocker, blocks=True) + fixed_links.append((bug, blocker)) + else: + broken_links.append((bug, blocker)) + else: + good_links.append((bug, blocker)) + for blockee in get_blocks(bugdir, bug): + blocked_by = get_blocked_by(bugdir, blockee) + if (blockee, bug) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocked_by: + if repair_broken_links == True: + _repair_one_way_link(blockee, bug, blocks=False) + fixed_links.append((blockee, bug)) + else: + broken_links.append((blockee, bug)) + else: + good_links.append((blockee, bug)) + return (good_links, fixed_links, broken_links) + +class DependencyTree (object): + """ + Note: should probably be DependencyDiGraph. + """ + def __init__(self, bugdir, root_bug, depth_limit=0): + self.bugdir = bugdir + self.root_bug = root_bug + self.depth_limit = depth_limit + def _build_tree(self, child_fn): + root = tree.Tree() + root.bug = self.root_bug + root.depth = 0 + stack = [root] + while len(stack) > 0: + node = stack.pop() + if self.depth_limit > 0 and node.depth == self.depth_limit: + continue + for bug in child_fn(self.bugdir, node.bug): + child = tree.Tree() + child.bug = bug + child.depth = node.depth+1 + node.append(child) + stack.append(child) + return root + def blocks_tree(self): + if not hasattr(self, "_blocks_tree"): + self._blocks_tree = self._build_tree(get_blocks) + return self._blocks_tree + def blocked_by_tree(self): + if not hasattr(self, "_blocked_by_tree"): + self._blocked_by_tree = self._build_tree(get_blocked_by) + return self._blocked_by_tree diff --git a/interfaces/email/interactive/becommands/diff.py b/interfaces/email/interactive/becommands/diff.py new file mode 100644 index 0000000..b6ac5b0 --- /dev/null +++ b/interfaces/email/interactive/becommands/diff.py @@ -0,0 +1,120 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Compare bug reports with older tree""" +from libbe import cmdutil, bugdir, diff +import os +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> original = bd.vcs.commit("Original status") + >>> bug = bd.bug_from_uuid("a") + >>> bug.status = "closed" + >>> changed = bd.vcs.commit("Closed bug a") + >>> os.chdir(bd.root) + >>> if bd.vcs.versioned == True: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + >>> if bd.vcs.versioned == True: + ... execute(["--modified", original], manipulate_encodings=False) + ... else: + ... print "a" + a + >>> if bd.vcs.versioned == False: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "This directory is not revision-controlled." + This directory is not revision-controlled. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) == 0: + revision = None + if len(args) == 1: + revision = args[0] + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if bd.vcs.versioned == False: + print "This directory is not revision-controlled." + else: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) + d = diff.Diff(old_bd, bd) + tree = d.report_tree() + + uuids = [] + if options.all == True: + options.new = options.modified = options.removed = True + if options.new == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) + if options.modified == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) + if options.removed == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) + if (options.new or options.modified or options.removed) == True: + print "\n".join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be diff [options] REVISION") + # boolean options + bools = (("n", "new", "Print UUIDS for new bugs"), + ("m", "modified", "Print UUIDS for modified bugs"), + ("r", "removed", "Print UUIDS for removed bugs"), + ("a", "all", "Print UUIDS for all changed bugs")) + 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", + default=False, dest=attr, help=help) + return parser + +longhelp=""" +Uses the VCS to compare the current tree with a previous tree, and +prints a pretty report. If REVISION is given, it is a specifier for +the particular previous tree to use. Specifiers are specific to their +VCS. + +For Arch your specifier must be a fully-qualified revision name. + +Besides the standard summary output, you can use the options to output +UUIDS for the different categories. This output can be used as the +input to 'be show' to get and understanding of the current status. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/help.py b/interfaces/email/interactive/becommands/help.py new file mode 100644 index 0000000..a8f346a --- /dev/null +++ b/interfaces/email/interactive/becommands/help.py @@ -0,0 +1,68 @@ +# Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Print help for given subcommand""" +from libbe import cmdutil, utility +__desc__ = __doc__ + +def execute(args, manipulate_encodings=False): + """ + Print help of specified command (the manipulate_encodings argument + is ignored). + + >>> execute(["help"]) + Usage: be help [COMMAND] + <BLANKLINE> + Options: + -h, --help Print a help message + --complete Print a list of available completions + <BLANKLINE> + Print help for specified command or list of all commands. + <BLANKLINE> + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + if len(args) == 0: + print cmdutil.help() + else: + try: + print cmdutil.help(args[0]) + except AttributeError: + print "No help available" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be help [COMMAND]") + return parser + +longhelp=""" +Print help for specified command or list of all commands. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + if "--complete" in args: + cmds = [command for command,module in cmdutil.iter_commands()] + raise cmdutil.GetCompletions(cmds) diff --git a/interfaces/email/interactive/becommands/html.py b/interfaces/email/interactive/becommands/html.py new file mode 100644 index 0000000..908c714 --- /dev/null +++ b/interfaces/email/interactive/becommands/html.py @@ -0,0 +1,588 @@ +# Copyright (C) 2009 Gianluca Montecchi <gian@grys.it> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Generate a static HTML dump of the current repository status""" +from libbe import cmdutil, bugdir, bug +#from html_data import * +import codecs, os, re, string, time +import xml.sax.saxutils, htmlentitydefs + +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + Creating the html output in html_export + >>> os.path.exists("./html_export") + True + >>> os.path.exists("./html_export/index.html") + True + >>> os.path.exists("./html_export/index_inactive.html") + True + >>> os.path.exists("./html_export/bugs") + True + >>> os.path.exists("./html_export/bugs/a.html") + True + >>> os.path.exists("./html_export/bugs/b.html") + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) + + if len(args) == 0: + out_dir = options.outdir + print "Creating the html output in %s"%out_dir + else: + out_dir = args[0] + if len(args) > 0: + raise cmdutil.UsageError, "Too many arguments." + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bd.load_all_bugs() + status_list = bug.status_values + severity_list = bug.severity_values + st = {} + se = {} + stime = {} + bugs_active = [] + bugs_inactive = [] + for s in status_list: + st[s] = 0 + for b in sorted(bd, reverse=True): + stime[b.uuid] = b.time + if b.active == True: + bugs_active.append(b) + else: + bugs_inactive.append(b) + st[b.status] += 1 + ordered_bug_list = sorted([(value,key) for (key,value) in stime.items()]) + ordered_bug_list_in = sorted([(value,key) for (key,value) in stime.items()]) + #open_bug_list = sorted([(value,key) for (key,value) in bugs.items()]) + + html_gen = BEHTMLGen(bd) + html_gen.create_index_file(out_dir, st, bugs_active, ordered_bug_list, "active", bd.encoding) + html_gen.create_index_file(out_dir, st, bugs_inactive, ordered_bug_list, "inactive", bd.encoding) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be open OUTPUT_DIR") + parser.add_option("-o", "--output", metavar="export_dir", dest="outdir", + help="Set the output path, default is ./html_export", default="html_export") + return parser + +longhelp=""" +Generate a set of html pages representing the current state of the bug +directory. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list + + +def escape(string): + if string == None: + return "" + chars = [] + for char in xml.sax.saxutils.escape(string): + codepoint = ord(char) + if codepoint in htmlentitydefs.codepoint2name: + char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + chars.append(char) + return "".join(chars) + +class BEHTMLGen(): + def __init__(self, bd): + self.index_value = "" + self.bd = bd + + self.css_file = """ + body { + font-family: "lucida grande", "sans serif"; + color: #333; + width: auto; + margin: auto; + } + + + div.main { + padding: 20px; + margin: auto; + padding-top: 0; + margin-top: 1em; + background-color: #fcfcfc; + } + + .comment { + padding: 20px; + margin: auto; + padding-top: 20px; + margin-top: 0; + } + + .commentF { + padding: 0px; + margin: auto; + padding-top: 0px; + paddin-bottom: 20px; + margin-top: 0; + } + + tb { + border = 1; + } + + .wishlist-row { + background-color: #B4FF9B; + width: auto; + } + + .minor-row { + background-color: #FCFF98; + width: auto; + } + + + .serious-row { + background-color: #FFB648; + width: auto; + } + + .critical-row { + background-color: #FF752A; + width: auto; + } + + .fatal-row { + background-color: #FF3300; + width: auto; + } + + .person { + font-family: courier; + } + + a, a:visited { + background: inherit; + text-decoration: none; + } + + a { + color: #003d41; + } + + a:visited { + color: #553d41; + } + + ul { + list-style-type: none; + padding: 0; + } + + p { + width: auto; + } + + .inline-status-image { + position: relative; + top: 0.2em; + } + + .dimmed { + color: #bbb; + } + + table { + border-style: 10px solid #313131; + border-spacing: 0; + width: auto; + } + + table.log { + } + + td { + border-width: 0; + border-style: none; + padding-right: 0.5em; + padding-left: 0.5em; + width: auto; + } + + .td_sel { + background-color: #afafaf; + border: 1px solid #afafaf; + font-weight:bold; + padding-right: 1em; + padding-left: 1em; + + } + + .td_nsel { + border: 0px; + padding-right: 1em; + padding-left: 1em; + } + + tr { + vertical-align: top; + width: auto; + } + + h1 { + padding: 0.5em; + background-color: #305275; + margin-top: 0; + margin-bottom: 0; + color: #fff; + margin-left: -20px; + margin-right: -20px; + } + + wid { + text-transform: uppercase; + font-size: smaller; + margin-top: 1em; + margin-left: -0.5em; + /*background: #fffbce;*/ + /*background: #628a0d;*/ + padding: 5px; + color: #305275; + } + + .attrname { + text-align: right; + font-size: smaller; + } + + .attrval { + color: #222; + } + + .issue-closed-fixed { + background-image: "green-check.png"; + } + + .issue-closed-wontfix { + background-image: "red-check.png"; + } + + .issue-closed-reorg { + background-image: "blue-check.png"; + } + + .inline-issue-link { + text-decoration: underline; + } + + img { + border: 0; + } + + + div.footer { + font-size: small; + padding-left: 20px; + padding-right: 20px; + padding-top: 5px; + padding-bottom: 5px; + margin: auto; + background: #305275; + color: #fffee7; + } + + .footer a { + color: #508d91; + } + + + .header { + font-family: "lucida grande", "sans serif"; + font-size: smaller; + background-color: #a9a9a9; + text-align: left; + + padding-right: 0.5em; + padding-left: 0.5em; + + } + + + .selected-cell { + background-color: #e9e9e2; + } + + .plain-cell { + background-color: #f9f9f9; + } + + + .logcomment { + padding-left: 4em; + font-size: smaller; + } + + .id { + font-family: courier; + } + + .table_bug { + background-color: #afafaf; + border: 2px solid #afafaf; + } + + .message { + } + + .progress-meter-done { + background-color: #03af00; + } + + .progress-meter-undone { + background-color: #ddd; + } + + .progress-meter { + } + + """ + + self.index_first = """ + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>BugsEverywhere Issue Tracker</title> + <meta http-equiv="Content-Type" content="text/html; charset=%s" /> + <link rel="stylesheet" href="style.css" type="text/css" /> + </head> + <body> + + + <div class="main"> + <h1>BugsEverywhere Bug List</h1> + <p></p> + <table> + + <tr> + <td class="%%s"><a href="index.html">Active Bugs</a></td> + <td class="%%s"><a href="index_inactive.html">Inactive Bugs</a></td> + </tr> + + </table> + <table class="table_bug"> + <tbody> + """ % self.bd.encoding + + self.bug_line =""" + <tr class="%s-row"> + <td ><a href="bugs/%s.html">%s</a></td> + <td ><a href="bugs/%s.html">%s</a></td> + <td><a href="bugs/%s.html">%s</a></td> + <td><a href="bugs/%s.html">%s</a></td> + <td><a href="bugs/%s.html">%s</a></td> + </tr> + """ + + self.detail_first = """ + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>BugsEverywhere Issue Tracker</title> + <meta http-equiv="Content-Type" content="text/html; charset=%s" /> + <link rel="stylesheet" href="../style.css" type="text/css" /> + </head> + <body> + + + <div class="main"> + <h1>BugsEverywhere Bug List</h1> + <h5><a href="%%s">Back to Index</a></h5> + <h2>Bug: _bug_id_</h2> + <table > + <tbody> + """ % self.bd.encoding + + + + self.detail_line =""" + <tr> + <td align="right">%s</td><td>%s</td> + </tr> + """ + + self.index_last = """ + </tbody> + </table> + + </div> + + <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a> on %s</div> + + </body> + </html> + """ + + self.comment_section = """ + """ + + self.begin_comment_section =""" + <tr> + <td align="right">Comments: + </td> + <td> + """ + + + self.end_comment_section =""" + </td> + </tr> + """ + + self.detail_last = """ + </tbody> + </table> + </div> + <h5><a href="%s">Back to Index</a></h5> + <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a>.</div> + </body> + </html> + """ + + + def create_index_file(self, out_dir_path, summary, bugs, ordered_bug, fileid, encoding): + try: + os.stat(out_dir_path) + except: + try: + os.mkdir(out_dir_path) + except: + raise cmdutil.UsageError, "Cannot create output directory." + try: + FO = codecs.open(out_dir_path+"/style.css", "w", encoding) + FO.write(self.css_file) + FO.close() + except: + raise cmdutil.UsageError, "Cannot create the style.css file." + + try: + os.mkdir(out_dir_path+"/bugs") + except: + pass + + try: + if fileid == "active": + FO = codecs.open(out_dir_path+"/index.html", "w", encoding) + FO.write(self.index_first%('td_sel','td_nsel')) + if fileid == "inactive": + FO = codecs.open(out_dir_path+"/index_inactive.html", "w", encoding) + FO.write(self.index_first%('td_nsel','td_sel')) + except: + raise cmdutil.UsageError, "Cannot create the index.html file." + + c = 0 + t = len(bugs) - 1 + for l in range(t, -1, -1): + line = self.bug_line%(escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].uuid[0:3]), + escape(bugs[l].uuid), escape(bugs[l].status), + escape(bugs[l].uuid), escape(bugs[l].severity), + escape(bugs[l].uuid), escape(bugs[l].summary), + escape(bugs[l].uuid), escape(bugs[l].time_string) + ) + FO.write(line) + c += 1 + self.create_detail_file(bugs[l], out_dir_path, fileid, encoding) + when = time.ctime() + FO.write(self.index_last%when) + + + def create_detail_file(self, bug, out_dir_path, fileid, encoding): + f = "%s.html"%bug.uuid + p = out_dir_path+"/bugs/"+f + try: + FD = codecs.open(p, "w", encoding) + except: + raise cmdutil.UsageError, "Cannot create the detail html file." + + detail_first_ = re.sub('_bug_id_', bug.uuid[0:3], self.detail_first) + if fileid == "active": + FD.write(detail_first_%"../index.html") + if fileid == "inactive": + FD.write(detail_first_%"../index_inactive.html") + + + + bug_ = self.bd.bug_from_shortname(bug.uuid) + bug_.load_comments(load_full=True) + + FD.write(self.detail_line%("ID : ", bug.uuid)) + FD.write(self.detail_line%("Short name : ", escape(bug.uuid[0:3]))) + FD.write(self.detail_line%("Severity : ", escape(bug.severity))) + FD.write(self.detail_line%("Status : ", escape(bug.status))) + FD.write(self.detail_line%("Assigned : ", escape(bug.assigned))) + FD.write(self.detail_line%("Target : ", escape(bug.target))) + FD.write(self.detail_line%("Reporter : ", escape(bug.reporter))) + FD.write(self.detail_line%("Creator : ", escape(bug.creator))) + FD.write(self.detail_line%("Created : ", escape(bug.time_string))) + FD.write(self.detail_line%("Summary : ", escape(bug.summary))) + FD.write("<tr><td colspan=\"2\"><hr /></td></tr>") + FD.write(self.begin_comment_section) + tr = [] + b = '' + level = 0 + stack = [] + for depth,comment in bug_.comment_root.thread(flatten=False): + while len(stack) > depth: + stack.pop(-1) # pop non-parents off the stack + FD.write("</div>\n") # close non-parent <div class="comment... + assert len(stack) == depth + stack.append(comment) + lines = ["--------- Comment ---------", + "Name: %s" % comment.uuid, + "From: %s" % escape(comment.author), + "Date: %s" % escape(comment.date), + ""] + lines.extend(escape(comment.body).splitlines()) + if depth == 0: + FD.write('<div class="commentF">') + else: + FD.write('<div class="comment">') + FD.write("<br />\n".join(lines)+"<br />\n") + while len(stack) > 0: + stack.pop(-1) + FD.write("</div>\n") # close every remaining <div class="comment... + FD.write(self.end_comment_section) + if fileid == "active": + FD.write(self.detail_last%"../index.html") + if fileid == "inactive": + FD.write(self.detail_last%"../index_inactive.html") + FD.close() + + diff --git a/interfaces/email/interactive/becommands/init.py b/interfaces/email/interactive/becommands/init.py new file mode 100644 index 0000000..1125d93 --- /dev/null +++ b/interfaces/email/interactive/becommands/init.py @@ -0,0 +1,100 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Assign the root directory for bug tracking""" +import os.path +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility, vcs + >>> import os + >>> dir = utility.Dir() + >>> try: + ... bugdir.BugDir(dir.path) + ... except bugdir.NoBugDir, e: + ... True + True + >>> execute(['--root', dir.path], manipulate_encodings=False) + No revision control detected. + Directory initialized. + >>> del(dir) + + >>> dir = utility.Dir() + >>> os.chdir(dir.path) + >>> vcs = vcs.installed_vcs() + >>> vcs.init('.') + >>> print vcs.name + Arch + >>> execute([], manipulate_encodings=False) + Using Arch for revision control. + Directory initialized. + >>> vcs.cleanup() + + >>> try: + ... execute(['--root', '.'], manipulate_encodings=False) + ... except cmdutil.UserError, e: + ... str(e).startswith("Directory already initialized: ") + True + >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) + Traceback (most recent call last): + UserError: No such directory: /highly-unlikely-to-exist + >>> os.chdir('/') + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError + try: + bd = bugdir.BugDir(options.root_dir, from_disk=False, + sink_to_existing_root=False, + assert_new_BugDir=True, + manipulate_encodings=manipulate_encodings) + except bugdir.NoRootEntry: + raise cmdutil.UserError("No such directory: %s" % options.root_dir) + except bugdir.AlreadyInitialized: + raise cmdutil.UserError("Directory already initialized: %s" % options.root_dir) + bd.save() + if bd.vcs.name is not "None": + print "Using %s for revision control." % bd.vcs.name + else: + print "No revision control detected." + print "Directory initialized." + +def get_parser(): + parser = cmdutil.CmdOptionParser("be init") + parser.add_option("-r", "--root", metavar="DIR", dest="root_dir", + help="Set root dir to something other than the current directory.", + default=".") + return parser + +longhelp=""" +This command initializes Bugs Everywhere support for the specified directory +and all its subdirectories. It will auto-detect any supported revision control +system. You can use "be set vcs_name" to change the vcs being used. + +The directory defaults to your current working directory. + +It is usually a good idea to put the Bugs Everywhere root at the source code +root, but you can put it anywhere. If you root Bugs Everywhere in a +subdirectory, then only bugs created in that subdirectory (and its children) +will appear there. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/list.py b/interfaces/email/interactive/becommands/list.py new file mode 100644 index 0000000..12e1e29 --- /dev/null +++ b/interfaces/email/interactive/becommands/list.py @@ -0,0 +1,248 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""List bugs""" +from libbe import cmdutil, bugdir, bug +import os +import re +__desc__ = __doc__ + +# get a list of * for cmp_*() comparing two bugs. +AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] +AVAILABLE_CMPS.remove("attr") # a cmp_* template. + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + a:om: Bug A + >>> execute(["--status", "all"], manipulate_encodings=False) + a:om: Bug A + b:cm: Bug B + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError("Too many arguments.") + cmp_list = [] + if options.sort_by != None: + for cmp in options.sort_by.split(','): + if cmp not in AVAILABLE_CMPS: + raise cmdutil.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) + cmp_list.append(eval('bug.cmp_%s' % cmp)) + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bd.load_all_bugs() + # select status + if options.status != None: + if options.status == "all": + status = bug.status_values + else: + status = options.status.split(',') + else: + status = [] + if options.active == True: + status.extend(list(bug.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 = bug.active_status_values + # select severity + if options.severity != None: + if options.severity == "all": + severity = bug.severity_values + else: + severity = options.severity.split(',') + else: + severity = [] + if options.wishlist == True: + severity.extend("wishlist") + if options.important == True: + serious = bug.severity_values.index("serious") + severity.append(list(bug.severity_values[serious:])) + if severity == []: # set the default value + severity = bug.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] = bd.user_id + # 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(bd.target) + if target == []: # set the default value + target = "all" + if options.extra_strings != None: + extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] + + def filter(bug): + if status != "all" and not bug.status in status: + return False + if severity != "all" and not bug.severity in severity: + return False + if assigned != "all" and not bug.assigned in assigned: + return False + if target != "all" and not bug.target in target: + return False + if options.extra_strings != None: + if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: + return False + for string in bug.extra_strings: + for regexp in extra_string_regexps: + if not regexp.match(string): + return False + return True + + bugs = [b for b in bd if filter(b) ] + if len(bugs) == 0 and options.xml == False: + print "No matching bugs found" + + def list_bugs(cur_bugs, title=None, just_uuids=False, xml=False): + if xml == True: + print '<?xml version="1.0" encoding="%s" ?>' % bd.encoding + print "<bugs>" + if len(cur_bugs) > 0: + if title != None and xml == False: + print cmdutil.underlined(title) + for bg in cur_bugs: + if xml == True: + print bg.xml(show_comments=True) + elif just_uuids: + print bg.uuid + else: + print bg.string(shortlist=True) + if xml == True: + print "</bugs>" + + # sort bugs + cmp_list.extend(bug.DEFAULT_CMP_FULL_CMP_LIST) + cmp_fn = bug.BugCompoundComparator(cmp_list=cmp_list) + bugs.sort(cmp_fn) + + # print list of bugs + list_bugs(bugs, just_uuids=options.uuids, xml=options.xml) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be list [options]") + parser.add_option("-s", "--status", metavar="STATUS", dest="status", + help="List bugs matching STATUS", default=None) + parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity", + help="List bugs matching SEVERITY", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="List bugs matching ASSIGNED", default=None) + parser.add_option("-t", "--target", metavar="TARGET", dest="target", + help="List bugs matching TARGET", default=None) + parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", + help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) + parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", + help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) + # boolean options. All but uuids and xml are special cases of long forms + bools = (("u", "uuids", "Only print the bug UUIDS"), + ("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"), + ("x", "xml", "Dump as XML")) + 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 + + +def help(): + longhelp=""" +This command lists bugs. Normally it prints a short string like + 576:om: Allow attachments +Where + 576 the bug id + o the bug status is 'open' (first letter) + m the bug severity is 'minor' (first letter) + Allo... the bug summary string + +You can optionally (-u) print only the bug ids. + +There are several criteria that you can filter 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(bug.status_values), + ','.join(bug.severity_values)) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + if option == "status": + raise cmdutil.GetCompletions(bug.status_values) + elif option == "severity": + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/interfaces/email/interactive/becommands/merge.py b/interfaces/email/interactive/becommands/merge.py new file mode 100644 index 0000000..f212b01 --- /dev/null +++ b/interfaces/email/interactive/becommands/merge.py @@ -0,0 +1,165 @@ +# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Merge duplicate bugs""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> a = bd.bug_from_shortname("a") + >>> a.comment_root.time = 0 + >>> dummy = a.new_comment("Testing") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("Testing...") + >>> dummy.time = 2 + >>> b = bd.bug_from_shortname("b") + >>> b.status = "open" + >>> b.comment_root.time = 0 + >>> dummy = b.new_comment("1 2") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("1 2 3 4") + >>> dummy.time = 2 + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + Merging bugs a and b + >>> bd._clear_bugs() + >>> a = bd.bug_from_shortname("a") + >>> a.load_comments() + >>> mergeA = a.comment_from_shortname(":3") + >>> mergeA.time = 3 + >>> print a.string(show_comments=True) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Reporter : + Creator : John Doe <jdoe@example.com> + Created : ... + Bug A + --------- Comment --------- + Name: a:1 + From: ... + Date: ... + <BLANKLINE> + Testing + --------- Comment --------- + Name: a:2 + From: ... + Date: ... + <BLANKLINE> + Testing... + --------- Comment --------- + Name: a:3 + From: ... + Date: ... + <BLANKLINE> + Merged from bug b + --------- Comment --------- + Name: a:4 + From: ... + Date: ... + <BLANKLINE> + 1 2 + --------- Comment --------- + Name: a:5 + From: ... + Date: ... + <BLANKLINE> + 1 2 3 4 + >>> b = bd.bug_from_shortname("b") + >>> b.load_comments() + >>> mergeB = b.comment_from_shortname(":3") + >>> mergeB.time = 3 + >>> print b.string(show_comments=True) # doctest: +ELLIPSIS + ID : b + Short name : b + Severity : minor + Status : closed + Assigned : + Target : + Reporter : + Creator : Jane Doe <jdoe@example.com> + Created : ... + Bug B + --------- Comment --------- + Name: b:1 + From: ... + Date: ... + <BLANKLINE> + 1 2 + --------- Comment --------- + Name: b:2 + From: ... + Date: ... + <BLANKLINE> + 1 2 3 4 + --------- Comment --------- + Name: b:3 + From: ... + Date: ... + <BLANKLINE> + Merged into bug a + >>> print b.status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if len(args) < 2: + raise cmdutil.UsageError("Please specify two bug ids.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bugA = cmdutil.bug_from_shortname(bd, args[0]) + bugA.load_comments() + bugB = cmdutil.bug_from_shortname(bd, args[1]) + bugB.load_comments() + mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) + newCommTree = copy.deepcopy(bugB.comment_root) + for comment in newCommTree.traverse(): # all descendant comments + comment.bug = bugA + comment.save() # force onto disk under bugA + for comment in newCommTree: # just the child comments + mergeA.add_reply(comment, allow_time_inversion=True) + bugB.new_comment("Merged into bug %s" % bugA.uuid) + bugB.status = "closed" + print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID") + return parser + +longhelp=""" +The second bug (B) is merged into the first (A). This adds merge +comments to both bugs, closes B, and appends B's comment tree to A's +merge comment. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/new.py b/interfaces/email/interactive/becommands/new.py new file mode 100644 index 0000000..a8ee2ec --- /dev/null +++ b/interfaces/email/interactive/becommands/new.py @@ -0,0 +1,80 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Create a new bug""" +from libbe import cmdutil, bugdir +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bug.uuid_gen = lambda: "X" + >>> execute (["this is a test",], manipulate_encodings=False) + Created bug with ID X + >>> bd._clear_bugs() + >>> bug = bd.bug_from_uuid("X") + >>> print bug.summary + this is a test + >>> bug.time <= int(time.time()) + True + >>> print bug.severity + minor + >>> bug.target == None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) != 1: + raise cmdutil.UsageError("Please supply a summary message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if args[0] == '-': # read summary from stdin + summary = sys.stdin.readline() + else: + summary = args[0] + bug = bd.new_bug(summary=summary.strip()) + if options.reporter != None: + bug.reporter = options.reporter + else: + bug.reporter = bug.creator + if options.assigned != None: + bug.assigned = options.assigned + elif bd.default_assignee != None: + bug.assigned = bd.default_assignee + print "Created bug with ID %s" % bd.bug_shortname(bug) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be new SUMMARY") + parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", + help="The user who reported the bug", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="The developer in charge of the bug", default=None) + return parser + +longhelp=""" +Create a new bug, with a new ID. The summary specified on the +commandline is a string (only one line) that describes the bug briefly +or "-", in which case the string will be read from stdin. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/open.py b/interfaces/email/interactive/becommands/open.py new file mode 100644 index 0000000..0c6bf05 --- /dev/null +++ b/interfaces/email/interactive/becommands/open.py @@ -0,0 +1,58 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Re-open a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("b").status + open + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) + if len(args) == 0: + raise cmdutil.UsageError, "Please specify a bug id." + if len(args) > 1: + raise cmdutil.UsageError, "Too many arguments." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bug.status = "open" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be open BUG-ID") + return parser + +longhelp=""" +Mark a bug as 'open'. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/remove.py b/interfaces/email/interactive/becommands/remove.py new file mode 100644 index 0000000..8d85033 --- /dev/null +++ b/interfaces/email/interactive/becommands/remove.py @@ -0,0 +1,62 @@ +# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Remove (delete) a bug and its comments""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import mapfile + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute (["b"], manipulate_encodings=False) + Removed bug b + >>> bd._clear_bugs() + >>> try: + ... bd.bug_from_shortname("b") + ... except bugdir.NoBugMatches: + ... print "Bug not found" + Bug not found + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) != 1: + raise cmdutil.UsageError, "Please specify a bug id." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + bd.remove_bug(bug) + print "Removed bug %s" % bug.uuid + +def get_parser(): + parser = cmdutil.CmdOptionParser("be remove BUG-ID") + return parser + +longhelp=""" +Remove (delete) an existing bug. Use with caution: if you're not using a +revision control system, there may be no way to recover the lost information. +You should use this command, for example, to get rid of blank or otherwise +mangled bugs. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/set.py b/interfaces/email/interactive/becommands/set.py new file mode 100644 index 0000000..f7e68d3 --- /dev/null +++ b/interfaces/email/interactive/becommands/set.py @@ -0,0 +1,130 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Change tree settings""" +import textwrap +from libbe import cmdutil, bugdir, vcs, settings_object +__desc__ = __doc__ + +def _value_string(bd, setting): + val = bd.settings.get(setting, settings_object.EMPTY) + if val == settings_object.EMPTY: + default = getattr(bd, bd._setting_name_to_attr_name(setting)) + if default not in [None, settings_object.EMPTY]: + val = "None (%s)" % default + else: + val = None + return str(val) + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["target"], manipulate_encodings=False) + None + >>> execute(["target", "tomorrow"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + tomorrow + >>> execute(["target", "none"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + None + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 2: + raise cmdutil.UsageError, "Too many arguments" + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if len(args) == 0: + keys = bd.settings_properties + keys.sort() + for key in keys: + print "%16s: %s" % (key, _value_string(bd, key)) + elif len(args) == 1: + print _value_string(bd, args[0]) + else: + if args[1] == "none": + setattr(bd, args[0], settings_object.EMPTY) + else: + if args[0] not in bd.settings_properties: + msg = "Invalid setting %s\n" % args[0] + msg += 'Allowed settings:\n ' + msg += '\n '.join(bd.settings_properties) + raise cmdutil.UserError(msg) + old_setting = bd.settings.get(args[0]) + setattr(bd, args[0], args[1]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") + return parser + +def get_bugdir_settings(): + settings = [] + for s in bugdir.BugDir.settings_properties: + settings.append(s) + settings.sort() + documented_settings = [] + for s in settings: + set = getattr(bugdir.BugDir, s) + dstr = set.__doc__.strip() + # per-setting comment adjustments + if s == "vcs_name": + lines = dstr.split('\n') + while lines[0].startswith("This property defaults to") == False: + lines.pop(0) + assert len(lines) != None, \ + "Unexpected vcs_name docstring:\n '%s'" % dstr + lines.insert( + 0, "The name of the revision control system to use.\n") + dstr = '\n'.join(lines) + doc = textwrap.wrap(dstr, width=70, initial_indent=' ', + subsequent_indent=' ') + documented_settings.append("%s\n%s" % (s, '\n'.join(doc))) + return documented_settings + +longhelp=""" +Show or change per-tree settings. + +If name and value are supplied, the name is set to a new value. +If no value is specified, the current value is printed. +If no arguments are provided, all names and values are listed. + +To unset a setting, set it to "none". + +Allowed settings are: + +%s""" % ('\n'.join(get_bugdir_settings()),) + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # first positional argument is a setting name + props = bugdir.BugDir.settings_properties + raise cmdutil.GetCompletions(props) + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/interfaces/email/interactive/becommands/severity.py b/interfaces/email/interactive/becommands/severity.py new file mode 100644 index 0000000..660586e --- /dev/null +++ b/interfaces/email/interactive/becommands/severity.py @@ -0,0 +1,103 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show or change a bug's severity level""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + minor + >>> execute(["a", "wishlist"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + wishlist + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid severity level: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + print bug.severity + elif len(args) == 2: + try: + bug.severity = args[1] + except ValueError, e: + if e.name != "severity": + raise e + raise cmdutil.UserError ("Invalid severity level: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") + return parser + +def help(): + 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: +"""] + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_severity_len = max([len(s) for s in bug.severity_values]) + for severity in bug.severity_values : + description = bug.severity_description[severity] + s = "%*s : %s\n" % (longest_severity_len, severity, description) + longhelp.append(s) + longhelp = ''.join(longhelp) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + filter = lambda bg : bg.active==True + bugs = [bg for bg in bd if filter(bg)==True] + ids = [bd.bug_shortname(bg) for bg in bugs] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a severity + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/show.py b/interfaces/email/interactive/becommands/show.py new file mode 100644 index 0000000..50bd6eb --- /dev/null +++ b/interfaces/email/interactive/becommands/show.py @@ -0,0 +1,116 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# Thomas Gerigk <tgerigk@gmx.de> +# Thomas Habets <thomas@habets.pp.se> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show a particular bug""" +import sys +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Reporter : + Creator : John Doe <jdoe@example.com> + Created : ... + Bug A + <BLANKLINE> + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS + <?xml version="1.0" encoding="..." ?> + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <creator>John Doe <jdoe@example.com></creator> + <created>...</created> + <summary>Bug A</summary> + </bug> + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={-1: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.XML: + print '<?xml version="1.0" encoding="%s" ?>' % bd.encoding + for shortname in args: + if shortname.count(':') > 1: + raise cmdutil.UserError("Invalid id '%s'." % shortname) + elif shortname.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bugname = shortname.split(':')[0] + is_comment = True + else: + bugname = shortname + is_comment = False + if is_comment == True and options.comments == False: + continue + bug = cmdutil.bug_from_shortname(bd, bugname) + if is_comment == False: + if options.XML: + print bug.xml(show_comments=options.comments) + else: + print bug.string(show_comments=options.comments) + else: + comment = bug.comment_root.comment_from_shortname( + shortname, bug_shortname=bugname) + if options.XML: + print comment.xml(shortname=shortname) + else: + if len(args) == 1 and options.only_raw_body == True: + sys.__stdout__.write(comment.body) + else: + print comment.string(shortname=shortname) + if shortname != args[-1] and options.XML == False: + print "" # add a blank line between bugs/comments + +def get_parser(): + parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") + parser.add_option("-x", "--xml", action="store_true", default=False, + dest='XML', help="Dump as XML") + parser.add_option("--only-raw-body", action="store_true", + dest='only_raw_body', + help="When printing only a single comment, just print it's body. This allows extraction of non-text content types.") + parser.add_option("-c", "--no-comments", dest="comments", + action="store_false", default=True, + help="Disable comment output. This is useful if you just want more details on a bug's current status.") + return parser + +longhelp=""" +Show all information about the bugs or comments whose IDs are given. + +It's probably not a good idea to mix bug and comment IDs in a single +call, but you're free to do so if you like. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/status.py b/interfaces/email/interactive/becommands/status.py new file mode 100644 index 0000000..f315003 --- /dev/null +++ b/interfaces/email/interactive/becommands/status.py @@ -0,0 +1,115 @@ +# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show or change a bug's status""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + open + >>> execute(["a", "closed"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + closed + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid status: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + print bug.status + else: + try: + bug.status = args[1] + except ValueError, e: + if e.name != "status": + raise + raise cmdutil.UserError ("Invalid status: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") + return parser + + +def help(): + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_status_len = max([len(s) for s in bug.status_values]) + active_statuses = [] + for status in bug.active_status_values : + description = bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + active_statuses.append(s) + inactive_statuses = [] + for status in bug.inactive_status_values : + description = bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + inactive_statuses.append(s) + longhelp=""" +Show or change a bug's status. + +If no status is specified, the current value is printed. If a status +is specified, it will be assigned to the bug. + +There are two classes of statuses, active and inactive, which are only +important for commands like "be list" that show only active bugs by +default. + +Active status levels are: + %s +Inactive status levels are: + %s + +You can overide the list of allowed statuses on a per-repository basis. +See "be set --help" for more details. +""" % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + ids = [bd.bug_shortname(bg) for bg in bd] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a status + raise cmdutil.GetCompletions(bug.status_values) + raise cmdutil.GetCompletions() diff --git a/interfaces/email/interactive/becommands/subscribe.py b/interfaces/email/interactive/becommands/subscribe.py new file mode 100644 index 0000000..0a23057 --- /dev/null +++ b/interfaces/email/interactive/becommands/subscribe.py @@ -0,0 +1,390 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""(Un)subscribe to change notification""" +from libbe import cmdutil, bugdir, tree +import os, copy +__desc__ = __doc__ + +TAG="SUBSCRIBE:" + +class SubscriptionType (tree.Tree): + """ + Trees of subscription types to allow users to select exactly what + notifications they want to subscribe to. + """ + def __init__(self, type_name, *args, **kwargs): + tree.Tree.__init__(self, *args, **kwargs) + self.type = type_name + def __str__(self): + return self.type + def __repr__(self): + return "<SubscriptionType: %s>" % str(self) + def string_tree(self, indent=0): + lines = [] + for depth,node in self.thread(): + lines.append("%s%s" % (" "*(indent+2*depth), node)) + return "\n".join(lines) + +BUGDIR_TYPE_NEW = SubscriptionType("new") +BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) + +# same name as BUGDIR_TYPE_ALL for consistency +BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) + +INVALID_TYPE = SubscriptionType("INVALID") + +class InvalidType (ValueError): + def __init__(self, type_name, type_root): + msg = "Invalid type %s for tree:\n%s" \ + % (type_name, type_root.string_tree(4)) + ValueError.__init__(self, msg) + self.type_name = type_name + self.type_root = type_root + + +def execute(args, manipulate_encodings=True): + """ + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["-s","John Doe <j@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe <j@doe.com> all * + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*'] + >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe <J@doe.com> all a.com,b.net + John Doe <j@doe.com> all * + >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe <J@doe.com> all a.com,a.edu,b.net + John Doe <j@doe.com> all * + >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe <J@doe.com> all a.edu,b.net + John Doe <j@doe.com> all * + >>> execute(["-s","Jane Doe <J@doe.com>", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe <J@doe.com> all * + John Doe <j@doe.com> all * + >>> execute(["-u", "-s","Jane Doe <J@doe.com>", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe <j@doe.com> all * + >>> execute(["-u", "-s","John Doe <j@doe.com>", "a"], manipulate_encodings=False) + >>> execute(["-s","Jane Doe <J@doe.com>", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe <J@doe.com> new * + >>> execute(["-s","Jane Doe <J@doe.com>", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe <J@doe.com> all * + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) > 1: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + + subscriber = options.subscriber + if subscriber == None: + subscriber = bd.user_id + if options.unsubscribe == True: + if options.servers == None: + options.servers = "INVALID" + if options.types == None: + options.types = "INVALID" + else: + if options.servers == None: + options.servers = "*" + if options.types == None: + options.types = "all" + servers = options.servers.split(",") + types = options.types.split(",") + + if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions + type_root = BUGDIR_TYPE_ALL + entity = bd + entity_name = "bug directory" + else: # bug-specific subscriptions + type_root = BUG_TYPE_ALL + bug = bd.bug_from_shortname(args[0]) + entity = bug + entity_name = bug.uuid + if options.list_all == True: + entity_name = "anything in the bug directory" + + types = [type_from_name(name, type_root, default=INVALID_TYPE, + default_ok=options.unsubscribe) + for name in types] + estrs = entity.extra_strings + if options.list == True or options.list_all == True: + pass + else: # alter subscriptions + if options.unsubscribe == True: + estrs = unsubscribe(estrs, subscriber, types, servers, type_root) + else: # add the tag + estrs = subscribe(estrs, subscriber, types, servers, type_root) + entity.extra_strings = estrs # reassign to notice change + + if options.list_all == True: + bd.load_all_bugs() + subscriptions = get_bugdir_subscribers(bd, servers[0]) + else: + subscriptions = [] + for estr in entity.extra_strings: + if estr.startswith(TAG): + subscriptions.append(estr[len(TAG):]) + + if len(subscriptions) > 0: + print "Subscriptions for %s:" % entity_name + print '\n'.join(subscriptions) + + +def get_parser(): + parser = cmdutil.CmdOptionParser("be subscribe ID") + parser.add_option("-u", "--unsubscribe", action="store_true", + dest="unsubscribe", default=False, + help="Unsubscribe instead of subscribing.") + parser.add_option("-a", "--list-all", action="store_true", + dest="list_all", default=False, + help="List all subscribers (no ID argument, read only action).") + parser.add_option("-l", "--list", action="store_true", + dest="list", default=False, + help="List subscribers (read only action).") + parser.add_option("-s", "--subscriber", dest="subscriber", + metavar="SUBSCRIBER", + help="Email address of the subscriber (defaults to bugdir.user_id).") + parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", + help="Servers from which you want notification.") + parser.add_option("-t", "--type", dest="types", metavar="TYPES", + help="Types of changes you wish to be notified about.") + return parser + +longhelp=""" +ID can be either a bug id, or blank/"DIR", in which case it refers to the +whole bug directory. + +SERVERS specifies the servers from which you would like to receive +notification. Multiple severs may be specified in a comma-separated +list, or you can use "*" to match all servers (the default). If you +have not selected a server, it should politely refrain from notifying +you of changes, although there is no way to guarantee this behavior. + +Available TYPES: + For bugs: +%s + For DIR : +%s + +For unsubscription, any listed SERVERS and TYPES are removed from your +subscription. Either the catch-all server "*" or type "%s" will +remove SUBSCRIBER entirely from the specified ID. + +This command is intended for use primarily by public interfaces, since +if you're just hacking away on your private repository, you'll known +what's changed ;). This command just (un)sets the appropriate +subscriptions, and leaves it up to each interface to perform the +notification. +""" % (BUG_TYPE_ALL.string_tree(6), BUGDIR_TYPE_ALL.string_tree(6), + BUGDIR_TYPE_ALL) + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_string(subscriber, types, servers): + types = sorted([str(t) for t in types]) + servers = sorted(servers) + return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers)) + +def _parse_string(string, type_root): + assert string.startswith(TAG), string + string = string[len(TAG):] + subscriber,types,servers = string.split("\t") + types = [type_from_name(name, type_root) for name in types.split(",")] + return (subscriber,types,servers.split(",")) + +def _get_subscriber(extra_strings, subscriber, type_root): + for i,string in enumerate(extra_strings): + if string.startswith(TAG): + s,ts,srvs = _parse_string(string, type_root) + if s == subscriber: + return i,s,ts,srvs # match! + return None # no match + +# functions exposed to other modules + +def type_from_name(name, type_root, default=None, default_ok=False): + if name == str(type_root): + return type_root + for t in type_root.traverse(): + if name == str(t): + return t + if default_ok: + return default + raise InvalidType(name, type_root) + +def subscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + extra_strings.append(_generate_string(subscriber, types, servers)) + return extra_strings + # Alter matched string + i,s,ts,srvs = args + for t in types: + if t not in ts: + ts.append(t) + # remove descendant types + all_ts = copy.copy(ts) + for t in all_ts: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = ["*"] + else: + srvs = list(set(servers+srvs)) + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def unsubscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + return extra_strings # pass + # Remove matched string + i,s,ts,srvs = args + all_ts = copy.copy(ts) + for t in types: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = [] + else: + for srv in servers: + if srv in srvs: + srvs.remove(srv) + if len(ts) == 0 or len(srvs) == 0: + extra_strings.pop(i) + else: + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def get_subscribers(extra_strings, type, server, type_root, + match_ancestor_types=False, + match_descendant_types=False): + """ + Set match_ancestor_types=True if you want to find eveyone who + cares about your particular type. + + Set match_descendant_types=True if you want to find subscribers + who may only care about some subset of your type. This is useful + for generating lists of all the subscribers in a given set of + extra_strings. + + >>> def sgs(*args, **kwargs): + ... return sorted(get_subscribers(*args, **kwargs)) + >>> es = [] + >>> es = subscribe(es, "John Doe <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL) + ['John Doe <j@doe.com>'] + >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>'] + >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True) + ['Jane Doe <J@doe.com>'] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL) + ['Jane Doe <J@doe.com>'] + >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True) + ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>'] + """ + for string in extra_strings: + if not string.startswith(TAG): + continue + subscriber,types,servers = _parse_string(string, type_root) + type_match = False + if type in types: + type_match = True + if type_match == False and match_ancestor_types == True: + for t in types: + if t.has_descendant(type): + type_match = True + break + if type_match == False and match_descendant_types == True: + for t in types: + if type.has_descendant(t): + type_match = True + break + server_match = False + if server in servers or servers == ["*"] or server == "*": + server_match = True + if type_match == True and server_match == True: + yield subscriber + +def get_bugdir_subscribers(bugdir, server): + """ + I have a bugdir. Who cares about it, and what do they care about? + Returns a dict of dicts: + subscribers[user][id] = types + where id is either a bug.uuid (in the case of a bug subscription) + or "DIR" (in the case of a bugdir subscription). + + Only checks bugs that are currently in memory, so you might want + to call bugdir.load_all_bugs() first. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_shortname("a") + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL) + >>> subscribers = get_bugdir_subscribers(bd, "a.com") + >>> subscribers["Jane Doe <J@doe.com>"]["DIR"] + [<SubscriptionType: new>] + >>> subscribers["John Doe <j@doe.com>"]["DIR"] + [<SubscriptionType: all>] + >>> subscribers["John Doe <j@doe.com>"]["a"] + [<SubscriptionType: all>] + >>> get_bugdir_subscribers(bd, "b.net") + {'Jane Doe <J@doe.com>': {'DIR': [<SubscriptionType: new>]}} + >>> bd.cleanup() + """ + subscribers = {} + for sub in get_subscribers(bugdir.extra_strings, BUGDIR_TYPE_ALL, server, + BUGDIR_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings,sub,BUGDIR_TYPE_ALL) + subscribers[sub] = {"DIR":ts} + for bug in bugdir: + for sub in get_subscribers(bug.extra_strings, BUG_TYPE_ALL, server, + BUG_TYPE_ALL, match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings,sub,BUG_TYPE_ALL) + if sub in subscribers: + subscribers[sub][bug.uuid] = ts + else: + subscribers[sub] = {bug.uuid:ts} + return subscribers diff --git a/interfaces/email/interactive/becommands/tag.py b/interfaces/email/interactive/becommands/tag.py new file mode 100644 index 0000000..ecd853f --- /dev/null +++ b/interfaces/email/interactive/becommands/tag.py @@ -0,0 +1,134 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Tag a bug, or search bugs for tags""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "GUI"], manipulate_encodings=False) + Tags for a: + GUI + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:GUI'] + >>> execute(["a", "later"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["a"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["--list"], manipulate_encodings=False) + GUI + later + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + GUI + later + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:Alphabetically first', 'TAG:GUI', 'TAG:later'] + >>> a.extra_strings = [] + >>> print a.extra_strings + [] + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) == 0 and options.list == False: + raise cmdutil.UsageError("Please specify a bug id.") + elif len(args) > 2 or (len(args) > 0 and options.list == True): + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.list: + bd.load_all_bugs() + tags = [] + for bug in bd: + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tag = estr[4:] + if tag not in tags: + tags.append(tag) + tags.sort() + if len(tags) > 0: + print '\n'.join(tags) + return + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 2: + given_tag = args[1] + estrs = bug.extra_strings + tag_string = "TAG:%s" % given_tag + if options.remove == True: + estrs.remove(tag_string) + else: # add the tag + estrs.append(tag_string) + bug.extra_strings = estrs # reassign to notice change + + tags = [] + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tags.append(estr[4:]) + + if len(tags) > 0: + print "Tags for %s:" % bug.uuid + print '\n'.join(tags) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be tag BUG-ID [TAG]\nor: be tag --list") + parser.add_option("-r", "--remove", action="store_true", dest="remove", + help="Remove TAG (instead of adding it)") + parser.add_option("-l", "--list", action="store_true", dest="list", + help="List all available tags and exit") + return parser + +longhelp=""" +If TAG is given, add TAG to BUG-ID. If it is not specified, just +print the tags for BUG-ID. + +To search for bugs with a particular tag, try + $ be list --extra-strings TAG:<your-tag> +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/interfaces/email/interactive/becommands/target.py b/interfaces/email/interactive/becommands/target.py new file mode 100644 index 0000000..7e41451 --- /dev/null +++ b/interfaces/email/interactive/becommands/target.py @@ -0,0 +1,95 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> +# Marien Zwart <marienz@gentoo.org> +# Thomas Gerigk <tgerigk@gmx.de> +# W. Trevor King <wking@drexel.edu> +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show or change a bug's target for fixing""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> execute(["a", "tomorrow"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + tomorrow + >>> execute(["--list"], manipulate_encodings=False) + tomorrow + >>> execute(["a", "none"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) not in (1, 2): + if not (options.list == True and len(args) == 0): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.list: + ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()]) + for target in sorted(ts): + if target and isinstance(target,str): + print target + return + bug = cmdutil.bug_from_shortname(bd, args[0]) + if len(args) == 1: + if bug.target is None: + print "No target assigned." + else: + print bug.target + else: + assert len(args) == 2 + if args[1] == "none": + bug.target = None + else: + bug.target = args[1] + +def get_parser(): + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --list") + parser.add_option("-l", "--list", action="store_true", dest="list", + help="List all available targets and exit") + return parser + +longhelp=""" +Show or change a bug's target for fixing. + +If no target is specified, the current value is printed. If a target +is specified, it will be assigned to the bug. + +Targets are freeform; any text may be specified. They will generally be +milestone names or release numbers. + +The value "none" can be used to unset the target. + +In the alternative `be target --list` form print a list of all +currently specified targets. Note that bug status +(i.e. opened/closed) is ignored. If you want to list all bugs +matching a current target, see `be list --target TARGET'. +""" + +def help(): + return get_parser().help_str() + longhelp |