aboutsummaryrefslogtreecommitdiffstats
path: root/libbe/ui
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-12-07 20:07:55 -0500
committerW. Trevor King <wking@drexel.edu>2009-12-07 20:07:55 -0500
commit49a7771336ce09f6d42c7699ef32aecea0e83182 (patch)
treef237c7413fb68e72b1d87b0ccc4c788944168f10 /libbe/ui
parentc3bcafe12034d35f5c46f76a7dab97ab08b84dfd (diff)
downloadbugseverywhere-49a7771336ce09f6d42c7699ef32aecea0e83182.tar.gz
Initial directory restructuring to clarify dependencies
Diffstat (limited to 'libbe/ui')
-rw-r--r--libbe/ui/util/cmdutil.py356
-rw-r--r--libbe/ui/util/editor.py113
-rw-r--r--libbe/ui/util/pager.py65
3 files changed, 534 insertions, 0 deletions
diff --git a/libbe/ui/util/cmdutil.py b/libbe/ui/util/cmdutil.py
new file mode 100644
index 0000000..c567984
--- /dev/null
+++ b/libbe/ui/util/cmdutil.py
@@ -0,0 +1,356 @@
+# 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.
+
+"""
+Define assorted utilities to make command-line handling easier.
+"""
+
+import glob
+import optparse
+import os
+from textwrap import TextWrapper
+from StringIO import StringIO
+import sys
+
+import libbe
+import bugdir
+import comment
+import plugin
+import encoding
+if libbe.TESTING == True:
+ import doctest
+
+
+class UserError(Exception):
+ def __init__(self, msg):
+ Exception.__init__(self, msg)
+
+class UnknownCommand(UserError):
+ def __init__(self, cmd):
+ Exception.__init__(self, "Unknown command '%s'" % cmd)
+ self.cmd = cmd
+
+class UsageError(Exception):
+ pass
+
+class GetHelp(Exception):
+ pass
+
+class GetCompletions(Exception):
+ def __init__(self, completions=[]):
+ msg = "Get allowed completions"
+ Exception.__init__(self, msg)
+ self.completions = completions
+
+def iter_commands():
+ for name, module in plugin.iter_plugins("becommands"):
+ yield name.replace("_", "-"), module
+
+def get_command(command_name):
+ """Retrieves the module for a user command
+
+ >>> try:
+ ... get_command("asdf")
+ ... except UnknownCommand, e:
+ ... print e
+ Unknown command 'asdf'
+ >>> repr(get_command("list")).startswith("<module 'becommands.list' from ")
+ True
+ """
+ cmd = plugin.get_plugin("becommands", command_name.replace("-", "_"))
+ if cmd is None:
+ raise UnknownCommand(command_name)
+ return cmd
+
+
+def execute(cmd, args,
+ manipulate_encodings=True, restrict_file_access=False,
+ dir="."):
+ enc = encoding.get_encoding()
+ cmd = get_command(cmd)
+ ret = cmd.execute([a.decode(enc) for a in args],
+ manipulate_encodings=manipulate_encodings,
+ restrict_file_access=restrict_file_access,
+ dir=dir)
+ if ret == None:
+ ret = 0
+ return ret
+
+def help(cmd=None, parser=None):
+ if cmd != None:
+ return get_command(cmd).help()
+ else:
+ cmdlist = []
+ for name, module in iter_commands():
+ cmdlist.append((name, module.__desc__))
+ longest_cmd_len = max([len(name) for name,desc in cmdlist])
+ ret = ["Bugs Everywhere - Distributed bug tracking",
+ "", "Supported commands"]
+ for name, desc in cmdlist:
+ numExtraSpaces = longest_cmd_len-len(name)
+ ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc))
+ ret.extend(["", "Run", " be help [command]", "for more information."])
+ longhelp = "\n".join(ret)
+ if parser == None:
+ return longhelp
+ return parser.help_str() + "\n" + longhelp
+
+def completions(cmd):
+ parser = get_command(cmd).get_parser()
+ longopts = []
+ for opt in parser.option_list:
+ longopts.append(opt.get_opt_string())
+ return longopts
+
+def raise_get_help(option, opt, value, parser):
+ raise GetHelp
+
+def raise_get_completions(option, opt, value, parser):
+ print "got completion arg"
+ if hasattr(parser, "command") and parser.command == "be":
+ comps = []
+ for command, module in iter_commands():
+ comps.append(command)
+ for opt in parser.option_list:
+ comps.append(opt.get_opt_string())
+ raise GetCompletions(comps)
+ raise GetCompletions(completions(sys.argv[1]))
+
+class CmdOptionParser(optparse.OptionParser):
+ def __init__(self, usage):
+ optparse.OptionParser.__init__(self, usage)
+ self.disable_interspersed_args()
+ self.remove_option("-h")
+ self.add_option("-h", "--help", action="callback",
+ callback=raise_get_help, help="Print a help message")
+ self.add_option("--complete", action="callback",
+ callback=raise_get_completions,
+ help="Print a list of available completions")
+
+ def error(self, message):
+ raise UsageError(message)
+
+ def iter_options(self):
+ return iter_combine([self._short_opt.iterkeys(),
+ self._long_opt.iterkeys()])
+
+ def help_str(self):
+ f = StringIO()
+ self.print_help(f)
+ return f.getvalue()
+
+def option_value_pairs(options, parser):
+ """
+ Iterate through OptionParser (option, value) pairs.
+ """
+ for option in [o.dest for o in parser.option_list if o.dest != None]:
+ value = getattr(options, option)
+ yield (option, value)
+
+def default_complete(options, args, parser, bugid_args={}):
+ """
+ A dud complete implementation for becommands so that the
+ --complete argument doesn't cause any problems. Use this
+ until you've set up a command-specific complete function.
+
+ bugid_args is an optional dict where the keys are positional
+ arguments taking bug shortnames and the values are functions for
+ filtering, since that's a common enough operation.
+ e.g. for "be open [options] BUGID"
+ bugid_args = {0: lambda bug : bug.active == False}
+ A positional argument of -1 specifies all remaining arguments
+ (e.g in the case of "be show BUGID BUGID ...").
+ """
+ for option,value in option_value_pairs(options, parser):
+ if value == "--complete":
+ raise GetCompletions()
+ if len(bugid_args.keys()) > 0:
+ max_pos_arg = max(bugid_args.keys())
+ else:
+ max_pos_arg = -1
+ for pos,value in enumerate(args):
+ if value == "--complete":
+ filter = None
+ if pos in bugid_args:
+ filter = bugid_args[pos]
+ if pos > max_pos_arg and -1 in bugid_args:
+ filter = bugid_args[-1]
+ if filter != None:
+ bugshortnames = []
+ try:
+ bd = bugdir.BugDir(from_disk=True,
+ manipulate_encodings=False)
+ bd.load_all_bugs()
+ bugs = [bug for bug in bd if filter(bug) == True]
+ bugshortnames = [bd.bug_shortname(bug) for bug in bugs]
+ except bugdir.NoBugDir:
+ pass
+ raise GetCompletions(bugshortnames)
+ raise GetCompletions()
+
+def complete_path(path):
+ """List possible path completions for path."""
+ comps = glob.glob(path+"*") + glob.glob(path+"/*")
+ if len(comps) == 1 and os.path.isdir(comps[0]):
+ comps.extend(glob.glob(comps[0]+"/*"))
+ return comps
+
+def underlined(instring):
+ """Produces a version of a string that is underlined with '='
+
+ >>> underlined("Underlined String")
+ 'Underlined String\\n================='
+ """
+
+ return "%s\n%s" % (instring, "="*len(instring))
+
+def select_values(string, possible_values, name="unkown"):
+ """
+ This function allows the user to select values from a list of
+ possible values. The default is to select all the values:
+
+ >>> select_values(None, ['abc', 'def', 'hij'])
+ ['abc', 'def', 'hij']
+
+ The user selects values with a comma-separated limit_string.
+ Prepending a minus sign to such a list denotes blacklist mode:
+
+ >>> select_values('-abc,hij', ['abc', 'def', 'hij'])
+ ['def']
+
+ Without the leading -, the selection is in whitelist mode:
+
+ >>> select_values('abc,hij', ['abc', 'def', 'hij'])
+ ['abc', 'hij']
+
+ In either case, appropriate errors are raised if on of the
+ user-values is not in the list of possible values. The name
+ parameter lets you make the error message more clear:
+
+ >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar")
+ Traceback (most recent call last):
+ ...
+ UserError: Invalid foobar xyz
+ ['abc', 'def', 'hij']
+ >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar")
+ Traceback (most recent call last):
+ ...
+ UserError: Invalid foobar xyz
+ ['abc', 'def', 'hij']
+ """
+ possible_values = list(possible_values) # don't alter the original
+ if string == None:
+ pass
+ elif string.startswith('-'):
+ blacklisted_values = set(string[1:].split(','))
+ for value in blacklisted_values:
+ if value not in possible_values:
+ raise UserError('Invalid %s %s\n %s'
+ % (name, value, possible_values))
+ possible_values.remove(value)
+ else:
+ whitelisted_values = string.split(',')
+ for value in whitelisted_values:
+ if value not in possible_values:
+ raise UserError('Invalid %s %s\n %s'
+ % (name, value, possible_values))
+ possible_values = whitelisted_values
+ return possible_values
+
+def restrict_file_access(bugdir, path):
+ """
+ Check that the file at path is inside bugdir.root. This is
+ important if you allow other users to execute becommands with your
+ username (e.g. if you're running be-handle-mail through your
+ ~/.procmailrc). If this check wasn't made, a user could e.g.
+ run
+ be commit -b ~/.ssh/id_rsa "Hack to expose ssh key"
+ which would expose your ssh key to anyone who could read the VCS
+ log.
+ """
+ in_root = bugdir.vcs.path_in_root(path, bugdir.root)
+ if in_root == False:
+ raise UserError('file access restricted!\n %s not in %s'
+ % (path, bugdir.root))
+
+def parse_id(id):
+ """
+ Return (bug_id, comment_id) tuple.
+ Basically inverts Comment.comment_shortnames()
+ >>> parse_id('XYZ')
+ ('XYZ', None)
+ >>> parse_id('XYZ:123')
+ ('XYZ', ':123')
+ >>> parse_id('')
+ Traceback (most recent call last):
+ ...
+ UserError: invalid id ''.
+ >>> parse_id('::')
+ Traceback (most recent call last):
+ ...
+ UserError: invalid id '::'.
+ """
+ if len(id) == 0:
+ raise UserError("invalid id '%s'." % id)
+ if id.count(':') > 1:
+ raise UserError("invalid id '%s'." % id)
+ elif id.count(':') == 1:
+ # Split shortname generated by Comment.comment_shortnames()
+ bug_id,comment_id = id.split(':')
+ comment_id = ':'+comment_id
+ else:
+ bug_id = id
+ comment_id = None
+ return (bug_id, comment_id)
+
+def bug_from_id(bdir, id):
+ """
+ Exception translation for the command-line interface.
+ id can be either the bug shortname or the full uuid.
+ """
+ try:
+ bug = bdir.bug_from_shortname(id)
+ except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e:
+ raise UserError(e.message)
+ return bug
+
+def bug_comment_from_id(bdir, id):
+ """
+ Return (bug,comment) tuple matching shortname. id can be either
+ the bug/comment shortname or the full uuid. If there is no
+ comment part to the id, the returned comment is the bug's
+ .comment_root.
+ """
+ bug_id,comment_id = parse_id(id)
+ try:
+ bug = bdir.bug_from_shortname(bug_id)
+ except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e:
+ raise UserError(e.message)
+ if comment_id == None:
+ comm = bug.comment_root
+ else:
+ #bug.load_comments(load_full=False)
+ try:
+ comm = bug.comment_root.comment_from_shortname(comment_id)
+ except comment.InvalidShortname, e:
+ raise UserError(e.message)
+ return (bug, comm)
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/ui/util/editor.py b/libbe/ui/util/editor.py
new file mode 100644
index 0000000..859cedc
--- /dev/null
+++ b/libbe/ui/util/editor.py
@@ -0,0 +1,113 @@
+# Bugs Everywhere, a distributed bugtracker
+# 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.
+
+"""
+Define editor_string(), a function that invokes an editor to accept
+user-produced text as a string.
+"""
+
+import codecs
+import locale
+import os
+import sys
+import tempfile
+
+import libbe
+if libbe.TESTING == True:
+ import doctest
+
+
+default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding()
+
+comment_marker = u"== Anything below this line will be ignored\n"
+
+class CantFindEditor(Exception):
+ def __init__(self):
+ Exception.__init__(self, "Can't find editor to get string from")
+
+def editor_string(comment=None, encoding=None):
+ """Invokes the editor, and returns the user-produced text as a string
+
+ >>> if "EDITOR" in os.environ:
+ ... del os.environ["EDITOR"]
+ >>> if "VISUAL" in os.environ:
+ ... del os.environ["VISUAL"]
+ >>> editor_string()
+ Traceback (most recent call last):
+ CantFindEditor: Can't find editor to get string from
+ >>> os.environ["EDITOR"] = "echo bar > "
+ >>> editor_string()
+ u'bar\\n'
+ >>> os.environ["VISUAL"] = "echo baz > "
+ >>> editor_string()
+ u'baz\\n'
+ >>> del os.environ["EDITOR"]
+ >>> del os.environ["VISUAL"]
+ """
+ if encoding == None:
+ encoding = default_encoding
+ for name in ('VISUAL', 'EDITOR'):
+ try:
+ editor = os.environ[name]
+ break
+ except KeyError:
+ pass
+ else:
+ raise CantFindEditor()
+ fhandle, fname = tempfile.mkstemp()
+ try:
+ if comment is not None:
+ cstring = u'\n'+comment_string(comment)
+ os.write(fhandle, cstring.encode(encoding))
+ os.close(fhandle)
+ oldmtime = os.path.getmtime(fname)
+ os.system("%s %s" % (editor, fname))
+ f = codecs.open(fname, "r", encoding)
+ output = trimmed_string(f.read())
+ f.close()
+ if output.rstrip('\n') == "":
+ output = None
+ finally:
+ os.unlink(fname)
+ return output
+
+
+def comment_string(comment):
+ """
+ >>> comment_string('hello') == comment_marker+"hello"
+ True
+ """
+ return comment_marker + comment
+
+
+def trimmed_string(instring):
+ """
+ >>> trimmed_string("hello\\n"+comment_marker)
+ u'hello\\n'
+ >>> trimmed_string("hi!\\n" + comment_string('Booga'))
+ u'hi!\\n'
+ """
+ out = []
+ for line in instring.splitlines(True):
+ if line.startswith(comment_marker):
+ break
+ out.append(line)
+ return ''.join(out)
+
+if libbe.TESTING == True:
+ suite = doctest.DocTestSuite()
diff --git a/libbe/ui/util/pager.py b/libbe/ui/util/pager.py
new file mode 100644
index 0000000..1ddc3fa
--- /dev/null
+++ b/libbe/ui/util/pager.py
@@ -0,0 +1,65 @@
+# 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.
+
+"""
+Automatic pager for terminal output (a la Git).
+"""
+
+import sys, os, select
+
+# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
+def run_pager(paginate='auto'):
+ """
+ paginate should be one of 'never', 'auto', or 'always'.
+
+ usage: just call this function and continue using sys.stdout like
+ you normally would.
+ """
+ if paginate == 'never' \
+ or sys.platform == 'win32' \
+ or not hasattr(sys.stdout, 'isatty') \
+ or sys.stdout.isatty() == False:
+ return
+
+ if paginate == 'auto':
+ if 'LESS' not in os.environ:
+ os.environ['LESS'] = '' # += doesn't work on undefined var
+ # don't page if the input is short enough
+ os.environ['LESS'] += ' -FRX'
+ if 'PAGER' in os.environ:
+ pager = os.environ['PAGER']
+ else:
+ pager = 'less'
+
+ read_fd, write_fd = os.pipe()
+ if os.fork() == 0:
+ # child process
+ os.close(read_fd)
+ os.close(0)
+ os.dup2(write_fd, 1)
+ os.close(write_fd)
+ if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True:
+ os.dup2(1, 2)
+ return
+
+ # parent process, become pager
+ os.close(write_fd)
+ os.dup2(read_fd, 0)
+ os.close(read_fd)
+
+ # Wait until we have input before we start the pager
+ select.select([0], [], [])
+ os.execlp(pager, pager)