diff options
author | W. Trevor King <wking@drexel.edu> | 2009-12-07 20:07:55 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2009-12-07 20:07:55 -0500 |
commit | 49a7771336ce09f6d42c7699ef32aecea0e83182 (patch) | |
tree | f237c7413fb68e72b1d87b0ccc4c788944168f10 /libbe/command | |
parent | c3bcafe12034d35f5c46f76a7dab97ab08b84dfd (diff) | |
download | bugseverywhere-49a7771336ce09f6d42c7699ef32aecea0e83182.tar.gz |
Initial directory restructuring to clarify dependencies
Diffstat (limited to 'libbe/command')
-rw-r--r-- | libbe/command/__init__.py | 16 | ||||
-rw-r--r-- | libbe/command/assign.py | 90 | ||||
-rw-r--r-- | libbe/command/close.py | 63 | ||||
-rw-r--r-- | libbe/command/comment.py | 164 | ||||
-rw-r--r-- | libbe/command/commit.py | 82 | ||||
-rw-r--r-- | libbe/command/depend.py | 371 | ||||
-rw-r--r-- | libbe/command/diff.py | 133 | ||||
-rw-r--r-- | libbe/command/due.py | 108 | ||||
-rw-r--r-- | libbe/command/email_bugs.py | 239 | ||||
-rw-r--r-- | libbe/command/help.py | 70 | ||||
-rw-r--r-- | libbe/command/html.py | 609 | ||||
-rw-r--r-- | libbe/command/import_xml.py | 434 | ||||
-rw-r--r-- | libbe/command/init.py | 104 | ||||
-rw-r--r-- | libbe/command/list.py | 240 | ||||
-rw-r--r-- | libbe/command/merge.py | 166 | ||||
-rw-r--r-- | libbe/command/new.py | 83 | ||||
-rw-r--r-- | libbe/command/open.py | 61 | ||||
-rw-r--r-- | libbe/command/remove.py | 65 | ||||
-rw-r--r-- | libbe/command/set.py | 132 | ||||
-rw-r--r-- | libbe/command/severity.py | 106 | ||||
-rw-r--r-- | libbe/command/show.py | 183 | ||||
-rw-r--r-- | libbe/command/status.py | 118 | ||||
-rw-r--r-- | libbe/command/subscribe.py | 360 | ||||
-rw-r--r-- | libbe/command/tag.py | 137 | ||||
-rw-r--r-- | libbe/command/target.py | 168 |
25 files changed, 4302 insertions, 0 deletions
diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py new file mode 100644 index 0000000..794013c --- /dev/null +++ b/libbe/command/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/libbe/command/assign.py b/libbe/command/assign.py new file mode 100644 index 0000000..9c971ae --- /dev/null +++ b/libbe/command/assign.py @@ -0,0 +1,90 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Assign an individual or group to fix a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/close.py b/libbe/command/close.py new file mode 100644 index 0000000..026c605 --- /dev/null +++ b/libbe/command/close.py @@ -0,0 +1,63 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Close a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/comment.py b/libbe/command/comment.py new file mode 100644 index 0000000..9919d1d --- /dev/null +++ b/libbe/command/comment.py @@ -0,0 +1,164 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Add a comment to a bug""" +from libbe import cmdutil, bugdir, comment, editor +import os +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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_id(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_id(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] + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug, parent = cmdutil.bug_comment_from_id(bd, shortname) + + 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' + + new = parent.new_reply(body=body, content_type=options.content_type) + if options.author != None: + new.author = options.author + if options.alt_id != None: + new.alt_id = options.alt_id + +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) + 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.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/libbe/command/commit.py b/libbe/command/commit.py new file mode 100644 index 0000000..cade355 --- /dev/null +++ b/libbe/command/commit.py @@ -0,0 +1,82 @@ +# 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, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> 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, + root=dir) + 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: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) + 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/libbe/command/depend.py b/libbe/command/depend.py new file mode 100644 index 0000000..c2cb2a4 --- /dev/null +++ b/libbe/command/depend.py @@ -0,0 +1,371 @@ +# 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. +"""Add/remove bug dependencies""" +from libbe import cmdutil, bugdir, bug, 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, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + 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 + + allowed_status_values = \ + cmdutil.select_values(options.status, bug.status_values) + allowed_severity_values = \ + cmdutil.select_values(options.severity, bug.severity_values) + + bugA = cmdutil.bug_from_id(bd, args[0]) + + if options.tree_depth != None: + dtree = DependencyTree(bd, bugA, options.tree_depth, + allowed_status_values, + allowed_severity_values) + 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_id(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("--status", dest="status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--severity", dest="severity", metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") + 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> + +The --status and --severity options allow you to either blacklist or +whitelist values, for example + $ be list --status open,assigned +will only follow and print dependencies with open or assigned status. +You select blacklist mode by starting the list with a minus sign, for +example + $ be list --severity -target +which will only follow and print dependencies with non-target severity. + +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. + """ + 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, + allowed_status_values=None, + allowed_severity_values=None): + self.bugdir = bugdir + self.root_bug = root_bug + self.depth_limit = depth_limit + self.allowed_status_values = allowed_status_values + self.allowed_severity_values = allowed_severity_values + 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): + if self.allowed_status_values != None \ + and not bug.status in self.allowed_status_values: + continue + if self.allowed_severity_values != None \ + and not bug.severity in self.allowed_severity_values: + continue + 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/libbe/command/diff.py b/libbe/command/diff.py new file mode 100644 index 0000000..c5c34f9 --- /dev/null +++ b/libbe/command/diff.py @@ -0,0 +1,133 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. + +"""Compare bug reports with older tree""" +from libbe import cmdutil, bugdir, diff +import os +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], + ... manipulate_encodings=False) + ... else: + ... print "a" + a + >>> if bd.vcs.versioned == False: + ... execute([original], manipulate_encodings=False) + ... else: + ... raise cmdutil.UsageError('This directory is not revision-controlled.') + Traceback (most recent call last): + ... + UsageError: This directory is not revision-controlled. + >>> bd.cleanup() + """ % {'bugdir_id':diff.BUGDIR_ID} + 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.') + try: + subscriptions = diff.subscriptions_from_string( + options.subscribe) + except ValueError, e: + raise cmdutil.UsageError(e.msg) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if bd.vcs.versioned == False: + raise cmdutil.UsageError('This directory is not revision-controlled.') + if options.dir == None: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) + else: + old_bd_current = bugdir.BugDir(root=os.path.abspath(options.dir), + from_disk=True, + manipulate_encodings=False) + if revision == None: # use the current working state + old_bd = old_bd_current + else: + if old_bd_current.vcs.versioned == False: + raise cmdutil.UsageError('%s is not revision-controlled.' + % options.dir) + old_bd = old_bd_current.duplicate_bugdir(revision) + d = diff.Diff(old_bd, bd) + tree = d.report_tree(subscriptions) + + if options.uuids == True: + uuids = [] + bugs = tree.child_by_path('/bugs') + for bug_type in bugs: + uuids.extend([bug.name for bug in bug_type]) + print '\n'.join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + if options.dir != None and revision != None: + old_bd_current.remove_duplicate_bugdir() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be diff [options] REVISION") + parser.add_option("-d", "--dir", dest="dir", metavar="DIR", + help="Compare with repository in DIR instead of the current directory.") + parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", + help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") + parser.add_option("-u", "--uuids", action="store_true", dest="uuids", + help="Only print the bug UUIDS.", default=False) + 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 an understanding of the current status. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/due.py b/libbe/command/due.py new file mode 100644 index 0000000..0b8d1e9 --- /dev/null +++ b/libbe/command/due.py @@ -0,0 +1,108 @@ +# 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. +"""Set bug due dates""" +from libbe import cmdutil, bugdir, utility +__desc__ = __doc__ + +DUE_TAG="DUE:" + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No due date assigned. + >>> execute(["a", "Thu, 01 Jan 1970 00:00:00 +0000"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + Thu, 01 Jan 1970 00:00:00 +0000 + >>> execute(["a", "none"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> execute(["a"], manipulate_encodings=False) + No due date 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): + raise cmdutil.UsageError('Incorrect number of arguments.') + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + due_time = get_due(bug) + if due_time is None: + print "No due date assigned." + else: + print utility.time_to_str(due_time) + else: + if args[1] == "none": + remove_due(bug) + else: + due_time = utility.str_to_time(args[1]) + set_due(bug, due_time) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be due BUG-ID [DATE]") + return parser + +longhelp=""" +If no DATE is specified, the bug's current due date is printed. If +DATE is specified, it will be assigned to the bug. +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_due_string(time): + return "%s%s" % (DUE_TAG, utility.time_to_str(time)) + +def _parse_due_string(string): + assert string.startswith(DUE_TAG) + return utility.str_to_time(string[len(DUE_TAG):]) + +# functions exposed to other modules + +def get_due(bug): + matched = [] + for line in bug.extra_strings: + if line.startswith(DUE_TAG): + matched.append(_parse_due_string(line)) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several due dates for %s?:\n %s' + % (bug.uuid, '\n '.join(matched))) + return matched[0] + +def remove_due(bug): + estrs = bug.extra_strings + for due_str in [s for s in estrs if s.startswith(DUE_TAG)]: + estrs.remove(due_str) + bug.extra_strings = estrs # reassign to notice change + +def set_due(bug, time): + remove_due(bug) + estrs = bug.extra_strings + estrs.append(_generate_due_string(time)) + bug.extra_strings = estrs # reassign to notice change diff --git a/libbe/command/email_bugs.py b/libbe/command/email_bugs.py new file mode 100644 index 0000000..f6641e3 --- /dev/null +++ b/libbe/command/email_bugs.py @@ -0,0 +1,239 @@ +# 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. +"""Email specified bugs in a be-handle-mail compatible format.""" + +import copy +from cStringIO import StringIO +from email import Message +from email.mime.text import MIMEText +from email.generator import Generator +import sys +import time + +from libbe import cmdutil, bugdir +from libbe.subproc import invoke +from libbe.utility import time_to_str +from libbe.vcs import detect_vcs, installed_vcs +import show + +__desc__ = __doc__ + +sendmail='/usr/sbin/sendmail -t' + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> bd.encoding = 'utf-8' + >>> os.chdir(bd.root) + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"], + ... manipulate_encodings=False) # doctest: +ELLIPSIS + Content-Type: text/xml; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: b@c.edu + To: a@b.com + Date: ... + Subject: [be-bug:xml] Updates to a, b + <BLANKLINE> + <?xml version=3D"1.0" encoding=3D"utf-8" ?> + <be-xml> + <version> + <tag>...</tag> + <branch-nick>...</branch-nick> + <revno>...</revno> + <revision-id>... + </version> + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <creator>John Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug A</summary> + </bug> + <bug> + <uuid>b</uuid> + <short-name>b</short-name> + <severity>minor</severity> + <status>closed</status> + <creator>Jane Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug B</summary> + </bug> + </be-xml> + >>> bd.cleanup() + + Note that the '=3D' bits in + <?xml version=3D"1.0" encoding=3D"utf-8" ?> + are the way quoted-printable escapes '='. + + The unclosed <revision-id>... is because revision ids can be long + enough to cause line wraps, and we want to ensure we match even if + the closing </revision-id> is split by the wrapping. + """ + 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, + root=dir) + xml = show.output(args, bd, as_xml=True, with_comments=True) + subject = options.subject + if subject == None: + subject = '[be-bug:xml] Updates to %s' % ', '.join(args) + submit_email = TextEmail(to_address=options.to_address, + from_address=options.from_address, + subject=subject, + body=xml, + encoding=bd.encoding, + subtype='xml') + if options.output == True: + print submit_email + else: + submit_email.send() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]") + parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address", + help="Submission email address (%default)", + default="be-devel@bugseverywhere.org") + parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address", + help="Senders email address, overriding auto-generated default", + default=None) + parser.add_option("-s", "--subject", metavar="STRING", dest="subject", + help="Subject line, overriding auto-generated default. If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'", + default=None) + parser.add_option('-o', '--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead. Useful for testing functionality.") + return parser + +longhelp=""" +Email specified bugs in a be-handle-mail compatible format. This is +the prefered method for reporting bugs if you did not install bzr by +branching a bzr repository. + +If you _did_ install bzr by branching a bzr repository, we suggest you +commit any new bug information with + bzr commit --message "Reported bug in demuxulizer" +and then email a bzr merge directive with + bzr send --mail-to "be-devel@bugseverywhere.org" +rather than using this command. +""" + +def help(): + return get_parser().help_str() + longhelp + +class TextEmail (object): + """ + Make it very easy to compose and send single-part text emails. + >>> msg = TextEmail(to_address='Monty <monty@a.com>', + ... from_address='Python <python@b.edu>', + ... subject='Parrots', + ... header={'x-special-header':'your info here'}, + ... body="Remarkable bird, id'nit, squire?\\nLovely plumage!") + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: base64 + From: Python <python@b.edu> + To: Monty <monty@a.com> + Date: ... + Subject: Parrots + x-special-header: your info here + <BLANKLINE> + UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh + <BLANKLINE> + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: Python <python@b.edu> + To: Monty <monty@a.com> + Date: ... + Subject: Parrots + x-special-header: your info here + <BLANKLINE> + Remarkable bird, id'nit, squire? + Lovely plumage! + """ + def __init__(self, to_address, from_address=None, subject=None, + header=None, body=None, encoding='utf-8', subtype='plain'): + self.to_address = to_address + self.from_address = from_address + if self.from_address == None: + self.from_address = self._guess_from_address() + self.subject = subject + self.header = header + if self.header == None: + self.header = {} + self.body = body + self.encoding = encoding + self.subtype = subtype + def _guess_from_address(self): + vcs = detect_vcs('.') + if vcs.name == "None": + vcs = installed_vcs() + return vcs.get_user_id() + def encoded_MIME_body(self): + return MIMEText(self.body.encode(self.encoding), + self.subtype, + self.encoding) + def message(self): + response = self.encoded_MIME_body() + response['From'] = self.from_address + response['To'] = self.to_address + response['Date'] = time_to_str(time.time()) + response['Subject'] = self.subject + for k,v in self.header.items(): + response[k] = v + return response + def flatten(self, to_unicode=False): + """ + This is a simplified version of send_pgp_mime.flatten(). + """ + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(self.message()) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() or "utf-8" + text = unicode(text, encoding=encoding) + return text + def __str__(self): + return self.flatten() + def __unicode__(self): + return self.flatten(to_unicode=True) + def send(self, sendmail=None): + """ + This is a simplified version of send_pgp_mime.mail(). + + Send an email Message instance on its merry way by shelling + out to the user specified sendmail. + """ + if sendmail == None: + sendmail = SENDMAIL + invoke(sendmail, stdin=self.flatten()) diff --git a/libbe/command/help.py b/libbe/command/help.py new file mode 100644 index 0000000..9e6d1aa --- /dev/null +++ b/libbe/command/help.py @@ -0,0 +1,70 @@ +# Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi <gian@grys.it> +# 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=True, restrict_file_access=False, + dir="."): + """ + 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/libbe/command/html.py b/libbe/command/html.py new file mode 100644 index 0000000..d9e0d73 --- /dev/null +++ b/libbe/command/html.py @@ -0,0 +1,609 @@ +# 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 +import codecs, os, os.path, re, string, time +import xml.sax.saxutils, htmlentitydefs + +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + >>> 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) + + if len(args) > 0: + raise cmdutil.UsageError, 'Too many arguments.' + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bd.load_all_bugs() + + html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, + title=options.title, index_header=options.index_header) + if options.exp_template == True: + html_gen.write_default_template(options.exp_template_dir) + return + html_gen.run(options.out_dir) + +def get_parser(): + parser = cmdutil.CmdOptionParser('be html [options]') + parser.add_option('-o', '--output', metavar='DIR', dest='out_dir', + help='Set the output path (%default)', default='./html_export') + parser.add_option('-t', '--template-dir', metavar='DIR', dest='template', + help='Use a different template, defaults to internal templates', + default=None) + parser.add_option('--title', metavar='STRING', dest='title', + help='Set the bug repository title (%default)', + default='BugsEverywhere Issue Tracker') + parser.add_option('--index-header', metavar='STRING', dest='index_header', + help='Set the index page headers (%default)', + default='BugsEverywhere Bug List') + parser.add_option('-v', '--verbose', action='store_true', + metavar='verbose', dest='verbose', + help='Verbose output, default is %default', default=False) + parser.add_option('-e', '--export-template', action='store_true', + dest='exp_template', + help='Export the default template and exit.', default=False) + parser.add_option('-d', '--export-template-dir', metavar='DIR', + dest='exp_template_dir', default='./default-templates/', + help='Set the directory for the template export (%default)') + 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 + +class HTMLGen (object): + def __init__(self, bd, template=None, verbose=False, encoding=None, + title="Site Title", index_header="Index Header", + ): + self.generation_time = time.ctime() + self.bd = bd + self.verbose = verbose + self.title = title + self.index_header = index_header + if encoding != None: + self.encoding = encoding + else: + self.encoding = self.bd.encoding + if template == None: + self.template = "default" + else: + self.template = os.path.abspath(os.path.expanduser(template)) + self._load_default_templates() + + if template != None: + self._load_user_templates() + + def run(self, out_dir): + if self.verbose == True: + print "Creating the html output in %s using templates in %s" \ + % (out_dir, self.template) + + bugs_active = [] + bugs_inactive = [] + bugs = [b for b in self.bd] + bugs.sort() + bugs_active = [b for b in bugs if b.active == True] + bugs_inactive = [b for b in bugs if b.active != True] + + self._create_output_directories(out_dir) + self._write_css_file() + for b in bugs: + if b.active: + up_link = "../index.html" + else: + up_link = "../index_inactive.html" + self._write_bug_file(b, up_link) + self._write_index_file( + bugs_active, title=self.title, + index_header=self.index_header, bug_type="active") + self._write_index_file( + bugs_inactive, title=self.title, + index_header=self.index_header, bug_type="inactive") + + def _create_output_directories(self, out_dir): + if self.verbose: + print "Creating output directories" + self.out_dir = self._make_dir(out_dir) + self.out_dir_bugs = self._make_dir( + os.path.join(self.out_dir, "bugs")) + + def _write_css_file(self): + if self.verbose: + print "Writing css file" + assert hasattr(self, "out_dir"), \ + "Must run after ._create_output_directories()" + self._write_file(self.css_file, + [self.out_dir,"style.css"]) + + def _write_bug_file(self, bug, up_link): + if self.verbose: + print "\tCreating bug file for %s" % self.bd.bug_shortname(bug) + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + bug.load_comments(load_full=True) + comment_entries = self._generate_bug_comment_entries(bug) + filename = "%s.html" % bug.uuid + fullpath = os.path.join(self.out_dir_bugs, filename) + template_info = {'title':self.title, + 'charset':self.encoding, + 'up_link':up_link, + 'shortname':self.bd.bug_shortname(bug), + 'comment_entries':comment_entries, + 'generation_time':self.generation_time} + for attr in ['uuid', 'severity', 'status', 'assigned', + 'reporter', 'creator', 'time_string', 'summary']: + template_info[attr] = self._escape(getattr(bug, attr)) + self._write_file(self.bug_file % template_info, [fullpath]) + + def _generate_bug_comment_entries(self, bug): + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + stack = [] + comment_entries = [] + for depth,comment in bug.comment_root.thread(flatten=False): + while len(stack) > depth: + # pop non-parents off the stack + stack.pop(-1) + # close non-parent <div class="comment... + comment_entries.append("</div>\n") + assert len(stack) == depth + stack.append(comment) + if depth == 0: + comment_entries.append('<div class="comment root">') + else: + comment_entries.append('<div class="comment">') + template_info = {} + for attr in ['uuid', 'author', 'date', 'body']: + value = getattr(comment, attr) + if attr == 'body': + save_body = False + if comment.content_type == 'text/html': + pass # no need to escape html... + elif comment.content_type.startswith('text/'): + value = '<pre>\n'+self._escape(value)+'\n</pre>' + elif comment.content_type.startswith('image/'): + save_body = True + value = '<img src="./%s/%s" />' \ + % (bug.uuid, comment.uuid) + else: + save_body = True + value = '<a href="./%s/%s">Link to %s file</a>.' \ + % (bug.uuid, comment.uuid, comment.content_type) + if save_body == True: + per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid) + if not os.path.exists(per_bug_dir): + os.mkdir(per_bug_dir) + comment_path = os.path.join(per_bug_dir, comment.uuid) + self._write_file( + '<Files %s>\n ForceType %s\n</Files>' \ + % (comment.uuid, comment.content_type), + [per_bug_dir, '.htaccess'], mode='a') + self._write_file( + comment.body, + [per_bug_dir, comment.uuid], mode='wb') + else: + value = self._escape(value) + template_info[attr] = value + comment_entries.append(self.bug_comment_entry % template_info) + while len(stack) > 0: + stack.pop(-1) + comment_entries.append("</div>\n") # close every remaining <div class="comment... + return '\n'.join(comment_entries) + + def _write_index_file(self, bugs, title, index_header, bug_type="active"): + if self.verbose: + print "Writing %s index file for %d bugs" % (bug_type, len(bugs)) + assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()" + esc = self._escape + + bug_entries = self._generate_index_bug_entries(bugs) + + if bug_type == "active": + filename = "index.html" + elif bug_type == "inactive": + filename = "index_inactive.html" + else: + raise Exception, "Unrecognized bug_type: '%s'" % bug_type + template_info = {'title':title, + 'index_header':index_header, + 'charset':self.encoding, + 'active_class':'tab sel', + 'inactive_class':'tab nsel', + 'bug_entries':bug_entries, + 'generation_time':self.generation_time} + if bug_type == "inactive": + template_info['active_class'] = 'tab nsel' + template_info['inactive_class'] = 'tab sel' + + self._write_file(self.index_file % template_info, + [self.out_dir, filename]) + + def _generate_index_bug_entries(self, bugs): + bug_entries = [] + for bug in bugs: + if self.verbose: + print "\tCreating bug entry for %s" % self.bd.bug_shortname(bug) + template_info = {'shortname':self.bd.bug_shortname(bug)} + for attr in ['uuid', 'severity', 'status', 'assigned', + 'reporter', 'creator', 'time_string', 'summary']: + template_info[attr] = self._escape(getattr(bug, attr)) + bug_entries.append(self.index_bug_entry % template_info) + return '\n'.join(bug_entries) + + def _escape(self, string): + if string == None: + return "" + chars = [] + for char in string: + codepoint = ord(char) + if codepoint in htmlentitydefs.codepoint2name: + char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + #else: xml.sax.saxutils.escape(char) + chars.append(char) + return "".join(chars) + + def _load_user_templates(self): + for filename,attr in [('style.css','css_file'), + ('index_file.tpl','index_file'), + ('index_bug_entry.tpl','index_bug_entry'), + ('bug_file.tpl','bug_file'), + ('bug_comment_entry.tpl','bug_comment_entry')]: + fullpath = os.path.join(self.template, filename) + if os.path.exists(fullpath): + setattr(self, attr, self._read_file([fullpath])) + + def _make_dir(self, dir_path): + dir_path = os.path.abspath(os.path.expanduser(dir_path)) + if not os.path.exists(dir_path): + try: + os.makedirs(dir_path) + except: + raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path + return dir_path + + def _write_file(self, content, path_array, mode='w'): + f = codecs.open(os.path.join(*path_array), mode, self.encoding) + f.write(content) + f.close() + + def _read_file(self, path_array, mode='r'): + f = codecs.open(os.path.join(*path_array), mode, self.encoding) + content = f.read() + f.close() + return content + + def write_default_template(self, out_dir): + if self.verbose: + print "Creating output directories" + self.out_dir = self._make_dir(out_dir) + if self.verbose: + print "Creating css file" + self._write_css_file() + if self.verbose: + print "Creating index_file.tpl file" + self._write_file(self.index_file, + [self.out_dir, "index_file.tpl"]) + if self.verbose: + print "Creating index_bug_entry.tpl file" + self._write_file(self.index_bug_entry, + [self.out_dir, "index_bug_entry.tpl"]) + if self.verbose: + print "Creating bug_file.tpl file" + self._write_file(self.bug_file, + [self.out_dir, "bug_file.tpl"]) + if self.verbose: + print "Creating bug_comment_entry.tpl file" + self._write_file(self.bug_comment_entry, + [self.out_dir, "bug_comment_entry.tpl"]) + + def _load_default_templates(self): + 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; + } + + div.footer { + font-size: small; + padding-left: 20px; + padding-right: 20px; + padding-top: 5px; + padding-bottom: 5px; + margin: auto; + background: #305275; + color: #fffee7; + } + + table { + border-style: solid; + border: 10px #313131; + border-spacing: 0; + width: auto; + } + + tb { border: 1px; } + + tr { + vertical-align: top; + width: auto; + } + + td { + border-width: 0; + border-style: none; + padding-right: 0.5em; + padding-left: 0.5em; + width: auto; + } + + img { border-style: none; } + + h1 { + padding: 0.5em; + background-color: #305275; + margin-top: 0; + margin-bottom: 0; + color: #fff; + margin-left: -20px; + margin-right: -20px; + } + + ul { + list-style-type: none; + padding: 0; + } + + p { width: auto; } + + a, a:visited { + background: inherit; + text-decoration: none; + } + + a { color: #003d41; } + a:visited { color: #553d41; } + .footer a { color: #508d91; } + + /* bug index pages */ + + td.tab { + padding-right: 1em; + padding-left: 1em; + } + + td.sel.tab { + background-color: #afafaf; + border: 1px solid #afafaf; + font-weight:bold; + } + + td.nsel.tab { border: 0px; } + + table.bug_list { + background-color: #afafaf; + border: 2px solid #afafaf; + } + + .bug_list tr { width: auto; } + tr.wishlist { background-color: #B4FF9B; } + tr.minor { background-color: #FCFF98; } + tr.serious { background-color: #FFB648; } + tr.critical { background-color: #FF752A; } + tr.fatal { background-color: #FF3300; } + + /* bug detail pages */ + + td.bug_detail_label { text-align: right; } + td.bug_detail { } + td.bug_comment_label { text-align: right; vertical-align: top; } + td.bug_comment { } + + div.comment { + padding: 20px; + padding-top: 20px; + margin: auto; + margin-top: 0; + } + + div.root.comment { + padding: 0px; + /* padding-top: 0px; */ + padding-bottom: 20px; + } + """ + + self.index_file = """ + <!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>%(title)s</title> + <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" /> + <link rel="stylesheet" href="style.css" type="text/css" /> + </head> + <body> + + <div class="main"> + <h1>%(index_header)s</h1> + <p></p> + <table> + + <tr> + <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td> + <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td> + </tr> + + </table> + <table class="bug_list"> + <tbody> + + %(bug_entries)s + + </tbody> + </table> + </div> + + <div class="footer"> + <p>Generated by <a href="http://www.bugseverywhere.org/"> + BugsEverywhere</a> on %(generation_time)s</p> + <p> + <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> | + <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a> + </p> + </div> + + </body> + </html> + """ + + self.index_bug_entry =""" + <tr class="%(severity)s"> + <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(status)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td> + <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td> + </tr> + """ + + self.bug_file = """ + <!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>%(title)s</title> + <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" /> + <link rel="stylesheet" href="../style.css" type="text/css" /> + </head> + <body> + + <div class="main"> + <h1>BugsEverywhere Bug List</h1> + <h5><a href="%(up_link)s">Back to Index</a></h5> + <h2>Bug: %(shortname)s</h2> + <table> + <tbody> + + <tr><td class="bug_detail_label">ID :</td> + <td class="bug_detail">%(uuid)s</td></tr> + <tr><td class="bug_detail_label">Short name :</td> + <td class="bug_detail">%(shortname)s</td></tr> + <tr><td class="bug_detail_label">Status :</td> + <td class="bug_detail">%(status)s</td></tr> + <tr><td class="bug_detail_label">Severity :</td> + <td class="bug_detail">%(severity)s</td></tr> + <tr><td class="bug_detail_label">Assigned :</td> + <td class="bug_detail">%(assigned)s</td></tr> + <tr><td class="bug_detail_label">Reporter :</td> + <td class="bug_detail">%(reporter)s</td></tr> + <tr><td class="bug_detail_label">Creator :</td> + <td class="bug_detail">%(creator)s</td></tr> + <tr><td class="bug_detail_label">Created :</td> + <td class="bug_detail">%(time_string)s</td></tr> + <tr><td class="bug_detail_label">Summary :</td> + <td class="bug_detail">%(summary)s</td></tr> + </tbody> + </table> + + <hr/> + + %(comment_entries)s + + </div> + <h5><a href="%(up_link)s">Back to Index</a></h5> + + <div class="footer"> + <p>Generated by <a href="http://www.bugseverywhere.org/"> + BugsEverywhere</a> on %(generation_time)s</p> + <p> + <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a> | + <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a> + </p> + </div> + + </body> + </html> + """ + + self.bug_comment_entry =""" + <table> + <tr> + <td class="bug_comment_label">Comment:</td> + <td class="bug_comment"> + --------- Comment ---------<br/> + Name: %(uuid)s<br/> + From: %(author)s<br/> + Date: %(date)s<br/> + <br/> + %(body)s + </td> + </tr> + </table> + """ + + # strip leading whitespace + for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file', + 'bug_comment_entry']: + value = getattr(self, attr) + value = value.replace('\n'+' '*12, '\n') + setattr(self, attr, value.strip()+'\n') diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py new file mode 100644 index 0000000..b985193 --- /dev/null +++ b/libbe/command/import_xml.py @@ -0,0 +1,434 @@ +# 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. +"""Import comments and bugs from XML""" +import libbe +from libbe import cmdutil, bugdir, bug, comment, utility +from becommands.comment import complete +import copy +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 +if libbe.TESTING == True: + import doctest + import unittest +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import time + >>> import StringIO + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> orig_stdin = sys.stdin + >>> sys.stdin = StringIO.StringIO("<be-xml><comment><uuid>c</uuid><body>This is a comment about a</body></comment></be-xml>") + >>> execute(["-c", "a", "-"], manipulate_encodings=False) + >>> sys.stdin = orig_stdin + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_id(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 + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) < 1: + raise cmdutil.UsageError("Please specify an XML file.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + filename = args[0] + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.comment_root != None: + croot_bug,croot_comment = \ + cmdutil.bug_comment_from_id(bd, options.comment_root) + croot_bug.load_comments(load_full=True) + croot_bug.set_sync_with_disk(False) + if croot_comment.uuid == comment.INVALID_UUID: + croot_comment = croot_bug.comment_root + else: + croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid) + new_croot_bug = bug.Bug(bugdir=bd, uuid=croot_bug.uuid) + new_croot_bug.explicit_attrs = [] + new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root) + if croot_comment.uuid == comment.INVALID_UUID: + new_croot_comment = new_croot_bug.comment_root + else: + new_croot_comment = \ + new_croot_bug.comment_from_uuid(croot_comment.uuid) + for new in new_croot_bug.comments(): + new.explicit_attrs = [] + else: + croot_bug,croot_comment = (None, None) + + if filename == '-': + xml = sys.stdin.read() + else: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) + xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True) + str_xml = xml.encode('unicode_escape').replace(r'\n', '\n') + # unicode read + encode to string so we know the encoding, + # which might not be given (?) in a binary string read? + + # parse the xml + root_bugs = [] + root_comments = [] + version = {} + be_xml = ElementTree.XML(str_xml) + if be_xml.tag != 'be-xml': + raise utility.InvalidXML( + 'import-xml', be_xml, 'root element must be <be-xml>') + for child in be_xml.getchildren(): + if child.tag == 'bug': + new = bug.Bug(bugdir=bd) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + root_bugs.append(new) + elif child.tag == 'comment': + new = comment.Comment(croot_bug) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + root_comments.append(new) + elif child.tag == 'version': + for gchild in child.getchildren(): + if child.tag in ['tag', 'nick', 'revision', 'revision-id']: + text = xml.sax.saxutils.unescape(child.text) + text = text.decode('unicode_escape').strip() + version[child.tag] = text + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (gchild.tag, child.tag) + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (child.tag, comment_list.tag) + + # merge the new root_comments + if options.add_only == True: + accept_changes = False + accept_extra_strings = False + else: + accept_changes = True + accept_extra_strings = True + accept_comments = True + if len(root_comments) > 0: + if croot_bug == None: + raise UserError( + '--comment-root option is required for your root comments:\n%s' + % '\n\n'.join([c.string() for c in root_comments])) + try: + # link new comments + new_croot_bug.add_comments(root_comments, + default_parent=new_croot_comment, + ignore_missing_references= \ + options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) + croot_bug.merge(new_croot_bug, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + + # merge the new croot_bugs + merged_bugs = [] + old_bugs = [] + for new in root_bugs: + try: + old = bd.bug_from_uuid(new.alt_id) + except KeyError: + old = None + if old == None: + bd.append(new) + else: + old.load_comments(load_full=True) + old.merge(new, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + merged_bugs.append(new) + old_bugs.append(old) + + # protect against programmer error causing data loss: + if croot_bug != None: + comms = [c.uuid for c in croot_comment.traverse()] + for new in root_comments: + assert new.uuid in comms, \ + "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) + for new in root_bugs: + if not new in merged_bugs: + assert bd.has_bug(new.uuid), \ + "bug %s wasn't added" % (new.uuid) + + # save new information + if croot_bug != None: + croot_bug.save() + for new in root_bugs: + if not new in merged_bugs: + new.save() + for old in old_bugs: + old.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be import-xml XMLFILE") + parser.add_option("-i", "--ignore-missing-references", action="store_true", + dest="ignore_missing_references", default=False, + help="If any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception).") + parser.add_option("-a", "--add-only", action='store_true', + dest="add_only", default=False, + help="If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.") + parser.add_option("-c", "--comment-root", dest="comment_root", + help="Supply a bug or comment ID as the root of any <comment> elements that are direct children of the <be-xml> element. If any such <comment> elements exist, you are required to set this option.") + return parser + +longhelp=""" +Import comments and bugs from XMLFILE. If XMLFILE is '-', the file is +read from stdin. + +This command provides a fallback mechanism for passing bugs between +repositories, in case the repositories VCSs are incompatible. If the +VCSs are compatible, it's better to use their builtin merge/push/pull +to share this information, as that will preserve a more detailed +history. + +The XML file should be formatted similarly to + <be-xml> + <version> + <tag>1.0.0</tag> + <branch-nick>be</branch-nick> + <revno>446</revno> + <revision-id>a@b.com-20091119214553-iqyw2cpqluww3zna</revision-id> + <version> + <bug> + ... + <comment>...</comment> + <comment>...</comment> + </bug> + <bug>...</bug> + <comment>...</comment> + <comment>...</comment> + </be-xml> +where the ellipses mark output commpatible with Bug.xml() and +Comment.xml(). Take a look at the output of `be show --xml` for some +explicit examples. Unrecognized tags are ignored. Missing tags are +left at the default value. The version tag is not required, but is +strongly recommended. + +The bug and 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. Bugs do not have a permantent alt-id, so they +the <uuid>s you specify are not saved. The <uuid>s _are_ used to +match agains prexisting bug and comment uuids, and comment alt-ids, +and fields explicitly given in the XML file will replace old versions +unless the --add-only flag. + +*.extra_strings recieves special treatment, and if --add-only is not +set, the resulting list concatenates both source lists and removes +repeats. + +Here's an example of import activity: + Repository + bug (uuid=B, creator=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + XML + bug (uuid=B, status=fixed) + estr (don't forget your towel) + estr (watch out for flying dolphins) + com (uuid=C1, body=So long) + com (uuid=C3, author=Jed, body=And thanks) + Result + bug (uuid=B, creator=John, status=fixed) + estr (don't forget your towel) + estr (helps with space travel) + estr (watch out for flying dolphins) + com (uuid=C1, author=Jane, body=So long) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + Result, with --add-only + bug (uuid=B, creator=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + +Examples: + +Import comments (e.g. emails from an mbox) and append to bug XYZ + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ - +Or you can append those emails underneath the prexisting comment XYZ-3 + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 - + +User creates a new bug + user$ be new "The demuxulizer is broken" + Created bug with ID 48f + user$ be comment 48f + <Describe bug> + ... +User exports bug as xml and emails it to the developers + user$ be show --xml 48f > 48f.xml + user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com +or equivalently (with a slightly fancier be-handle-mail compatible +email): + user$ be email-bugs 48f +Devs recieve email, and save it's contents as demux-bug.xml + dev$ cat demux-bug.xml | be import-xml - +""" + +def help(): + return get_parser().help_str() + longhelp + + +if libbe.TESTING == True: + class LonghelpTestCase (unittest.TestCase): + """ + Test import scenarios given in longhelp. + """ + def setUp(self): + self.bugdir = bugdir.SimpleBugDir() + self.original_working_dir = os.getcwd() + os.chdir(self.bugdir.root) + bugA = self.bugdir.bug_from_uuid('a') + self.bugdir.remove_bug(bugA) + self.bugdir.set_sync_with_disk(False) + bugB = self.bugdir.bug_from_uuid('b') + bugB.creator = 'John' + bugB.status = 'open' + bugB.extra_strings += ["don't forget your towel"] + bugB.extra_strings += ['helps with space travel'] + comm1 = bugB.comment_root.new_reply(body='Hello\n') + comm1.uuid = 'c1' + comm1.author = 'Jane' + comm2 = bugB.comment_root.new_reply(body='World\n') + comm2.uuid = 'c2' + comm2.author = 'Jess' + bugB.save() + self.bugdir.set_sync_with_disk(True) + self.xml = """ + <be-xml> + <bug> + <uuid>b</uuid> + <status>fixed</status> + <summary>a test bug</summary> + <extra-string>don't forget your towel</extra-string> + <extra-string>watch out for flying dolphins</extra-string> + <comment> + <uuid>c1</uuid> + <body>So long</body> + </comment> + <comment> + <uuid>c3</uuid> + <author>Jed</author> + <body>And thanks</body> + </comment> + </bug> + </be-xml> + """ + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + def _execute(self, *args): + import StringIO + orig_stdin = sys.stdin + sys.stdin = StringIO.StringIO(self.xml) + execute(list(args)+["-"], manipulate_encodings=False, + restrict_file_access=True) + sys.stdin = orig_stdin + self.bugdir._clear_bugs() + def testCleanBugdir(self): + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + def testNotAddOnly(self): + self._execute() + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'fixed', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel', + 'watch out for flying dolphins'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) + for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'So long\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + def testAddOnly(self): + self._execute('--add-only') + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'open', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'Hello\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/command/init.py b/libbe/command/init.py new file mode 100644 index 0000000..ab9255b --- /dev/null +++ b/libbe/command/init.py @@ -0,0 +1,104 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Assign the root directory for bug tracking""" +import os.path +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import utility, vcs + >>> import os + >>> dir = utility.Dir() + >>> try: + ... bugdir.BugDir(dir.path) + ... except bugdir.NoBugDir, e: + ... True + True + >>> execute([], manipulate_encodings=False, dir=dir.path) + No revision control detected. + Directory initialized. + >>> dir.cleanup() + + >>> dir = utility.Dir() + >>> os.chdir(dir.path) + >>> _vcs = vcs.installed_vcs() + >>> _vcs.init('.') + >>> _vcs.name in vcs.VCS_ORDER + True + >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS + Using ... for revision control. + Directory initialized. + >>> _vcs.cleanup() + + >>> try: + ... execute([], manipulate_encodings=False, dir=".") + ... except cmdutil.UserError, e: + ... str(e).startswith("Directory already initialized: ") + True + >>> execute([], manipulate_encodings=False, + ... dir='/highly-unlikely-to-exist') + Traceback (most recent call last): + UserError: No such directory: /highly-unlikely-to-exist + >>> os.chdir('/') + >>> dir.cleanup() + """ + 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(from_disk=False, + sink_to_existing_root=False, + assert_new_BugDir=True, + manipulate_encodings=manipulate_encodings, + root=dir) + except bugdir.NoRootEntry: + raise cmdutil.UserError("No such directory: %s" % dir) + except bugdir.AlreadyInitialized: + raise cmdutil.UserError("Directory already initialized: %s" % 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") + 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, but you can +change that by passing the --dir option to be + $ be --dir path/to/new/bug/root init + +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/libbe/command/list.py b/libbe/command/list.py new file mode 100644 index 0000000..1c3e78d --- /dev/null +++ b/libbe/command/list.py @@ -0,0 +1,240 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi <gian@grys.it> +# 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, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + a:om: Bug A + >>> execute(["--status", "closed"], manipulate_encodings=False) + 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, + root=dir) + bd.load_all_bugs() + # select status + if options.status != None: + if options.status == "all": + status = bug.status_values + else: + status = cmdutil.select_values(options.status, bug.status_values) + 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 = cmdutil.select_values(options.severity, + bug.severity_values) + 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: + possible_assignees = [] + for _bug in bd: + if _bug.assigned != None \ + and not _bug.assigned in possible_assignees: + possible_assignees.append(_bug.assigned) + assigned = cmdutil.select_values(options.assigned, + possible_assignees) + print 'assigned', assigned + 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 + 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 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("--status", dest="status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--severity", dest="severity", metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="List bugs matching ASSIGNED", 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"), + ("x", "xml", "Dump as XML"), + ("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")) + 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, default=False) + 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) +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. As with the --status and +--severity options for `be depend`, starting the list with a minus +sign makes your selections a blacklist instead of the default +whitelist. + +status + %s +severity + %s +assigned + free form, with the string '-' being a shortcut for yourself. + +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/libbe/command/merge.py b/libbe/command/merge.py new file mode 100644 index 0000000..ac09b40 --- /dev/null +++ b/libbe/command/merge.py @@ -0,0 +1,166 @@ +# Copyright (C) 2008-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. +"""Merge duplicate bugs""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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 : + 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 : + 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, + root=dir) + bugA = cmdutil.bug_from_id(bd, args[0]) + bugA.load_comments() + bugB = cmdutil.bug_from_id(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/libbe/command/new.py b/libbe/command/new.py new file mode 100644 index 0000000..30f8834 --- /dev/null +++ b/libbe/command/new.py @@ -0,0 +1,83 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Create a new bug""" +from libbe import cmdutil, bugdir +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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 + >>> print bug.status + open + >>> 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, + root=dir) + 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/libbe/command/open.py b/libbe/command/open.py new file mode 100644 index 0000000..a6fe48d --- /dev/null +++ b/libbe/command/open.py @@ -0,0 +1,61 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""Re-open a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/remove.py b/libbe/command/remove.py new file mode 100644 index 0000000..bac06c0 --- /dev/null +++ b/libbe/command/remove.py @@ -0,0 +1,65 @@ +# Copyright (C) 2008-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. +"""Remove (delete) a bug and its comments""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/set.py b/libbe/command/set.py new file mode 100644 index 0000000..4d54a59 --- /dev/null +++ b/libbe/command/set.py @@ -0,0 +1,132 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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. +"""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, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + 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/libbe/command/severity.py b/libbe/command/severity.py new file mode 100644 index 0000000..804dc4e --- /dev/null +++ b/libbe/command/severity.py @@ -0,0 +1,106 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# 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 severity level""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/show.py b/libbe/command/show.py new file mode 100644 index 0000000..7757aaa --- /dev/null +++ b/libbe/command/show.py @@ -0,0 +1,183 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi <gian@grys.it> +# 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, comment, or combination of both.""" +import sys +from libbe import cmdutil, bugdir, comment, version, _version +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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 : + Reporter : + Creator : John Doe <jdoe@example.com> + Created : ... + Bug A + <BLANKLINE> + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS + <?xml version="1.0" encoding="..." ?> + <be-xml> + <version> + <tag>...</tag> + <branch-nick>...</branch-nick> + <revno>...</revno> + <revision-id>...</revision-id> + </version> + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <creator>John Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug A</summary> + </bug> + </be-xml> + >>> 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, + root=dir) + + if options.only_raw_body == True: + if len(args) != 1: + raise cmdutil.UsageError( + 'only one ID accepted with --only-raw-body') + bug,comment = cmdutil.bug_comment_from_id(bd, args[0]) + if comment == bug.comment_root: + raise cmdutil.UsageError( + "--only-raw-body requires a comment ID, not '%s'" % args[0]) + sys.__stdout__.write(comment.body) + sys.exit(0) + print output(args, bd, as_xml=options.XML, with_comments=options.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. + +Without the --xml flag set, 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. With the --xml flag set, there will never be any root comments, +so mix and match away (the bug listings for directly requested +comments will be restricted to the bug uuid and the requested +comment(s)). + +Directly requested comments will be grouped by their parent bug and +placed at the end of the output, so the ordering may not match the +order of the listed IDs. +""" + +def help(): + return get_parser().help_str() + longhelp + +def _sort_ids(ids, with_comments=True): + bugs = [] + root_comments = {} + for id in ids: + bugname,commname = cmdutil.parse_id(id) + if commname == None: + bugs.append(bugname) + elif with_comments == True: + if bugname not in root_comments: + root_comments[bugname] = [commname] + else: + root_comments[bugname].append(commname) + for bugname in root_comments.keys(): + assert bugname not in bugs, \ + "specifically requested both '%s%s' and '%s'" \ + % (bugname, root_comments[bugname][0], bugname) + return (bugs, root_comments) + +def _xml_header(encoding): + lines = ['<?xml version="1.0" encoding="%s" ?>' % encoding, + '<be-xml>', + ' <version>', + ' <tag>%s</tag>' % version.version()] + for tag in ['branch-nick', 'revno', 'revision-id']: + value = _version.version_info[tag.replace('-', '_')] + lines.append(' <%s>%s</%s>' % (tag, value, tag)) + lines.append(' </version>') + return lines + +def _xml_footer(): + return ['</be-xml>'] + +def output(ids, bd, as_xml=True, with_comments=True): + bugs,root_comments = _sort_ids(ids, with_comments) + lines = [] + if as_xml: + lines.extend(_xml_header(bd.encoding)) + else: + spaces_left = len(ids) - 1 + for bugname in bugs: + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.append(bug.xml(indent=2, show_comments=with_comments)) + else: + lines.append(bug.string(show_comments=with_comments)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + for bugname,comments in root_comments.items(): + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.extend([' <bug>', ' <uuid>%s</uuid>' % bug.uuid]) + for commname in comments: + try: + comment = bug.comment_root.comment_from_shortname(commname) + except comment.InvalidShortname, e: + raise UserError(e.message) + if as_xml: + lines.append(comment.xml(indent=4, shortname=bugname)) + else: + lines.append(comment.string(shortname=bugname)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + if as_xml: + lines.append('</bug>') + if as_xml: + lines.extend(_xml_footer()) + return '\n'.join(lines) diff --git a/libbe/command/status.py b/libbe/command/status.py new file mode 100644 index 0000000..58b6f63 --- /dev/null +++ b/libbe/command/status.py @@ -0,0 +1,118 @@ +# Copyright (C) 2008-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. +"""Show or change a bug's status""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + bug = cmdutil.bug_from_id(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/libbe/command/subscribe.py b/libbe/command/subscribe.py new file mode 100644 index 0000000..69554f7 --- /dev/null +++ b/libbe/command/subscribe.py @@ -0,0 +1,360 @@ +# 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, diff +import os, copy +__desc__ = __doc__ + +TAG="SUBSCRIBE:" + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + + 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] == diff.BUGDIR_ID: # directory-wide subscriptions + type_root = diff.BUGDIR_TYPE_ALL + entity = bd + entity_name = "bug directory" + else: # bug-specific subscriptions + type_root = diff.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 = [diff.type_from_name(name, type_root, default=diff.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 %s: +%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. +""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, + diff.BUGDIR_TYPE_ALL.string_tree(6), + diff.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 = [diff.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 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>", [diff.BUGDIR_TYPE_ALL], + ... ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe <J@doe.com>", [diff.BUGDIR_TYPE_NEW], + ... ["*"], diff.BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) + ['John Doe <j@doe.com>'] + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) + ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>'] + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) + ['Jane Doe <J@doe.com>'] + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) + ['Jane Doe <J@doe.com>'] + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.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 "%(bugdir_id)s" (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>", + ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>", + ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>", + ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) + >>> subscribers = get_bugdir_subscribers(bd, "a.com") + >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"] + [<SubscriptionType: new>] + >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"] + [<SubscriptionType: all>] + >>> subscribers["John Doe <j@doe.com>"]["a"] + [<SubscriptionType: all>] + >>> get_bugdir_subscribers(bd, "b.net") + {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}} + >>> bd.cleanup() + """ % {'bugdir_id':diff.BUGDIR_ID} + subscribers = {} + for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, + server, diff.BUGDIR_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, + diff.BUGDIR_TYPE_ALL) + subscribers[sub] = {"DIR":ts} + for bug in bugdir: + for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, + server, diff.BUG_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, + diff.BUG_TYPE_ALL) + if sub in subscribers: + subscribers[sub][bug.uuid] = ts + else: + subscribers[sub] = {bug.uuid:ts} + return subscribers diff --git a/libbe/command/tag.py b/libbe/command/tag.py new file mode 100644 index 0000000..f3819bd --- /dev/null +++ b/libbe/command/tag.py @@ -0,0 +1,137 @@ +# 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. +"""Tag a bug, or search bugs for tags""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> 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, + root=dir) + 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_id(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/libbe/command/target.py b/libbe/command/target.py new file mode 100644 index 0000000..5dd5d38 --- /dev/null +++ b/libbe/command/target.py @@ -0,0 +1,168 @@ +# 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. +"""Assorted bug target manipulations and queries""" +from libbe import cmdutil, bugdir +from becommands import depend +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os, StringIO, sys + >>> 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 + + >>> orig_stdout = sys.stdout + >>> tmp_stdout = StringIO.StringIO() + >>> sys.stdout = tmp_stdout + >>> execute(["--resolve", "tomorrow"], manipulate_encodings=False) + >>> sys.stdout = orig_stdout + >>> output = tmp_stdout.getvalue().strip() + >>> target = bd.bug_from_uuid(output) + >>> print target.summary + tomorrow + >>> print target.severity + target + + >>> 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 (options.resolve == False and len(args) not in (1, 2)) \ + or (options.resolve == True and len(args) not in (0, 1)): + raise cmdutil.UsageError('Incorrect number of arguments.') + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.resolve == True: + if len(args) == 0: + summary = None + else: + summary = args[0] + bug = bug_from_target_summary(bd, summary) + if bug == None: + print 'No target assigned.' + else: + print bug.uuid + return + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + target = bug_target(bd, bug) + if target is None: + print "No target assigned." + else: + print target.summary + else: + if args[1] == "none": + target = remove_target(bd, bug) + else: + target = add_target(bd, bug, args[1]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --resolve [TARGET]") + parser.add_option("-r", "--resolve", action="store_true", dest="resolve", + help="Print the UUID for the target bug whose summary matches TARGET. If TARGET is not given, print the UUID of the current bugdir target. If that is not set, don't print anything.", + default=False) + return parser + +longhelp=""" +Assorted bug target manipulations and queries. + +If no target is specified, the bug's current target is printed. If +TARGET is specified, it will be assigned to the bug, creating a new +target bug if necessary. + +Targets are free-form; 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 --resolve TARGET` form, print the UUID +of the target-bug with summary TARGET. If target is not given, return +use the bugdir's current target (see `be set`). + +If you want to list all bugs blocking the current target, try + $ be depend --status -closed,fixed,wontfix --severity -target \ + $(be target --resolve) + +If you want to set the current bugdir target by summary (rather than +by UUID), try + $ be set target $(be target --resolve SUMMARY) +""" + +def help(): + return get_parser().help_str() + longhelp + +def bug_from_target_summary(bugdir, summary=None): + if summary == None: + if bugdir.target == None: + return None + else: + return bugdir.bug_from_uuid(bugdir.target) + matched = [] + for uuid in bugdir.uuids(): + bug = bugdir.bug_from_uuid(uuid) + if bug.severity == 'target' and bug.summary == summary: + matched.append(bug) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several targets with same summary: %s' + % '\n '.join([bug.uuid for bug in matched])) + return matched[0] + +def bug_target(bugdir, bug): + if bug.severity == 'target': + return bug + matched = [] + for blocked in depend.get_blocks(bugdir, bug): + if blocked.severity == 'target': + matched.append(blocked) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('This bug (%s) blocks several targets: %s' + % (bug.uuid, + '\n '.join([b.uuid for b in matched]))) + return matched[0] + +def remove_target(bugdir, bug): + target = bug_target(bugdir, bug) + depend.remove_block(target, bug) + return target + +def add_target(bugdir, bug, summary): + target = bug_from_target_summary(bugdir, summary) + if target == None: + target = bugdir.new_bug(summary=summary) + target.severity = 'target' + depend.add_block(target, bug) + return target |