diff options
Diffstat (limited to 'bzrlib/commands.py')
-rw-r--r-- | bzrlib/commands.py | 1303 |
1 files changed, 1303 insertions, 0 deletions
diff --git a/bzrlib/commands.py b/bzrlib/commands.py new file mode 100644 index 0000000..4c2b389 --- /dev/null +++ b/bzrlib/commands.py @@ -0,0 +1,1303 @@ +# Copyright (C) 2005-2011 Canonical Ltd +# +# 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 + +from __future__ import absolute_import + +# TODO: Define arguments by objects, rather than just using names. +# Those objects can specify the expected type of the argument, which +# would help with validation and shell completion. They could also provide +# help/explanation for that argument in a structured way. + +# TODO: Specific "examples" property on commands for consistent formatting. + +import os +import sys + +from bzrlib.lazy_import import lazy_import +lazy_import(globals(), """ +import errno +import threading + +import bzrlib +from bzrlib import ( + config, + cleanup, + cmdline, + debug, + errors, + i18n, + option, + osutils, + trace, + ui, + ) +""") + +from bzrlib.hooks import Hooks +from bzrlib.i18n import gettext +# Compatibility - Option used to be in commands. +from bzrlib.option import Option +from bzrlib.plugin import disable_plugins, load_plugins +from bzrlib import registry + + +class CommandInfo(object): + """Information about a command.""" + + def __init__(self, aliases): + """The list of aliases for the command.""" + self.aliases = aliases + + @classmethod + def from_command(klass, command): + """Factory to construct a CommandInfo from a command.""" + return klass(command.aliases) + + +class CommandRegistry(registry.Registry): + """Special registry mapping command names to command classes. + + :ivar overridden_registry: Look in this registry for commands being + overridden by this registry. This can be used to tell plugin commands + about the builtin they're decorating. + """ + + def __init__(self): + registry.Registry.__init__(self) + self.overridden_registry = None + # map from aliases to the real command that implements the name + self._alias_dict = {} + + def get(self, command_name): + real_name = self._alias_dict.get(command_name, command_name) + return registry.Registry.get(self, real_name) + + @staticmethod + def _get_name(command_name): + if command_name.startswith("cmd_"): + return _unsquish_command_name(command_name) + else: + return command_name + + def register(self, cmd, decorate=False): + """Utility function to help register a command + + :param cmd: Command subclass to register + :param decorate: If true, allow overriding an existing command + of the same name; the old command is returned by this function. + Otherwise it is an error to try to override an existing command. + """ + k = cmd.__name__ + k_unsquished = self._get_name(k) + try: + previous = self.get(k_unsquished) + except KeyError: + previous = None + if self.overridden_registry: + try: + previous = self.overridden_registry.get(k_unsquished) + except KeyError: + pass + info = CommandInfo.from_command(cmd) + try: + registry.Registry.register(self, k_unsquished, cmd, + override_existing=decorate, info=info) + except KeyError: + trace.warning('Two plugins defined the same command: %r' % k) + trace.warning('Not loading the one in %r' % + sys.modules[cmd.__module__]) + trace.warning('Previously this command was registered from %r' % + sys.modules[previous.__module__]) + for a in cmd.aliases: + self._alias_dict[a] = k_unsquished + return previous + + def register_lazy(self, command_name, aliases, module_name): + """Register a command without loading its module. + + :param command_name: The primary name of the command. + :param aliases: A list of aliases for the command. + :module_name: The module that the command lives in. + """ + key = self._get_name(command_name) + registry.Registry.register_lazy(self, key, module_name, command_name, + info=CommandInfo(aliases)) + for a in aliases: + self._alias_dict[a] = key + + +plugin_cmds = CommandRegistry() +builtin_command_registry = CommandRegistry() +plugin_cmds.overridden_registry = builtin_command_registry + + +def register_command(cmd, decorate=False): + """Register a plugin command. + + Should generally be avoided in favor of lazy registration. + """ + global plugin_cmds + return plugin_cmds.register(cmd, decorate) + + +def _squish_command_name(cmd): + return 'cmd_' + cmd.replace('-', '_') + + +def _unsquish_command_name(cmd): + return cmd[4:].replace('_','-') + + +def _register_builtin_commands(): + if builtin_command_registry.keys(): + # only load once + return + import bzrlib.builtins + for cmd_class in _scan_module_for_commands(bzrlib.builtins).values(): + builtin_command_registry.register(cmd_class) + bzrlib.builtins._register_lazy_builtins() + + +def _scan_module_for_commands(module): + r = {} + for name, obj in module.__dict__.iteritems(): + if name.startswith("cmd_"): + real_name = _unsquish_command_name(name) + r[real_name] = obj + return r + + +def _list_bzr_commands(names): + """Find commands from bzr's core and plugins. + + This is not the public interface, just the default hook called by all_command_names. + """ + # to eliminate duplicates + names.update(builtin_command_names()) + names.update(plugin_command_names()) + return names + + +def all_command_names(): + """Return a set of all command names.""" + names = set() + for hook in Command.hooks['list_commands']: + names = hook(names) + if names is None: + raise AssertionError( + 'hook %s returned None' % Command.hooks.get_hook_name(hook)) + return names + + +def builtin_command_names(): + """Return list of builtin command names. + + Use of all_command_names() is encouraged rather than builtin_command_names + and/or plugin_command_names. + """ + _register_builtin_commands() + return builtin_command_registry.keys() + + +def plugin_command_names(): + """Returns command names from commands registered by plugins.""" + return plugin_cmds.keys() + + +def get_cmd_object(cmd_name, plugins_override=True): + """Return the command object for a command. + + plugins_override + If true, plugin commands can override builtins. + """ + try: + return _get_cmd_object(cmd_name, plugins_override) + except KeyError: + raise errors.BzrCommandError(gettext('unknown command "%s"') % cmd_name) + + +def _get_cmd_object(cmd_name, plugins_override=True, check_missing=True): + """Get a command object. + + :param cmd_name: The name of the command. + :param plugins_override: Allow plugins to override builtins. + :param check_missing: Look up commands not found in the regular index via + the get_missing_command hook. + :return: A Command object instance + :raises KeyError: If no command is found. + """ + # We want only 'ascii' command names, but the user may have typed + # in a Unicode name. In that case, they should just get a + # 'command not found' error later. + # In the future, we may actually support Unicode command names. + cmd = None + # Get a command + for hook in Command.hooks['get_command']: + cmd = hook(cmd, cmd_name) + if cmd is not None and not plugins_override and not cmd.plugin_name(): + # We've found a non-plugin command, don't permit it to be + # overridden. + break + if cmd is None and check_missing: + for hook in Command.hooks['get_missing_command']: + cmd = hook(cmd_name) + if cmd is not None: + break + if cmd is None: + # No command found. + raise KeyError + # Allow plugins to extend commands + for hook in Command.hooks['extend_command']: + hook(cmd) + if getattr(cmd, 'invoked_as', None) is None: + cmd.invoked_as = cmd_name + return cmd + + +def _try_plugin_provider(cmd_name): + """Probe for a plugin provider having cmd_name.""" + try: + plugin_metadata, provider = probe_for_provider(cmd_name) + raise errors.CommandAvailableInPlugin(cmd_name, + plugin_metadata, provider) + except errors.NoPluginAvailable: + pass + + +def probe_for_provider(cmd_name): + """Look for a provider for cmd_name. + + :param cmd_name: The command name. + :return: plugin_metadata, provider for getting cmd_name. + :raises NoPluginAvailable: When no provider can supply the plugin. + """ + # look for providers that provide this command but aren't installed + for provider in command_providers_registry: + try: + return provider.plugin_for_command(cmd_name), provider + except errors.NoPluginAvailable: + pass + raise errors.NoPluginAvailable(cmd_name) + + +def _get_bzr_command(cmd_or_None, cmd_name): + """Get a command from bzr's core.""" + try: + cmd_class = builtin_command_registry.get(cmd_name) + except KeyError: + pass + else: + return cmd_class() + return cmd_or_None + + +def _get_external_command(cmd_or_None, cmd_name): + """Lookup a command that is a shell script.""" + # Only do external command lookups when no command is found so far. + if cmd_or_None is not None: + return cmd_or_None + from bzrlib.externalcommand import ExternalCommand + cmd_obj = ExternalCommand.find_command(cmd_name) + if cmd_obj: + return cmd_obj + + +def _get_plugin_command(cmd_or_None, cmd_name): + """Get a command from bzr's plugins.""" + try: + return plugin_cmds.get(cmd_name)() + except KeyError: + pass + for key in plugin_cmds.keys(): + info = plugin_cmds.get_info(key) + if cmd_name in info.aliases: + return plugin_cmds.get(key)() + return cmd_or_None + + +class Command(object): + """Base class for commands. + + Commands are the heart of the command-line bzr interface. + + The command object mostly handles the mapping of command-line + parameters into one or more bzrlib operations, and of the results + into textual output. + + Commands normally don't have any state. All their arguments are + passed in to the run method. (Subclasses may take a different + policy if the behaviour of the instance needs to depend on e.g. a + shell plugin and not just its Python class.) + + The docstring for an actual command should give a single-line + summary, then a complete description of the command. A grammar + description will be inserted. + + :cvar aliases: Other accepted names for this command. + + :cvar takes_args: List of argument forms, marked with whether they are + optional, repeated, etc. Examples:: + + ['to_location', 'from_branch?', 'file*'] + + * 'to_location' is required + * 'from_branch' is optional + * 'file' can be specified 0 or more times + + :cvar takes_options: List of options that may be given for this command. + These can be either strings, referring to globally-defined options, or + option objects. Retrieve through options(). + + :cvar hidden: If true, this command isn't advertised. This is typically + for commands intended for expert users. + + :cvar encoding_type: Command objects will get a 'outf' attribute, which has + been setup to properly handle encoding of unicode strings. + encoding_type determines what will happen when characters cannot be + encoded: + + * strict - abort if we cannot decode + * replace - put in a bogus character (typically '?') + * exact - do not encode sys.stdout + + NOTE: by default on Windows, sys.stdout is opened as a text stream, + therefore LF line-endings are converted to CRLF. When a command uses + encoding_type = 'exact', then sys.stdout is forced to be a binary + stream, and line-endings will not mangled. + + :cvar invoked_as: + A string indicating the real name under which this command was + invoked, before expansion of aliases. + (This may be None if the command was constructed and run in-process.) + + :cvar hooks: An instance of CommandHooks. + + :cvar __doc__: The help shown by 'bzr help command' for this command. + This is set by assigning explicitly to __doc__ so that -OO can + be used:: + + class Foo(Command): + __doc__ = "My help goes here" + """ + aliases = [] + takes_args = [] + takes_options = [] + encoding_type = 'strict' + invoked_as = None + l10n = True + + hidden = False + + def __init__(self): + """Construct an instance of this command.""" + # List of standard options directly supported + self.supported_std_options = [] + self._setup_run() + + def add_cleanup(self, cleanup_func, *args, **kwargs): + """Register a function to call after self.run returns or raises. + + Functions will be called in LIFO order. + """ + self._operation.add_cleanup(cleanup_func, *args, **kwargs) + + def cleanup_now(self): + """Execute and empty pending cleanup functions immediately. + + After cleanup_now all registered cleanups are forgotten. add_cleanup + may be called again after cleanup_now; these cleanups will be called + after self.run returns or raises (or when cleanup_now is next called). + + This is useful for releasing expensive or contentious resources (such + as write locks) before doing further work that does not require those + resources (such as writing results to self.outf). Note though, that + as it releases all resources, this may release locks that the command + wants to hold, so use should be done with care. + """ + self._operation.cleanup_now() + + def _usage(self): + """Return single-line grammar for this command. + + Only describes arguments, not options. + """ + s = 'bzr ' + self.name() + ' ' + for aname in self.takes_args: + aname = aname.upper() + if aname[-1] in ['$', '+']: + aname = aname[:-1] + '...' + elif aname[-1] == '?': + aname = '[' + aname[:-1] + ']' + elif aname[-1] == '*': + aname = '[' + aname[:-1] + '...]' + s += aname + ' ' + s = s[:-1] # remove last space + return s + + def get_help_text(self, additional_see_also=None, plain=True, + see_also_as_links=False, verbose=True): + """Return a text string with help for this command. + + :param additional_see_also: Additional help topics to be + cross-referenced. + :param plain: if False, raw help (reStructuredText) is + returned instead of plain text. + :param see_also_as_links: if True, convert items in 'See also' + list to internal links (used by bzr_man rstx generator) + :param verbose: if True, display the full help, otherwise + leave out the descriptive sections and just display + usage help (e.g. Purpose, Usage, Options) with a + message explaining how to obtain full help. + """ + if self.l10n: + i18n.install() # Install i18n only for get_help_text for now. + doc = self.help() + if doc: + # Note: If self.gettext() translates ':Usage:\n', the section will + # be shown after "Description" section and we don't want to + # translate the usage string. + # Though, bzr export-pot don't exports :Usage: section and it must + # not be translated. + doc = self.gettext(doc) + else: + doc = gettext("No help for this command.") + + # Extract the summary (purpose) and sections out from the text + purpose,sections,order = self._get_help_parts(doc) + + # If a custom usage section was provided, use it + if sections.has_key('Usage'): + usage = sections.pop('Usage') + else: + usage = self._usage() + + # The header is the purpose and usage + result = "" + result += gettext(':Purpose: %s\n') % (purpose,) + if usage.find('\n') >= 0: + result += gettext(':Usage:\n%s\n') % (usage,) + else: + result += gettext(':Usage: %s\n') % (usage,) + result += '\n' + + # Add the options + # + # XXX: optparse implicitly rewraps the help, and not always perfectly, + # so we get <https://bugs.launchpad.net/bzr/+bug/249908>. -- mbp + # 20090319 + parser = option.get_optparser(self.options()) + options = parser.format_option_help() + # FIXME: According to the spec, ReST option lists actually don't + # support options like --1.14 so that causes syntax errors (in Sphinx + # at least). As that pattern always appears in the commands that + # break, we trap on that and then format that block of 'format' options + # as a literal block. We use the most recent format still listed so we + # don't have to do that too often -- vila 20110514 + if not plain and options.find(' --1.14 ') != -1: + options = options.replace(' format:\n', ' format::\n\n', 1) + if options.startswith('Options:'): + result += gettext(':Options:%s') % (options[len('options:'):],) + else: + result += options + result += '\n' + + if verbose: + # Add the description, indenting it 2 spaces + # to match the indentation of the options + if sections.has_key(None): + text = sections.pop(None) + text = '\n '.join(text.splitlines()) + result += gettext(':Description:\n %s\n\n') % (text,) + + # Add the custom sections (e.g. Examples). Note that there's no need + # to indent these as they must be indented already in the source. + if sections: + for label in order: + if label in sections: + result += ':%s:\n%s\n' % (label, sections[label]) + result += '\n' + else: + result += (gettext("See bzr help %s for more details and examples.\n\n") + % self.name()) + + # Add the aliases, source (plug-in) and see also links, if any + if self.aliases: + result += gettext(':Aliases: ') + result += ', '.join(self.aliases) + '\n' + plugin_name = self.plugin_name() + if plugin_name is not None: + result += gettext(':From: plugin "%s"\n') % plugin_name + see_also = self.get_see_also(additional_see_also) + if see_also: + if not plain and see_also_as_links: + see_also_links = [] + for item in see_also: + if item == 'topics': + # topics doesn't have an independent section + # so don't create a real link + see_also_links.append(item) + else: + # Use a Sphinx link for this entry + link_text = gettext(":doc:`{0} <{1}-help>`").format( + item, item) + see_also_links.append(link_text) + see_also = see_also_links + result += gettext(':See also: %s') % ', '.join(see_also) + '\n' + + # If this will be rendered as plain text, convert it + if plain: + import bzrlib.help_topics + result = bzrlib.help_topics.help_as_plain_text(result) + return result + + @staticmethod + def _get_help_parts(text): + """Split help text into a summary and named sections. + + :return: (summary,sections,order) where summary is the top line and + sections is a dictionary of the rest indexed by section name. + order is the order the section appear in the text. + A section starts with a heading line of the form ":xxx:". + Indented text on following lines is the section value. + All text found outside a named section is assigned to the + default section which is given the key of None. + """ + def save_section(sections, order, label, section): + if len(section) > 0: + if sections.has_key(label): + sections[label] += '\n' + section + else: + order.append(label) + sections[label] = section + + lines = text.rstrip().splitlines() + summary = lines.pop(0) + sections = {} + order = [] + label,section = None,'' + for line in lines: + if line.startswith(':') and line.endswith(':') and len(line) > 2: + save_section(sections, order, label, section) + label,section = line[1:-1],'' + elif (label is not None) and len(line) > 1 and not line[0].isspace(): + save_section(sections, order, label, section) + label,section = None,line + else: + if len(section) > 0: + section += '\n' + line + else: + section = line + save_section(sections, order, label, section) + return summary, sections, order + + def get_help_topic(self): + """Return the commands help topic - its name.""" + return self.name() + + def get_see_also(self, additional_terms=None): + """Return a list of help topics that are related to this command. + + The list is derived from the content of the _see_also attribute. Any + duplicates are removed and the result is in lexical order. + :param additional_terms: Additional help topics to cross-reference. + :return: A list of help topics. + """ + see_also = set(getattr(self, '_see_also', [])) + if additional_terms: + see_also.update(additional_terms) + return sorted(see_also) + + def options(self): + """Return dict of valid options for this command. + + Maps from long option name to option object.""" + r = Option.STD_OPTIONS.copy() + std_names = r.keys() + for o in self.takes_options: + if isinstance(o, basestring): + o = option.Option.OPTIONS[o] + r[o.name] = o + if o.name in std_names: + self.supported_std_options.append(o.name) + return r + + def _setup_outf(self): + """Return a file linked to stdout, which has proper encoding.""" + self.outf = ui.ui_factory.make_output_stream( + encoding_type=self.encoding_type) + + def run_argv_aliases(self, argv, alias_argv=None): + """Parse the command line and run with extra aliases in alias_argv.""" + args, opts = parse_args(self, argv, alias_argv) + self._setup_outf() + + # Process the standard options + if 'help' in opts: # e.g. bzr add --help + self.outf.write(self.get_help_text()) + return 0 + if 'usage' in opts: # e.g. bzr add --usage + self.outf.write(self.get_help_text(verbose=False)) + return 0 + trace.set_verbosity_level(option._verbosity_level) + if 'verbose' in self.supported_std_options: + opts['verbose'] = trace.is_verbose() + elif opts.has_key('verbose'): + del opts['verbose'] + if 'quiet' in self.supported_std_options: + opts['quiet'] = trace.is_quiet() + elif opts.has_key('quiet'): + del opts['quiet'] + # mix arguments and options into one dictionary + cmdargs = _match_argform(self.name(), self.takes_args, args) + cmdopts = {} + for k, v in opts.items(): + cmdopts[k.replace('-', '_')] = v + + all_cmd_args = cmdargs.copy() + all_cmd_args.update(cmdopts) + + try: + return self.run(**all_cmd_args) + finally: + # reset it, so that other commands run in the same process won't + # inherit state. Before we reset it, log any activity, so that it + # gets properly tracked. + ui.ui_factory.log_transport_activity( + display=('bytes' in debug.debug_flags)) + trace.set_verbosity_level(0) + + def _setup_run(self): + """Wrap the defined run method on self with a cleanup. + + This is called by __init__ to make the Command be able to be run + by just calling run(), as it could be before cleanups were added. + + If a different form of cleanups are in use by your Command subclass, + you can override this method. + """ + class_run = self.run + def run(*args, **kwargs): + for hook in Command.hooks['pre_command']: + hook(self) + self._operation = cleanup.OperationWithCleanups(class_run) + try: + return self._operation.run_simple(*args, **kwargs) + finally: + del self._operation + for hook in Command.hooks['post_command']: + hook(self) + self.run = run + + def run(self): + """Actually run the command. + + This is invoked with the options and arguments bound to + keyword parameters. + + Return 0 or None if the command was successful, or a non-zero + shell error code if not. It's OK for this method to allow + an exception to raise up. + + This method is automatically wrapped by Command.__init__ with a + cleanup operation, stored as self._operation. This can be used + via self.add_cleanup to perform automatic cleanups at the end of + run(). + + The argument for run are assembled by introspection. So for instance, + if your command takes an argument files, you would declare:: + + def run(self, files=None): + pass + """ + raise NotImplementedError('no implementation of command %r' + % self.name()) + + def help(self): + """Return help message for this class.""" + from inspect import getdoc + if self.__doc__ is Command.__doc__: + return None + return getdoc(self) + + def gettext(self, message): + """Returns the gettext function used to translate this command's help. + + Commands provided by plugins should override this to use their + own i18n system. + """ + return i18n.gettext_per_paragraph(message) + + def name(self): + """Return the canonical name for this command. + + The name under which it was actually invoked is available in invoked_as. + """ + return _unsquish_command_name(self.__class__.__name__) + + def plugin_name(self): + """Get the name of the plugin that provides this command. + + :return: The name of the plugin or None if the command is builtin. + """ + mod_parts = self.__module__.split('.') + if len(mod_parts) >= 3 and mod_parts[1] == 'plugins': + return mod_parts[2] + else: + return None + + +class CommandHooks(Hooks): + """Hooks related to Command object creation/enumeration.""" + + def __init__(self): + """Create the default hooks. + + These are all empty initially, because by default nothing should get + notified. + """ + Hooks.__init__(self, "bzrlib.commands", "Command.hooks") + self.add_hook('extend_command', + "Called after creating a command object to allow modifications " + "such as adding or removing options, docs etc. Called with the " + "new bzrlib.commands.Command object.", (1, 13)) + self.add_hook('get_command', + "Called when creating a single command. Called with " + "(cmd_or_None, command_name). get_command should either return " + "the cmd_or_None parameter, or a replacement Command object that " + "should be used for the command. Note that the Command.hooks " + "hooks are core infrastructure. Many users will prefer to use " + "bzrlib.commands.register_command or plugin_cmds.register_lazy.", + (1, 17)) + self.add_hook('get_missing_command', + "Called when creating a single command if no command could be " + "found. Called with (command_name). get_missing_command should " + "either return None, or a Command object to be used for the " + "command.", (1, 17)) + self.add_hook('list_commands', + "Called when enumerating commands. Called with a set of " + "cmd_name strings for all the commands found so far. This set " + " is safe to mutate - e.g. to remove a command. " + "list_commands should return the updated set of command names.", + (1, 17)) + self.add_hook('pre_command', + "Called prior to executing a command. Called with the command " + "object.", (2, 6)) + self.add_hook('post_command', + "Called after executing a command. Called with the command " + "object.", (2, 6)) + +Command.hooks = CommandHooks() + + +def parse_args(command, argv, alias_argv=None): + """Parse command line. + + Arguments and options are parsed at this level before being passed + down to specific command handlers. This routine knows, from a + lookup table, something about the available options, what optargs + they take, and which commands will accept them. + """ + # TODO: make it a method of the Command? + parser = option.get_optparser(command.options()) + if alias_argv is not None: + args = alias_argv + argv + else: + args = argv + + # for python 2.5 and later, optparse raises this exception if a non-ascii + # option name is given. See http://bugs.python.org/issue2931 + try: + options, args = parser.parse_args(args) + except UnicodeEncodeError,e: + raise errors.BzrCommandError( + gettext('Only ASCII permitted in option names')) + + opts = dict([(k, v) for k, v in options.__dict__.iteritems() if + v is not option.OptionParser.DEFAULT_VALUE]) + return args, opts + + +def _match_argform(cmd, takes_args, args): + argdict = {} + + # step through args and takes_args, allowing appropriate 0-many matches + for ap in takes_args: + argname = ap[:-1] + if ap[-1] == '?': + if args: + argdict[argname] = args.pop(0) + elif ap[-1] == '*': # all remaining arguments + if args: + argdict[argname + '_list'] = args[:] + args = [] + else: + argdict[argname + '_list'] = None + elif ap[-1] == '+': + if not args: + raise errors.BzrCommandError(gettext( + "command {0!r} needs one or more {1}").format( + cmd, argname.upper())) + else: + argdict[argname + '_list'] = args[:] + args = [] + elif ap[-1] == '$': # all but one + if len(args) < 2: + raise errors.BzrCommandError( + gettext("command {0!r} needs one or more {1}").format( + cmd, argname.upper())) + argdict[argname + '_list'] = args[:-1] + args[:-1] = [] + else: + # just a plain arg + argname = ap + if not args: + raise errors.BzrCommandError( + gettext("command {0!r} requires argument {1}").format( + cmd, argname.upper())) + else: + argdict[argname] = args.pop(0) + + if args: + raise errors.BzrCommandError( gettext( + "extra argument to command {0}: {1}").format( + cmd, args[0]) ) + + return argdict + +def apply_coveraged(dirname, the_callable, *args, **kwargs): + # Cannot use "import trace", as that would import bzrlib.trace instead of + # the standard library's trace. + trace = __import__('trace') + + tracer = trace.Trace(count=1, trace=0) + sys.settrace(tracer.globaltrace) + threading.settrace(tracer.globaltrace) + + try: + return exception_to_return_code(the_callable, *args, **kwargs) + finally: + sys.settrace(None) + results = tracer.results() + results.write_results(show_missing=1, summary=False, + coverdir=dirname) + + +def apply_profiled(the_callable, *args, **kwargs): + import hotshot + import tempfile + import hotshot.stats + pffileno, pfname = tempfile.mkstemp() + try: + prof = hotshot.Profile(pfname) + try: + ret = prof.runcall(exception_to_return_code, the_callable, *args, + **kwargs) or 0 + finally: + prof.close() + stats = hotshot.stats.load(pfname) + stats.strip_dirs() + stats.sort_stats('cum') # 'time' + ## XXX: Might like to write to stderr or the trace file instead but + ## print_stats seems hardcoded to stdout + stats.print_stats(20) + return ret + finally: + os.close(pffileno) + os.remove(pfname) + + +def exception_to_return_code(the_callable, *args, **kwargs): + """UI level helper for profiling and coverage. + + This transforms exceptions into a return value of 3. As such its only + relevant to the UI layer, and should never be called where catching + exceptions may be desirable. + """ + try: + return the_callable(*args, **kwargs) + except (KeyboardInterrupt, Exception), e: + # used to handle AssertionError and KeyboardInterrupt + # specially here, but hopefully they're handled ok by the logger now + exc_info = sys.exc_info() + exitcode = trace.report_exception(exc_info, sys.stderr) + if os.environ.get('BZR_PDB'): + print '**** entering debugger' + tb = exc_info[2] + import pdb + if sys.version_info[:2] < (2, 6): + # XXX: we want to do + # pdb.post_mortem(tb) + # but because pdb.post_mortem gives bad results for tracebacks + # from inside generators, we do it manually. + # (http://bugs.python.org/issue4150, fixed in Python 2.6) + + # Setup pdb on the traceback + p = pdb.Pdb() + p.reset() + p.setup(tb.tb_frame, tb) + # Point the debugger at the deepest frame of the stack + p.curindex = len(p.stack) - 1 + p.curframe = p.stack[p.curindex][0] + # Start the pdb prompt. + p.print_stack_entry(p.stack[p.curindex]) + p.execRcLines() + p.cmdloop() + else: + pdb.post_mortem(tb) + return exitcode + + +def apply_lsprofiled(filename, the_callable, *args, **kwargs): + from bzrlib.lsprof import profile + ret, stats = profile(exception_to_return_code, the_callable, + *args, **kwargs) + stats.sort() + if filename is None: + stats.pprint() + else: + stats.save(filename) + trace.note(gettext('Profile data written to "%s".'), filename) + return ret + + +def get_alias(cmd, config=None): + """Return an expanded alias, or None if no alias exists. + + cmd + Command to be checked for an alias. + config + Used to specify an alternative config to use, + which is especially useful for testing. + If it is unspecified, the global config will be used. + """ + if config is None: + import bzrlib.config + config = bzrlib.config.GlobalConfig() + alias = config.get_alias(cmd) + if (alias): + return cmdline.split(alias) + return None + + +def run_bzr(argv, load_plugins=load_plugins, disable_plugins=disable_plugins): + """Execute a command. + + :param argv: The command-line arguments, without the program name from + argv[0] These should already be decoded. All library/test code calling + run_bzr should be passing valid strings (don't need decoding). + :param load_plugins: What function to call when triggering plugin loading. + This function should take no arguments and cause all plugins to be + loaded. + :param disable_plugins: What function to call when disabling plugin + loading. This function should take no arguments and cause all plugin + loading to be prohibited (so that code paths in your application that + know about some plugins possibly being present will fail to import + those plugins even if they are installed.) + :return: Returns a command exit code or raises an exception. + + Special master options: these must come before the command because + they control how the command is interpreted. + + --no-plugins + Do not load plugin modules at all + + --no-aliases + Do not allow aliases + + --builtin + Only use builtin commands. (Plugins are still allowed to change + other behaviour.) + + --profile + Run under the Python hotshot profiler. + + --lsprof + Run under the Python lsprof profiler. + + --coverage + Generate line coverage report in the specified directory. + + --concurrency + Specify the number of processes that can be run concurrently (selftest). + """ + trace.mutter("bazaar version: " + bzrlib.__version__) + argv = _specified_or_unicode_argv(argv) + trace.mutter("bzr arguments: %r", argv) + + opt_lsprof = opt_profile = opt_no_plugins = opt_builtin = \ + opt_no_l10n = opt_no_aliases = False + opt_lsprof_file = opt_coverage_dir = None + + # --no-plugins is handled specially at a very early stage. We need + # to load plugins before doing other command parsing so that they + # can override commands, but this needs to happen first. + + argv_copy = [] + i = 0 + override_config = [] + while i < len(argv): + a = argv[i] + if a == '--profile': + opt_profile = True + elif a == '--lsprof': + opt_lsprof = True + elif a == '--lsprof-file': + opt_lsprof = True + opt_lsprof_file = argv[i + 1] + i += 1 + elif a == '--no-plugins': + opt_no_plugins = True + elif a == '--no-aliases': + opt_no_aliases = True + elif a == '--no-l10n': + opt_no_l10n = True + elif a == '--builtin': + opt_builtin = True + elif a == '--concurrency': + os.environ['BZR_CONCURRENCY'] = argv[i + 1] + i += 1 + elif a == '--coverage': + opt_coverage_dir = argv[i + 1] + i += 1 + elif a == '--profile-imports': + pass # already handled in startup script Bug #588277 + elif a.startswith('-D'): + debug.debug_flags.add(a[2:]) + elif a.startswith('-O'): + override_config.append(a[2:]) + else: + argv_copy.append(a) + i += 1 + + if bzrlib.global_state is None: + # FIXME: Workaround for users that imported bzrlib but didn't call + # bzrlib.initialize -- vila 2012-01-19 + cmdline_overrides = config.CommandLineStore() + else: + cmdline_overrides = bzrlib.global_state.cmdline_overrides + cmdline_overrides._from_cmdline(override_config) + + debug.set_debug_flags_from_config() + + if not opt_no_plugins: + load_plugins() + else: + disable_plugins() + + argv = argv_copy + if (not argv): + get_cmd_object('help').run_argv_aliases([]) + return 0 + + if argv[0] == '--version': + get_cmd_object('version').run_argv_aliases([]) + return 0 + + alias_argv = None + + if not opt_no_aliases: + alias_argv = get_alias(argv[0]) + if alias_argv: + argv[0] = alias_argv.pop(0) + + cmd = argv.pop(0) + cmd_obj = get_cmd_object(cmd, plugins_override=not opt_builtin) + if opt_no_l10n: + cmd.l10n = False + run = cmd_obj.run_argv_aliases + run_argv = [argv, alias_argv] + + try: + # We can be called recursively (tests for example), but we don't want + # the verbosity level to propagate. + saved_verbosity_level = option._verbosity_level + option._verbosity_level = 0 + if opt_lsprof: + if opt_coverage_dir: + trace.warning( + '--coverage ignored, because --lsprof is in use.') + ret = apply_lsprofiled(opt_lsprof_file, run, *run_argv) + elif opt_profile: + if opt_coverage_dir: + trace.warning( + '--coverage ignored, because --profile is in use.') + ret = apply_profiled(run, *run_argv) + elif opt_coverage_dir: + ret = apply_coveraged(opt_coverage_dir, run, *run_argv) + else: + ret = run(*run_argv) + return ret or 0 + finally: + # reset, in case we may do other commands later within the same + # process. Commands that want to execute sub-commands must propagate + # --verbose in their own way. + if 'memory' in debug.debug_flags: + trace.debug_memory('Process status after command:', short=False) + option._verbosity_level = saved_verbosity_level + # Reset the overrides + cmdline_overrides._reset() + + +def display_command(func): + """Decorator that suppresses pipe/interrupt errors.""" + def ignore_pipe(*args, **kwargs): + try: + result = func(*args, **kwargs) + sys.stdout.flush() + return result + except IOError, e: + if getattr(e, 'errno', None) is None: + raise + if e.errno != errno.EPIPE: + # Win32 raises IOError with errno=0 on a broken pipe + if sys.platform != 'win32' or (e.errno not in (0, errno.EINVAL)): + raise + pass + except KeyboardInterrupt: + pass + return ignore_pipe + + +def install_bzr_command_hooks(): + """Install the hooks to supply bzr's own commands.""" + if _list_bzr_commands in Command.hooks["list_commands"]: + return + Command.hooks.install_named_hook("list_commands", _list_bzr_commands, + "bzr commands") + Command.hooks.install_named_hook("get_command", _get_bzr_command, + "bzr commands") + Command.hooks.install_named_hook("get_command", _get_plugin_command, + "bzr plugin commands") + Command.hooks.install_named_hook("get_command", _get_external_command, + "bzr external command lookup") + Command.hooks.install_named_hook("get_missing_command", + _try_plugin_provider, + "bzr plugin-provider-db check") + + + +def _specified_or_unicode_argv(argv): + # For internal or testing use, argv can be passed. Otherwise, get it from + # the process arguments in a unicode-safe way. + if argv is None: + return osutils.get_unicode_argv() + else: + new_argv = [] + try: + # ensure all arguments are unicode strings + for a in argv: + if isinstance(a, unicode): + new_argv.append(a) + else: + new_argv.append(a.decode('ascii')) + except UnicodeDecodeError: + raise errors.BzrError("argv should be list of unicode strings.") + return new_argv + + +def main(argv=None): + """Main entry point of command-line interface. + + Typically `bzrlib.initialize` should be called first. + + :param argv: list of unicode command-line arguments similar to sys.argv. + argv[0] is script name usually, it will be ignored. + Don't pass here sys.argv because this list contains plain strings + and not unicode; pass None instead. + + :return: exit code of bzr command. + """ + if argv is not None: + argv = argv[1:] + _register_builtin_commands() + ret = run_bzr_catch_errors(argv) + trace.mutter("return code %d", ret) + return ret + + +def run_bzr_catch_errors(argv): + """Run a bzr command with parameters as described by argv. + + This function assumed that that UI layer is setup, that symbol deprecations + are already applied, and that unicode decoding has already been performed on argv. + """ + # done here so that they're covered for every test run + install_bzr_command_hooks() + return exception_to_return_code(run_bzr, argv) + + +def run_bzr_catch_user_errors(argv): + """Run bzr and report user errors, but let internal errors propagate. + + This is used for the test suite, and might be useful for other programs + that want to wrap the commandline interface. + """ + # done here so that they're covered for every test run + install_bzr_command_hooks() + try: + return run_bzr(argv) + except Exception, e: + if (isinstance(e, (OSError, IOError)) + or not getattr(e, 'internal_error', True)): + trace.report_exception(sys.exc_info(), sys.stderr) + return 3 + else: + raise + + +class HelpCommandIndex(object): + """A index for bzr help that returns commands.""" + + def __init__(self): + self.prefix = 'commands/' + + def get_topics(self, topic): + """Search for topic amongst commands. + + :param topic: A topic to search for. + :return: A list which is either empty or contains a single + Command entry. + """ + if topic and topic.startswith(self.prefix): + topic = topic[len(self.prefix):] + try: + cmd = _get_cmd_object(topic, check_missing=False) + except KeyError: + return [] + else: + return [cmd] + + +class Provider(object): + """Generic class to be overriden by plugins""" + + def plugin_for_command(self, cmd_name): + """Takes a command and returns the information for that plugin + + :return: A dictionary with all the available information + for the requested plugin + """ + raise NotImplementedError + + +class ProvidersRegistry(registry.Registry): + """This registry exists to allow other providers to exist""" + + def __iter__(self): + for key, provider in self.iteritems(): + yield provider + +command_providers_registry = ProvidersRegistry() |