summaryrefslogtreecommitdiff
path: root/bzrlib/plugins
diff options
context:
space:
mode:
authorLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
committerLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
commit25335618bf8755ce6b116ee14f47f5a1f2c821e9 (patch)
treed889d7ab3f9f985d0c54c534cb8052bd2e6d7163 /bzrlib/plugins
downloadbzr-tarball-25335618bf8755ce6b116ee14f47f5a1f2c821e9.tar.gz
Tarball conversion
Diffstat (limited to 'bzrlib/plugins')
-rw-r--r--bzrlib/plugins/__init__.py19
-rw-r--r--bzrlib/plugins/bash_completion/README.txt143
-rw-r--r--bzrlib/plugins/bash_completion/__init__.py41
-rw-r--r--bzrlib/plugins/bash_completion/bashcomp.py482
-rw-r--r--bzrlib/plugins/bash_completion/tests/__init__.py23
-rw-r--r--bzrlib/plugins/bash_completion/tests/test_bashcomp.py332
-rw-r--r--bzrlib/plugins/changelog_merge/__init__.py78
-rw-r--r--bzrlib/plugins/changelog_merge/changelog_merge.py199
-rw-r--r--bzrlib/plugins/changelog_merge/tests/__init__.py24
-rw-r--r--bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py222
-rw-r--r--bzrlib/plugins/launchpad/__init__.py201
-rw-r--r--bzrlib/plugins/launchpad/account.py113
-rw-r--r--bzrlib/plugins/launchpad/cmds.py410
-rw-r--r--bzrlib/plugins/launchpad/lp_api.py313
-rw-r--r--bzrlib/plugins/launchpad/lp_api_lite.py288
-rw-r--r--bzrlib/plugins/launchpad/lp_directory.py209
-rw-r--r--bzrlib/plugins/launchpad/lp_propose.py221
-rw-r--r--bzrlib/plugins/launchpad/lp_registration.py358
-rw-r--r--bzrlib/plugins/launchpad/test_account.py117
-rw-r--r--bzrlib/plugins/launchpad/test_lp_api.py100
-rw-r--r--bzrlib/plugins/launchpad/test_lp_api_lite.py549
-rw-r--r--bzrlib/plugins/launchpad/test_lp_directory.py639
-rw-r--r--bzrlib/plugins/launchpad/test_lp_login.py58
-rw-r--r--bzrlib/plugins/launchpad/test_lp_open.py103
-rw-r--r--bzrlib/plugins/launchpad/test_lp_service.py181
-rw-r--r--bzrlib/plugins/launchpad/test_register.py366
-rw-r--r--bzrlib/plugins/netrc_credential_store/__init__.py75
-rw-r--r--bzrlib/plugins/netrc_credential_store/tests/__init__.py23
-rw-r--r--bzrlib/plugins/netrc_credential_store/tests/test_netrc.py86
-rw-r--r--bzrlib/plugins/news_merge/README7
-rw-r--r--bzrlib/plugins/news_merge/__init__.py58
-rw-r--r--bzrlib/plugins/news_merge/news_merge.py78
-rw-r--r--bzrlib/plugins/news_merge/parser.py71
-rw-r--r--bzrlib/plugins/news_merge/tests/__init__.py23
-rw-r--r--bzrlib/plugins/news_merge/tests/test_news_merge.py27
-rw-r--r--bzrlib/plugins/po_merge/README7
-rw-r--r--bzrlib/plugins/po_merge/__init__.py92
-rw-r--r--bzrlib/plugins/po_merge/po_merge.py174
-rw-r--r--bzrlib/plugins/po_merge/tests/__init__.py23
-rw-r--r--bzrlib/plugins/po_merge/tests/test_po_merge.py451
-rw-r--r--bzrlib/plugins/weave_fmt/__init__.py128
-rw-r--r--bzrlib/plugins/weave_fmt/branch.py219
-rw-r--r--bzrlib/plugins/weave_fmt/bzrdir.py1006
-rw-r--r--bzrlib/plugins/weave_fmt/repository.py883
-rw-r--r--bzrlib/plugins/weave_fmt/test_bzrdir.py584
-rw-r--r--bzrlib/plugins/weave_fmt/test_repository.py331
-rw-r--r--bzrlib/plugins/weave_fmt/test_workingtree.py89
-rw-r--r--bzrlib/plugins/weave_fmt/workingtree.py243
-rw-r--r--bzrlib/plugins/weave_fmt/xml4.py190
49 files changed, 10657 insertions, 0 deletions
diff --git a/bzrlib/plugins/__init__.py b/bzrlib/plugins/__init__.py
new file mode 100644
index 0000000..efcb160
--- /dev/null
+++ b/bzrlib/plugins/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2006 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
+
+"""Null placeholder plugin"""
+from __future__ import absolute_import
+
diff --git a/bzrlib/plugins/bash_completion/README.txt b/bzrlib/plugins/bash_completion/README.txt
new file mode 100644
index 0000000..3bd1355
--- /dev/null
+++ b/bzrlib/plugins/bash_completion/README.txt
@@ -0,0 +1,143 @@
+.. comment
+
+ Copyright (C) 2010 Canonical Ltd
+
+ This file is part of bzr-bash-completion
+
+ bzr-bash-completion 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.
+
+ bzr-bash-completion 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, see <http://www.gnu.org/licenses/>.
+
+==========================
+bzr bash-completion plugin
+==========================
+
+This plugin generates a shell function which can be used by bash to
+automatically complete the currently typed command when the user
+presses the completion key (usually tab).
+
+It is intended as a bzr plugin, but can be used to some extend as a
+standalone python script as well.
+
+| Copyright (C) 2009, 2010 Canonical Ltd
+
+.. contents::
+
+-------------------------------
+Bundled and standalone versions
+-------------------------------
+
+This plugin has been merged_ into the main source tree of Bazaar.
+Starting with the bzr 2.3 series, a common bzr installation will
+include this plugin.
+
+There is still a standalone version available. It makes the plugin
+available for users of older bzr versions. When using both versions,
+local configuration might determine which version actually gets used,
+and some installations might even overwrite one another, so don't use
+the standalone version if you have the bundled one, unless you know
+what you are doing. Some effort will be made to keep the two versions
+reasonably in sync for some time yet.
+
+This text here documents the bundled version.
+
+.. _merged: http://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/revision/5240
+
+-----
+Using
+-----
+
+Using as a plugin
+-----------------
+
+This is the preferred method of generating the completion function, as
+it will ensure proper bzr initialization.
+
+::
+
+ eval "`bzr bash-completion`"
+
+Lazy initialization
+-------------------
+
+Running the above command automatically from your ``~/.bashrc`` file
+or similar can cause annoying delays in the startup of your shell.
+To avoid this problem, you can delay the generation of the completion
+function until you actually need it.
+
+To do so, source the file ``contrib/bash/bzr`` shipped with the bzr
+source distribution from your ``~/.bashrc`` file
+or add it to your ``~/.bash_completion`` if
+your setup uses such a file. On a system-wide installation, the
+directory ``/usr/share/bash-completion/`` might contain such bash
+completion scripts.
+
+Note that the full completion function is generated only once per
+shell session. If you update your bzr installation or change the set
+of installed plugins, then you might wish to regenerate the completion
+function manually as described above in order for completion to take
+these changes into account.
+
+--------------
+Design concept
+--------------
+
+The plugin is designed to generate a completion function
+containing all the required information about the possible
+completions. This is usually only done once when bash
+initializes. After that, no more invocations of bzr are required. This
+makes the function much faster than a possible implementation talking
+to bzr for each and every completion. On the other hand, this has the
+effect that updates to bzr or its plugins won't show up in the
+completions immediately, but only after the completion function has
+been regenerated.
+
+-------
+License
+-------
+
+As this is built upon a bash completion script originally included in
+the bzr source tree, and as the bzr sources are covered by the GPL 2,
+this plugin here is licensed under these same terms.
+
+If you require a more liberal license, you'll have to contact all
+those who contributed code to this plugin, be it for bash or for
+python.
+
+-------
+History
+-------
+
+The plugin was created by Martin von Gagern in 2009, building on a
+static completion function of very limited scope distributed together
+with bzr.
+
+A version of it was merged into the bzr source tree in May 2010.
+
+----------
+References
+----------
+
+Bazaar homepage
+ | http://bazaar.canonical.com/
+Standalone plugin homepages
+ | https://launchpad.net/bzr-bash-completion
+ | http://pypi.python.org/pypi/bzr-bash-completion
+
+
+
+.. vim: ft=rst
+
+.. emacs
+ Local Variables:
+ mode: rst
+ End:
diff --git a/bzrlib/plugins/bash_completion/__init__.py b/bzrlib/plugins/bash_completion/__init__.py
new file mode 100644
index 0000000..64d506f
--- /dev/null
+++ b/bzrlib/plugins/bash_completion/__init__.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2009, 2010 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
+
+__doc__ = """Generate a shell function for bash command line completion.
+
+This plugin provides a command called bash-completion that generates a
+bash completion function for bzr. See its documentation for details.
+"""
+
+from bzrlib import commands, version_info
+
+
+bzr_plugin_name = 'bash_completion'
+bzr_commands = [ 'bash-completion' ]
+
+commands.plugin_cmds.register_lazy('cmd_bash_completion', [],
+ 'bzrlib.plugins.bash_completion.bashcomp')
+
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/bash_completion/bashcomp.py b/bzrlib/plugins/bash_completion/bashcomp.py
new file mode 100644
index 0000000..52d740e
--- /dev/null
+++ b/bzrlib/plugins/bash_completion/bashcomp.py
@@ -0,0 +1,482 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2009, 2010 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
+
+from bzrlib import (
+ cmdline,
+ commands,
+ config,
+ help_topics,
+ option,
+ plugin,
+)
+import bzrlib
+import re
+import sys
+
+
+class BashCodeGen(object):
+ """Generate a bash script for given completion data."""
+
+ def __init__(self, data, function_name='_bzr', debug=False):
+ self.data = data
+ self.function_name = function_name
+ self.debug = debug
+
+ def script(self):
+ return ("""\
+# Programmable completion for the Bazaar-NG bzr command under bash.
+# Known to work with bash 2.05a as well as bash 4.1.2, and probably
+# all versions in between as well.
+
+# Based originally on the svn bash completition script.
+# Customized by Sven Wilhelm/Icecrash.com
+# Adjusted for automatic generation by Martin von Gagern
+
+# Generated using the bash_completion plugin.
+# See https://launchpad.net/bzr-bash-completion for details.
+
+# Commands and options of bzr %(bzr_version)s
+
+shopt -s progcomp
+%(function)s
+complete -F %(function_name)s -o default bzr
+""" % {
+ "function_name": self.function_name,
+ "function": self.function(),
+ "bzr_version": self.bzr_version(),
+ })
+
+ def function(self):
+ return ("""\
+%(function_name)s ()
+{
+ local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
+ local curOpt optEnums
+ local IFS=$' \\n'
+
+ COMPREPLY=()
+ cur=${COMP_WORDS[COMP_CWORD]}
+
+ cmds='%(cmds)s'
+ globalOpts=( %(global_options)s )
+
+ # do ordinary expansion if we are anywhere after a -- argument
+ for ((i = 1; i < COMP_CWORD; ++i)); do
+ [[ ${COMP_WORDS[i]} == "--" ]] && return 0
+ done
+
+ # find the command; it's the first word not starting in -
+ cmd=
+ for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do
+ if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then
+ cmd=${COMP_WORDS[cmdIdx]}
+ break
+ fi
+ done
+
+ # complete command name if we are not already past the command
+ if [[ $COMP_CWORD -le cmdIdx ]]; then
+ COMPREPLY=( $( compgen -W "$cmds ${globalOpts[*]}" -- $cur ) )
+ return 0
+ fi
+
+ # find the option for which we want to complete a value
+ curOpt=
+ if [[ $cur != -* ]] && [[ $COMP_CWORD -gt 1 ]]; then
+ curOpt=${COMP_WORDS[COMP_CWORD - 1]}
+ if [[ $curOpt == = ]]; then
+ curOpt=${COMP_WORDS[COMP_CWORD - 2]}
+ elif [[ $cur == : ]]; then
+ cur=
+ curOpt="$curOpt:"
+ elif [[ $curOpt == : ]]; then
+ curOpt=${COMP_WORDS[COMP_CWORD - 2]}:
+ fi
+ fi
+%(debug)s
+ cmdOpts=( )
+ optEnums=( )
+ fixedWords=( )
+ case $cmd in
+%(cases)s\
+ *)
+ cmdOpts=(--help -h)
+ ;;
+ esac
+
+ IFS=$'\\n'
+ if [[ ${#fixedWords[@]} -eq 0 ]] && [[ ${#optEnums[@]} -eq 0 ]] && [[ $cur != -* ]]; then
+ case $curOpt in
+ tag:|*..tag:)
+ fixedWords=( $(bzr tags 2>/dev/null | sed 's/ *[^ ]*$//; s/ /\\\\\\\\ /g;') )
+ ;;
+ esac
+ case $cur in
+ [\\"\\']tag:*)
+ fixedWords=( $(bzr tags 2>/dev/null | sed 's/ *[^ ]*$//; s/^/tag:/') )
+ ;;
+ [\\"\\']*..tag:*)
+ fixedWords=( $(bzr tags 2>/dev/null | sed 's/ *[^ ]*$//') )
+ fixedWords=( $(for i in "${fixedWords[@]}"; do echo "${cur%%..tag:*}..tag:${i}"; done) )
+ ;;
+ esac
+ elif [[ $cur == = ]] && [[ ${#optEnums[@]} -gt 0 ]]; then
+ # complete directly after "--option=", list all enum values
+ COMPREPLY=( "${optEnums[@]}" )
+ return 0
+ else
+ fixedWords=( "${cmdOpts[@]}"
+ "${globalOpts[@]}"
+ "${optEnums[@]}"
+ "${fixedWords[@]}" )
+ fi
+
+ if [[ ${#fixedWords[@]} -gt 0 ]]; then
+ COMPREPLY=( $( compgen -W "${fixedWords[*]}" -- $cur ) )
+ fi
+
+ return 0
+}
+""" % {
+ "cmds": self.command_names(),
+ "function_name": self.function_name,
+ "cases": self.command_cases(),
+ "global_options": self.global_options(),
+ "debug": self.debug_output(),
+ })
+ # Help Emacs terminate strings: "
+
+ def command_names(self):
+ return " ".join(self.data.all_command_aliases())
+
+ def debug_output(self):
+ if not self.debug:
+ return ''
+ else:
+ return (r"""
+ # Debugging code enabled using the --debug command line switch.
+ # Will dump some variables to the top portion of the terminal.
+ echo -ne '\e[s\e[H'
+ for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do
+ echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K'
+ done
+ for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do
+ echo "\$${i}=\"${!i}\""$'\e[K'
+ done
+ echo -ne '---\e[K\e[u'
+""")
+
+ def bzr_version(self):
+ bzr_version = bzrlib.version_string
+ if not self.data.plugins:
+ bzr_version += "."
+ else:
+ bzr_version += " and the following plugins:"
+ for name, plugin in sorted(self.data.plugins.iteritems()):
+ bzr_version += "\n# %s" % plugin
+ return bzr_version
+
+ def global_options(self):
+ return " ".join(sorted(self.data.global_options))
+
+ def command_cases(self):
+ cases = ""
+ for command in self.data.commands:
+ cases += self.command_case(command)
+ return cases
+
+ def command_case(self, command):
+ case = "\t%s)\n" % "|".join(command.aliases)
+ if command.plugin:
+ case += "\t\t# plugin \"%s\"\n" % command.plugin
+ options = []
+ enums = []
+ for option in command.options:
+ for message in option.error_messages:
+ case += "\t\t# %s\n" % message
+ if option.registry_keys:
+ for key in option.registry_keys:
+ options.append("%s=%s" % (option, key))
+ enums.append("%s) optEnums=( %s ) ;;" %
+ (option, ' '.join(option.registry_keys)))
+ else:
+ options.append(str(option))
+ case += "\t\tcmdOpts=( %s )\n" % " ".join(options)
+ if command.fixed_words:
+ fixed_words = command.fixed_words
+ if isinstance(fixed_words, list):
+ fixed_words = "( %s )" + ' '.join(fixed_words)
+ case += "\t\tfixedWords=%s\n" % fixed_words
+ if enums:
+ case += "\t\tcase $curOpt in\n\t\t\t"
+ case += "\n\t\t\t".join(enums)
+ case += "\n\t\tesac\n"
+ case += "\t\t;;\n"
+ return case
+
+
+class CompletionData(object):
+
+ def __init__(self):
+ self.plugins = {}
+ self.global_options = set()
+ self.commands = []
+
+ def all_command_aliases(self):
+ for c in self.commands:
+ for a in c.aliases:
+ yield a
+
+
+class CommandData(object):
+
+ def __init__(self, name):
+ self.name = name
+ self.aliases = [name]
+ self.plugin = None
+ self.options = []
+ self.fixed_words = None
+
+
+class PluginData(object):
+
+ def __init__(self, name, version=None):
+ if version is None:
+ try:
+ version = bzrlib.plugin.plugins()[name].__version__
+ except:
+ version = 'unknown'
+ self.name = name
+ self.version = version
+
+ def __str__(self):
+ if self.version == 'unknown':
+ return self.name
+ return '%s %s' % (self.name, self.version)
+
+
+class OptionData(object):
+
+ def __init__(self, name):
+ self.name = name
+ self.registry_keys = None
+ self.error_messages = []
+
+ def __str__(self):
+ return self.name
+
+ def __cmp__(self, other):
+ return cmp(self.name, other.name)
+
+
+class DataCollector(object):
+
+ def __init__(self, no_plugins=False, selected_plugins=None):
+ self.data = CompletionData()
+ self.user_aliases = {}
+ if no_plugins:
+ self.selected_plugins = set()
+ elif selected_plugins is None:
+ self.selected_plugins = None
+ else:
+ self.selected_plugins = set([x.replace('-', '_')
+ for x in selected_plugins])
+
+ def collect(self):
+ self.global_options()
+ self.aliases()
+ self.commands()
+ return self.data
+
+ def global_options(self):
+ re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
+ help_text = help_topics.topic_registry.get_detail('global-options')
+ for long, short in re_switch.findall(help_text):
+ self.data.global_options.add(long)
+ if short:
+ self.data.global_options.add(short)
+
+ def aliases(self):
+ for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
+ for token in cmdline.split(expansion):
+ if not token.startswith("-"):
+ self.user_aliases.setdefault(token, set()).add(alias)
+ break
+
+ def commands(self):
+ for name in sorted(commands.all_command_names()):
+ self.command(name)
+
+ def command(self, name):
+ cmd = commands.get_cmd_object(name)
+ cmd_data = CommandData(name)
+
+ plugin_name = cmd.plugin_name()
+ if plugin_name is not None:
+ if (self.selected_plugins is not None and
+ plugin not in self.selected_plugins):
+ return None
+ plugin_data = self.data.plugins.get(plugin_name)
+ if plugin_data is None:
+ plugin_data = PluginData(plugin_name)
+ self.data.plugins[plugin_name] = plugin_data
+ cmd_data.plugin = plugin_data
+ self.data.commands.append(cmd_data)
+
+ # Find all aliases to the command; both cmd-defined and user-defined.
+ # We assume a user won't override one command with a different one,
+ # but will choose completely new names or add options to existing
+ # ones while maintaining the actual command name unchanged.
+ cmd_data.aliases.extend(cmd.aliases)
+ cmd_data.aliases.extend(sorted([useralias
+ for cmdalias in cmd_data.aliases
+ if cmdalias in self.user_aliases
+ for useralias in self.user_aliases[cmdalias]
+ if useralias not in cmd_data.aliases]))
+
+ opts = cmd.options()
+ for optname, opt in sorted(opts.iteritems()):
+ cmd_data.options.extend(self.option(opt))
+
+ if 'help' == name or 'help' in cmd.aliases:
+ cmd_data.fixed_words = ('($cmds %s)' %
+ " ".join(sorted(help_topics.topic_registry.keys())))
+
+ return cmd_data
+
+ def option(self, opt):
+ optswitches = {}
+ parser = option.get_optparser({opt.name: opt})
+ parser = self.wrap_parser(optswitches, parser)
+ optswitches.clear()
+ opt.add_option(parser, opt.short_name())
+ if isinstance(opt, option.RegistryOption) and opt.enum_switch:
+ enum_switch = '--%s' % opt.name
+ enum_data = optswitches.get(enum_switch)
+ if enum_data:
+ try:
+ enum_data.registry_keys = opt.registry.keys()
+ except ImportError, e:
+ enum_data.error_messages.append(
+ "ERROR getting registry keys for '--%s': %s"
+ % (opt.name, str(e).split('\n')[0]))
+ return sorted(optswitches.values())
+
+ def wrap_container(self, optswitches, parser):
+ def tweaked_add_option(*opts, **attrs):
+ for name in opts:
+ optswitches[name] = OptionData(name)
+ parser.add_option = tweaked_add_option
+ return parser
+
+ def wrap_parser(self, optswitches, parser):
+ orig_add_option_group = parser.add_option_group
+ def tweaked_add_option_group(*opts, **attrs):
+ return self.wrap_container(optswitches,
+ orig_add_option_group(*opts, **attrs))
+ parser.add_option_group = tweaked_add_option_group
+ return self.wrap_container(optswitches, parser)
+
+
+def bash_completion_function(out, function_name="_bzr", function_only=False,
+ debug=False,
+ no_plugins=False, selected_plugins=None):
+ dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
+ data = dc.collect()
+ cg = BashCodeGen(data, function_name=function_name, debug=debug)
+ if function_only:
+ res = cg.function()
+ else:
+ res = cg.script()
+ out.write(res)
+
+
+class cmd_bash_completion(commands.Command):
+ __doc__ = """Generate a shell function for bash command line completion.
+
+ This command generates a shell function which can be used by bash to
+ automatically complete the currently typed command when the user presses
+ the completion key (usually tab).
+
+ Commonly used like this:
+ eval "`bzr bash-completion`"
+ """
+
+ takes_options = [
+ option.Option("function-name", short_name="f", type=str, argname="name",
+ help="Name of the generated function (default: _bzr)"),
+ option.Option("function-only", short_name="o", type=None,
+ help="Generate only the shell function, don't enable it"),
+ option.Option("debug", type=None, hidden=True,
+ help="Enable shell code useful for debugging"),
+ option.ListOption("plugin", type=str, argname="name",
+ # param_name="selected_plugins", # doesn't work, bug #387117
+ help="Enable completions for the selected plugin"
+ + " (default: all plugins)"),
+ ]
+
+ def run(self, **kwargs):
+ if 'plugin' in kwargs:
+ # work around bug #387117 which prevents us from using param_name
+ if len(kwargs['plugin']) > 0:
+ kwargs['selected_plugins'] = kwargs['plugin']
+ del kwargs['plugin']
+ bash_completion_function(sys.stdout, **kwargs)
+
+
+if __name__ == '__main__':
+
+ import locale
+ import optparse
+
+ def plugin_callback(option, opt, value, parser):
+ values = parser.values.selected_plugins
+ if value == '-':
+ del values[:]
+ else:
+ values.append(value)
+
+ parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
+ parser.add_option("--function-name", "-f", metavar="NAME",
+ help="Name of the generated function (default: _bzr)")
+ parser.add_option("--function-only", "-o", action="store_true",
+ help="Generate only the shell function, don't enable it")
+ parser.add_option("--debug", action="store_true",
+ help=optparse.SUPPRESS_HELP)
+ parser.add_option("--no-plugins", action="store_true",
+ help="Don't load any bzr plugins")
+ parser.add_option("--plugin", metavar="NAME", type="string",
+ dest="selected_plugins", default=[],
+ action="callback", callback=plugin_callback,
+ help="Enable completions for the selected plugin"
+ + " (default: all plugins)")
+ (opts, args) = parser.parse_args()
+ if args:
+ parser.error("script does not take positional arguments")
+ kwargs = dict()
+ for name, value in opts.__dict__.iteritems():
+ if value is not None:
+ kwargs[name] = value
+
+ locale.setlocale(locale.LC_ALL, '')
+ if not kwargs.get('no_plugins', False):
+ plugin.load_plugins()
+ commands.install_bzr_command_hooks()
+ bash_completion_function(sys.stdout, **kwargs)
diff --git a/bzrlib/plugins/bash_completion/tests/__init__.py b/bzrlib/plugins/bash_completion/tests/__init__.py
new file mode 100644
index 0000000..fbe9415
--- /dev/null
+++ b/bzrlib/plugins/bash_completion/tests/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2010 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_bashcomp',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/bash_completion/tests/test_bashcomp.py b/bzrlib/plugins/bash_completion/tests/test_bashcomp.py
new file mode 100644
index 0000000..e22e6be
--- /dev/null
+++ b/bzrlib/plugins/bash_completion/tests/test_bashcomp.py
@@ -0,0 +1,332 @@
+# Copyright (C) 2010 by 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
+
+import sys
+
+import bzrlib
+from bzrlib import commands, tests
+from bzrlib.tests import features
+from bzrlib.plugins.bash_completion.bashcomp import *
+
+import subprocess
+
+
+class BashCompletionMixin(object):
+ """Component for testing execution of a bash completion script."""
+
+ _test_needs_features = [features.bash_feature]
+ script = None
+
+ def complete(self, words, cword=-1):
+ """Perform a bash completion.
+
+ :param words: a list of words representing the current command.
+ :param cword: the current word to complete, defaults to the last one.
+ """
+ if self.script is None:
+ self.script = self.get_script()
+ proc = subprocess.Popen([features.bash_feature.path,
+ '--noprofile'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ if cword < 0:
+ cword = len(words) + cword
+ input = '%s\n' % self.script
+ input += ('COMP_WORDS=( %s )\n' %
+ ' '.join(["'"+w.replace("'", "'\\''")+"'" for w in words]))
+ input += 'COMP_CWORD=%d\n' % cword
+ input += '%s\n' % getattr(self, 'script_name', '_bzr')
+ input += 'echo ${#COMPREPLY[*]}\n'
+ input += "IFS=$'\\n'\n"
+ input += 'echo "${COMPREPLY[*]}"\n'
+ (out, err) = proc.communicate(input)
+ if '' != err:
+ raise AssertionError('Unexpected error message:\n%s' % err)
+ self.assertEqual('', err, 'No messages to standard error')
+ #import sys
+ #print >>sys.stdout, '---\n%s\n---\n%s\n---\n' % (input, out)
+ lines = out.split('\n')
+ nlines = int(lines[0])
+ del lines[0]
+ self.assertEqual('', lines[-1], 'Newline at end')
+ del lines[-1]
+ if nlines == 0 and len(lines) == 1 and lines[0] == '':
+ del lines[0]
+ self.assertEqual(nlines, len(lines), 'No newlines in generated words')
+ self.completion_result = set(lines)
+ return self.completion_result
+
+ def assertCompletionEquals(self, *words):
+ self.assertEqual(set(words), self.completion_result)
+
+ def assertCompletionContains(self, *words):
+ missing = set(words) - self.completion_result
+ if missing:
+ raise AssertionError('Completion should contain %r but it has %r'
+ % (missing, self.completion_result))
+
+ def assertCompletionOmits(self, *words):
+ surplus = set(words) & self.completion_result
+ if surplus:
+ raise AssertionError('Completion should omit %r but it has %r'
+ % (surplus, res, self.completion_result))
+
+ def get_script(self):
+ commands.install_bzr_command_hooks()
+ dc = DataCollector()
+ data = dc.collect()
+ cg = BashCodeGen(data)
+ res = cg.function()
+ return res
+
+
+class TestBashCompletion(tests.TestCase, BashCompletionMixin):
+ """Test bash completions that don't execute bzr."""
+
+ def test_simple_scipt(self):
+ """Ensure that the test harness works as expected"""
+ self.script = """
+_bzr() {
+ COMPREPLY=()
+ # add all words in reverse order, with some markup around them
+ for ((i = ${#COMP_WORDS[@]}; i > 0; --i)); do
+ COMPREPLY+=( "-${COMP_WORDS[i-1]}+" )
+ done
+ # and append the current word
+ COMPREPLY+=( "+${COMP_WORDS[COMP_CWORD]}-" )
+}
+"""
+ self.complete(['foo', '"bar', "'baz"], cword=1)
+ self.assertCompletionEquals("-'baz+", '-"bar+', '-foo+', '+"bar-')
+
+ def test_cmd_ini(self):
+ self.complete(['bzr', 'ini'])
+ self.assertCompletionContains('init', 'init-repo', 'init-repository')
+ self.assertCompletionOmits('commit')
+
+ def test_init_opts(self):
+ self.complete(['bzr', 'init', '-'])
+ self.assertCompletionContains('-h', '--2a', '--format=2a')
+
+ def test_global_opts(self):
+ self.complete(['bzr', '-', 'init'], cword=1)
+ self.assertCompletionContains('--no-plugins', '--builtin')
+
+ def test_commit_dashm(self):
+ self.complete(['bzr', 'commit', '-m'])
+ self.assertCompletionEquals('-m')
+
+ def test_status_negated(self):
+ self.complete(['bzr', 'status', '--n'])
+ self.assertCompletionContains('--no-versioned', '--no-verbose')
+
+ def test_init_format_any(self):
+ self.complete(['bzr', 'init', '--format', '=', 'directory'], cword=3)
+ self.assertCompletionContains('1.9', '2a')
+
+ def test_init_format_2(self):
+ self.complete(['bzr', 'init', '--format', '=', '2', 'directory'],
+ cword=4)
+ self.assertCompletionContains('2a')
+ self.assertCompletionOmits('1.9')
+
+
+class TestBashCompletionInvoking(tests.TestCaseWithTransport,
+ BashCompletionMixin):
+ """Test bash completions that might execute bzr.
+
+ Only the syntax ``$(bzr ...`` is supported so far. The bzr command
+ will be replaced by the bzr instance running this selftest.
+ """
+
+ def setUp(self):
+ super(TestBashCompletionInvoking, self).setUp()
+ if sys.platform == 'win32':
+ raise tests.KnownFailure(
+ 'see bug #709104, completion is broken on windows')
+
+ def get_script(self):
+ s = super(TestBashCompletionInvoking, self).get_script()
+ return s.replace("$(bzr ", "$('%s' " % self.get_bzr_path())
+
+ def test_revspec_tag_all(self):
+ self.requireFeature(features.sed_feature)
+ wt = self.make_branch_and_tree('.', format='dirstate-tags')
+ wt.branch.tags.set_tag('tag1', 'null:')
+ wt.branch.tags.set_tag('tag2', 'null:')
+ wt.branch.tags.set_tag('3tag', 'null:')
+ self.complete(['bzr', 'log', '-r', 'tag', ':'])
+ self.assertCompletionEquals('tag1', 'tag2', '3tag')
+
+ def test_revspec_tag_prefix(self):
+ self.requireFeature(features.sed_feature)
+ wt = self.make_branch_and_tree('.', format='dirstate-tags')
+ wt.branch.tags.set_tag('tag1', 'null:')
+ wt.branch.tags.set_tag('tag2', 'null:')
+ wt.branch.tags.set_tag('3tag', 'null:')
+ self.complete(['bzr', 'log', '-r', 'tag', ':', 't'])
+ self.assertCompletionEquals('tag1', 'tag2')
+
+ def test_revspec_tag_spaces(self):
+ self.requireFeature(features.sed_feature)
+ wt = self.make_branch_and_tree('.', format='dirstate-tags')
+ wt.branch.tags.set_tag('tag with spaces', 'null:')
+ self.complete(['bzr', 'log', '-r', 'tag', ':', 't'])
+ self.assertCompletionEquals(r'tag\ with\ spaces')
+ self.complete(['bzr', 'log', '-r', '"tag:t'])
+ self.assertCompletionEquals('tag:tag with spaces')
+ self.complete(['bzr', 'log', '-r', "'tag:t"])
+ self.assertCompletionEquals('tag:tag with spaces')
+
+ def test_revspec_tag_endrange(self):
+ self.requireFeature(features.sed_feature)
+ wt = self.make_branch_and_tree('.', format='dirstate-tags')
+ wt.branch.tags.set_tag('tag1', 'null:')
+ wt.branch.tags.set_tag('tag2', 'null:')
+ self.complete(['bzr', 'log', '-r', '3..tag', ':', 't'])
+ self.assertCompletionEquals('tag1', 'tag2')
+ self.complete(['bzr', 'log', '-r', '"3..tag:t'])
+ self.assertCompletionEquals('3..tag:tag1', '3..tag:tag2')
+ self.complete(['bzr', 'log', '-r', "'3..tag:t"])
+ self.assertCompletionEquals('3..tag:tag1', '3..tag:tag2')
+
+
+class TestBashCodeGen(tests.TestCase):
+
+ def test_command_names(self):
+ data = CompletionData()
+ bar = CommandData('bar')
+ bar.aliases.append('baz')
+ data.commands.append(bar)
+ data.commands.append(CommandData('foo'))
+ cg = BashCodeGen(data)
+ self.assertEqual('bar baz foo', cg.command_names())
+
+ def test_debug_output(self):
+ data = CompletionData()
+ self.assertEqual('', BashCodeGen(data, debug=False).debug_output())
+ self.assertTrue(BashCodeGen(data, debug=True).debug_output())
+
+ def test_bzr_version(self):
+ data = CompletionData()
+ cg = BashCodeGen(data)
+ self.assertEqual('%s.' % bzrlib.version_string, cg.bzr_version())
+ data.plugins['foo'] = PluginData('foo', '1.0')
+ data.plugins['bar'] = PluginData('bar', '2.0')
+ cg = BashCodeGen(data)
+ self.assertEqual('''\
+%s and the following plugins:
+# bar 2.0
+# foo 1.0''' % bzrlib.version_string, cg.bzr_version())
+
+ def test_global_options(self):
+ data = CompletionData()
+ data.global_options.add('--foo')
+ data.global_options.add('--bar')
+ cg = BashCodeGen(data)
+ self.assertEqual('--bar --foo', cg.global_options())
+
+ def test_command_cases(self):
+ data = CompletionData()
+ bar = CommandData('bar')
+ bar.aliases.append('baz')
+ bar.options.append(OptionData('--opt'))
+ data.commands.append(bar)
+ data.commands.append(CommandData('foo'))
+ cg = BashCodeGen(data)
+ self.assertEqualDiff('''\
+\tbar|baz)
+\t\tcmdOpts=( --opt )
+\t\t;;
+\tfoo)
+\t\tcmdOpts=( )
+\t\t;;
+''', cg.command_cases())
+
+ def test_command_case(self):
+ cmd = CommandData('cmd')
+ cmd.plugin = PluginData('plugger', '1.0')
+ bar = OptionData('--bar')
+ bar.registry_keys = ['that', 'this']
+ bar.error_messages.append('Some error message')
+ cmd.options.append(bar)
+ cmd.options.append(OptionData('--foo'))
+ data = CompletionData()
+ data.commands.append(cmd)
+ cg = BashCodeGen(data)
+ self.assertEqualDiff('''\
+\tcmd)
+\t\t# plugin "plugger 1.0"
+\t\t# Some error message
+\t\tcmdOpts=( --bar=that --bar=this --foo )
+\t\tcase $curOpt in
+\t\t\t--bar) optEnums=( that this ) ;;
+\t\tesac
+\t\t;;
+''', cg.command_case(cmd))
+
+
+class TestDataCollector(tests.TestCase):
+
+ def setUp(self):
+ super(TestDataCollector, self).setUp()
+ commands.install_bzr_command_hooks()
+
+ def test_global_options(self):
+ dc = DataCollector()
+ dc.global_options()
+ self.assertSubset(['--no-plugins', '--builtin'],
+ dc.data.global_options)
+
+ def test_commands(self):
+ dc = DataCollector()
+ dc.commands()
+ self.assertSubset(['init', 'init-repo', 'init-repository'],
+ dc.data.all_command_aliases())
+
+ def test_commands_from_plugins(self):
+ dc = DataCollector()
+ dc.commands()
+ self.assertSubset(['bash-completion'],
+ dc.data.all_command_aliases())
+
+ def test_commit_dashm(self):
+ dc = DataCollector()
+ cmd = dc.command('commit')
+ self.assertSubset(['-m'],
+ [str(o) for o in cmd.options])
+
+ def test_status_negated(self):
+ dc = DataCollector()
+ cmd = dc.command('status')
+ self.assertSubset(['--no-versioned', '--no-verbose'],
+ [str(o) for o in cmd.options])
+
+ def test_init_format(self):
+ dc = DataCollector()
+ cmd = dc.command('init')
+ for opt in cmd.options:
+ if opt.name == '--format':
+ self.assertSubset(['2a'], opt.registry_keys)
+ return
+ raise AssertionError('Option --format not found')
+
+
+class BlackboxTests(tests.TestCase):
+
+ def test_bash_completion(self):
+ self.run_bzr("bash-completion")
diff --git a/bzrlib/plugins/changelog_merge/__init__.py b/bzrlib/plugins/changelog_merge/__init__.py
new file mode 100644
index 0000000..3a48bd4
--- /dev/null
+++ b/bzrlib/plugins/changelog_merge/__init__.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2010 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
+
+__doc__ = """Merge hook for GNU-format ChangeLog files
+
+To enable this plugin, add a section to your locations.conf
+like::
+
+ [/home/user/proj]
+ changelog_merge_files = ChangeLog
+
+Or add an entry to your branch.conf like::
+
+ changelog_merge_files = ChangeLog
+
+The changelog_merge_files config option takes a list of file names (not paths),
+separated by commas. (This is unlike the news_merge plugin, which matches
+paths.) e.g. the above config examples would match both
+``src/foolib/ChangeLog`` and ``docs/ChangeLog``.
+
+The algorithm used to merge the changes can be summarised as:
+
+ * new entries added to the top of OTHER are emitted first
+ * all other additions, deletions and edits from THIS and OTHER are preserved
+ * edits (e.g. to fix typos) at the top of OTHER are hard to distinguish from
+ adding and deleting independent entries; the algorithm tries to guess which
+ based on how similar the old and new entries are.
+
+Caveats
+-------
+
+Most changes can be merged, but conflicts are possible if the plugin finds
+edits at the top of OTHER to entries that have been deleted (or also edited) by
+THIS. In that case the plugin gives up and bzr's default merge logic will be
+used.
+
+No effort is made to deduplicate entries added by both sides.
+
+The results depend on the choice of the 'base' version, so it might give
+strange results if there is a criss-cross merge.
+"""
+
+# Since we are a built-in plugin we share the bzrlib version
+from bzrlib import version_info
+from bzrlib.hooks import install_lazy_named_hook
+
+# Put most of the code in a separate module that we lazy-import to keep the
+# overhead of this plugin as minimal as possible.
+def changelog_merge_hook(merger):
+ """Merger.merge_file_content hook for GNU-format ChangeLog files."""
+ from bzrlib.plugins.changelog_merge.changelog_merge import ChangeLogMerger
+ return ChangeLogMerger(merger)
+
+install_lazy_named_hook("bzrlib.merge", "Merger.hooks", "merge_file_content",
+ changelog_merge_hook, 'GNU ChangeLog file merge')
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/changelog_merge/changelog_merge.py b/bzrlib/plugins/changelog_merge/changelog_merge.py
new file mode 100644
index 0000000..2b3ce8a
--- /dev/null
+++ b/bzrlib/plugins/changelog_merge/changelog_merge.py
@@ -0,0 +1,199 @@
+# Copyright (C) 2010 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
+
+"""Merge logic for changelog_merge plugin."""
+
+from __future__ import absolute_import
+
+import difflib
+
+from bzrlib import (
+ debug,
+ merge,
+ urlutils,
+ )
+from bzrlib.merge3 import Merge3
+from bzrlib.trace import mutter
+
+
+def changelog_entries(lines):
+ """Return a list of changelog entries.
+
+ :param lines: lines of a changelog file.
+ :returns: list of entries. Each entry is a tuple of lines.
+ """
+ entries = []
+ for line in lines:
+ if line[0] not in (' ', '\t', '\n'):
+ # new entry
+ entries.append([line])
+ else:
+ try:
+ entry = entries[-1]
+ except IndexError:
+ # Cope with leading blank lines.
+ entries.append([])
+ entry = entries[-1]
+ entry.append(line)
+ return map(tuple, entries)
+
+
+def entries_to_lines(entries):
+ """Turn a list of entries into a flat iterable of lines."""
+ for entry in entries:
+ for line in entry:
+ yield line
+
+
+class ChangeLogMerger(merge.ConfigurableFileMerger):
+ """Merge GNU-format ChangeLog files."""
+
+ name_prefix = "changelog"
+
+ def get_filepath(self, params, tree):
+ """Calculate the path to the file in a tree.
+
+ This is overridden to return just the basename, rather than full path,
+ so that e.g. if the config says ``changelog_merge_files = ChangeLog``,
+ then all ChangeLog files in the tree will match (not just one in the
+ root of the tree).
+
+ :param params: A MergeHookParams describing the file to merge
+ :param tree: a Tree, e.g. self.merger.this_tree.
+ """
+ return urlutils.basename(tree.id2path(params.file_id))
+
+ def merge_text(self, params):
+ """Merge changelog changes.
+
+ * new entries from other will float to the top
+ * edits to older entries are preserved
+ """
+ # Transform files into lists of changelog entries
+ this_entries = changelog_entries(params.this_lines)
+ other_entries = changelog_entries(params.other_lines)
+ base_entries = changelog_entries(params.base_lines)
+ try:
+ result_entries = merge_entries(
+ base_entries, this_entries, other_entries)
+ except EntryConflict:
+ # XXX: generating a nice conflict file would be better
+ return 'not_applicable', None
+ # Transform the merged elements back into real blocks of lines.
+ return 'success', entries_to_lines(result_entries)
+
+
+class EntryConflict(Exception):
+ pass
+
+
+def default_guess_edits(new_entries, deleted_entries, entry_as_str=''.join):
+ """Default implementation of guess_edits param of merge_entries.
+
+ This algorithm does O(N^2 * logN) SequenceMatcher.ratio() calls, which is
+ pretty bad, but it shouldn't be used very often.
+ """
+ deleted_entries_as_strs = map(entry_as_str, deleted_entries)
+ new_entries_as_strs = map(entry_as_str, new_entries)
+ result_new = list(new_entries)
+ result_deleted = list(deleted_entries)
+ result_edits = []
+ sm = difflib.SequenceMatcher()
+ CUTOFF = 0.8
+ while True:
+ best = None
+ best_score = CUTOFF
+ # Compare each new entry with each old entry to find the best match
+ for new_entry_as_str in new_entries_as_strs:
+ sm.set_seq1(new_entry_as_str)
+ for old_entry_as_str in deleted_entries_as_strs:
+ sm.set_seq2(old_entry_as_str)
+ score = sm.ratio()
+ if score > best_score:
+ best = new_entry_as_str, old_entry_as_str
+ best_score = score
+ if best is not None:
+ # Add the best match to the list of edits, and remove it from the
+ # the list of new/old entries. Also remove it from the new/old
+ # lists for the next round.
+ del_index = deleted_entries_as_strs.index(best[1])
+ new_index = new_entries_as_strs.index(best[0])
+ result_edits.append(
+ (result_deleted[del_index], result_new[new_index]))
+ del deleted_entries_as_strs[del_index], result_deleted[del_index]
+ del new_entries_as_strs[new_index], result_new[new_index]
+ else:
+ # No match better than CUTOFF exists in the remaining new and old
+ # entries.
+ break
+ return result_new, result_deleted, result_edits
+
+
+def merge_entries(base_entries, this_entries, other_entries,
+ guess_edits=default_guess_edits):
+ """Merge changelog given base, this, and other versions."""
+ m3 = Merge3(base_entries, this_entries, other_entries, allow_objects=True)
+ result_entries = []
+ at_top = True
+ for group in m3.merge_groups():
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('merge group:\n%r', group)
+ group_kind = group[0]
+ if group_kind == 'conflict':
+ _, base, this, other = group
+ # Find additions
+ new_in_other = [
+ entry for entry in other if entry not in base]
+ # Find deletions
+ deleted_in_other = [
+ entry for entry in base if entry not in other]
+ if at_top and deleted_in_other:
+ # Magic! Compare deletions and additions to try spot edits
+ new_in_other, deleted_in_other, edits_in_other = guess_edits(
+ new_in_other, deleted_in_other)
+ else:
+ # Changes not made at the top are always preserved as is, no
+ # need to try distinguish edits from adds and deletes.
+ edits_in_other = []
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('at_top: %r', at_top)
+ mutter('new_in_other: %r', new_in_other)
+ mutter('deleted_in_other: %r', deleted_in_other)
+ mutter('edits_in_other: %r', edits_in_other)
+ # Apply deletes and edits
+ updated_this = [
+ entry for entry in this if entry not in deleted_in_other]
+ for old_entry, new_entry in edits_in_other:
+ try:
+ index = updated_this.index(old_entry)
+ except ValueError:
+ # edited entry no longer present in this! Just give up and
+ # declare a conflict.
+ raise EntryConflict()
+ updated_this[index] = new_entry
+ if 'changelog_merge' in debug.debug_flags:
+ mutter('updated_this: %r', updated_this)
+ if at_top:
+ # Float new entries from other to the top
+ result_entries = new_in_other + result_entries
+ else:
+ result_entries.extend(new_in_other)
+ result_entries.extend(updated_this)
+ else: # unchanged, same, a, or b.
+ lines = group[1]
+ result_entries.extend(lines)
+ at_top = False
+ return result_entries
diff --git a/bzrlib/plugins/changelog_merge/tests/__init__.py b/bzrlib/plugins/changelog_merge/tests/__init__.py
new file mode 100644
index 0000000..62c2658
--- /dev/null
+++ b/bzrlib/plugins/changelog_merge/tests/__init__.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2011 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_changelog_merge',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
diff --git a/bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py b/bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py
new file mode 100644
index 0000000..a7b0d3f
--- /dev/null
+++ b/bzrlib/plugins/changelog_merge/tests/test_changelog_merge.py
@@ -0,0 +1,222 @@
+# Copyright (C) 2011 by 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 bzrlib import (
+ merge,
+ tests,
+ )
+from bzrlib.tests import test_merge_core
+from bzrlib.plugins.changelog_merge import changelog_merge
+
+
+sample_base_entries = [
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample_this_entries = [
+ 'This entry T1',
+ 'This entry T2',
+ #'Base entry B1 updated',
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample_other_entries = [
+ 'Other entry O1',
+ #'Base entry B1',
+ 'Base entry B1',
+ 'Base entry B2 updated',
+ 'Base entry B3',
+ ]
+
+
+sample2_base_entries = [
+ 'Base entry B1',
+ 'Base entry B2',
+ 'Base entry B3',
+ ]
+
+sample2_this_entries = [
+ 'This entry T1',
+ 'This entry T2',
+ #'Base entry B1 updated',
+ 'Base entry B1',
+ 'Base entry B2',
+ ]
+
+sample2_other_entries = [
+ 'Other entry O1',
+ #'Base entry B1',
+ 'Base entry B1 edit', # > 80% similar according to difflib
+ 'Base entry B2',
+ ]
+
+
+class TestMergeCoreLogic(tests.TestCase):
+
+ def test_new_in_other_floats_to_top(self):
+ """Changes at the top of 'other' float to the top.
+
+ Given a changelog in THIS containing::
+
+ NEW-1
+ OLD-1
+
+ and a changelog in OTHER containing::
+
+ NEW-2
+ OLD-1
+
+ it will merge as::
+
+ NEW-2
+ NEW-1
+ OLD-1
+ """
+ base_entries = ['OLD-1']
+ this_entries = ['NEW-1', 'OLD-1']
+ other_entries = ['NEW-2', 'OLD-1']
+ result_entries = changelog_merge.merge_entries(
+ base_entries, this_entries, other_entries)
+ self.assertEqual(
+ ['NEW-2', 'NEW-1', 'OLD-1'], result_entries)
+
+ def test_acceptance_bug_723968(self):
+ """Merging a branch that:
+
+ 1. adds a new entry, and
+ 2. edits an old entry (e.g. to fix a typo or twiddle formatting)
+
+ will:
+
+ 1. add the new entry to the top
+ 2. keep the edit, without duplicating the edited entry or moving it.
+ """
+ result_entries = changelog_merge.merge_entries(
+ sample_base_entries, sample_this_entries, sample_other_entries)
+ self.assertEqual([
+ 'Other entry O1',
+ 'This entry T1',
+ 'This entry T2',
+ 'Base entry B1',
+ 'Base entry B2 updated',
+ 'Base entry B3',
+ ],
+ list(result_entries))
+
+ def test_more_complex_conflict(self):
+ """Like test_acceptance_bug_723968, but with a more difficult conflict:
+ the new entry and the edited entry are adjacent.
+ """
+ def guess_edits(new, deleted):
+ #import pdb; pdb.set_trace()
+ return changelog_merge.default_guess_edits(new, deleted,
+ entry_as_str=lambda x: x)
+ result_entries = changelog_merge.merge_entries(
+ sample2_base_entries, sample2_this_entries, sample2_other_entries,
+ guess_edits=guess_edits)
+ self.assertEqual([
+ 'Other entry O1',
+ 'This entry T1',
+ 'This entry T2',
+ 'Base entry B1 edit',
+ 'Base entry B2',
+ ],
+ list(result_entries))
+
+ def test_too_hard(self):
+ """A conflict this plugin cannot resolve raises EntryConflict.
+ """
+ # An entry edited in other but deleted in this is a conflict we can't
+ # resolve. (Ideally perhaps we'd generate a nice conflict file, but
+ # for now we just give up.)
+ self.assertRaises(changelog_merge.EntryConflict,
+ changelog_merge.merge_entries,
+ sample2_base_entries, [], sample2_other_entries)
+
+ def test_default_guess_edits(self):
+ """default_guess_edits matches a new entry only once.
+
+ (Even when that entry is the best match for multiple old entries.)
+ """
+ new_in_other = [('AAAAA',), ('BBBBB',)]
+ deleted_in_other = [('DDDDD',), ('BBBBBx',), ('BBBBBxx',)]
+ # BBBBB is the best match for both BBBBBx and BBBBBxx
+ result = changelog_merge.default_guess_edits(
+ new_in_other, deleted_in_other)
+ self.assertEqual(
+ ([('AAAAA',)], # new
+ [('DDDDD',), ('BBBBBxx',)], # deleted
+ [(('BBBBBx',), ('BBBBB',))]), # edits
+ result)
+
+
+class TestChangeLogMerger(tests.TestCaseWithTransport):
+ """Tests for ChangeLogMerger class.
+
+ Most tests should be unit tests for merge_entries (and its helpers).
+ This class is just to cover the handful of lines of code in ChangeLogMerger
+ itself.
+ """
+
+ def make_builder(self):
+ builder = test_merge_core.MergeBuilder(self.test_base_dir)
+ self.addCleanup(builder.cleanup)
+ return builder
+
+ def make_changelog_merger(self, base_text, this_text, other_text):
+ builder = self.make_builder()
+ builder.add_file('clog-id', builder.tree_root, 'ChangeLog',
+ base_text, True)
+ builder.change_contents('clog-id', other=other_text, this=this_text)
+ merger = builder.make_merger(merge.Merge3Merger, ['clog-id'])
+ # The following can't use config stacks until the plugin itself does
+ # ('this_branch' is already write locked at this point and as such
+ # won't write the new value to disk where get_user_option can get it).
+ merger.this_branch.get_config().set_user_option(
+ 'changelog_merge_files', 'ChangeLog')
+ merge_hook_params = merge.MergeFileHookParams(merger, 'clog-id', None,
+ 'file', 'file', 'conflict')
+ changelog_merger = changelog_merge.ChangeLogMerger(merger)
+ return changelog_merger, merge_hook_params
+
+ def test_merge_text_returns_not_applicable(self):
+ """A conflict this plugin cannot resolve returns (not_applicable, None).
+ """
+ # Build same example as TestMergeCoreLogic.test_too_hard: edit an entry
+ # in other but delete it in this.
+ def entries_as_str(entries):
+ return ''.join(entry + '\n' for entry in entries)
+ changelog_merger, merge_hook_params = self.make_changelog_merger(
+ entries_as_str(sample2_base_entries),
+ '',
+ entries_as_str(sample2_other_entries))
+ self.assertEqual(
+ ('not_applicable', None),
+ changelog_merger.merge_contents(merge_hook_params))
+
+ def test_merge_text_returns_success(self):
+ """A successful merge returns ('success', lines)."""
+ changelog_merger, merge_hook_params = self.make_changelog_merger(
+ '', 'this text\n', 'other text\n')
+ status, lines = changelog_merger.merge_contents(merge_hook_params)
+ self.assertEqual(
+ ('success', ['other text\n', 'this text\n']),
+ (status, list(lines)))
+
diff --git a/bzrlib/plugins/launchpad/__init__.py b/bzrlib/plugins/launchpad/__init__.py
new file mode 100644
index 0000000..5673d95
--- /dev/null
+++ b/bzrlib/plugins/launchpad/__init__.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2006-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
+
+"""Launchpad.net integration plugin for Bazaar.
+
+This plugin provides facilities for working with Bazaar branches that are
+hosted on Launchpad (http://launchpad.net). It provides a directory service
+for referring to Launchpad branches using the "lp:" prefix. For example,
+lp:bzr refers to the Bazaar's main development branch and
+lp:~username/project/branch-name can be used to refer to a specific branch.
+
+This plugin provides a bug tracker so that "bzr commit --fixes lp:1234" will
+record that revision as fixing Launchpad's bug 1234.
+
+The plugin also provides the following commands:
+
+ launchpad-login: Show or set the Launchpad user ID
+ launchpad-open: Open a Launchpad branch page in your web browser
+ lp-propose-merge: Propose merging a branch on Launchpad
+ register-branch: Register a branch with launchpad.net
+ launchpad-mirror: Ask Launchpad to mirror a branch now
+
+"""
+
+from __future__ import absolute_import
+
+# The XMLRPC server address can be overridden by setting the environment
+# variable $BZR_LP_XMLRPC_URL
+
+# see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
+
+from bzrlib import (
+ branch as _mod_branch,
+ config as _mod_config,
+ lazy_regex,
+ # Since we are a built-in plugin we share the bzrlib version
+ trace,
+ version_info,
+ )
+from bzrlib.commands import (
+ plugin_cmds,
+ )
+from bzrlib.directory_service import directories
+from bzrlib.help_topics import topic_registry
+
+for klsname, aliases in [
+ ("cmd_register_branch", []),
+ ("cmd_launchpad_open", ["lp-open"]),
+ ("cmd_launchpad_login", ["lp-login"]),
+ ("cmd_launchpad_mirror", ["lp-mirror"]),
+ ("cmd_lp_propose_merge", ["lp-submit", "lp-propose"]),
+ ("cmd_lp_find_proposal", [])]:
+ plugin_cmds.register_lazy(klsname, aliases,
+ "bzrlib.plugins.launchpad.cmds")
+
+
+def _register_directory():
+ directories.register_lazy('lp:', 'bzrlib.plugins.launchpad.lp_directory',
+ 'LaunchpadDirectory',
+ 'Launchpad-based directory service',)
+ directories.register_lazy(
+ 'debianlp:', 'bzrlib.plugins.launchpad.lp_directory',
+ 'LaunchpadDirectory',
+ 'debianlp: shortcut')
+ directories.register_lazy(
+ 'ubuntu:', 'bzrlib.plugins.launchpad.lp_directory',
+ 'LaunchpadDirectory',
+ 'ubuntu: shortcut')
+
+_register_directory()
+
+# This is kept in __init__ so that we don't load lp_api_lite unless the branch
+# actually matches. That way we can avoid importing extra dependencies like
+# json.
+_package_branch = lazy_regex.lazy_compile(
+ r'bazaar.launchpad.net.*?/'
+ r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
+ r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
+ )
+
+def _get_package_branch_info(url):
+ """Determine the packaging information for this URL.
+
+ :return: If this isn't a packaging branch, return None. If it is, return
+ (archive, series, project)
+ """
+ if url is None:
+ return None
+ m = _package_branch.search(url)
+ if m is None:
+ return None
+ archive, series, project, user = m.group('archive', 'series',
+ 'project', 'user')
+ if series is not None:
+ # series is optional, so the regex includes the extra '/', we don't
+ # want to send that on (it causes Internal Server Errors.)
+ series = series.strip('/')
+ if user is not None:
+ user = user.strip('~/')
+ if user != 'ubuntu-branches':
+ return None
+ return archive, series, project
+
+
+def _check_is_up_to_date(the_branch):
+ info = _get_package_branch_info(the_branch.base)
+ if info is None:
+ return
+ c = the_branch.get_config_stack()
+ verbosity = c.get('launchpad.packaging_verbosity')
+ if not verbosity:
+ trace.mutter('not checking %s because verbosity is turned off'
+ % (the_branch.base,))
+ return
+ archive, series, project = info
+ from bzrlib.plugins.launchpad import lp_api_lite
+ latest_pub = lp_api_lite.LatestPublication(archive, series, project)
+ lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
+
+
+def _register_hooks():
+ _mod_branch.Branch.hooks.install_named_hook('open',
+ _check_is_up_to_date, 'package-branch-up-to-date')
+
+
+_register_hooks()
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_account',
+ 'test_register',
+ 'test_lp_api',
+ 'test_lp_api_lite',
+ 'test_lp_directory',
+ 'test_lp_login',
+ 'test_lp_open',
+ 'test_lp_service',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
+
+_launchpad_help = """Integration with Launchpad.net
+
+Launchpad.net provides free Bazaar branch hosting with integrated bug and
+specification tracking.
+
+The bzr client (through the plugin called 'launchpad') has special
+features to communicate with Launchpad:
+
+ * The launchpad-login command tells Bazaar your Launchpad user name. This
+ is then used by the 'lp:' transport to download your branches using
+ bzr+ssh://.
+
+ * The 'lp:' transport uses Launchpad as a directory service: for example
+ 'lp:bzr' and 'lp:python' refer to the main branches of the relevant
+ projects and may be branched, logged, etc. You can also use the 'lp:'
+ transport to refer to specific branches, e.g. lp:~bzr/bzr/trunk.
+
+ * The 'lp:' bug tracker alias can expand launchpad bug numbers to their
+ URLs for use with 'bzr commit --fixes', e.g. 'bzr commit --fixes lp:12345'
+ will record a revision property that marks that revision as fixing
+ Launchpad bug 12345. When you push that branch to Launchpad it will
+ automatically be linked to the bug report.
+
+ * The register-branch command tells Launchpad about the url of a
+ public branch. Launchpad will then mirror the branch, display
+ its contents and allow it to be attached to bugs and other
+ objects.
+
+For more information see http://help.launchpad.net/
+"""
+topic_registry.register('launchpad',
+ _launchpad_help,
+ 'Using Bazaar with Launchpad.net')
+
+_mod_config.option_registry.register(
+ _mod_config.Option('launchpad.packaging_verbosity', default=True,
+ from_unicode=_mod_config.bool_from_store,
+ help="""\
+Whether to warn if a UDD package import branch is accessed that is out of date.
+
+Setting this option to 'off' will disable verbosity.
+"""))
+_mod_config.option_registry.register(
+ _mod_config.Option('launchpad_username', default=None,
+ help="The username to login with when conneting to Launchpad."))
diff --git a/bzrlib/plugins/launchpad/account.py b/bzrlib/plugins/launchpad/account.py
new file mode 100644
index 0000000..657b261
--- /dev/null
+++ b/bzrlib/plugins/launchpad/account.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2007-2010 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
+
+"""Functions to manage the user's Launchpad user ID.
+
+This allows the user to configure their Launchpad user ID once, rather
+than once for each place that needs to take it into account.
+"""
+
+from __future__ import absolute_import
+
+from bzrlib import (
+ errors,
+ trace,
+ transport,
+ )
+from bzrlib.config import AuthenticationConfig, GlobalStack
+from bzrlib.i18n import gettext
+
+LAUNCHPAD_BASE = 'https://launchpad.net/'
+
+
+class UnknownLaunchpadUsername(errors.BzrError):
+ _fmt = "The user name %(user)s is not registered on Launchpad."
+
+
+class NoRegisteredSSHKeys(errors.BzrError):
+ _fmt = "The user %(user)s has not registered any SSH keys with Launchpad.\n" \
+ "See <https://launchpad.net/people/+me>"
+
+
+class MismatchedUsernames(errors.BzrError):
+
+ _fmt = ('bazaar.conf and authentication.conf disagree about launchpad'
+ ' account name. Please re-run launchpad-login.')
+
+
+def get_lp_login(_config=None):
+ """Return the user's Launchpad username.
+
+ :raises: MismatchedUsername if authentication.conf and bazaar.conf
+ disagree about username.
+ """
+ if _config is None:
+ _config = GlobalStack()
+
+ username = _config.get('launchpad_username')
+ if username is not None:
+ auth = AuthenticationConfig()
+ auth_username = _get_auth_user(auth)
+ # Auto-upgrading
+ if auth_username is None:
+ trace.note(gettext('Setting ssh/sftp usernames for launchpad.net.'))
+ _set_auth_user(username, auth)
+ elif auth_username != username:
+ raise MismatchedUsernames()
+ return username
+
+
+def _set_global_option(username, _config=None):
+ if _config is None:
+ _config = GlobalStack()
+ _config.set('launchpad_username', username)
+
+
+def set_lp_login(username, _config=None):
+ """Set the user's Launchpad username"""
+ _set_global_option(username, _config)
+ _set_auth_user(username)
+
+
+def _get_auth_user(auth=None):
+ if auth is None:
+ auth = AuthenticationConfig()
+ username = auth.get_user('ssh', '.launchpad.net')
+ return username
+
+def _set_auth_user(username, auth=None):
+ if auth is None:
+ auth = AuthenticationConfig()
+ auth.set_credentials(
+ 'Launchpad', '.launchpad.net', username, 'ssh')
+
+
+def check_lp_login(username, _transport=None):
+ """Check whether the given Launchpad username is okay.
+
+ This will check for both existence and whether the user has
+ uploaded SSH keys.
+ """
+ if _transport is None:
+ _transport = transport.get_transport_from_url(LAUNCHPAD_BASE)
+
+ try:
+ data = _transport.get_bytes('~%s/+sshkeys' % username)
+ except errors.NoSuchFile:
+ raise UnknownLaunchpadUsername(user=username)
+
+ if not data:
+ raise NoRegisteredSSHKeys(user=username)
diff --git a/bzrlib/plugins/launchpad/cmds.py b/bzrlib/plugins/launchpad/cmds.py
new file mode 100644
index 0000000..1b2844e
--- /dev/null
+++ b/bzrlib/plugins/launchpad/cmds.py
@@ -0,0 +1,410 @@
+# Copyright (C) 2006-2012 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
+
+"""Launchpad plugin commands."""
+
+from __future__ import absolute_import
+
+from bzrlib import (
+ branch as _mod_branch,
+ controldir,
+ trace,
+ )
+from bzrlib.commands import (
+ Command,
+ )
+from bzrlib.errors import (
+ BzrCommandError,
+ InvalidRevisionSpec,
+ InvalidURL,
+ NoPublicBranch,
+ NotBranchError,
+ )
+from bzrlib.i18n import gettext
+from bzrlib.option import (
+ Option,
+ ListOption,
+ )
+
+
+class cmd_register_branch(Command):
+ __doc__ = """Register a branch with launchpad.net.
+
+ This command lists a bzr branch in the directory of branches on
+ launchpad.net. Registration allows the branch to be associated with
+ bugs or specifications.
+
+ Before using this command you must register the project to which the
+ branch belongs, and create an account for yourself on launchpad.net.
+
+ arguments:
+ public_url: The publicly visible url for the branch to register.
+ This must be an http or https url (which Launchpad can read
+ from to access the branch). Local file urls, SFTP urls, and
+ bzr+ssh urls will not work.
+ If no public_url is provided, bzr will use the configured
+ public_url if there is one for the current branch, and
+ otherwise error.
+
+ example:
+ bzr register-branch http://foo.com/bzr/fooproject.mine \\
+ --project fooproject
+ """
+ takes_args = ['public_url?']
+ takes_options = [
+ Option('project',
+ 'Launchpad project short name to associate with the branch.',
+ unicode),
+ Option('product',
+ 'Launchpad product short name to associate with the branch.',
+ unicode,
+ hidden=True),
+ Option('branch-name',
+ 'Short name for the branch; '
+ 'by default taken from the last component of the url.',
+ unicode),
+ Option('branch-title',
+ 'One-sentence description of the branch.',
+ unicode),
+ Option('branch-description',
+ 'Longer description of the purpose or contents of the branch.',
+ unicode),
+ Option('author',
+ "Branch author's email address, if not yourself.",
+ unicode),
+ Option('link-bug',
+ 'The bug this branch fixes.',
+ int),
+ Option('dry-run',
+ 'Prepare the request but don\'t actually send it.')
+ ]
+
+
+ def run(self,
+ public_url=None,
+ project='',
+ product=None,
+ branch_name='',
+ branch_title='',
+ branch_description='',
+ author='',
+ link_bug=None,
+ dry_run=False):
+ from bzrlib.plugins.launchpad.lp_registration import (
+ BranchRegistrationRequest, BranchBugLinkRequest,
+ DryRunLaunchpadService, LaunchpadService)
+ if public_url is None:
+ try:
+ b = _mod_branch.Branch.open_containing('.')[0]
+ except NotBranchError:
+ raise BzrCommandError(gettext(
+ 'register-branch requires a public '
+ 'branch url - see bzr help register-branch.'))
+ public_url = b.get_public_branch()
+ if public_url is None:
+ raise NoPublicBranch(b)
+ if product is not None:
+ project = product
+ trace.note(gettext(
+ '--product is deprecated; please use --project.'))
+
+
+ rego = BranchRegistrationRequest(branch_url=public_url,
+ branch_name=branch_name,
+ branch_title=branch_title,
+ branch_description=branch_description,
+ product_name=project,
+ author_email=author,
+ )
+ linko = BranchBugLinkRequest(branch_url=public_url,
+ bug_id=link_bug)
+ if not dry_run:
+ service = LaunchpadService()
+ # This gives back the xmlrpc url that can be used for future
+ # operations on the branch. It's not so useful to print to the
+ # user since they can't do anything with it from a web browser; it
+ # might be nice for the server to tell us about an html url as
+ # well.
+ else:
+ # Run on service entirely in memory
+ service = DryRunLaunchpadService()
+ service.gather_user_credentials()
+ rego.submit(service)
+ if link_bug:
+ linko.submit(service)
+ self.outf.write('Branch registered.\n')
+
+
+class cmd_launchpad_open(Command):
+ __doc__ = """Open a Launchpad branch page in your web browser."""
+
+ aliases = ['lp-open']
+ takes_options = [
+ Option('dry-run',
+ 'Do not actually open the browser. Just say the URL we would '
+ 'use.'),
+ ]
+ takes_args = ['location?']
+
+ def _possible_locations(self, location):
+ """Yield possible external locations for the branch at 'location'."""
+ yield location
+ try:
+ branch = _mod_branch.Branch.open_containing(location)[0]
+ except NotBranchError:
+ return
+ branch_url = branch.get_public_branch()
+ if branch_url is not None:
+ yield branch_url
+ branch_url = branch.get_push_location()
+ if branch_url is not None:
+ yield branch_url
+
+ def _get_web_url(self, service, location):
+ from bzrlib.plugins.launchpad.lp_registration import (
+ NotLaunchpadBranch)
+ for branch_url in self._possible_locations(location):
+ try:
+ return service.get_web_url_from_branch_url(branch_url)
+ except (NotLaunchpadBranch, InvalidURL):
+ pass
+ raise NotLaunchpadBranch(branch_url)
+
+ def run(self, location=None, dry_run=False):
+ from bzrlib.plugins.launchpad.lp_registration import (
+ LaunchpadService)
+ if location is None:
+ location = u'.'
+ web_url = self._get_web_url(LaunchpadService(), location)
+ trace.note(gettext('Opening %s in web browser') % web_url)
+ if not dry_run:
+ import webbrowser # this import should not be lazy
+ # otherwise bzr.exe lacks this module
+ webbrowser.open(web_url)
+
+
+class cmd_launchpad_login(Command):
+ __doc__ = """Show or set the Launchpad user ID.
+
+ When communicating with Launchpad, some commands need to know your
+ Launchpad user ID. This command can be used to set or show the
+ user ID that Bazaar will use for such communication.
+
+ :Examples:
+ Show the Launchpad ID of the current user::
+
+ bzr launchpad-login
+
+ Set the Launchpad ID of the current user to 'bob'::
+
+ bzr launchpad-login bob
+ """
+ aliases = ['lp-login']
+ takes_args = ['name?']
+ takes_options = [
+ 'verbose',
+ Option('no-check',
+ "Don't check that the user name is valid."),
+ ]
+
+ def run(self, name=None, no_check=False, verbose=False):
+ # This is totally separate from any launchpadlib login system.
+ from bzrlib.plugins.launchpad import account
+ check_account = not no_check
+
+ if name is None:
+ username = account.get_lp_login()
+ if username:
+ if check_account:
+ account.check_lp_login(username)
+ if verbose:
+ self.outf.write(gettext(
+ "Launchpad user ID exists and has SSH keys.\n"))
+ self.outf.write(username + '\n')
+ else:
+ self.outf.write(gettext('No Launchpad user ID configured.\n'))
+ return 1
+ else:
+ name = name.lower()
+ if check_account:
+ account.check_lp_login(name)
+ if verbose:
+ self.outf.write(gettext(
+ "Launchpad user ID exists and has SSH keys.\n"))
+ account.set_lp_login(name)
+ if verbose:
+ self.outf.write(gettext("Launchpad user ID set to '%s'.\n") %
+ (name,))
+
+
+# XXX: cmd_launchpad_mirror is untested
+class cmd_launchpad_mirror(Command):
+ __doc__ = """Ask Launchpad to mirror a branch now."""
+
+ aliases = ['lp-mirror']
+ takes_args = ['location?']
+
+ def run(self, location='.'):
+ from bzrlib.plugins.launchpad import lp_api
+ from bzrlib.plugins.launchpad.lp_registration import LaunchpadService
+ branch, _ = _mod_branch.Branch.open_containing(location)
+ service = LaunchpadService()
+ launchpad = lp_api.login(service)
+ lp_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, branch,
+ create_missing=False)
+ lp_branch.lp.requestMirror()
+
+
+class cmd_lp_propose_merge(Command):
+ __doc__ = """Propose merging a branch on Launchpad.
+
+ This will open your usual editor to provide the initial comment. When it
+ has created the proposal, it will open it in your default web browser.
+
+ The branch will be proposed to merge into SUBMIT_BRANCH. If SUBMIT_BRANCH
+ is not supplied, the remembered submit branch will be used. If no submit
+ branch is remembered, the development focus will be used.
+
+ By default, the SUBMIT_BRANCH's review team will be requested to review
+ the merge proposal. This can be overriden by specifying --review (-R).
+ The parameter the launchpad account name of the desired reviewer. This
+ may optionally be followed by '=' and the review type. For example:
+
+ bzr lp-propose-merge --review jrandom --review review-team=qa
+
+ This will propose a merge, request "jrandom" to perform a review of
+ unspecified type, and request "review-team" to perform a "qa" review.
+ """
+
+ takes_options = [Option('staging',
+ help='Propose the merge on staging.'),
+ Option('message', short_name='m', type=unicode,
+ help='Commit message.'),
+ Option('approve',
+ help='Mark the proposal as approved immediately.'),
+ Option('fixes', 'The bug this proposal fixes.', str),
+ ListOption('review', short_name='R', type=unicode,
+ help='Requested reviewer and optional type.')]
+
+ takes_args = ['submit_branch?']
+
+ aliases = ['lp-submit', 'lp-propose']
+
+ def run(self, submit_branch=None, review=None, staging=False,
+ message=None, approve=False, fixes=None):
+ from bzrlib.plugins.launchpad import lp_propose
+ tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
+ '.')
+ if review is None:
+ reviews = None
+ else:
+ reviews = []
+ for review in review:
+ if '=' in review:
+ reviews.append(review.split('=', 2))
+ else:
+ reviews.append((review, ''))
+ if submit_branch is None:
+ submit_branch = branch.get_submit_branch()
+ if submit_branch is None:
+ target = None
+ else:
+ target = _mod_branch.Branch.open(submit_branch)
+ proposer = lp_propose.Proposer(tree, branch, target, message,
+ reviews, staging, approve=approve,
+ fixes=fixes)
+ proposer.check_proposal()
+ proposer.create_proposal()
+
+
+class cmd_lp_find_proposal(Command):
+
+ __doc__ = """Find the proposal to merge this revision.
+
+ Finds the merge proposal(s) that discussed landing the specified revision.
+ This works only if the selected branch was the merge proposal target, and
+ if the merged_revno is recorded for the merge proposal. The proposal(s)
+ are opened in a web browser.
+
+ Any revision involved in the merge may be specified-- the revision in
+ which the merge was performed, or one of the revisions that was merged.
+
+ So, to find the merge proposal that reviewed line 1 of README::
+
+ bzr lp-find-proposal -r annotate:README:1
+ """
+
+ takes_options = ['revision']
+
+ def run(self, revision=None):
+ from bzrlib import ui
+ from bzrlib.plugins.launchpad import lp_api
+ import webbrowser
+ b = _mod_branch.Branch.open_containing('.')[0]
+ pb = ui.ui_factory.nested_progress_bar()
+ b.lock_read()
+ try:
+ revno = self._find_merged_revno(revision, b, pb)
+ merged = self._find_proposals(revno, b, pb)
+ if len(merged) == 0:
+ raise BzrCommandError(gettext('No review found.'))
+ trace.note(gettext('%d proposals(s) found.') % len(merged))
+ for mp in merged:
+ webbrowser.open(lp_api.canonical_url(mp))
+ finally:
+ b.unlock()
+ pb.finished()
+
+ def _find_merged_revno(self, revision, b, pb):
+ if revision is None:
+ return b.revno()
+ pb.update(gettext('Finding revision-id'))
+ revision_id = revision[0].as_revision_id(b)
+ # a revno spec is necessarily on the mainline.
+ if self._is_revno_spec(revision[0]):
+ merging_revision = revision_id
+ else:
+ graph = b.repository.get_graph()
+ pb.update(gettext('Finding merge'))
+ merging_revision = graph.find_lefthand_merger(
+ revision_id, b.last_revision())
+ if merging_revision is None:
+ raise InvalidRevisionSpec(revision[0].user_spec, b)
+ pb.update(gettext('Finding revno'))
+ return b.revision_id_to_revno(merging_revision)
+
+ def _find_proposals(self, revno, b, pb):
+ from bzrlib.plugins.launchpad import (lp_api, lp_registration)
+ launchpad = lp_api.login(lp_registration.LaunchpadService())
+ pb.update(gettext('Finding Launchpad branch'))
+ lpb = lp_api.LaunchpadBranch.from_bzr(launchpad, b,
+ create_missing=False)
+ pb.update(gettext('Finding proposals'))
+ return list(lpb.lp.getMergeProposals(status=['Merged'],
+ merged_revnos=[revno]))
+
+
+ @staticmethod
+ def _is_revno_spec(spec):
+ try:
+ int(spec.user_spec)
+ except ValueError:
+ return False
+ else:
+ return True
+
+
+
diff --git a/bzrlib/plugins/launchpad/lp_api.py b/bzrlib/plugins/launchpad/lp_api.py
new file mode 100644
index 0000000..6d5a7a0
--- /dev/null
+++ b/bzrlib/plugins/launchpad/lp_api.py
@@ -0,0 +1,313 @@
+# Copyright (C) 2009, 2010 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
+
+"""Tools for dealing with the Launchpad API."""
+
+from __future__ import absolute_import
+
+# Importing this module will be expensive, since it imports launchpadlib and
+# its dependencies. However, our plan is to only load this module when it is
+# needed by a command that uses it.
+
+
+import os
+import re
+import urlparse
+
+from bzrlib import (
+ branch,
+ config,
+ errors,
+ osutils,
+ trace,
+ transport,
+ )
+from bzrlib.i18n import gettext
+from bzrlib.plugins.launchpad.lp_registration import (
+ InvalidLaunchpadInstance,
+ )
+
+try:
+ import launchpadlib
+except ImportError, e:
+ raise errors.DependencyNotPresent('launchpadlib', e)
+
+from launchpadlib.launchpad import (
+ STAGING_SERVICE_ROOT,
+ Launchpad,
+ )
+
+
+# Declare the minimum version of launchpadlib that we need in order to work.
+# 1.5.1 is the version of launchpadlib packaged in Ubuntu 9.10, the most
+# recent Ubuntu release at the time of writing.
+MINIMUM_LAUNCHPADLIB_VERSION = (1, 5, 1)
+
+
+def get_cache_directory():
+ """Return the directory to cache launchpadlib objects in."""
+ return osutils.pathjoin(config.config_dir(), 'launchpad')
+
+
+def parse_launchpadlib_version(version_number):
+ """Parse a version number of the style used by launchpadlib."""
+ return tuple(map(int, version_number.split('.')))
+
+
+def check_launchpadlib_compatibility():
+ """Raise an error if launchpadlib has the wrong version number."""
+ installed_version = parse_launchpadlib_version(launchpadlib.__version__)
+ if installed_version < MINIMUM_LAUNCHPADLIB_VERSION:
+ raise errors.IncompatibleAPI(
+ 'launchpadlib', MINIMUM_LAUNCHPADLIB_VERSION,
+ installed_version, installed_version)
+
+
+# The older versions of launchpadlib only provided service root constants for
+# edge and staging, whilst newer versions drop edge. Therefore service root
+# URIs for which we do not always have constants are derived from the staging
+# one, which does always exist.
+#
+# It is necessary to derive, rather than use hardcoded URIs because
+# launchpadlib <= 1.5.4 requires service root URIs that end in a path of
+# /beta/, whilst launchpadlib >= 1.5.5 requires service root URIs with no path
+# info.
+#
+# Once we have a hard dependency on launchpadlib >= 1.5.4 we can replace all of
+# bzr's local knowledge of individual Launchpad instances with use of the
+# launchpadlib.uris module.
+LAUNCHPAD_API_URLS = {
+ 'production': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
+ 'api.launchpad.net'),
+ 'qastaging': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
+ 'api.qastaging.launchpad.net'),
+ 'staging': STAGING_SERVICE_ROOT,
+ 'dev': STAGING_SERVICE_ROOT.replace('api.staging.launchpad.net',
+ 'api.launchpad.dev'),
+ }
+
+
+def _get_api_url(service):
+ """Return the root URL of the Launchpad API.
+
+ e.g. For the 'staging' Launchpad service, this function returns
+ launchpadlib.launchpad.STAGING_SERVICE_ROOT.
+
+ :param service: A `LaunchpadService` object.
+ :return: A URL as a string.
+ """
+ if service._lp_instance is None:
+ lp_instance = service.DEFAULT_INSTANCE
+ else:
+ lp_instance = service._lp_instance
+ try:
+ return LAUNCHPAD_API_URLS[lp_instance]
+ except KeyError:
+ raise InvalidLaunchpadInstance(lp_instance)
+
+
+class NoLaunchpadBranch(errors.BzrError):
+ _fmt = 'No launchpad branch could be found for branch "%(url)s".'
+
+ def __init__(self, branch):
+ errors.BzrError.__init__(self, branch=branch, url=branch.base)
+
+
+def login(service, timeout=None, proxy_info=None):
+ """Log in to the Launchpad API.
+
+ :return: The root `Launchpad` object from launchpadlib.
+ """
+ cache_directory = get_cache_directory()
+ launchpad = Launchpad.login_with(
+ 'bzr', _get_api_url(service), cache_directory, timeout=timeout,
+ proxy_info=proxy_info)
+ # XXX: Work-around a minor security bug in launchpadlib 1.5.1, which would
+ # create this directory with default umask.
+ osutils.chmod_if_possible(cache_directory, 0700)
+ return launchpad
+
+
+class LaunchpadBranch(object):
+ """Provide bzr and lp API access to a Launchpad branch."""
+
+ def __init__(self, lp_branch, bzr_url, bzr_branch=None, check_update=True):
+ """Constructor.
+
+ :param lp_branch: The Launchpad branch.
+ :param bzr_url: The URL of the Bazaar branch.
+ :param bzr_branch: An instance of the Bazaar branch.
+ """
+ self.bzr_url = bzr_url
+ self._bzr = bzr_branch
+ self._push_bzr = None
+ self._check_update = check_update
+ self.lp = lp_branch
+
+ @property
+ def bzr(self):
+ """Return the bzr branch for this branch."""
+ if self._bzr is None:
+ self._bzr = branch.Branch.open(self.bzr_url)
+ return self._bzr
+
+ @property
+ def push_bzr(self):
+ """Return the push branch for this branch."""
+ if self._push_bzr is None:
+ self._push_bzr = branch.Branch.open(self.lp.bzr_identity)
+ return self._push_bzr
+
+ @staticmethod
+ def plausible_launchpad_url(url):
+ """Is 'url' something that could conceivably be pushed to LP?
+
+ :param url: A URL that may refer to a Launchpad branch.
+ :return: A boolean.
+ """
+ if url is None:
+ return False
+ if url.startswith('lp:'):
+ return True
+ regex = re.compile('([a-z]*\+)*(bzr\+ssh|http)'
+ '://bazaar.*.launchpad.net')
+ return bool(regex.match(url))
+
+ @staticmethod
+ def candidate_urls(bzr_branch):
+ """Iterate through related URLs that might be Launchpad URLs.
+
+ :param bzr_branch: A Bazaar branch to find URLs from.
+ :return: a generator of URL strings.
+ """
+ url = bzr_branch.get_public_branch()
+ if url is not None:
+ yield url
+ url = bzr_branch.get_push_location()
+ if url is not None:
+ yield url
+ url = bzr_branch.get_parent()
+ if url is not None:
+ yield url
+ yield bzr_branch.base
+
+ @staticmethod
+ def tweak_url(url, launchpad):
+ """Adjust a URL to work with staging, if needed."""
+ if str(launchpad._root_uri) == STAGING_SERVICE_ROOT:
+ return url.replace('bazaar.launchpad.net',
+ 'bazaar.staging.launchpad.net')
+ elif str(launchpad._root_uri) == LAUNCHPAD_API_URLS['qastaging']:
+ return url.replace('bazaar.launchpad.net',
+ 'bazaar.qastaging.launchpad.net')
+ return url
+
+ @classmethod
+ def from_bzr(cls, launchpad, bzr_branch, create_missing=True):
+ """Find a Launchpad branch from a bzr branch."""
+ check_update = True
+ for url in cls.candidate_urls(bzr_branch):
+ url = cls.tweak_url(url, launchpad)
+ if not cls.plausible_launchpad_url(url):
+ continue
+ lp_branch = launchpad.branches.getByUrl(url=url)
+ if lp_branch is not None:
+ break
+ else:
+ if not create_missing:
+ raise NoLaunchpadBranch(bzr_branch)
+ lp_branch = cls.create_now(launchpad, bzr_branch)
+ check_update = False
+ return cls(lp_branch, bzr_branch.base, bzr_branch, check_update)
+
+ @classmethod
+ def create_now(cls, launchpad, bzr_branch):
+ """Create a Bazaar branch on Launchpad for the supplied branch."""
+ url = cls.tweak_url(bzr_branch.get_push_location(), launchpad)
+ if not cls.plausible_launchpad_url(url):
+ raise errors.BzrError(gettext('%s is not registered on Launchpad') %
+ bzr_branch.base)
+ bzr_branch.create_clone_on_transport(transport.get_transport(url))
+ lp_branch = launchpad.branches.getByUrl(url=url)
+ if lp_branch is None:
+ raise errors.BzrError(gettext('%s is not registered on Launchpad') %
+ url)
+ return lp_branch
+
+ def get_target(self):
+ """Return the 'LaunchpadBranch' for the target of this one."""
+ lp_branch = self.lp
+ if lp_branch.project is not None:
+ dev_focus = lp_branch.project.development_focus
+ if dev_focus is None:
+ raise errors.BzrError(gettext('%s has no development focus.') %
+ lp_branch.bzr_identity)
+ target = dev_focus.branch
+ if target is None:
+ raise errors.BzrError(gettext(
+ 'development focus %s has no branch.') % dev_focus)
+ elif lp_branch.sourcepackage is not None:
+ target = lp_branch.sourcepackage.getBranch(pocket="Release")
+ if target is None:
+ raise errors.BzrError(gettext(
+ 'source package %s has no branch.') %
+ lp_branch.sourcepackage)
+ else:
+ raise errors.BzrError(gettext(
+ '%s has no associated product or source package.') %
+ lp_branch.bzr_identity)
+ return LaunchpadBranch(target, target.bzr_identity)
+
+ def update_lp(self):
+ """Update the Launchpad copy of this branch."""
+ if not self._check_update:
+ return
+ self.bzr.lock_read()
+ try:
+ if self.lp.last_scanned_id is not None:
+ if self.bzr.last_revision() == self.lp.last_scanned_id:
+ trace.note(gettext('%s is already up-to-date.') %
+ self.lp.bzr_identity)
+ return
+ graph = self.bzr.repository.get_graph()
+ if not graph.is_ancestor(self.lp.last_scanned_id,
+ self.bzr.last_revision()):
+ raise errors.DivergedBranches(self.bzr, self.push_bzr)
+ trace.note(gettext('Pushing to %s') % self.lp.bzr_identity)
+ self.bzr.push(self.push_bzr)
+ finally:
+ self.bzr.unlock()
+
+ def find_lca_tree(self, other):
+ """Find the revision tree for the LCA of this branch and other.
+
+ :param other: Another LaunchpadBranch
+ :return: The RevisionTree of the LCA of this branch and other.
+ """
+ graph = self.bzr.repository.get_graph(other.bzr.repository)
+ lca = graph.find_unique_lca(self.bzr.last_revision(),
+ other.bzr.last_revision())
+ return self.bzr.repository.revision_tree(lca)
+
+
+def canonical_url(object):
+ """Return the canonical URL for a branch."""
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(
+ str(object.self_link))
+ path = '/'.join(path.split('/')[2:])
+ netloc = netloc.replace('api.', 'code.')
+ return urlparse.urlunparse((scheme, netloc, path, params, query,
+ fragment))
diff --git a/bzrlib/plugins/launchpad/lp_api_lite.py b/bzrlib/plugins/launchpad/lp_api_lite.py
new file mode 100644
index 0000000..f28a2eb
--- /dev/null
+++ b/bzrlib/plugins/launchpad/lp_api_lite.py
@@ -0,0 +1,288 @@
+# Copyright (C) 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
+
+"""Tools for dealing with the Launchpad API without using launchpadlib.
+
+The api itself is a RESTful interface, so we can make HTTP queries directly.
+loading launchpadlib itself has a fairly high overhead (just calling
+Launchpad.login_anonymously() takes a 500ms once the WADL is cached, and 5+s to
+get the WADL.
+"""
+
+from __future__ import absolute_import
+
+try:
+ # Use simplejson if available, much faster, and can be easily installed in
+ # older versions of python
+ import simplejson as json
+except ImportError:
+ # Is present since python 2.6
+ try:
+ import json
+ except ImportError:
+ json = None
+
+import time
+import urllib
+import urllib2
+
+from bzrlib import (
+ revision,
+ trace,
+ )
+
+
+class LatestPublication(object):
+ """Encapsulate how to find the latest publication for a given project."""
+
+ LP_API_ROOT = 'https://api.launchpad.net/1.0'
+
+ def __init__(self, archive, series, project):
+ self._archive = archive
+ self._project = project
+ self._setup_series_and_pocket(series)
+
+ def _setup_series_and_pocket(self, series):
+ """Parse the 'series' info into a series and a pocket.
+
+ eg::
+ _setup_series_and_pocket('natty-proposed')
+ => _series == 'natty'
+ _pocket == 'Proposed'
+ """
+ self._series = series
+ self._pocket = None
+ if self._series is not None and '-' in self._series:
+ self._series, self._pocket = self._series.split('-', 1)
+ self._pocket = self._pocket.title()
+ else:
+ self._pocket = 'Release'
+
+ def _archive_URL(self):
+ """Return the Launchpad 'Archive' URL that we will query.
+ This is everything in the URL except the query parameters.
+ """
+ return '%s/%s/+archive/primary' % (self.LP_API_ROOT, self._archive)
+
+ def _publication_status(self):
+ """Handle the 'status' field.
+ It seems that Launchpad tracks all 'debian' packages as 'Pending', while
+ for 'ubuntu' we care about the 'Published' packages.
+ """
+ if self._archive == 'debian':
+ # Launchpad only tracks debian packages as "Pending", it doesn't mark
+ # them Published
+ return 'Pending'
+ return 'Published'
+
+ def _query_params(self):
+ """Get the parameters defining our query.
+ This defines the actions we are making against the archive.
+ :return: A dict of query parameters.
+ """
+ params = {'ws.op': 'getPublishedSources',
+ 'exact_match': 'true',
+ # If we need to use "" shouldn't we quote the project somehow?
+ 'source_name': '"%s"' % (self._project,),
+ 'status': self._publication_status(),
+ # We only need the latest one, the results seem to be properly
+ # most-recent-debian-version sorted
+ 'ws.size': '1',
+ }
+ if self._series is not None:
+ params['distro_series'] = '/%s/%s' % (self._archive, self._series)
+ if self._pocket is not None:
+ params['pocket'] = self._pocket
+ return params
+
+ def _query_URL(self):
+ """Create the full URL that we need to query, including parameters."""
+ params = self._query_params()
+ # We sort to give deterministic results for testing
+ encoded = urllib.urlencode(sorted(params.items()))
+ return '%s?%s' % (self._archive_URL(), encoded)
+
+ def _get_lp_info(self):
+ """Place an actual HTTP query against the Launchpad service."""
+ if json is None:
+ return None
+ query_URL = self._query_URL()
+ try:
+ req = urllib2.Request(query_URL)
+ response = urllib2.urlopen(req)
+ json_info = response.read()
+ # TODO: We haven't tested the HTTPError
+ except (urllib2.URLError, urllib2.HTTPError), e:
+ trace.mutter('failed to place query to %r' % (query_URL,))
+ trace.log_exception_quietly()
+ return None
+ return json_info
+
+ def _parse_json_info(self, json_info):
+ """Parse the json response from Launchpad into objects."""
+ if json is None:
+ return None
+ try:
+ return json.loads(json_info)
+ except Exception:
+ trace.mutter('Failed to parse json info: %r' % (json_info,))
+ trace.log_exception_quietly()
+ return None
+
+ def get_latest_version(self):
+ """Get the latest published version for the given package."""
+ json_info = self._get_lp_info()
+ if json_info is None:
+ return None
+ info = self._parse_json_info(json_info)
+ if info is None:
+ return None
+ try:
+ entries = info['entries']
+ if len(entries) == 0:
+ return None
+ return entries[0]['source_package_version']
+ except KeyError:
+ trace.log_exception_quietly()
+ return None
+
+ def place(self):
+ """Text-form for what location this represents.
+
+ Example::
+ ubuntu, natty => Ubuntu Natty
+ ubuntu, natty-proposed => Ubuntu Natty Proposed
+ :return: A string representing the location we are checking.
+ """
+ place = self._archive
+ if self._series is not None:
+ place = '%s %s' % (place, self._series)
+ if self._pocket is not None and self._pocket != 'Release':
+ place = '%s %s' % (place, self._pocket)
+ return place.title()
+
+
+def get_latest_publication(archive, series, project):
+ """Get the most recent publication for a given project.
+
+ :param archive: Either 'ubuntu' or 'debian'
+ :param series: Something like 'natty', 'sid', etc. Can be set as None. Can
+ also include a pocket such as 'natty-proposed'.
+ :param project: Something like 'bzr'
+ :return: A version string indicating the most-recent version published in
+ Launchpad. Might return None if there is an error.
+ """
+ lp = LatestPublication(archive, series, project)
+ return lp.get_latest_version()
+
+
+def get_most_recent_tag(tag_dict, the_branch):
+ """Get the most recent revision that has been tagged."""
+ # Note: this assumes that a given rev won't get tagged multiple times. But
+ # it should be valid for the package importer branches that we care
+ # about
+ reverse_dict = dict((rev, tag) for tag, rev in tag_dict.iteritems())
+ the_branch.lock_read()
+ try:
+ last_rev = the_branch.last_revision()
+ graph = the_branch.repository.get_graph()
+ stop_revisions = (None, revision.NULL_REVISION)
+ for rev_id in graph.iter_lefthand_ancestry(last_rev, stop_revisions):
+ if rev_id in reverse_dict:
+ return reverse_dict[rev_id]
+ finally:
+ the_branch.unlock()
+
+
+def _get_newest_versions(the_branch, latest_pub):
+ """Get information about how 'fresh' this packaging branch is.
+
+ :param the_branch: The Branch to check
+ :param latest_pub: The LatestPublication used to check most recent
+ published version.
+ :return: (latest_ver, branch_latest_ver)
+ """
+ t = time.time()
+ latest_ver = latest_pub.get_latest_version()
+ t_latest_ver = time.time() - t
+ trace.mutter('LatestPublication.get_latest_version took: %.3fs'
+ % (t_latest_ver,))
+ if latest_ver is None:
+ return None, None
+ t = time.time()
+ tags = the_branch.tags.get_tag_dict()
+ t_tag_dict = time.time() - t
+ trace.mutter('LatestPublication.get_tag_dict took: %.3fs' % (t_tag_dict,))
+ if latest_ver in tags:
+ # branch might have a newer tag, but we don't really care
+ return latest_ver, latest_ver
+ else:
+ best_tag = get_most_recent_tag(tags, the_branch)
+ return latest_ver, best_tag
+
+
+def _report_freshness(latest_ver, branch_latest_ver, place, verbosity,
+ report_func):
+ """Report if the branch is up-to-date."""
+ if latest_ver is None:
+ if verbosity == 'all':
+ report_func('Most recent %s version: MISSING' % (place,))
+ elif verbosity == 'short':
+ report_func('%s is MISSING a version' % (place,))
+ return
+ elif latest_ver == branch_latest_ver:
+ if verbosity == 'minimal':
+ return
+ elif verbosity == 'short':
+ report_func('%s is CURRENT in %s' % (latest_ver, place))
+ else:
+ report_func('Most recent %s version: %s\n'
+ 'Packaging branch status: CURRENT'
+ % (place, latest_ver))
+ else:
+ if verbosity in ('minimal', 'short'):
+ if branch_latest_ver is None:
+ branch_latest_ver = 'Branch'
+ report_func('%s is OUT-OF-DATE, %s has %s'
+ % (branch_latest_ver, place, latest_ver))
+ else:
+ report_func('Most recent %s version: %s\n'
+ 'Packaging branch version: %s\n'
+ 'Packaging branch status: OUT-OF-DATE'
+ % (place, latest_ver, branch_latest_ver))
+
+
+def report_freshness(the_branch, verbosity, latest_pub):
+ """Report to the user how up-to-date the packaging branch is.
+
+ :param the_branch: A Branch object
+ :param verbosity: Can be one of:
+ off: Do not print anything, and skip all checks.
+ all: Print all information that we have in a verbose manner, this
+ includes misses, etc.
+ short: Print information, but only one-line summaries
+ minimal: Only print a one-line summary when the package branch is
+ out-of-date
+ :param latest_pub: A LatestPublication instance
+ """
+ if verbosity == 'off':
+ return
+ if verbosity is None:
+ verbosity = 'all'
+ latest_ver, branch_ver = _get_newest_versions(the_branch, latest_pub)
+ place = latest_pub.place()
+ _report_freshness(latest_ver, branch_ver, place, verbosity,
+ trace.note)
diff --git a/bzrlib/plugins/launchpad/lp_directory.py b/bzrlib/plugins/launchpad/lp_directory.py
new file mode 100644
index 0000000..3e88f69
--- /dev/null
+++ b/bzrlib/plugins/launchpad/lp_directory.py
@@ -0,0 +1,209 @@
+# Copyright (C) 2007-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
+
+"""Directory lookup that uses Launchpad."""
+
+from __future__ import absolute_import
+
+from urlparse import urlsplit
+import xmlrpclib
+
+from bzrlib import (
+ debug,
+ errors,
+ trace,
+ transport,
+ )
+from bzrlib.i18n import gettext
+
+from bzrlib.plugins.launchpad.lp_registration import (
+ LaunchpadService, ResolveLaunchpadPathRequest)
+from bzrlib.plugins.launchpad.account import get_lp_login
+
+
+# As bzrlib.transport.remote may not be loaded yet, make sure bzr+ssh
+# is counted as a netloc protocol.
+transport.register_urlparse_netloc_protocol('bzr+ssh')
+transport.register_urlparse_netloc_protocol('lp')
+
+_ubuntu_series_shortcuts = {
+ 'n': 'natty',
+ 'm': 'maverick',
+ 'l': 'lucid',
+ 'k': 'karmic',
+ 'j': 'jaunty',
+ 'h': 'hardy',
+ 'd': 'dapper',
+ }
+
+
+class LaunchpadDirectory(object):
+
+ def _requires_launchpad_login(self, scheme, netloc, path, query,
+ fragment):
+ """Does the URL require a Launchpad login in order to be reached?
+
+ The URL is specified by its parsed components, as returned from
+ urlsplit.
+ """
+ return (scheme in ('bzr+ssh', 'sftp')
+ and (netloc.endswith('launchpad.net')
+ or netloc.endswith('launchpad.dev')))
+
+ def look_up(self, name, url):
+ """See DirectoryService.look_up"""
+ return self._resolve(url)
+
+ def _resolve_locally(self, path, url, _request_factory):
+ # This is the best I could work out about XMLRPC. If an lp: url
+ # includes ~user, then it is specially validated. Otherwise, it is just
+ # sent to +branch/$path.
+ _, netloc, _, _, _ = urlsplit(url)
+ if netloc == '':
+ netloc = LaunchpadService.DEFAULT_INSTANCE
+ base_url = LaunchpadService.LAUNCHPAD_DOMAINS[netloc]
+ base = 'bzr+ssh://bazaar.%s/' % (base_url,)
+ maybe_invalid = False
+ if path.startswith('~'):
+ # A ~user style path, validate it a bit.
+ # If a path looks fishy, fall back to asking XMLRPC to
+ # resolve it for us. That way we still get their nicer error
+ # messages.
+ parts = path.split('/')
+ if (len(parts) < 3
+ or (parts[1] in ('ubuntu', 'debian') and len(parts) < 5)):
+ # This special case requires 5-parts to be valid.
+ maybe_invalid = True
+ else:
+ base += '+branch/'
+ if maybe_invalid:
+ return self._resolve_via_xmlrpc(path, url, _request_factory)
+ return {'urls': [base + path]}
+
+ def _resolve_via_xmlrpc(self, path, url, _request_factory):
+ service = LaunchpadService.for_url(url)
+ resolve = _request_factory(path)
+ try:
+ result = resolve.submit(service)
+ except xmlrpclib.Fault, fault:
+ raise errors.InvalidURL(
+ path=url, extra=fault.faultString)
+ return result
+
+ def _update_url_scheme(self, url):
+ # Do ubuntu: and debianlp: expansions.
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ if scheme in ('ubuntu', 'debianlp'):
+ if scheme == 'ubuntu':
+ distro = 'ubuntu'
+ distro_series = _ubuntu_series_shortcuts
+ elif scheme == 'debianlp':
+ distro = 'debian'
+ # No shortcuts for Debian distroseries.
+ distro_series = {}
+ else:
+ raise AssertionError('scheme should be ubuntu: or debianlp:')
+ # Split the path. It's either going to be 'project' or
+ # 'series/project', but recognize that it may be a series we don't
+ # know about.
+ path_parts = path.split('/')
+ if len(path_parts) == 1:
+ # It's just a project name.
+ lp_url_template = 'lp:%(distro)s/%(project)s'
+ project = path_parts[0]
+ series = None
+ elif len(path_parts) == 2:
+ # It's a series and project.
+ lp_url_template = 'lp:%(distro)s/%(series)s/%(project)s'
+ series, project = path_parts
+ else:
+ # There are either 0 or > 2 path parts, neither of which is
+ # supported for these schemes.
+ raise errors.InvalidURL('Bad path: %s' % url)
+ # Expand any series shortcuts, but keep unknown series.
+ series = distro_series.get(series, series)
+ # Hack the url and let the following do the final resolution.
+ url = lp_url_template % dict(
+ distro=distro,
+ series=series,
+ project=project)
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ return url, path
+
+ def _expand_user(self, path, url, lp_login):
+ if path.startswith('~/'):
+ if lp_login is None:
+ raise errors.InvalidURL(path=url,
+ extra='Cannot resolve "~" to your username.'
+ ' See "bzr help launchpad-login"')
+ path = '~' + lp_login + path[1:]
+ return path
+
+ def _resolve(self, url,
+ _request_factory=ResolveLaunchpadPathRequest,
+ _lp_login=None):
+ """Resolve the base URL for this transport."""
+ url, path = self._update_url_scheme(url)
+ if _lp_login is None:
+ _lp_login = get_lp_login()
+ path = path.strip('/')
+ path = self._expand_user(path, url, _lp_login)
+ if _lp_login is not None:
+ result = self._resolve_locally(path, url, _request_factory)
+ if 'launchpad' in debug.debug_flags:
+ local_res = result
+ result = self._resolve_via_xmlrpc(path, url, _request_factory)
+ trace.note(gettext(
+ 'resolution for {0}\n local: {1}\n remote: {2}').format(
+ url, local_res['urls'], result['urls']))
+ else:
+ result = self._resolve_via_xmlrpc(path, url, _request_factory)
+
+ if 'launchpad' in debug.debug_flags:
+ trace.mutter("resolve_lp_path(%r) == %r", url, result)
+
+ _warned_login = False
+ for url in result['urls']:
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ if self._requires_launchpad_login(scheme, netloc, path, query,
+ fragment):
+ # Only accept launchpad.net bzr+ssh URLs if we know
+ # the user's Launchpad login:
+ if _lp_login is not None:
+ break
+ if _lp_login is None:
+ if not _warned_login:
+ trace.warning(
+'You have not informed bzr of your Launchpad ID, and you must do this to\n'
+'write to Launchpad or access private data. See "bzr help launchpad-login".')
+ _warned_login = True
+ else:
+ # Use the URL if we can create a transport for it.
+ try:
+ transport.get_transport(url)
+ except (errors.PathError, errors.TransportError):
+ pass
+ else:
+ break
+ else:
+ raise errors.InvalidURL(path=url, extra='no supported schemes')
+ return url
+
+
+def get_test_permutations():
+ # Since this transport doesn't do anything once opened, it's not subjected
+ # to the usual transport tests.
+ return []
diff --git a/bzrlib/plugins/launchpad/lp_propose.py b/bzrlib/plugins/launchpad/lp_propose.py
new file mode 100644
index 0000000..7142515
--- /dev/null
+++ b/bzrlib/plugins/launchpad/lp_propose.py
@@ -0,0 +1,221 @@
+# Copyright (C) 2010, 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
+
+from bzrlib import (
+ errors,
+ hooks,
+ )
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import webbrowser
+
+from bzrlib import (
+ msgeditor,
+ )
+from bzrlib.i18n import gettext
+from bzrlib.plugins.launchpad import (
+ lp_api,
+ lp_registration,
+ )
+""")
+
+
+class ProposeMergeHooks(hooks.Hooks):
+ """Hooks for proposing a merge on Launchpad."""
+
+ def __init__(self):
+ hooks.Hooks.__init__(self, "bzrlib.plugins.launchpad.lp_propose",
+ "Proposer.hooks")
+ self.add_hook('get_prerequisite',
+ "Return the prerequisite branch for proposing as merge.", (2, 1))
+ self.add_hook('merge_proposal_body',
+ "Return an initial body for the merge proposal message.", (2, 1))
+
+
+class Proposer(object):
+
+ hooks = ProposeMergeHooks()
+
+ def __init__(self, tree, source_branch, target_branch, message, reviews,
+ staging=False, approve=False, fixes=None):
+ """Constructor.
+
+ :param tree: The working tree for the source branch.
+ :param source_branch: The branch to propose for merging.
+ :param target_branch: The branch to merge into.
+ :param message: The commit message to use. (May be None.)
+ :param reviews: A list of tuples of reviewer, review type.
+ :param staging: If True, propose the merge against staging instead of
+ production.
+ :param approve: If True, mark the new proposal as approved immediately.
+ This is useful when a project permits some things to be approved
+ by the submitter (e.g. merges between release and deployment
+ branches).
+ """
+ self.tree = tree
+ if staging:
+ lp_instance = 'staging'
+ else:
+ lp_instance = 'production'
+ service = lp_registration.LaunchpadService(lp_instance=lp_instance)
+ self.launchpad = lp_api.login(service)
+ self.source_branch = lp_api.LaunchpadBranch.from_bzr(
+ self.launchpad, source_branch)
+ if target_branch is None:
+ self.target_branch = self.source_branch.get_target()
+ else:
+ self.target_branch = lp_api.LaunchpadBranch.from_bzr(
+ self.launchpad, target_branch)
+ self.commit_message = message
+ # XXX: this is where bug lp:583638 could be tackled.
+ if reviews == []:
+ self.reviews = []
+ else:
+ self.reviews = [(self.launchpad.people[reviewer], review_type)
+ for reviewer, review_type in
+ reviews]
+ self.approve = approve
+ self.fixes = fixes
+
+ def get_comment(self, prerequisite_branch):
+ """Determine the initial comment for the merge proposal."""
+ info = ["Source: %s\n" % self.source_branch.lp.bzr_identity]
+ info.append("Target: %s\n" % self.target_branch.lp.bzr_identity)
+ if prerequisite_branch is not None:
+ info.append("Prereq: %s\n" % prerequisite_branch.lp.bzr_identity)
+ for rdata in self.reviews:
+ uniquename = "%s (%s)" % (rdata[0].display_name, rdata[0].name)
+ info.append('Reviewer: %s, type "%s"\n' % (uniquename, rdata[1]))
+ self.source_branch.bzr.lock_read()
+ try:
+ self.target_branch.bzr.lock_read()
+ try:
+ body = self.get_initial_body()
+ finally:
+ self.target_branch.bzr.unlock()
+ finally:
+ self.source_branch.bzr.unlock()
+ initial_comment = msgeditor.edit_commit_message(''.join(info),
+ start_message=body)
+ return initial_comment.strip().encode('utf-8')
+
+ def get_initial_body(self):
+ """Get a body for the proposal for the user to modify.
+
+ :return: a str or None.
+ """
+ def list_modified_files():
+ lca_tree = self.source_branch.find_lca_tree(
+ self.target_branch)
+ source_tree = self.source_branch.bzr.basis_tree()
+ files = modified_files(lca_tree, source_tree)
+ return list(files)
+ target_loc = ('bzr+ssh://bazaar.launchpad.net/%s' %
+ self.target_branch.lp.unique_name)
+ body = None
+ for hook in self.hooks['merge_proposal_body']:
+ body = hook({
+ 'tree': self.tree,
+ 'target_branch': target_loc,
+ 'modified_files_callback': list_modified_files,
+ 'old_body': body,
+ })
+ return body
+
+ def check_proposal(self):
+ """Check that the submission is sensible."""
+ if self.source_branch.lp.self_link == self.target_branch.lp.self_link:
+ raise errors.BzrCommandError(
+ 'Source and target branches must be different.')
+ for mp in self.source_branch.lp.landing_targets:
+ if mp.queue_status in ('Merged', 'Rejected'):
+ continue
+ if mp.target_branch.self_link == self.target_branch.lp.self_link:
+ raise errors.BzrCommandError(gettext(
+ 'There is already a branch merge proposal: %s') %
+ lp_api.canonical_url(mp))
+
+ def _get_prerequisite_branch(self):
+ hooks = self.hooks['get_prerequisite']
+ prerequisite_branch = None
+ for hook in hooks:
+ prerequisite_branch = hook(
+ {'launchpad': self.launchpad,
+ 'source_branch': self.source_branch,
+ 'target_branch': self.target_branch,
+ 'prerequisite_branch': prerequisite_branch})
+ return prerequisite_branch
+
+ def call_webservice(self, call, *args, **kwargs):
+ """Make a call to the webservice, wrapping failures.
+
+ :param call: The call to make.
+ :param *args: *args for the call.
+ :param **kwargs: **kwargs for the call.
+ :return: The result of calling call(*args, *kwargs).
+ """
+ from lazr.restfulclient import errors as restful_errors
+ try:
+ return call(*args, **kwargs)
+ except restful_errors.HTTPError, e:
+ error_lines = []
+ for line in e.content.splitlines():
+ if line.startswith('Traceback (most recent call last):'):
+ break
+ error_lines.append(line)
+ raise Exception(''.join(error_lines))
+
+ def create_proposal(self):
+ """Perform the submission."""
+ prerequisite_branch = self._get_prerequisite_branch()
+ if prerequisite_branch is None:
+ prereq = None
+ else:
+ prereq = prerequisite_branch.lp
+ prerequisite_branch.update_lp()
+ self.source_branch.update_lp()
+ reviewers = []
+ review_types = []
+ for reviewer, review_type in self.reviews:
+ review_types.append(review_type)
+ reviewers.append(reviewer.self_link)
+ initial_comment = self.get_comment(prerequisite_branch)
+ mp = self.call_webservice(
+ self.source_branch.lp.createMergeProposal,
+ target_branch=self.target_branch.lp,
+ prerequisite_branch=prereq,
+ initial_comment=initial_comment,
+ commit_message=self.commit_message, reviewers=reviewers,
+ review_types=review_types)
+ if self.approve:
+ self.call_webservice(mp.setStatus, status='Approved')
+ if self.fixes:
+ if self.fixes.startswith('lp:'):
+ self.fixes = self.fixes[3:]
+ self.call_webservice(
+ self.source_branch.lp.linkBug,
+ bug=self.launchpad.bugs[int(self.fixes)])
+ webbrowser.open(lp_api.canonical_url(mp))
+
+
+def modified_files(old_tree, new_tree):
+ """Return a list of paths in the new tree with modified contents."""
+ for f, (op, path), c, v, p, n, (ok, k), e in new_tree.iter_changes(
+ old_tree):
+ if c and k == 'file':
+ yield str(path)
diff --git a/bzrlib/plugins/launchpad/lp_registration.py b/bzrlib/plugins/launchpad/lp_registration.py
new file mode 100644
index 0000000..dae825f
--- /dev/null
+++ b/bzrlib/plugins/launchpad/lp_registration.py
@@ -0,0 +1,358 @@
+# Copyright (C) 2006-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
+
+
+import os
+import socket
+from urlparse import urlsplit, urlunsplit
+import urllib
+import xmlrpclib
+
+from bzrlib import (
+ config,
+ errors,
+ urlutils,
+ __version__ as _bzrlib_version,
+ )
+from bzrlib.transport.http import _urllib2_wrappers
+
+
+# for testing, do
+'''
+export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
+'''
+
+class InvalidLaunchpadInstance(errors.BzrError):
+
+ _fmt = "%(lp_instance)s is not a valid Launchpad instance."
+
+ def __init__(self, lp_instance):
+ errors.BzrError.__init__(self, lp_instance=lp_instance)
+
+
+class NotLaunchpadBranch(errors.BzrError):
+
+ _fmt = "%(url)s is not registered on Launchpad."
+
+ def __init__(self, url):
+ errors.BzrError.__init__(self, url=url)
+
+
+class XMLRPCTransport(xmlrpclib.Transport):
+
+ def __init__(self, scheme):
+ xmlrpclib.Transport.__init__(self)
+ self._scheme = scheme
+ self._opener = _urllib2_wrappers.Opener()
+ self.verbose = 0
+
+ def request(self, host, handler, request_body, verbose=0):
+ self.verbose = verbose
+ url = self._scheme + "://" + host + handler
+ request = _urllib2_wrappers.Request("POST", url, request_body)
+ # FIXME: _urllib2_wrappers will override user-agent with its own
+ # request.add_header("User-Agent", self.user_agent)
+ request.add_header("Content-Type", "text/xml")
+
+ response = self._opener.open(request)
+ if response.code != 200:
+ raise xmlrpclib.ProtocolError(host + handler, response.code,
+ response.msg, response.info())
+ return self.parse_response(response)
+
+
+class LaunchpadService(object):
+ """A service to talk to Launchpad via XMLRPC.
+
+ See http://wiki.bazaar.canonical.com/Specs/LaunchpadRpc for the methods we can call.
+ """
+
+ LAUNCHPAD_DOMAINS = {
+ 'production': 'launchpad.net',
+ 'staging': 'staging.launchpad.net',
+ 'qastaging': 'qastaging.launchpad.net',
+ 'demo': 'demo.launchpad.net',
+ 'dev': 'launchpad.dev',
+ }
+
+ # NB: these should always end in a slash to avoid xmlrpclib appending
+ # '/RPC2'
+ LAUNCHPAD_INSTANCE = {}
+ for instance, domain in LAUNCHPAD_DOMAINS.iteritems():
+ LAUNCHPAD_INSTANCE[instance] = 'https://xmlrpc.%s/bazaar/' % domain
+
+ # We use production as the default because edge has been deprecated circa
+ # 2010-11 (see bug https://bugs.launchpad.net/bzr/+bug/583667)
+ DEFAULT_INSTANCE = 'production'
+ DEFAULT_SERVICE_URL = LAUNCHPAD_INSTANCE[DEFAULT_INSTANCE]
+
+ transport = None
+ registrant_email = None
+ registrant_password = None
+
+
+ def __init__(self, transport=None, lp_instance=None):
+ """Construct a new service talking to the launchpad rpc server"""
+ self._lp_instance = lp_instance
+ if transport is None:
+ uri_type = urllib.splittype(self.service_url)[0]
+ transport = XMLRPCTransport(uri_type)
+ transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
+ % (_bzrlib_version, xmlrpclib.__version__)
+ self.transport = transport
+
+ @property
+ def service_url(self):
+ """Return the http or https url for the xmlrpc server.
+
+ This does not include the username/password credentials.
+ """
+ key = 'BZR_LP_XMLRPC_URL'
+ if key in os.environ:
+ return os.environ[key]
+ elif self._lp_instance is not None:
+ try:
+ return self.LAUNCHPAD_INSTANCE[self._lp_instance]
+ except KeyError:
+ raise InvalidLaunchpadInstance(self._lp_instance)
+ else:
+ return self.DEFAULT_SERVICE_URL
+
+ @classmethod
+ def for_url(cls, url, **kwargs):
+ """Return the Launchpad service corresponding to the given URL."""
+ result = urlsplit(url)
+ lp_instance = result[1]
+ if lp_instance == '':
+ lp_instance = None
+ elif lp_instance not in cls.LAUNCHPAD_INSTANCE:
+ raise errors.InvalidURL(path=url)
+ return cls(lp_instance=lp_instance, **kwargs)
+
+ def get_proxy(self, authenticated):
+ """Return the proxy for XMLRPC requests."""
+ if authenticated:
+ # auth info must be in url
+ # TODO: if there's no registrant email perhaps we should
+ # just connect anonymously?
+ scheme, hostinfo, path = urlsplit(self.service_url)[:3]
+ if '@' in hostinfo:
+ raise AssertionError(hostinfo)
+ if self.registrant_email is None:
+ raise AssertionError()
+ if self.registrant_password is None:
+ raise AssertionError()
+ # TODO: perhaps fully quote the password to make it very slightly
+ # obscured
+ # TODO: can we perhaps add extra Authorization headers
+ # directly to the request, rather than putting this into
+ # the url? perhaps a bit more secure against accidentally
+ # revealing it. std66 s3.2.1 discourages putting the
+ # password in the url.
+ hostinfo = '%s:%s@%s' % (urlutils.quote(self.registrant_email),
+ urlutils.quote(self.registrant_password),
+ hostinfo)
+ url = urlunsplit((scheme, hostinfo, path, '', ''))
+ else:
+ url = self.service_url
+ return xmlrpclib.ServerProxy(url, transport=self.transport)
+
+ def gather_user_credentials(self):
+ """Get the password from the user."""
+ the_config = config.GlobalConfig()
+ self.registrant_email = the_config.user_email()
+ if self.registrant_password is None:
+ auth = config.AuthenticationConfig()
+ scheme, hostinfo = urlsplit(self.service_url)[:2]
+ prompt = 'launchpad.net password for %s: ' % \
+ self.registrant_email
+ # We will reuse http[s] credentials if we can, prompt user
+ # otherwise
+ self.registrant_password = auth.get_password(scheme, hostinfo,
+ self.registrant_email,
+ prompt=prompt)
+
+ def send_request(self, method_name, method_params, authenticated):
+ proxy = self.get_proxy(authenticated)
+ method = getattr(proxy, method_name)
+ try:
+ result = method(*method_params)
+ except xmlrpclib.ProtocolError, e:
+ if e.errcode == 301:
+ # TODO: This can give a ProtocolError representing a 301 error, whose
+ # e.headers['location'] tells where to go and e.errcode==301; should
+ # probably log something and retry on the new url.
+ raise NotImplementedError("should resend request to %s, but this isn't implemented"
+ % e.headers.get('Location', 'NO-LOCATION-PRESENT'))
+ else:
+ # we don't want to print the original message because its
+ # str representation includes the plaintext password.
+ # TODO: print more headers to help in tracking down failures
+ raise errors.BzrError("xmlrpc protocol error connecting to %s: %s %s"
+ % (self.service_url, e.errcode, e.errmsg))
+ except socket.gaierror, e:
+ raise errors.ConnectionError(
+ "Could not resolve '%s'" % self.domain,
+ orig_error=e)
+ return result
+
+ @property
+ def domain(self):
+ if self._lp_instance is None:
+ instance = self.DEFAULT_INSTANCE
+ else:
+ instance = self._lp_instance
+ return self.LAUNCHPAD_DOMAINS[instance]
+
+ def _guess_branch_path(self, branch_url, _request_factory=None):
+ scheme, hostinfo, path = urlsplit(branch_url)[:3]
+ if _request_factory is None:
+ _request_factory = ResolveLaunchpadPathRequest
+ if scheme == 'lp':
+ resolve = _request_factory(path)
+ try:
+ result = resolve.submit(self)
+ except xmlrpclib.Fault, fault:
+ raise errors.InvalidURL(branch_url, str(fault))
+ branch_url = result['urls'][0]
+ path = urlsplit(branch_url)[2]
+ else:
+ domains = (
+ 'bazaar.%s' % domain
+ for domain in self.LAUNCHPAD_DOMAINS.itervalues())
+ if hostinfo not in domains:
+ raise NotLaunchpadBranch(branch_url)
+ return path.lstrip('/')
+
+ def get_web_url_from_branch_url(self, branch_url, _request_factory=None):
+ """Get the Launchpad web URL for the given branch URL.
+
+ :raise errors.InvalidURL: if 'branch_url' cannot be identified as a
+ Launchpad branch URL.
+ :return: The URL of the branch on Launchpad.
+ """
+ path = self._guess_branch_path(branch_url, _request_factory)
+ return urlutils.join('https://code.%s' % self.domain, path)
+
+
+class BaseRequest(object):
+ """Base request for talking to a XMLRPC server."""
+
+ # Set this to the XMLRPC method name.
+ _methodname = None
+ _authenticated = True
+
+ def _request_params(self):
+ """Return the arguments to pass to the method"""
+ raise NotImplementedError(self._request_params)
+
+ def submit(self, service):
+ """Submit request to Launchpad XMLRPC server.
+
+ :param service: LaunchpadService indicating where to send
+ the request and the authentication credentials.
+ """
+ return service.send_request(self._methodname, self._request_params(),
+ self._authenticated)
+
+
+class DryRunLaunchpadService(LaunchpadService):
+ """Service that just absorbs requests without sending to server.
+
+ The dummy service does not need authentication.
+ """
+
+ def send_request(self, method_name, method_params, authenticated):
+ pass
+
+ def gather_user_credentials(self):
+ pass
+
+
+class BranchRegistrationRequest(BaseRequest):
+ """Request to tell Launchpad about a bzr branch."""
+
+ _methodname = 'register_branch'
+
+ def __init__(self, branch_url,
+ branch_name='',
+ branch_title='',
+ branch_description='',
+ author_email='',
+ product_name='',
+ ):
+ if not branch_url:
+ raise errors.InvalidURL(branch_url, "You need to specify a non-empty branch URL.")
+ self.branch_url = branch_url
+ if branch_name:
+ self.branch_name = branch_name
+ else:
+ self.branch_name = self._find_default_branch_name(self.branch_url)
+ self.branch_title = branch_title
+ self.branch_description = branch_description
+ self.author_email = author_email
+ self.product_name = product_name
+
+ def _request_params(self):
+ """Return xmlrpc request parameters"""
+ # This must match the parameter tuple expected by Launchpad for this
+ # method
+ return (self.branch_url,
+ self.branch_name,
+ self.branch_title,
+ self.branch_description,
+ self.author_email,
+ self.product_name,
+ )
+
+ def _find_default_branch_name(self, branch_url):
+ i = branch_url.rfind('/')
+ return branch_url[i+1:]
+
+
+class BranchBugLinkRequest(BaseRequest):
+ """Request to link a bzr branch in Launchpad to a bug."""
+
+ _methodname = 'link_branch_to_bug'
+
+ def __init__(self, branch_url, bug_id):
+ self.bug_id = bug_id
+ self.branch_url = branch_url
+
+ def _request_params(self):
+ """Return xmlrpc request parameters"""
+ # This must match the parameter tuple expected by Launchpad for this
+ # method
+ return (self.branch_url, self.bug_id, '')
+
+
+class ResolveLaunchpadPathRequest(BaseRequest):
+ """Request to resolve the path component of an lp: URL."""
+
+ _methodname = 'resolve_lp_path'
+ _authenticated = False
+
+ def __init__(self, path):
+ if not path:
+ raise errors.InvalidURL(path=path,
+ extra="You must specify a project.")
+ self.path = path
+
+ def _request_params(self):
+ """Return xmlrpc request parameters"""
+ return (self.path,)
diff --git a/bzrlib/plugins/launchpad/test_account.py b/bzrlib/plugins/launchpad/test_account.py
new file mode 100644
index 0000000..ca058a6
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_account.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2007-2010 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
+
+"""Tests for Launchpad user ID management functions."""
+
+from bzrlib import config
+from bzrlib.tests import TestCaseInTempDir, TestCaseWithMemoryTransport
+from bzrlib.plugins.launchpad import account
+
+
+class LaunchpadAccountTests(TestCaseInTempDir):
+
+ def test_get_lp_login_unconfigured(self):
+ # Test that get_lp_login() returns None if no username has
+ # been configured.
+ my_config = config.MemoryStack('')
+ self.assertEqual(None, account.get_lp_login(my_config))
+
+ def test_get_lp_login(self):
+ # Test that get_lp_login() returns the configured username
+ my_config = config.MemoryStack(
+ '[DEFAULT]\nlaunchpad_username=test-user\n')
+ self.assertEqual('test-user', account.get_lp_login(my_config))
+
+ def test_set_lp_login(self):
+ # Test that set_lp_login() updates the config file.
+ my_config = config.MemoryStack('')
+ self.assertEqual(None, my_config.get('launchpad_username'))
+ account.set_lp_login('test-user', my_config)
+ self.assertEqual(
+ 'test-user', my_config.get('launchpad_username'))
+
+ def test_unknown_launchpad_username(self):
+ # Test formatting of UnknownLaunchpadUsername exception
+ error = account.UnknownLaunchpadUsername(user='test-user')
+ self.assertEqualDiff('The user name test-user is not registered '
+ 'on Launchpad.', str(error))
+
+ def test_no_registered_ssh_keys(self):
+ # Test formatting of NoRegisteredSSHKeys exception
+ error = account.NoRegisteredSSHKeys(user='test-user')
+ self.assertEqualDiff('The user test-user has not registered any '
+ 'SSH keys with Launchpad.\n'
+ 'See <https://launchpad.net/people/+me>',
+ str(error))
+
+ def test_set_lp_login_updates_authentication_conf(self):
+ self.assertIs(None, account._get_auth_user())
+ account.set_lp_login('foo')
+ self.assertEqual('foo', account._get_auth_user())
+
+ def test_get_lp_login_does_not_update_for_none_user(self):
+ account.get_lp_login()
+ self.assertIs(None, account._get_auth_user())
+
+ def test_get_lp_login_updates_authentication_conf(self):
+ account._set_global_option('foo')
+ self.assertIs(None, account._get_auth_user())
+ account.get_lp_login()
+ auth = config.AuthenticationConfig()
+ self.assertEqual('foo', account._get_auth_user(auth))
+ self.assertEqual('foo', auth.get_user('ssh', 'bazaar.launchpad.net'))
+ self.assertEqual('foo', auth.get_user('ssh',
+ 'bazaar.staging.launchpad.net'))
+
+ def test_get_lp_login_leaves_existing_credentials(self):
+ auth = config.AuthenticationConfig()
+ auth.set_credentials('Foo', 'bazaar.launchpad.net', 'foo', 'ssh')
+ auth.set_credentials('Bar', 'bazaar.staging.launchpad.net', 'foo',
+ 'ssh')
+ account._set_global_option('foo')
+ account.get_lp_login()
+ auth = config.AuthenticationConfig()
+ credentials = auth.get_credentials('ssh', 'bazaar.launchpad.net')
+ self.assertEqual('Foo', credentials['name'])
+
+ def test_get_lp_login_errors_on_mismatch(self):
+ account._set_auth_user('foo')
+ account._set_global_option('bar')
+ e = self.assertRaises(account.MismatchedUsernames,
+ account.get_lp_login)
+ self.assertEqual('bazaar.conf and authentication.conf disagree about'
+ ' launchpad account name. Please re-run launchpad-login.', str(e))
+
+
+class CheckAccountTests(TestCaseWithMemoryTransport):
+
+ def test_check_lp_login_valid_user(self):
+ transport = self.get_transport()
+ transport.mkdir('~test-user')
+ transport.put_bytes('~test-user/+sshkeys', 'some keys here')
+ account.check_lp_login('test-user', transport)
+
+ def test_check_lp_login_no_user(self):
+ transport = self.get_transport()
+ self.assertRaises(account.UnknownLaunchpadUsername,
+ account.check_lp_login, 'test-user', transport)
+
+ def test_check_lp_login_no_ssh_keys(self):
+ transport = self.get_transport()
+ transport.mkdir('~test-user')
+ transport.put_bytes('~test-user/+sshkeys', '')
+ self.assertRaises(account.NoRegisteredSSHKeys,
+ account.check_lp_login, 'test-user', transport)
diff --git a/bzrlib/plugins/launchpad/test_lp_api.py b/bzrlib/plugins/launchpad/test_lp_api.py
new file mode 100644
index 0000000..ba8e12f
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_api.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2009, 2010 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 bzrlib import config, errors, osutils
+from bzrlib.tests import (
+ TestCase,
+ TestCaseWithTransport,
+ )
+from bzrlib.tests.features import (
+ ModuleAvailableFeature,
+ )
+
+
+launchpadlib_feature = ModuleAvailableFeature('launchpadlib')
+
+
+class TestDependencyManagement(TestCase):
+ """Tests for managing the dependency on launchpadlib."""
+
+ _test_needs_features = [launchpadlib_feature]
+
+ def setUp(self):
+ TestCase.setUp(self)
+ from bzrlib.plugins.launchpad import lp_api
+ self.lp_api = lp_api
+
+ def patch(self, obj, name, value):
+ """Temporarily set the 'name' attribute of 'obj' to 'value'."""
+ self.overrideAttr(obj, name, value)
+
+ def test_get_launchpadlib_version(self):
+ # parse_launchpadlib_version returns a tuple of a version number of
+ # the style used by launchpadlib.
+ version_info = self.lp_api.parse_launchpadlib_version('1.5.1')
+ self.assertEqual((1, 5, 1), version_info)
+
+ def test_supported_launchpadlib_version(self):
+ # If the installed version of launchpadlib is greater than the minimum
+ # required version of launchpadlib, check_launchpadlib_compatibility
+ # doesn't raise an error.
+ launchpadlib = launchpadlib_feature.module
+ self.patch(launchpadlib, '__version__', '1.5.1')
+ self.lp_api.MINIMUM_LAUNCHPADLIB_VERSION = (1, 5, 1)
+ # Doesn't raise an exception.
+ self.lp_api.check_launchpadlib_compatibility()
+
+ def test_unsupported_launchpadlib_version(self):
+ # If the installed version of launchpadlib is less than the minimum
+ # required version of launchpadlib, check_launchpadlib_compatibility
+ # raises an IncompatibleAPI error.
+ launchpadlib = launchpadlib_feature.module
+ self.patch(launchpadlib, '__version__', '1.5.0')
+ self.lp_api.MINIMUM_LAUNCHPADLIB_VERSION = (1, 5, 1)
+ self.assertRaises(
+ errors.IncompatibleAPI,
+ self.lp_api.check_launchpadlib_compatibility)
+
+
+class TestCacheDirectory(TestCase):
+ """Tests for get_cache_directory."""
+
+ _test_needs_features = [launchpadlib_feature]
+
+ def test_get_cache_directory(self):
+ # get_cache_directory returns the path to a directory inside the
+ # Bazaar configuration directory.
+ from bzrlib.plugins.launchpad import lp_api
+ expected_path = osutils.pathjoin(config.config_dir(), 'launchpad')
+ self.assertEqual(expected_path, lp_api.get_cache_directory())
+
+
+class TestLaunchpadMirror(TestCaseWithTransport):
+ """Tests for the 'bzr lp-mirror' command."""
+
+ # Testing the lp-mirror command is quite hard, since it must talk to a
+ # Launchpad server. Here, we just test that the command exists.
+
+ _test_needs_features = [launchpadlib_feature]
+
+ def test_command_exists(self):
+ out, err = self.run_bzr(['launchpad-mirror', '--help'], retcode=0)
+ self.assertEqual('', err)
+
+ def test_alias_exists(self):
+ out, err = self.run_bzr(['lp-mirror', '--help'], retcode=0)
+ self.assertEqual('', err)
diff --git a/bzrlib/plugins/launchpad/test_lp_api_lite.py b/bzrlib/plugins/launchpad/test_lp_api_lite.py
new file mode 100644
index 0000000..f4b5ea6
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_api_lite.py
@@ -0,0 +1,549 @@
+# Copyright (C) 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
+
+"""Tools for dealing with the Launchpad API without using launchpadlib.
+"""
+
+import doctest
+import socket
+
+from bzrlib import tests
+from bzrlib.tests import features
+from bzrlib.plugins import launchpad
+from bzrlib.plugins.launchpad import lp_api_lite
+from testtools.matchers import DocTestMatches
+
+
+class _JSONParserFeature(features.Feature):
+
+ def _probe(self):
+ return lp_api_lite.json is not None
+
+ def feature_name(self):
+ return 'simplejson or json'
+
+
+JSONParserFeature = _JSONParserFeature()
+
+
+_example_response = r"""
+{
+ "total_size": 2,
+ "start": 0,
+ "next_collection_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary?distro_series=%2Fubuntu%2Flucid&exact_match=true&source_name=%22bzr%22&status=Published&ws.op=getPublishedSources&ws.start=1&ws.size=1",
+ "entries": [
+ {
+ "package_creator_link": "https://api.launchpad.net/1.0/~maxb",
+ "package_signer_link": "https://api.launchpad.net/1.0/~jelmer",
+ "source_package_name": "bzr",
+ "removal_comment": null,
+ "display_name": "bzr 2.1.4-0ubuntu1 in lucid",
+ "date_made_pending": null,
+ "source_package_version": "2.1.4-0ubuntu1",
+ "date_superseded": null,
+ "http_etag": "\"9ba966152dec474dc0fe1629d0bbce2452efaf3b-5f4c3fbb3eaf26d502db4089777a9b6a0537ffab\"",
+ "self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/1750327",
+ "distro_series_link": "https://api.launchpad.net/1.0/ubuntu/lucid",
+ "component_name": "main",
+ "status": "Published",
+ "date_removed": null,
+ "pocket": "Updates",
+ "date_published": "2011-05-30T06:09:58.653984+00:00",
+ "removed_by_link": null,
+ "section_name": "devel",
+ "resource_type_link": "https://api.launchpad.net/1.0/#source_package_publishing_history",
+ "archive_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary",
+ "package_maintainer_link": "https://api.launchpad.net/1.0/~ubuntu-devel-discuss-lists",
+ "date_created": "2011-05-30T05:19:12.233621+00:00",
+ "scheduled_deletion_date": null
+ }
+ ]
+}"""
+
+_no_versions_response = '{"total_size": 0, "start": 0, "entries": []}'
+
+
+class TestLatestPublication(tests.TestCase):
+
+ def make_latest_publication(self, archive='ubuntu', series='natty',
+ project='bzr'):
+ return lp_api_lite.LatestPublication(archive, series, project)
+
+ def assertPlace(self, place, archive, series, project):
+ lp = lp_api_lite.LatestPublication(archive, series, project)
+ self.assertEqual(place, lp.place())
+
+ def test_init(self):
+ latest_pub = self.make_latest_publication()
+ self.assertEqual('ubuntu', latest_pub._archive)
+ self.assertEqual('natty', latest_pub._series)
+ self.assertEqual('bzr', latest_pub._project)
+ self.assertEqual('Release', latest_pub._pocket)
+
+ def test__archive_URL(self):
+ latest_pub = self.make_latest_publication()
+ self.assertEqual(
+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary',
+ latest_pub._archive_URL())
+
+ def test__publication_status_for_ubuntu(self):
+ latest_pub = self.make_latest_publication()
+ self.assertEqual('Published', latest_pub._publication_status())
+
+ def test__publication_status_for_debian(self):
+ latest_pub = self.make_latest_publication(archive='debian')
+ self.assertEqual('Pending', latest_pub._publication_status())
+
+ def test_pocket(self):
+ latest_pub = self.make_latest_publication(series='natty-proposed')
+ self.assertEqual('natty', latest_pub._series)
+ self.assertEqual('Proposed', latest_pub._pocket)
+
+ def test_series_None(self):
+ latest_pub = self.make_latest_publication(series=None)
+ self.assertEqual('ubuntu', latest_pub._archive)
+ self.assertEqual(None, latest_pub._series)
+ self.assertEqual('bzr', latest_pub._project)
+ self.assertEqual('Release', latest_pub._pocket)
+
+ def test__query_params(self):
+ latest_pub = self.make_latest_publication()
+ self.assertEqual({'ws.op': 'getPublishedSources',
+ 'exact_match': 'true',
+ 'source_name': '"bzr"',
+ 'status': 'Published',
+ 'ws.size': '1',
+ 'distro_series': '/ubuntu/natty',
+ 'pocket': 'Release',
+ }, latest_pub._query_params())
+
+ def test__query_params_no_series(self):
+ latest_pub = self.make_latest_publication(series=None)
+ self.assertEqual({'ws.op': 'getPublishedSources',
+ 'exact_match': 'true',
+ 'source_name': '"bzr"',
+ 'status': 'Published',
+ 'ws.size': '1',
+ 'pocket': 'Release',
+ }, latest_pub._query_params())
+
+ def test__query_params_pocket(self):
+ latest_pub = self.make_latest_publication(series='natty-proposed')
+ self.assertEqual({'ws.op': 'getPublishedSources',
+ 'exact_match': 'true',
+ 'source_name': '"bzr"',
+ 'status': 'Published',
+ 'ws.size': '1',
+ 'distro_series': '/ubuntu/natty',
+ 'pocket': 'Proposed',
+ }, latest_pub._query_params())
+
+ def test__query_URL(self):
+ latest_pub = self.make_latest_publication()
+ # we explicitly sort params, so we can be sure this URL matches exactly
+ self.assertEqual(
+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary'
+ '?distro_series=%2Fubuntu%2Fnatty&exact_match=true'
+ '&pocket=Release&source_name=%22bzr%22&status=Published'
+ '&ws.op=getPublishedSources&ws.size=1',
+ latest_pub._query_URL())
+
+ def DONT_test__gracefully_handle_failed_rpc_connection(self):
+ # TODO: This test kind of sucks. We intentionally create an arbitrary
+ # port and don't listen to it, because we want the request to fail.
+ # However, it seems to take 1s for it to timeout. Is there a way
+ # to make it fail faster?
+ latest_pub = self.make_latest_publication()
+ s = socket.socket()
+ s.bind(('127.0.0.1', 0))
+ addr, port = s.getsockname()
+ latest_pub.LP_API_ROOT = 'http://%s:%s/' % (addr, port)
+ s.close()
+ self.assertIs(None, latest_pub._get_lp_info())
+
+ def DONT_test__query_launchpad(self):
+ # TODO: This is a test that we are making a valid request against
+ # launchpad. This seems important, but it is slow, requires net
+ # access, and requires launchpad to be up and running. So for
+ # now, it is commented out for production tests.
+ latest_pub = self.make_latest_publication()
+ json_txt = latest_pub._get_lp_info()
+ self.assertIsNot(None, json_txt)
+ if lp_api_lite.json is None:
+ # We don't have a way to parse the text
+ return
+ # The content should be a valid json result
+ content = lp_api_lite.json.loads(json_txt)
+ entries = content['entries'] # It should have an 'entries' field.
+ # ws.size should mean we get 0 or 1, and there should be something
+ self.assertEqual(1, len(entries))
+ entry = entries[0]
+ self.assertEqual('bzr', entry['source_package_name'])
+ version = entry['source_package_version']
+ self.assertIsNot(None, version)
+
+ def test__get_lp_info_no_json(self):
+ # If we can't parse the json, we don't make the query.
+ self.overrideAttr(lp_api_lite, 'json', None)
+ latest_pub = self.make_latest_publication()
+ self.assertIs(None, latest_pub._get_lp_info())
+
+ def test__parse_json_info_no_module(self):
+ # If a json parsing module isn't available, we just return None here.
+ self.overrideAttr(lp_api_lite, 'json', None)
+ latest_pub = self.make_latest_publication()
+ self.assertIs(None, latest_pub._parse_json_info(_example_response))
+
+ def test__parse_json_example_response(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ content = latest_pub._parse_json_info(_example_response)
+ self.assertIsNot(None, content)
+ self.assertEqual(2, content['total_size'])
+ entries = content['entries']
+ self.assertEqual(1, len(entries))
+ entry = entries[0]
+ self.assertEqual('bzr', entry['source_package_name'])
+ self.assertEqual("2.1.4-0ubuntu1", entry["source_package_version"])
+
+ def test__parse_json_not_json(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ self.assertIs(None, latest_pub._parse_json_info('Not_valid_json'))
+
+ def test_get_latest_version_no_response(self):
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: None
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_no_json(self):
+ self.overrideAttr(lp_api_lite, 'json', None)
+ latest_pub = self.make_latest_publication()
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_invalid_json(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: "not json"
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_no_versions(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: _no_versions_response
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_missing_entries(self):
+ # Launchpad's no-entries response does have an empty entries value.
+ # However, lets test that we handle other failures without tracebacks
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: '{}'
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_invalid_entries(self):
+ # Make sure we sanely handle a json response we don't understand
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: '{"entries": {"a": 1}}'
+ self.assertEqual(None, latest_pub.get_latest_version())
+
+ def test_get_latest_version_example(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ latest_pub._get_lp_info = lambda: _example_response
+ self.assertEqual("2.1.4-0ubuntu1", latest_pub.get_latest_version())
+
+ def DONT_test_get_latest_version_from_launchpad(self):
+ self.requireFeature(JSONParserFeature)
+ latest_pub = self.make_latest_publication()
+ self.assertIsNot(None, latest_pub.get_latest_version())
+
+ def test_place(self):
+ self.assertPlace('Ubuntu', 'ubuntu', None, 'bzr')
+ self.assertPlace('Ubuntu Natty', 'ubuntu', 'natty', 'bzr')
+ self.assertPlace('Ubuntu Natty Proposed', 'ubuntu', 'natty-proposed',
+ 'bzr')
+ self.assertPlace('Debian', 'debian', None, 'bzr')
+ self.assertPlace('Debian Sid', 'debian', 'sid', 'bzr')
+
+
+class TestIsUpToDate(tests.TestCase):
+
+ def assertPackageBranchRe(self, url, user, archive, series, project):
+ m = launchpad._package_branch.search(url)
+ if m is None:
+ self.fail('package_branch regex did not match url: %s' % (url,))
+ self.assertEqual(
+ (user, archive, series, project),
+ m.group('user', 'archive', 'series', 'project'))
+
+ def assertNotPackageBranch(self, url):
+ self.assertIs(None, launchpad._get_package_branch_info(url))
+
+ def assertBranchInfo(self, url, archive, series, project):
+ self.assertEqual((archive, series, project),
+ launchpad._get_package_branch_info(url))
+
+ def test_package_branch_regex(self):
+ self.assertPackageBranchRe(
+ 'http://bazaar.launchpad.net/+branch/ubuntu/foo',
+ None, 'ubuntu', None, 'foo')
+ self.assertPackageBranchRe(
+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo',
+ None, 'ubuntu', 'natty/', 'foo')
+ self.assertPackageBranchRe(
+ 'sftp://bazaar.launchpad.net/+branch/debian/foo',
+ None, 'debian', None, 'foo')
+ self.assertPackageBranchRe(
+ 'http://bazaar.launchpad.net/+branch/debian/sid/foo',
+ None, 'debian', 'sid/', 'foo')
+ self.assertPackageBranchRe(
+ 'http://bazaar.launchpad.net/+branch'
+ '/~ubuntu-branches/ubuntu/natty/foo/natty',
+ '~ubuntu-branches/', 'ubuntu', 'natty/', 'foo')
+ self.assertPackageBranchRe(
+ 'http://bazaar.launchpad.net/+branch'
+ '/~user/ubuntu/natty/foo/test',
+ '~user/', 'ubuntu', 'natty/', 'foo')
+
+ def test_package_branch_doesnt_match(self):
+ self.assertNotPackageBranch('http://example.com/ubuntu/foo')
+ self.assertNotPackageBranch(
+ 'http://bazaar.launchpad.net/+branch/bzr')
+ self.assertNotPackageBranch(
+ 'http://bazaar.launchpad.net/+branch/~bzr-pqm/bzr/bzr.dev')
+ # Not a packaging branch because ~user isn't ~ubuntu-branches
+ self.assertNotPackageBranch(
+ 'http://bazaar.launchpad.net/+branch'
+ '/~user/ubuntu/natty/foo/natty')
+ # Older versions of bzr-svn/hg/git did not set Branch.base until after
+ # they called Branch.__init__().
+ self.assertNotPackageBranch(None)
+
+ def test__get_package_branch_info(self):
+ self.assertBranchInfo(
+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo',
+ 'ubuntu', 'natty', 'foo')
+ self.assertBranchInfo(
+ 'bzr+ssh://bazaar.launchpad.net/+branch'
+ '/~ubuntu-branches/ubuntu/natty/foo/natty',
+ 'ubuntu', 'natty', 'foo')
+ self.assertBranchInfo(
+ 'http://bazaar.launchpad.net/+branch'
+ '/~ubuntu-branches/debian/sid/foo/sid',
+ 'debian', 'sid', 'foo')
+
+
+class TestGetMostRecentTag(tests.TestCaseWithMemoryTransport):
+
+ def make_simple_builder(self):
+ builder = self.make_branch_builder('tip')
+ builder.build_snapshot('A', [], [
+ ('add', ('', 'root-id', 'directory', None))])
+ b = builder.get_branch()
+ b.tags.set_tag('tip-1.0', 'A')
+ return builder, b, b.tags.get_tag_dict()
+
+ def test_get_most_recent_tag_tip(self):
+ builder, b, tag_dict = self.make_simple_builder()
+ self.assertEqual('tip-1.0',
+ lp_api_lite.get_most_recent_tag(tag_dict, b))
+
+ def test_get_most_recent_tag_older(self):
+ builder, b, tag_dict = self.make_simple_builder()
+ builder.build_snapshot('B', ['A'], [])
+ self.assertEqual('B', b.last_revision())
+ self.assertEqual('tip-1.0',
+ lp_api_lite.get_most_recent_tag(tag_dict, b))
+
+
+class StubLatestPublication(object):
+
+ def __init__(self, latest):
+ self.called = False
+ self.latest = latest
+
+ def get_latest_version(self):
+ self.called = True
+ return self.latest
+
+ def place(self):
+ return 'Ubuntu Natty'
+
+
+class TestReportFreshness(tests.TestCaseWithMemoryTransport):
+
+ def setUp(self):
+ super(TestReportFreshness, self).setUp()
+ builder = self.make_branch_builder('tip')
+ builder.build_snapshot('A', [], [
+ ('add', ('', 'root-id', 'directory', None))])
+ self.branch = builder.get_branch()
+
+ def assertFreshnessReports(self, verbosity, latest_version, content):
+ """Assert that lp_api_lite.report_freshness reports the given content.
+
+ :param verbosity: The reporting level
+ :param latest_version: The version reported by StubLatestPublication
+ :param content: The expected content. This should be in DocTest form.
+ """
+ orig_log_len = len(self.get_log())
+ lp_api_lite.report_freshness(self.branch, verbosity,
+ StubLatestPublication(latest_version))
+ new_content = self.get_log()[orig_log_len:]
+ # Strip out lines that have LatestPublication.get_* because those are
+ # timing related lines. While interesting to log for now, they aren't
+ # something we want to be testing
+ new_content = new_content.split('\n')
+ for i in range(2):
+ if (len(new_content) > 0
+ and 'LatestPublication.get_' in new_content[0]):
+ new_content = new_content[1:]
+ new_content = '\n'.join(new_content)
+ self.assertThat(new_content,
+ DocTestMatches(content,
+ doctest.ELLIPSIS | doctest.REPORT_UDIFF))
+
+ def test_verbosity_off_skips_check(self):
+ # We force _get_package_branch_info so that we know it would otherwise
+ # try to connect to launcphad
+ self.overrideAttr(launchpad, '_get_package_branch_info',
+ lambda x: ('ubuntu', 'natty', 'bzr'))
+ self.overrideAttr(lp_api_lite, 'LatestPublication',
+ lambda *args: self.fail('Tried to query launchpad'))
+ c = self.branch.get_config_stack()
+ c.set('launchpad.packaging_verbosity', 'off')
+ orig_log_len = len(self.get_log())
+ launchpad._check_is_up_to_date(self.branch)
+ new_content = self.get_log()[orig_log_len:]
+ self.assertContainsRe(new_content,
+ 'not checking memory.*/tip/ because verbosity is turned off')
+
+ def test_verbosity_off(self):
+ latest_pub = StubLatestPublication('1.0-1ubuntu2')
+ lp_api_lite.report_freshness(self.branch, 'off', latest_pub)
+ self.assertFalse(latest_pub.called)
+
+ def test_verbosity_all_out_of_date_smoke(self):
+ self.branch.tags.set_tag('1.0-1ubuntu1', 'A')
+ self.assertFreshnessReports('all', '1.0-1ubuntu2',
+ ' INFO Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
+ 'Packaging branch version: 1.0-1ubuntu1\n'
+ 'Packaging branch status: OUT-OF-DATE\n')
+
+
+class Test_GetNewestVersions(tests.TestCaseWithMemoryTransport):
+
+ def setUp(self):
+ super(Test_GetNewestVersions, self).setUp()
+ builder = self.make_branch_builder('tip')
+ builder.build_snapshot('A', [], [
+ ('add', ('', 'root-id', 'directory', None))])
+ self.branch = builder.get_branch()
+
+ def assertLatestVersions(self, latest_branch_version, pub_version):
+ if latest_branch_version is not None:
+ self.branch.tags.set_tag(latest_branch_version, 'A')
+ latest_pub = StubLatestPublication(pub_version)
+ self.assertEqual((pub_version, latest_branch_version),
+ lp_api_lite._get_newest_versions(self.branch, latest_pub))
+
+ def test_no_tags(self):
+ self.assertLatestVersions(None, '1.0-1ubuntu2')
+
+ def test_out_of_date(self):
+ self.assertLatestVersions('1.0-1ubuntu1', '1.0-1ubuntu2')
+
+ def test_up_to_date(self):
+ self.assertLatestVersions('1.0-1ubuntu2', '1.0-1ubuntu2')
+
+ def test_missing(self):
+ self.assertLatestVersions(None, None)
+
+
+class Test_ReportFreshness(tests.TestCase):
+
+ def assertReportedFreshness(self, verbosity, latest_ver, branch_latest_ver,
+ content, place='Ubuntu Natty'):
+ """Assert that lp_api_lite.report_freshness reports the given content.
+ """
+ reported = []
+ def report_func(value):
+ reported.append(value)
+
+ lp_api_lite._report_freshness(latest_ver, branch_latest_ver, place,
+ verbosity, report_func)
+ new_content = '\n'.join(reported)
+ self.assertThat(new_content,
+ DocTestMatches(content,
+ doctest.ELLIPSIS | doctest.REPORT_UDIFF))
+
+ def test_verbosity_minimal_no_tags(self):
+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', None,
+ 'Branch is OUT-OF-DATE, Ubuntu Natty has 1.0-1ubuntu2\n')
+
+ def test_verbosity_minimal_out_of_date(self):
+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu1',
+ '1.0-1ubuntu1 is OUT-OF-DATE,'
+ ' Ubuntu Natty has 1.0-1ubuntu2\n')
+
+ def test_verbosity_minimal_up_to_date(self):
+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu2',
+ '')
+
+ def test_verbosity_minimal_missing(self):
+ self.assertReportedFreshness('minimal', None, None,
+ '')
+
+ def test_verbosity_short_out_of_date(self):
+ self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu1',
+ '1.0-1ubuntu1 is OUT-OF-DATE,'
+ ' Ubuntu Natty has 1.0-1ubuntu2\n')
+
+ def test_verbosity_short_up_to_date(self):
+ self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu2',
+ '1.0-1ubuntu2 is CURRENT in Ubuntu Natty')
+
+ def test_verbosity_short_missing(self):
+ self.assertReportedFreshness('short', None, None,
+ 'Ubuntu Natty is MISSING a version')
+
+ def test_verbosity_all_no_tags(self):
+ self.assertReportedFreshness('all', '1.0-1ubuntu2', None,
+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
+ 'Packaging branch version: None\n'
+ 'Packaging branch status: OUT-OF-DATE\n')
+
+ def test_verbosity_all_out_of_date(self):
+ self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu1',
+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
+ 'Packaging branch version: 1.0-1ubuntu1\n'
+ 'Packaging branch status: OUT-OF-DATE\n')
+
+ def test_verbosity_all_up_to_date(self):
+ self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu2',
+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
+ 'Packaging branch status: CURRENT\n')
+
+ def test_verbosity_all_missing(self):
+ self.assertReportedFreshness('all', None, None,
+ 'Most recent Ubuntu Natty version: MISSING\n')
+
+ def test_verbosity_None_is_all(self):
+ self.assertReportedFreshness(None, '1.0-1ubuntu2', '1.0-1ubuntu2',
+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
+ 'Packaging branch status: CURRENT\n')
diff --git a/bzrlib/plugins/launchpad/test_lp_directory.py b/bzrlib/plugins/launchpad/test_lp_directory.py
new file mode 100644
index 0000000..f678ff3
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_directory.py
@@ -0,0 +1,639 @@
+# Copyright (C) 2007-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
+
+"""Tests for directory lookup through Launchpad.net"""
+
+import os
+import xmlrpclib
+
+import bzrlib
+from bzrlib import (
+ debug,
+ errors,
+ tests,
+ transport,
+ )
+from bzrlib.branch import Branch
+from bzrlib.directory_service import directories
+from bzrlib.tests import (
+ features,
+ ssl_certs,
+ TestCaseInTempDir,
+ TestCaseWithMemoryTransport
+)
+from bzrlib.plugins.launchpad import (
+ _register_directory,
+ lp_registration,
+ )
+from bzrlib.plugins.launchpad.lp_directory import (
+ LaunchpadDirectory)
+from bzrlib.plugins.launchpad.account import get_lp_login, set_lp_login
+from bzrlib.tests import http_server
+
+
+def load_tests(standard_tests, module, loader):
+ result = loader.suiteClass()
+ t_tests, remaining_tests = tests.split_suite_by_condition(
+ standard_tests, tests.condition_isinstance((
+ TestXMLRPCTransport,
+ )))
+ transport_scenarios = [
+ ('http', dict(server_class=PreCannedHTTPServer,)),
+ ]
+ if features.HTTPSServerFeature.available():
+ transport_scenarios.append(
+ ('https', dict(server_class=PreCannedHTTPSServer,)),
+ )
+ tests.multiply_tests(t_tests, transport_scenarios, result)
+
+ # No parametrization for the remaining tests
+ result.addTests(remaining_tests)
+
+ return result
+
+
+class FakeResolveFactory(object):
+
+ def __init__(self, test, expected_path, result):
+ self._test = test
+ self._expected_path = expected_path
+ self._result = result
+ self._submitted = False
+
+ def __call__(self, path):
+ self._test.assertEqual(self._expected_path, path)
+ return self
+
+ def submit(self, service):
+ self._service_url = service.service_url
+ self._submitted = True
+ return self._result
+
+
+class LocalDirectoryURLTests(TestCaseInTempDir):
+ """Tests for branch urls that we try to pass through local resolution."""
+
+ def assertResolve(self, expected, url, submitted=False):
+ path = url[url.index(':')+1:].lstrip('/')
+ factory = FakeResolveFactory(self, path,
+ dict(urls=['bzr+ssh://fake-resolved']))
+ directory = LaunchpadDirectory()
+ self.assertEqual(expected,
+ directory._resolve(url, factory, _lp_login='user'))
+ # We are testing local resolution, and the fallback when necessary.
+ self.assertEqual(submitted, factory._submitted)
+
+ def test_short_form(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt',
+ 'lp:apt')
+
+ def test_two_part_form(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt/2.2',
+ 'lp:apt/2.2')
+
+ def test_two_part_plus_subdir(self):
+ # We allow you to pass more than just what resolves. That way you can
+ # do things like "bzr log lp:apt/2.2/BUGS"
+ # Though the virtual FS implementation currently aborts when given a
+ # URL like this, rather than letting you recurse upwards to find the
+ # real branch at lp:apt/2.2
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt/2.2/BUGS',
+ 'lp:apt/2.2/BUGS')
+
+ def test_user_expansion(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/~user/apt/foo',
+ 'lp:~/apt/foo')
+
+ def test_ubuntu(self):
+ # Confirmed against xmlrpc. If you don't have a ~user, xmlrpc doesn't
+ # care that you are asking for 'ubuntu'
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/ubuntu',
+ 'lp:ubuntu')
+
+ def test_ubuntu_invalid(self):
+ """Invalid ubuntu urls don't crash.
+
+ :seealso: http://pad.lv/843900
+ """
+ # This ought to be natty-updates.
+ self.assertRaises(errors.InvalidURL,
+ self.assertResolve,
+ '',
+ 'ubuntu:natty/updates/smartpm')
+
+ def test_ubuntu_apt(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/apt',
+ 'lp:ubuntu/apt')
+
+ def test_ubuntu_natty_apt(self):
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/apt',
+ 'lp:ubuntu/natty/apt')
+
+ def test_ubuntu_natty_apt_filename(self):
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/apt/filename',
+ 'lp:ubuntu/natty/apt/filename')
+
+ def test_user_two_part(self):
+ # We fall back to the ResolveFactory. The real Launchpad one will raise
+ # InvalidURL for this case.
+ self.assertResolve('bzr+ssh://fake-resolved', 'lp:~jameinel/apt',
+ submitted=True)
+
+ def test_user_three_part(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/~jameinel/apt/foo',
+ 'lp:~jameinel/apt/foo')
+
+ def test_user_three_part_plus_filename(self):
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/~jameinel/apt/foo/fname',
+ 'lp:~jameinel/apt/foo/fname')
+
+ def test_user_ubuntu_two_part(self):
+ self.assertResolve('bzr+ssh://fake-resolved', 'lp:~jameinel/ubuntu',
+ submitted=True)
+ self.assertResolve('bzr+ssh://fake-resolved', 'lp:~jameinel/debian',
+ submitted=True)
+
+ def test_user_ubuntu_three_part(self):
+ self.assertResolve('bzr+ssh://fake-resolved',
+ 'lp:~jameinel/ubuntu/natty', submitted=True)
+ self.assertResolve('bzr+ssh://fake-resolved',
+ 'lp:~jameinel/debian/sid', submitted=True)
+
+ def test_user_ubuntu_four_part(self):
+ self.assertResolve('bzr+ssh://fake-resolved',
+ 'lp:~jameinel/ubuntu/natty/project', submitted=True)
+ self.assertResolve('bzr+ssh://fake-resolved',
+ 'lp:~jameinel/debian/sid/project', submitted=True)
+
+ def test_user_ubuntu_five_part(self):
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/~jameinel/ubuntu/natty/apt/branch',
+ 'lp:~jameinel/ubuntu/natty/apt/branch')
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/~jameinel/debian/sid/apt/branch',
+ 'lp:~jameinel/debian/sid/apt/branch')
+
+ def test_user_ubuntu_five_part_plus_subdir(self):
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/~jameinel/ubuntu/natty/apt/branch/f',
+ 'lp:~jameinel/ubuntu/natty/apt/branch/f')
+ self.assertResolve(
+ 'bzr+ssh://bazaar.launchpad.net/~jameinel/debian/sid/apt/branch/f',
+ 'lp:~jameinel/debian/sid/apt/branch/f')
+
+ def test_handles_special_lp(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt', 'lp:apt')
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt',
+ 'lp:///apt')
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/apt',
+ 'lp://production/apt')
+ self.assertResolve('bzr+ssh://bazaar.launchpad.dev/+branch/apt',
+ 'lp://dev/apt')
+ self.assertResolve('bzr+ssh://bazaar.staging.launchpad.net/+branch/apt',
+ 'lp://staging/apt')
+ self.assertResolve('bzr+ssh://bazaar.qastaging.launchpad.net/+branch/apt',
+ 'lp://qastaging/apt')
+ self.assertResolve('bzr+ssh://bazaar.demo.launchpad.net/+branch/apt',
+ 'lp://demo/apt')
+
+ def test_debug_launchpad_uses_resolver(self):
+ self.assertResolve('bzr+ssh://bazaar.launchpad.net/+branch/bzr',
+ 'lp:bzr', submitted=False)
+ debug.debug_flags.add('launchpad')
+ self.addCleanup(debug.debug_flags.discard, 'launchpad')
+ self.assertResolve('bzr+ssh://fake-resolved', 'lp:bzr', submitted=True)
+
+
+class DirectoryUrlTests(TestCaseInTempDir):
+ """Tests for branch urls through Launchpad.net directory"""
+
+ def test_short_form(self):
+ """A launchpad url should map to a http url"""
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.launchpad.net/~apt/apt/devel',
+ directory._resolve('lp:apt', factory))
+ # Make sure that resolve went to the production server.
+ self.assertEquals('https://xmlrpc.launchpad.net/bazaar/',
+ factory._service_url)
+
+ def test_qastaging(self):
+ """A launchpad url should map to a http url"""
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'http://bazaar.qastaging.launchpad.net/~apt/apt/devel']))
+ url = 'lp://qastaging/apt'
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.qastaging.launchpad.net/~apt/apt/devel',
+ directory._resolve(url, factory))
+ # Make sure that resolve went to the qastaging server.
+ self.assertEquals('https://xmlrpc.qastaging.launchpad.net/bazaar/',
+ factory._service_url)
+
+ def test_staging(self):
+ """A launchpad url should map to a http url"""
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'http://bazaar.staging.launchpad.net/~apt/apt/devel']))
+ url = 'lp://staging/apt'
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.staging.launchpad.net/~apt/apt/devel',
+ directory._resolve(url, factory))
+ # Make sure that resolve went to the staging server.
+ self.assertEquals('https://xmlrpc.staging.launchpad.net/bazaar/',
+ factory._service_url)
+
+ def test_url_from_directory(self):
+ """A launchpad url should map to a http url"""
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.launchpad.net/~apt/apt/devel',
+ directory._resolve('lp:///apt', factory))
+
+ def test_directory_skip_bad_schemes(self):
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'bad-scheme://bazaar.launchpad.net/~apt/apt/devel',
+ 'http://bazaar.launchpad.net/~apt/apt/devel',
+ 'http://another/location']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.launchpad.net/~apt/apt/devel',
+ directory._resolve('lp:///apt', factory))
+
+ def test_directory_no_matching_schemes(self):
+ # If the XMLRPC call does not return any protocols we support,
+ # invalidURL is raised.
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'bad-scheme://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertRaises(errors.InvalidURL,
+ directory._resolve, 'lp:///apt', factory)
+
+ def test_directory_fault(self):
+ # Test that XMLRPC faults get converted to InvalidURL errors.
+ factory = FakeResolveFactory(self, 'apt', None)
+ def submit(service):
+ raise xmlrpclib.Fault(42, 'something went wrong')
+ factory.submit = submit
+ directory = LaunchpadDirectory()
+ self.assertRaises(errors.InvalidURL,
+ directory._resolve, 'lp:///apt', factory)
+
+ def test_skip_bzr_ssh_launchpad_net_when_anonymous(self):
+ # Test that bzr+ssh://bazaar.launchpad.net gets skipped if
+ # Bazaar does not know the user's Launchpad ID:
+ self.assertEqual(None, get_lp_login())
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'bzr+ssh://bazaar.launchpad.net/~apt/apt/devel',
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.launchpad.net/~apt/apt/devel',
+ directory._resolve('lp:///apt', factory))
+
+ def test_skip_sftp_launchpad_net_when_anonymous(self):
+ # Test that sftp://bazaar.launchpad.net gets skipped if
+ # Bazaar does not know the user's Launchpad ID:
+ self.assertEqual(None, get_lp_login())
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'sftp://bazaar.launchpad.net/~apt/apt/devel',
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('http://bazaar.launchpad.net/~apt/apt/devel',
+ directory._resolve('lp:///apt', factory))
+
+ def test_with_login_avoid_resolve_factory(self):
+ # Test that bzr+ssh URLs get rewritten to include the user's
+ # Launchpad ID (assuming we know the Launchpad ID).
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'bzr+ssh://my-super-custom/special/devel',
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals(
+ 'bzr+ssh://bazaar.launchpad.net/+branch/apt',
+ directory._resolve('lp:///apt', factory, _lp_login='username'))
+
+ def test_no_rewrite_of_other_bzr_ssh(self):
+ # Test that we don't rewrite bzr+ssh URLs for other
+ self.assertEqual(None, get_lp_login())
+ factory = FakeResolveFactory(
+ self, 'apt', dict(urls=[
+ 'bzr+ssh://example.com/~apt/apt/devel',
+ 'http://bazaar.launchpad.net/~apt/apt/devel']))
+ directory = LaunchpadDirectory()
+ self.assertEquals('bzr+ssh://example.com/~apt/apt/devel',
+ directory._resolve('lp:///apt', factory))
+
+ # TODO: check we get an error if the url is unreasonable
+ def test_error_for_bad_url(self):
+ directory = LaunchpadDirectory()
+ self.assertRaises(errors.InvalidURL,
+ directory._resolve, 'lp://ratotehunoahu')
+
+ def test_resolve_tilde_to_user(self):
+ factory = FakeResolveFactory(
+ self, '~username/apt/test', dict(urls=[
+ 'bzr+ssh://bazaar.launchpad.net/~username/apt/test']))
+ directory = LaunchpadDirectory()
+ self.assertEquals(
+ 'bzr+ssh://bazaar.launchpad.net/~username/apt/test',
+ directory._resolve('lp:~/apt/test', factory, _lp_login='username'))
+ # Should also happen when the login is just set by config
+ set_lp_login('username')
+ self.assertEquals(
+ 'bzr+ssh://bazaar.launchpad.net/~username/apt/test',
+ directory._resolve('lp:~/apt/test', factory))
+
+ def test_tilde_fails_no_login(self):
+ factory = FakeResolveFactory(
+ self, '~username/apt/test', dict(urls=[
+ 'bzr+ssh://bazaar.launchpad.net/~username/apt/test']))
+ self.assertIs(None, get_lp_login())
+ directory = LaunchpadDirectory()
+ self.assertRaises(errors.InvalidURL,
+ directory._resolve, 'lp:~/apt/test', factory)
+
+
+class DirectoryOpenBranchTests(TestCaseWithMemoryTransport):
+
+ def test_directory_open_branch(self):
+ # Test that opening an lp: branch redirects to the real location.
+ target_branch = self.make_branch('target')
+ class FooService(object):
+ """A directory service that maps the name to a FILE url"""
+
+ def look_up(self, name, url):
+ if 'lp:///apt' == url:
+ return target_branch.base.rstrip('/')
+ return '!unexpected look_up value!'
+
+ directories.remove('lp:')
+ directories.remove('ubuntu:')
+ directories.remove('debianlp:')
+ directories.register('lp:', FooService, 'Map lp URLs to local urls')
+ self.addCleanup(_register_directory)
+ self.addCleanup(directories.remove, 'lp:')
+ t = transport.get_transport('lp:///apt')
+ branch = Branch.open_from_transport(t)
+ self.assertEqual(target_branch.base, branch.base)
+
+
+class PredefinedRequestHandler(http_server.TestingHTTPRequestHandler):
+ """Request handler for a unique and pre-defined request.
+
+ The only thing we care about here is that we receive a connection. But
+ since we want to dialog with a real http client, we have to send it correct
+ responses.
+
+ We expect to receive a *single* request nothing more (and we won't even
+ check what request it is), the tests will recognize us from our response.
+ """
+
+ def handle_one_request(self):
+ tcs = self.server.test_case_server
+ requestline = self.rfile.readline()
+ self.MessageClass(self.rfile, 0)
+ if requestline.startswith('POST'):
+ # The body should be a single line (or we don't know where it ends
+ # and we don't want to issue a blocking read)
+ self.rfile.readline()
+
+ self.wfile.write(tcs.canned_response)
+
+
+class PreCannedServerMixin(object):
+
+ def __init__(self):
+ super(PreCannedServerMixin, self).__init__(
+ request_handler=PredefinedRequestHandler)
+ # Bytes read and written by the server
+ self.bytes_read = 0
+ self.bytes_written = 0
+ self.canned_response = None
+
+
+class PreCannedHTTPServer(PreCannedServerMixin, http_server.HttpServer):
+ pass
+
+
+if features.HTTPSServerFeature.available():
+ from bzrlib.tests import https_server
+ class PreCannedHTTPSServer(PreCannedServerMixin, https_server.HTTPSServer):
+ pass
+
+
+class TestXMLRPCTransport(tests.TestCase):
+
+ # set by load_tests
+ server_class = None
+
+ def setUp(self):
+ tests.TestCase.setUp(self)
+ self.server = self.server_class()
+ self.server.start_server()
+ self.addCleanup(self.server.stop_server)
+ # Ensure we don't clobber env
+ self.overrideEnv('BZR_LP_XMLRPC_URL', None)
+ # Ensure we use the right certificates for https.
+ # FIXME: There should be a better way but the only alternative I can
+ # think of involves carrying the ca_certs through the lp_registration
+ # infrastructure to _urllib2_wrappers... -- vila 2012-01-20
+ bzrlib.global_state.cmdline_overrides._from_cmdline(
+ ['ssl.ca_certs=%s' % ssl_certs.build_path('ca.crt')])
+
+ def set_canned_response(self, server, path):
+ response_format = '''HTTP/1.1 200 OK\r
+Date: Tue, 11 Jul 2006 04:32:56 GMT\r
+Server: Apache/2.0.54 (Fedora)\r
+Last-Modified: Sun, 23 Apr 2006 19:35:20 GMT\r
+ETag: "56691-23-38e9ae00"\r
+Accept-Ranges: bytes\r
+Content-Length: %(length)d\r
+Connection: close\r
+Content-Type: text/plain; charset=UTF-8\r
+\r
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>urls</name>
+<value><array><data>
+<value><string>bzr+ssh://bazaar.launchpad.net/%(path)s</string></value>
+<value><string>http://bazaar.launchpad.net/%(path)s</string></value>
+</data></array></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+'''
+ length = 334 + 2 * len(path)
+ server.canned_response = response_format % dict(length=length,
+ path=path)
+
+ def do_request(self, server_url):
+ os.environ['BZR_LP_XMLRPC_URL'] = self.server.get_url()
+ service = lp_registration.LaunchpadService()
+ resolve = lp_registration.ResolveLaunchpadPathRequest('bzr')
+ result = resolve.submit(service)
+ return result
+
+ def test_direct_request(self):
+ self.set_canned_response(self.server, '~bzr-pqm/bzr/bzr.dev')
+ result = self.do_request(self.server.get_url())
+ urls = result.get('urls', None)
+ self.assertIsNot(None, urls)
+ self.assertEquals(
+ ['bzr+ssh://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev',
+ 'http://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev'],
+ urls)
+ # FIXME: we need to test with a real proxy, I can't find a way so simulate
+ # CONNECT without leaving one server hanging the test :-/ Since that maybe
+ # related to the leaking tests problems, I'll punt for now -- vila 20091030
+
+
+class TestDebuntuExpansions(TestCaseInTempDir):
+ """Test expansions for ubuntu: and debianlp: schemes."""
+
+ def setUp(self):
+ super(TestDebuntuExpansions, self).setUp()
+ self.directory = LaunchpadDirectory()
+
+ def _make_factory(self, package='foo', distro='ubuntu', series=None):
+ if series is None:
+ path = '%s/%s' % (distro, package)
+ url_suffix = '~branch/%s/%s' % (distro, package)
+ else:
+ path = '%s/%s/%s' % (distro, series, package)
+ url_suffix = '~branch/%s/%s/%s' % (distro, series, package)
+ return FakeResolveFactory(
+ self, path, dict(urls=[
+ 'http://bazaar.launchpad.net/' + url_suffix]))
+
+ def assertURL(self, expected_url, shortcut, package='foo', distro='ubuntu',
+ series=None):
+ factory = self._make_factory(package=package, distro=distro,
+ series=series)
+ self.assertEqual('http://bazaar.launchpad.net/~branch/' + expected_url,
+ self.directory._resolve(shortcut, factory))
+
+ # Bogus distro.
+
+ def test_bogus_distro(self):
+ self.assertRaises(errors.InvalidURL,
+ self.directory._resolve, 'gentoo:foo')
+
+ def test_trick_bogus_distro_u(self):
+ self.assertRaises(errors.InvalidURL,
+ self.directory._resolve, 'utube:foo')
+
+ def test_trick_bogus_distro_d(self):
+ self.assertRaises(errors.InvalidURL,
+ self.directory._resolve, 'debuntu:foo')
+
+ def test_missing_ubuntu_distroseries_without_project(self):
+ # Launchpad does not hold source packages for Intrepid. Missing or
+ # bogus distroseries with no project name is treated like a project.
+ self.assertURL('ubuntu/intrepid', 'ubuntu:intrepid', package='intrepid')
+
+ def test_missing_ubuntu_distroseries_with_project(self):
+ # Launchpad does not hold source packages for Intrepid. Missing or
+ # bogus distroseries with a project name is treated like an unknown
+ # series (i.e. we keep it verbatim).
+ self.assertURL('ubuntu/intrepid/foo',
+ 'ubuntu:intrepid/foo', series='intrepid')
+
+ def test_missing_debian_distroseries(self):
+ # Launchpad does not hold source packages for unstable. Missing or
+ # bogus distroseries is treated like a project.
+ self.assertURL('debian/sid',
+ 'debianlp:sid', package='sid', distro='debian')
+
+ # Ubuntu Default distro series.
+
+ def test_ubuntu_default_distroseries_expansion(self):
+ self.assertURL('ubuntu/foo', 'ubuntu:foo')
+
+ def test_ubuntu_natty_distroseries_expansion(self):
+ self.assertURL('ubuntu/natty/foo', 'ubuntu:natty/foo', series='natty')
+
+ def test_ubuntu_n_distroseries_expansion(self):
+ self.assertURL('ubuntu/natty/foo', 'ubuntu:n/foo', series='natty')
+
+ def test_ubuntu_maverick_distroseries_expansion(self):
+ self.assertURL('ubuntu/maverick/foo', 'ubuntu:maverick/foo',
+ series='maverick')
+
+ def test_ubuntu_m_distroseries_expansion(self):
+ self.assertURL('ubuntu/maverick/foo', 'ubuntu:m/foo', series='maverick')
+
+ def test_ubuntu_lucid_distroseries_expansion(self):
+ self.assertURL('ubuntu/lucid/foo', 'ubuntu:lucid/foo', series='lucid')
+
+ def test_ubuntu_l_distroseries_expansion(self):
+ self.assertURL('ubuntu/lucid/foo', 'ubuntu:l/foo', series='lucid')
+
+ def test_ubuntu_karmic_distroseries_expansion(self):
+ self.assertURL('ubuntu/karmic/foo', 'ubuntu:karmic/foo',
+ series='karmic')
+
+ def test_ubuntu_k_distroseries_expansion(self):
+ self.assertURL('ubuntu/karmic/foo', 'ubuntu:k/foo', series='karmic')
+
+ def test_ubuntu_jaunty_distroseries_expansion(self):
+ self.assertURL('ubuntu/jaunty/foo', 'ubuntu:jaunty/foo',
+ series='jaunty')
+
+ def test_ubuntu_j_distroseries_expansion(self):
+ self.assertURL('ubuntu/jaunty/foo', 'ubuntu:j/foo', series='jaunty')
+
+ def test_ubuntu_hardy_distroseries_expansion(self):
+ self.assertURL('ubuntu/hardy/foo', 'ubuntu:hardy/foo', series='hardy')
+
+ def test_ubuntu_h_distroseries_expansion(self):
+ self.assertURL('ubuntu/hardy/foo', 'ubuntu:h/foo', series='hardy')
+
+ def test_ubuntu_dapper_distroseries_expansion(self):
+ self.assertURL('ubuntu/dapper/foo', 'ubuntu:dapper/foo',
+ series='dapper')
+
+ def test_ubuntu_d_distroseries_expansion(self):
+ self.assertURL('ubuntu/dapper/foo', 'ubuntu:d/foo', series='dapper')
+
+ # Debian default distro series.
+
+ def test_debian_default_distroseries_expansion(self):
+ self.assertURL('debian/foo', 'debianlp:foo', distro='debian')
+
+ def test_debian_squeeze_distroseries_expansion(self):
+ self.assertURL('debian/squeeze/foo', 'debianlp:squeeze/foo',
+ distro='debian', series='squeeze')
+
+ def test_debian_lenny_distroseries_expansion(self):
+ self.assertURL('debian/lenny/foo', 'debianlp:lenny/foo',
+ distro='debian', series='lenny')
diff --git a/bzrlib/plugins/launchpad/test_lp_login.py b/bzrlib/plugins/launchpad/test_lp_login.py
new file mode 100644
index 0000000..8439fef
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_login.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2009 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
+
+"""Tests for the launchpad-login command."""
+
+from bzrlib.plugins.launchpad import account
+from bzrlib.tests import TestCaseWithTransport
+
+
+class TestLaunchpadLogin(TestCaseWithTransport):
+ """Tests for launchpad-login."""
+
+ def test_login_without_name_when_not_logged_in(self):
+ # lp-login without a 'name' parameter returns the user ID of the
+ # logged in user. If no one is logged in, we tell the user as much.
+ out, err = self.run_bzr(['launchpad-login', '--no-check'], retcode=1)
+ self.assertEqual('No Launchpad user ID configured.\n', out)
+ self.assertEqual('', err)
+
+ def test_login_with_name_sets_login(self):
+ # lp-login with a 'name' parameter sets the Launchpad login.
+ self.run_bzr(['launchpad-login', '--no-check', 'foo'])
+ self.assertEqual('foo', account.get_lp_login())
+
+ def test_login_without_name_when_logged_in(self):
+ # lp-login without a 'name' parameter returns the user ID of the
+ # logged in user.
+ account.set_lp_login('foo')
+ out, err = self.run_bzr(['launchpad-login', '--no-check'])
+ self.assertEqual('foo\n', out)
+ self.assertEqual('', err)
+
+ def test_login_with_name_no_output_by_default(self):
+ # lp-login with a 'name' parameter produces no output by default.
+ out, err = self.run_bzr(['launchpad-login', '--no-check', 'foo'])
+ self.assertEqual('', out)
+ self.assertEqual('', err)
+
+ def test_login_with_name_verbose(self):
+ # lp-login with a 'name' parameter and a verbose flag produces some
+ # information about what Bazaar just did.
+ out, err = self.run_bzr(
+ ['launchpad-login', '-v', '--no-check', 'foo'])
+ self.assertEqual("Launchpad user ID set to 'foo'.\n", out)
+ self.assertEqual('', err)
diff --git a/bzrlib/plugins/launchpad/test_lp_open.py b/bzrlib/plugins/launchpad/test_lp_open.py
new file mode 100644
index 0000000..5eb8345
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_open.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2009, 2010, 2012 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
+
+"""Tests for the launchpad-open command."""
+
+from bzrlib.tests import TestCaseWithTransport
+
+
+class TestLaunchpadOpen(TestCaseWithTransport):
+
+ def run_open(self, location, retcode=0, working_dir='.'):
+ out, err = self.run_bzr(['launchpad-open', '--dry-run', location],
+ retcode=retcode,
+ working_dir=working_dir)
+ return err.splitlines()
+
+ def test_non_branch(self):
+ # If given a branch with no public or push locations, lp-open will try
+ # to guess the Launchpad page for the given URL / path. If it cannot
+ # find one, it will raise an error.
+ self.assertEqual(
+ ['bzr: ERROR: . is not registered on Launchpad.'],
+ self.run_open('.', retcode=3))
+
+ def test_no_public_location_no_push_location(self):
+ self.make_branch('not-public')
+ self.assertEqual(
+ ['bzr: ERROR: not-public is not registered on Launchpad.'],
+ self.run_open('not-public', retcode=3))
+
+ def test_non_launchpad_branch(self):
+ branch = self.make_branch('non-lp')
+ url = 'http://example.com/non-lp'
+ branch.set_public_branch(url)
+ self.assertEqual(
+ ['bzr: ERROR: %s is not registered on Launchpad.' % url],
+ self.run_open('non-lp', retcode=3))
+
+ def test_launchpad_branch_with_public_location(self):
+ branch = self.make_branch('lp')
+ branch.set_public_branch('bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ ['Opening https://code.launchpad.net/~foo/bar/baz in web '
+ 'browser'],
+ self.run_open('lp'))
+
+ def test_launchpad_branch_with_public_and_push_location(self):
+ branch = self.make_branch('lp')
+ branch.lock_write()
+ try:
+ branch.set_public_branch(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/public')
+ branch.set_push_location(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/push')
+ finally:
+ branch.unlock()
+ self.assertEqual(
+ ['Opening https://code.launchpad.net/~foo/bar/public in web '
+ 'browser'],
+ self.run_open('lp'))
+
+ def test_launchpad_branch_with_no_public_but_with_push(self):
+ # lp-open falls back to the push location if it cannot find a public
+ # location.
+ branch = self.make_branch('lp')
+ branch.set_push_location('bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ ['Opening https://code.launchpad.net/~foo/bar/baz in web '
+ 'browser'],
+ self.run_open('lp'))
+
+ def test_launchpad_branch_with_no_public_no_push(self):
+ # If lp-open is given a branch URL and that branch has no public
+ # location and no push location, then just try to look up the
+ # Launchpad page for that URL.
+ self.assertEqual(
+ ['Opening https://code.launchpad.net/~foo/bar/baz in web '
+ 'browser'],
+ self.run_open('bzr+ssh://bazaar.launchpad.net/~foo/bar/baz'))
+
+ def test_launchpad_branch_subdirectory(self):
+ # lp-open in a subdirectory of a registered branch should work
+ wt = self.make_branch_and_tree('lp')
+ wt.branch.set_push_location(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.build_tree(['lp/a/'])
+ self.assertEqual(
+ ['Opening https://code.launchpad.net/~foo/bar/baz in web '
+ 'browser'],
+ self.run_open('.', working_dir='lp/a'))
diff --git a/bzrlib/plugins/launchpad/test_lp_service.py b/bzrlib/plugins/launchpad/test_lp_service.py
new file mode 100644
index 0000000..6e28c90
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_lp_service.py
@@ -0,0 +1,181 @@
+# Copyright (C) 2008-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
+
+"""Tests for selection of the right Launchpad service by environment"""
+
+import os
+import xmlrpclib
+
+from bzrlib import errors
+from bzrlib.plugins.launchpad.lp_registration import (
+ InvalidLaunchpadInstance, LaunchpadService, NotLaunchpadBranch)
+from bzrlib.plugins.launchpad.test_lp_directory import FakeResolveFactory
+from bzrlib.tests import TestCase
+
+
+class LaunchpadServiceTests(TestCase):
+ """Test that the correct Launchpad instance is chosen."""
+
+ def setUp(self):
+ super(LaunchpadServiceTests, self).setUp()
+ # make sure we have a reproducible standard environment
+ self.overrideEnv('BZR_LP_XMLRPC_URL', None)
+
+ def test_default_service(self):
+ service = LaunchpadService()
+ self.assertEqual('https://xmlrpc.launchpad.net/bazaar/',
+ service.service_url)
+
+ def test_alter_default_service_url(self):
+ LaunchpadService.DEFAULT_SERVICE_URL = 'http://example.com/'
+ try:
+ service = LaunchpadService()
+ self.assertEqual('http://example.com/',
+ service.service_url)
+ finally:
+ LaunchpadService.DEFAULT_SERVICE_URL = \
+ LaunchpadService.LAUNCHPAD_INSTANCE['production']
+
+ def test_staging_service(self):
+ service = LaunchpadService(lp_instance='staging')
+ self.assertEqual('https://xmlrpc.staging.launchpad.net/bazaar/',
+ service.service_url)
+
+ def test_dev_service(self):
+ service = LaunchpadService(lp_instance='dev')
+ self.assertEqual('https://xmlrpc.launchpad.dev/bazaar/',
+ service.service_url)
+
+ def test_demo_service(self):
+ service = LaunchpadService(lp_instance='demo')
+ self.assertEqual('https://xmlrpc.demo.launchpad.net/bazaar/',
+ service.service_url)
+
+ def test_unknown_service(self):
+ error = self.assertRaises(InvalidLaunchpadInstance,
+ LaunchpadService,
+ lp_instance='fubar')
+ self.assertEqual('fubar is not a valid Launchpad instance.',
+ str(error))
+
+ def test_environment_overrides_default(self):
+ os.environ['BZR_LP_XMLRPC_URL'] = 'http://example.com/'
+ service = LaunchpadService()
+ self.assertEqual('http://example.com/',
+ service.service_url)
+
+ def test_environment_overrides_specified_service(self):
+ os.environ['BZR_LP_XMLRPC_URL'] = 'http://example.com/'
+ service = LaunchpadService(lp_instance='staging')
+ self.assertEqual('http://example.com/',
+ service.service_url)
+
+
+class TestURLInference(TestCase):
+ """Test the way we infer Launchpad web pages from branch URLs."""
+
+ def test_default_bzr_ssh_url(self):
+ service = LaunchpadService()
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_product_bzr_ssh_url(self):
+ service = LaunchpadService(lp_instance='production')
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_sftp_branch_url(self):
+ service = LaunchpadService(lp_instance='production')
+ web_url = service.get_web_url_from_branch_url(
+ 'sftp://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_staging_branch_url(self):
+ service = LaunchpadService(lp_instance='production')
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.staging.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_non_launchpad_url(self):
+ service = LaunchpadService()
+ error = self.assertRaises(
+ NotLaunchpadBranch, service.get_web_url_from_branch_url,
+ 'bzr+ssh://example.com/~foo/bar/baz')
+ self.assertEqual(
+ 'bzr+ssh://example.com/~foo/bar/baz is not registered on Launchpad.',
+ str(error))
+
+ def test_dodgy_launchpad_url(self):
+ service = LaunchpadService()
+ self.assertRaises(
+ NotLaunchpadBranch, service.get_web_url_from_branch_url,
+ 'bzr+ssh://launchpad.net/~foo/bar/baz')
+
+ def test_lp_branch_url(self):
+ service = LaunchpadService(lp_instance='production')
+ factory = FakeResolveFactory(
+ self, '~foo/bar/baz',
+ dict(urls=['http://bazaar.launchpad.net/~foo/bar/baz']))
+ web_url = service.get_web_url_from_branch_url(
+ 'lp:~foo/bar/baz', factory)
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_lp_branch_shortcut(self):
+ service = LaunchpadService()
+ factory = FakeResolveFactory(
+ self, 'foo',
+ dict(urls=['http://bazaar.launchpad.net/~foo/bar/baz']))
+ web_url = service.get_web_url_from_branch_url('lp:foo', factory)
+ self.assertEqual(
+ 'https://code.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_lp_branch_fault(self):
+ service = LaunchpadService()
+ factory = FakeResolveFactory(self, 'foo', None)
+ def submit(service):
+ raise xmlrpclib.Fault(42, 'something went wrong')
+ factory.submit = submit
+ self.assertRaises(
+ errors.InvalidURL, service.get_web_url_from_branch_url, 'lp:foo',
+ factory)
+
+ def test_staging_url(self):
+ service = LaunchpadService(lp_instance='staging')
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.staging.launchpad.net/~foo/bar/baz', web_url)
+
+ def test_dev_url(self):
+ service = LaunchpadService(lp_instance='dev')
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.launchpad.dev/~foo/bar/baz', web_url)
+
+ def test_demo_url(self):
+ service = LaunchpadService(lp_instance='demo')
+ web_url = service.get_web_url_from_branch_url(
+ 'bzr+ssh://bazaar.launchpad.net/~foo/bar/baz')
+ self.assertEqual(
+ 'https://code.demo.launchpad.net/~foo/bar/baz', web_url)
diff --git a/bzrlib/plugins/launchpad/test_register.py b/bzrlib/plugins/launchpad/test_register.py
new file mode 100644
index 0000000..81aadc7
--- /dev/null
+++ b/bzrlib/plugins/launchpad/test_register.py
@@ -0,0 +1,366 @@
+# Copyright (C) 2006-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
+
+import base64
+from StringIO import StringIO
+import urlparse
+import xmlrpclib
+
+from bzrlib import (
+ config,
+ tests,
+ ui,
+ )
+from bzrlib.tests import TestCaseWithTransport
+
+# local import
+from bzrlib.plugins.launchpad.lp_registration import (
+ BaseRequest,
+ BranchBugLinkRequest,
+ BranchRegistrationRequest,
+ ResolveLaunchpadPathRequest,
+ LaunchpadService,
+ )
+
+
+# TODO: Test that the command-line client, making sure that it'll pass the
+# request through to a dummy transport, and that the transport will validate
+# the results passed in. Not sure how to get the transport object back out to
+# validate that its OK - may not be necessary.
+
+# TODO: Add test for (and implement) other command-line options to set
+# project, author_email, description.
+
+# TODO: project_id is not properly handled -- must be passed in rpc or path.
+
+class InstrumentedXMLRPCConnection(object):
+ """Stands in place of an http connection for the purposes of testing"""
+
+ def __init__(self, testcase):
+ self.testcase = testcase
+
+ def getreply(self):
+ """Fake the http reply.
+
+ :returns: (errcode, errmsg, headers)
+ """
+ return (200, 'OK', [])
+
+ def getresponse(self, buffering=True):
+ """Fake the http reply.
+
+ This is used when running on Python 2.7, where xmlrpclib uses
+ httplib.HTTPConnection in a different way than before.
+ """
+ class FakeHttpResponse(object):
+
+ def __init__(self, status, reason, body):
+ self.status = status
+ self.reason = reason
+ self.body = body
+
+ def read(self, size=-1):
+ return self.body.read(size)
+
+ def getheader(self, name, default):
+ # We don't have headers
+ return default
+
+ return FakeHttpResponse(200, 'OK', self.getfile())
+
+ def getfile(self):
+ """Return a fake file containing the response content."""
+ return StringIO('''\
+<?xml version="1.0" ?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <string>victoria dock</string>
+ </value>
+ </param>
+ </params>
+</methodResponse>''')
+
+
+
+class InstrumentedXMLRPCTransport(xmlrpclib.Transport):
+
+ # Python 2.5's xmlrpclib looks for this.
+ _use_datetime = False
+
+ def __init__(self, testcase, expect_auth):
+ self.testcase = testcase
+ self.expect_auth = expect_auth
+ self._connection = (None, None)
+
+ def make_connection(self, host):
+ host, http_headers, x509 = self.get_host_info(host)
+ test = self.testcase
+ self.connected_host = host
+ if self.expect_auth:
+ auth_hdrs = [v for k,v in http_headers if k == 'Authorization']
+ if len(auth_hdrs) != 1:
+ raise AssertionError("multiple auth headers: %r"
+ % (auth_hdrs,))
+ authinfo = auth_hdrs[0]
+ expected_auth = 'testuser@launchpad.net:testpassword'
+ test.assertEquals(authinfo,
+ 'Basic ' + base64.encodestring(expected_auth).strip())
+ elif http_headers:
+ raise AssertionError()
+ return InstrumentedXMLRPCConnection(test)
+
+ def send_request(self, connection, handler_path, request_body):
+ test = self.testcase
+ self.got_request = True
+
+ def send_host(self, conn, host):
+ pass
+
+ def send_user_agent(self, conn):
+ # TODO: send special user agent string, including bzrlib version
+ # number
+ pass
+
+ def send_content(self, conn, request_body):
+ unpacked, method = xmlrpclib.loads(request_body)
+ if None in unpacked:
+ raise AssertionError(
+ "xmlrpc result %r shouldn't contain None" % (unpacked,))
+ self.sent_params = unpacked
+
+
+class MockLaunchpadService(LaunchpadService):
+
+ def send_request(self, method_name, method_params, authenticated):
+ """Stash away the method details rather than sending them to a real server"""
+ self.called_method_name = method_name
+ self.called_method_params = method_params
+ self.called_authenticated = authenticated
+
+
+class TestBranchRegistration(TestCaseWithTransport):
+
+ def setUp(self):
+ super(TestBranchRegistration, self).setUp()
+ # make sure we have a reproducible standard environment
+ self.overrideEnv('BZR_LP_XMLRPC_URL', None)
+
+ def test_register_help(self):
+ """register-branch accepts --help"""
+ out, err = self.run_bzr(['register-branch', '--help'])
+ self.assertContainsRe(out, r'Register a branch')
+
+ def test_register_no_url_no_branch(self):
+ """register-branch command requires parameters"""
+ self.make_repository('.')
+ self.run_bzr_error(
+ ['register-branch requires a public branch url - '
+ 'see bzr help register-branch'],
+ 'register-branch')
+
+ def test_register_no_url_in_published_branch_no_error(self):
+ b = self.make_branch('.')
+ b.set_public_branch('http://test-server.com/bzr/branch')
+ out, err = self.run_bzr(['register-branch', '--dry-run'])
+ self.assertEqual('Branch registered.\n', out)
+ self.assertEqual('', err)
+
+ def test_register_no_url_in_unpublished_branch_errors(self):
+ b = self.make_branch('.')
+ out, err = self.run_bzr_error(['no public branch'],
+ ['register-branch', '--dry-run'])
+ self.assertEqual('', out)
+
+ def test_register_dry_run(self):
+ out, err = self.run_bzr(['register-branch',
+ 'http://test-server.com/bzr/branch',
+ '--dry-run'])
+ self.assertEquals(out, 'Branch registered.\n')
+
+ def test_onto_transport(self):
+ """How the request is sent by transmitting across a mock Transport"""
+ # use a real transport, but intercept at the http/xml layer
+ transport = InstrumentedXMLRPCTransport(self, expect_auth=True)
+ service = LaunchpadService(transport)
+ service.registrant_email = 'testuser@launchpad.net'
+ service.registrant_password = 'testpassword'
+ rego = BranchRegistrationRequest('http://test-server.com/bzr/branch',
+ 'branch-id',
+ 'my test branch',
+ 'description',
+ 'author@launchpad.net',
+ 'product')
+ rego.submit(service)
+ self.assertEquals(transport.connected_host, 'xmlrpc.launchpad.net')
+ self.assertEquals(len(transport.sent_params), 6)
+ self.assertEquals(transport.sent_params,
+ ('http://test-server.com/bzr/branch', # branch_url
+ 'branch-id', # branch_name
+ 'my test branch', # branch_title
+ 'description',
+ 'author@launchpad.net',
+ 'product'))
+ self.assertTrue(transport.got_request)
+
+ def test_onto_transport_unauthenticated(self):
+ """An unauthenticated request is transmitted across a mock Transport"""
+ transport = InstrumentedXMLRPCTransport(self, expect_auth=False)
+ service = LaunchpadService(transport)
+ resolve = ResolveLaunchpadPathRequest('bzr')
+ resolve.submit(service)
+ self.assertEquals(transport.connected_host, 'xmlrpc.launchpad.net')
+ self.assertEquals(len(transport.sent_params), 1)
+ self.assertEquals(transport.sent_params, ('bzr', ))
+ self.assertTrue(transport.got_request)
+
+ def test_subclass_request(self):
+ """Define a new type of xmlrpc request"""
+ class DummyRequest(BaseRequest):
+ _methodname = 'dummy_request'
+ def _request_params(self):
+ return (42,)
+
+ service = MockLaunchpadService()
+ service.registrant_email = 'test@launchpad.net'
+ service.registrant_password = ''
+ request = DummyRequest()
+ request.submit(service)
+ self.assertEquals(service.called_method_name, 'dummy_request')
+ self.assertEquals(service.called_method_params, (42,))
+
+ def test_mock_server_registration(self):
+ """Send registration to mock server"""
+ test_case = self
+ class MockRegistrationService(MockLaunchpadService):
+ def send_request(self, method_name, method_params, authenticated):
+ test_case.assertEquals(method_name, "register_branch")
+ test_case.assertEquals(list(method_params),
+ ['url', 'name', 'title', 'description', 'email', 'name'])
+ test_case.assertEquals(authenticated, True)
+ return 'result'
+ service = MockRegistrationService()
+ rego = BranchRegistrationRequest('url', 'name', 'title',
+ 'description', 'email', 'name')
+ result = rego.submit(service)
+ self.assertEquals(result, 'result')
+
+ def test_mock_server_registration_with_defaults(self):
+ """Send registration to mock server"""
+ test_case = self
+ class MockRegistrationService(MockLaunchpadService):
+ def send_request(self, method_name, method_params, authenticated):
+ test_case.assertEquals(method_name, "register_branch")
+ test_case.assertEquals(list(method_params),
+ ['http://server/branch', 'branch', '', '', '', ''])
+ test_case.assertEquals(authenticated, True)
+ return 'result'
+ service = MockRegistrationService()
+ rego = BranchRegistrationRequest('http://server/branch')
+ result = rego.submit(service)
+ self.assertEquals(result, 'result')
+
+ def test_mock_bug_branch_link(self):
+ """Send bug-branch link to mock server"""
+ test_case = self
+ class MockService(MockLaunchpadService):
+ def send_request(self, method_name, method_params, authenticated):
+ test_case.assertEquals(method_name, "link_branch_to_bug")
+ test_case.assertEquals(list(method_params),
+ ['http://server/branch', 1234, ''])
+ test_case.assertEquals(authenticated, True)
+ return 'http://launchpad.net/bug/1234'
+ service = MockService()
+ rego = BranchBugLinkRequest('http://server/branch', 1234)
+ result = rego.submit(service)
+ self.assertEquals(result, 'http://launchpad.net/bug/1234')
+
+ def test_mock_resolve_lp_url(self):
+ test_case = self
+ class MockService(MockLaunchpadService):
+ def send_request(self, method_name, method_params, authenticated):
+ test_case.assertEquals(method_name, "resolve_lp_path")
+ test_case.assertEquals(list(method_params), ['bzr'])
+ test_case.assertEquals(authenticated, False)
+ return dict(urls=[
+ 'bzr+ssh://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'sftp://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'bzr+http://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'http://bazaar.launchpad.net~bzr/bzr/trunk'])
+ service = MockService()
+ resolve = ResolveLaunchpadPathRequest('bzr')
+ result = resolve.submit(service)
+ self.assertTrue('urls' in result)
+ self.assertEquals(result['urls'], [
+ 'bzr+ssh://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'sftp://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'bzr+http://bazaar.launchpad.net~bzr/bzr/trunk',
+ 'http://bazaar.launchpad.net~bzr/bzr/trunk'])
+
+
+class TestGatherUserCredentials(tests.TestCaseInTempDir):
+
+ def setUp(self):
+ super(TestGatherUserCredentials, self).setUp()
+ # make sure we have a reproducible standard environment
+ self.overrideEnv('BZR_LP_XMLRPC_URL', None)
+
+ def test_gather_user_credentials_has_password(self):
+ service = LaunchpadService()
+ service.registrant_password = 'mypassword'
+ # This should be a basic no-op, since we already have the password
+ service.gather_user_credentials()
+ self.assertEqual('mypassword', service.registrant_password)
+
+ def test_gather_user_credentials_from_auth_conf(self):
+ auth_path = config.authentication_config_filename()
+ service = LaunchpadService()
+ g_conf = config.GlobalStack()
+ g_conf.set('email', 'Test User <test@user.com>')
+ f = open(auth_path, 'wb')
+ try:
+ scheme, hostinfo = urlparse.urlsplit(service.service_url)[:2]
+ f.write('[section]\n'
+ 'scheme=%s\n'
+ 'host=%s\n'
+ 'user=test@user.com\n'
+ 'password=testpass\n'
+ % (scheme, hostinfo))
+ finally:
+ f.close()
+ self.assertIs(None, service.registrant_password)
+ service.gather_user_credentials()
+ self.assertEqual('test@user.com', service.registrant_email)
+ self.assertEqual('testpass', service.registrant_password)
+
+ def test_gather_user_credentials_prompts(self):
+ service = LaunchpadService()
+ self.assertIs(None, service.registrant_password)
+ g_conf = config.GlobalStack()
+ g_conf.set('email', 'Test User <test@user.com>')
+ stdout = tests.StringIOWrapper()
+ stderr = tests.StringIOWrapper()
+ ui.ui_factory = tests.TestUIFactory(stdin='userpass\n',
+ stdout=stdout, stderr=stderr)
+ self.assertIs(None, service.registrant_password)
+ service.gather_user_credentials()
+ self.assertEqual('test@user.com', service.registrant_email)
+ self.assertEqual('userpass', service.registrant_password)
+ self.assertEquals('', stdout.getvalue())
+ self.assertContainsRe(stderr.getvalue(),
+ 'launchpad.net password for test@user\\.com')
+
diff --git a/bzrlib/plugins/netrc_credential_store/__init__.py b/bzrlib/plugins/netrc_credential_store/__init__.py
new file mode 100644
index 0000000..ad8aba0
--- /dev/null
+++ b/bzrlib/plugins/netrc_credential_store/__init__.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2008-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
+
+__doc__ = """Use ~/.netrc as a credential store for authentication.conf."""
+
+# Since we are a built-in plugin we share the bzrlib version
+from bzrlib import version_info
+
+from bzrlib import (
+ config,
+ lazy_import,
+ )
+
+lazy_import.lazy_import(globals(), """
+import errno
+import netrc
+
+from bzrlib import (
+ errors,
+ )
+""")
+
+
+class NetrcCredentialStore(config.CredentialStore):
+
+ def __init__(self):
+ super(NetrcCredentialStore, self).__init__()
+ try:
+ self._netrc = netrc.netrc()
+ except IOError, e:
+ if e.args[0] == errno.ENOENT:
+ raise errors.NoSuchFile(e.filename)
+ else:
+ raise
+
+ def decode_password(self, credentials):
+ auth = self._netrc.authenticators(credentials['host'])
+ password = None
+ if auth is not None:
+ user, account, password = auth
+ cred_user = credentials.get('user', None)
+ if cred_user is None or user != cred_user:
+ # We don't use the netrc ability to provide a user since there
+ # is no way to give it back to AuthConfig. So if the user
+ # doesn't match, we don't return a password.
+ password = None
+ return password
+
+
+config.credential_store_registry.register_lazy(
+ 'netrc', __name__, 'NetrcCredentialStore', help=__doc__)
+
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/netrc_credential_store/tests/__init__.py b/bzrlib/plugins/netrc_credential_store/tests/__init__.py
new file mode 100644
index 0000000..ab4bd39
--- /dev/null
+++ b/bzrlib/plugins/netrc_credential_store/tests/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2008 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_netrc',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/netrc_credential_store/tests/test_netrc.py b/bzrlib/plugins/netrc_credential_store/tests/test_netrc.py
new file mode 100644
index 0000000..872ee57
--- /dev/null
+++ b/bzrlib/plugins/netrc_credential_store/tests/test_netrc.py
@@ -0,0 +1,86 @@
+# Copyright (C) 2008 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 cStringIO import StringIO
+
+from bzrlib import (
+ config,
+ errors,
+ osutils,
+ tests,
+ )
+
+from bzrlib.plugins import netrc_credential_store
+
+
+class TestNetrcCSNoNetrc(tests.TestCaseInTempDir):
+
+ def test_home_netrc_does_not_exist(self):
+ self.assertRaises(errors.NoSuchFile,
+ config.credential_store_registry.get_credential_store,
+ 'netrc')
+
+
+class TestNetrcCS(tests.TestCaseInTempDir):
+
+ def setUp(self):
+ super(TestNetrcCS, self).setUp()
+ # Create a .netrc file
+ netrc_content = """
+machine host login joe password secret
+default login anonymous password joe@home
+"""
+ f = open(osutils.pathjoin(self.test_home_dir, '.netrc'), 'wb')
+ try:
+ f.write(netrc_content)
+ finally:
+ f.close()
+
+ def _get_netrc_cs(self):
+ return config.credential_store_registry.get_credential_store('netrc')
+
+ def test_not_matching_user(self):
+ cs = self._get_netrc_cs()
+ password = cs.decode_password(dict(host='host', user='jim'))
+ self.assertIs(None, password)
+
+ def test_matching_user(self):
+ cs = self._get_netrc_cs()
+ password = cs.decode_password(dict(host='host', user='joe'))
+ self.assertEquals('secret', password)
+
+ def test_default_password(self):
+ cs = self._get_netrc_cs()
+ password = cs.decode_password(dict(host='other', user='anonymous'))
+ self.assertEquals('joe@home', password)
+
+ def test_default_password_without_user(self):
+ cs = self._get_netrc_cs()
+ password = cs.decode_password(dict(host='other'))
+ self.assertIs(None, password)
+
+ def test_get_netrc_credentials_via_auth_config(self):
+ # Create a test AuthenticationConfig object
+ ac_content = """
+[host1]
+host = host
+user = joe
+password_encoding = netrc
+"""
+ conf = config.AuthenticationConfig(_file=StringIO(ac_content))
+ credentials = conf.get_credentials('scheme', 'host', user='joe')
+ self.assertIsNot(None, credentials)
+ self.assertEquals('secret', credentials.get('password', None))
diff --git a/bzrlib/plugins/news_merge/README b/bzrlib/plugins/news_merge/README
new file mode 100644
index 0000000..020f047
--- /dev/null
+++ b/bzrlib/plugins/news_merge/README
@@ -0,0 +1,7 @@
+A plugin for merging bzr's NEWS file.
+
+This plugin is activated via configuration variables, see 'bzr help news_merge'.
+
+This hook can resolve conflicts where both sides add entries at the same place.
+If it encounters a more difficult conflict it gives up and bzr will fallback to
+the default merge algorithm.
diff --git a/bzrlib/plugins/news_merge/__init__.py b/bzrlib/plugins/news_merge/__init__.py
new file mode 100644
index 0000000..2e0c33a
--- /dev/null
+++ b/bzrlib/plugins/news_merge/__init__.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2010 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
+
+__doc__ = """Merge hook for bzr's NEWS file.
+
+To enable this plugin, add a section to your branch.conf or location.conf
+like::
+
+ [/home/user/code/bzr]
+ news_merge_files = NEWS
+
+The news_merge_files config option takes a list of file paths, separated by
+commas.
+
+Limitations:
+
+* if there's a conflict in more than just bullet points, this doesn't yet know
+ how to resolve that, so bzr will fallback to the default line-based merge.
+"""
+
+# Since we are a built-in plugin we share the bzrlib version
+from bzrlib import version_info
+from bzrlib.hooks import install_lazy_named_hook
+
+
+def news_merge_hook(merger):
+ """Merger.merge_file_content hook for bzr-format NEWS files."""
+ from bzrlib.plugins.news_merge.news_merge import NewsMerger
+ return NewsMerger(merger)
+
+
+install_lazy_named_hook("bzrlib.merge", "Merger.hooks", "merge_file_content",
+ news_merge_hook, "NEWS file merge")
+
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
diff --git a/bzrlib/plugins/news_merge/news_merge.py b/bzrlib/plugins/news_merge/news_merge.py
new file mode 100644
index 0000000..a8b0a22
--- /dev/null
+++ b/bzrlib/plugins/news_merge/news_merge.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2010 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
+
+"""Merge logic for news_merge plugin."""
+
+from __future__ import absolute_import
+
+
+from bzrlib.plugins.news_merge.parser import simple_parse_lines
+from bzrlib import merge, merge3
+
+
+class NewsMerger(merge.ConfigurableFileMerger):
+ """Merge bzr NEWS files."""
+
+ name_prefix = "news"
+
+ def merge_text(self, params):
+ """Perform a simple 3-way merge of a bzr NEWS file.
+
+ Each section of a bzr NEWS file is essentially an ordered set of bullet
+ points, so we can simply take a set of bullet points, determine which
+ bullets to add and which to remove, sort, and reserialize.
+ """
+ # Transform the different versions of the NEWS file into a bunch of
+ # text lines where each line matches one part of the overall
+ # structure, e.g. a heading or bullet.
+ this_lines = list(simple_parse_lines(params.this_lines))
+ other_lines = list(simple_parse_lines(params.other_lines))
+ base_lines = list(simple_parse_lines(params.base_lines))
+ m3 = merge3.Merge3(base_lines, this_lines, other_lines,
+ allow_objects=True)
+ result_chunks = []
+ for group in m3.merge_groups():
+ if group[0] == 'conflict':
+ _, base, a, b = group
+ # Are all the conflicting lines bullets? If so, we can merge
+ # this.
+ for line_set in [base, a, b]:
+ for line in line_set:
+ if line[0] != 'bullet':
+ # Something else :(
+ # Maybe the default merge can cope.
+ return 'not_applicable', None
+ # Calculate additions and deletions.
+ new_in_a = set(a).difference(base)
+ new_in_b = set(b).difference(base)
+ all_new = new_in_a.union(new_in_b)
+ deleted_in_a = set(base).difference(a)
+ deleted_in_b = set(base).difference(b)
+ # Combine into the final set of bullet points.
+ final = all_new.difference(deleted_in_a).difference(
+ deleted_in_b)
+ # Sort, and emit.
+ final = sorted(final, key=sort_key)
+ result_chunks.extend(final)
+ else:
+ result_chunks.extend(group[1])
+ # Transform the merged elements back into real blocks of lines.
+ result_lines = '\n\n'.join(chunk[1] for chunk in result_chunks)
+ return 'success', result_lines
+
+
+def sort_key(chunk):
+ return chunk[1].replace('`', '').lower()
diff --git a/bzrlib/plugins/news_merge/parser.py b/bzrlib/plugins/news_merge/parser.py
new file mode 100644
index 0000000..4b34543
--- /dev/null
+++ b/bzrlib/plugins/news_merge/parser.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2010 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
+
+"""Simple parser for bzr's NEWS file.
+
+Simple as this is, it's a bit over-powered for news_merge's needs, which only
+cares about 'bullet' and 'everything else'.
+
+This module can be run as a standalone Python program; pass it a filename and
+it will print the parsed form of a file (a series of 2-tuples, see
+simple_parse's docstring).
+"""
+
+from __future__ import absolute_import
+
+
+def simple_parse_lines(lines):
+ """Same as simple_parse, but takes an iterable of strs rather than a single
+ str.
+ """
+ return simple_parse(''.join(lines))
+
+
+def simple_parse(content):
+ """Returns blocks, where each block is a 2-tuple (kind, text).
+
+ :kind: one of 'heading', 'release', 'section', 'empty' or 'text'.
+ :text: a str, including newlines.
+ """
+ blocks = content.split('\n\n')
+ for block in blocks:
+ if block.startswith('###'):
+ # First line is ###...: Top heading
+ yield 'heading', block
+ continue
+ last_line = block.rsplit('\n', 1)[-1]
+ if last_line.startswith('###'):
+ # last line is ###...: 2nd-level heading
+ yield 'release', block
+ elif last_line.startswith('***'):
+ # last line is ***...: 3rd-level heading
+ yield 'section', block
+ elif block.startswith('* '):
+ # bullet
+ yield 'bullet', block
+ elif block.strip() == '':
+ # empty
+ yield 'empty', block
+ else:
+ # plain text
+ yield 'text', block
+
+
+if __name__ == '__main__':
+ import sys
+ content = open(sys.argv[1], 'rb').read()
+ for result in simple_parse(content):
+ print result
diff --git a/bzrlib/plugins/news_merge/tests/__init__.py b/bzrlib/plugins/news_merge/tests/__init__.py
new file mode 100644
index 0000000..e4171d1
--- /dev/null
+++ b/bzrlib/plugins/news_merge/tests/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2010 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_news_merge',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/news_merge/tests/test_news_merge.py b/bzrlib/plugins/news_merge/tests/test_news_merge.py
new file mode 100644
index 0000000..927ca1a
--- /dev/null
+++ b/bzrlib/plugins/news_merge/tests/test_news_merge.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2010 by 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
+
+# FIXME: This is totally incomplete but I'm only the patch pilot :-)
+# -- vila 100120
+# Note that the single test from this file is now in
+# test_merge.TestConfigurableFileMerger -- rbc 20100129.
+
+from bzrlib import (
+ option,
+ tests,
+ )
+from bzrlib.merge import Merger
+from bzrlib.plugins import news_merge
diff --git a/bzrlib/plugins/po_merge/README b/bzrlib/plugins/po_merge/README
new file mode 100644
index 0000000..f6e661c
--- /dev/null
+++ b/bzrlib/plugins/po_merge/README
@@ -0,0 +1,7 @@
+A plugin for merging .po files.
+
+This plugin is controlled via configuration variables, see 'bzr help po_merge'.
+
+This hook can avoid conflicts in ``.po` files by invoking msgmerge with the
+appropriate options. If it can't apply, it falls back to the default bzr
+merge algorithm.
diff --git a/bzrlib/plugins/po_merge/__init__.py b/bzrlib/plugins/po_merge/__init__.py
new file mode 100644
index 0000000..97c00db
--- /dev/null
+++ b/bzrlib/plugins/po_merge/__init__.py
@@ -0,0 +1,92 @@
+# Copyright (C) 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
+
+__doc__ = """Merge hook for ``.po`` files.
+
+To enable this plugin, add a section to your branch.conf or location.conf
+like::
+
+ [/home/user/code/bzr]
+ po_merge.pot_dirs = po,doc/po4a/po
+
+The ``po_merge.pot_dirs`` config option takes a list of directories that can
+contain ``.po`` files, separated by commas (if several directories are
+needed). Each directory should contain a single ``.pot`` file.
+
+The ``po_merge.command`` is the command whose output is used as the result of
+the merge. It defaults to::
+
+ msgmerge -N "{other}" "{pot_file}" -C "{this}" -o "{result}"
+
+where:
+
+* ``this`` is the ``.po`` file content before the merge in the current branch,
+* ``other`` is the ``.po`` file content in the branch merged from,
+* ``pot_file`` is the path to the ``.pot`` file corresponding to the ``.po``
+ file being merged.
+
+If conflicts occur in a ``.pot`` file during a given merge, the ``.po`` files
+will use the ``.pot`` file present in tree before the merge. If this doesn't
+suit your needs, you should can disable the plugin during the merge with::
+
+ bzr merge <usual merge args> -Opo_merge.po_dirs=
+
+This will allow you to resolve the conflicts in the ``.pot`` file and then
+merge the ``.po`` files again with::
+
+ bzr remerge po/*.po doc/po4a/po/*.po
+
+"""
+
+from bzrlib import (
+ config,
+ # Since we are a built-in plugin we share the bzrlib version
+ version_info,
+ )
+from bzrlib.hooks import install_lazy_named_hook
+
+
+def register_lazy_option(key, member):
+ config.option_registry.register_lazy(
+ key, 'bzrlib.plugins.po_merge.po_merge', member)
+
+
+register_lazy_option('po_merge.command', 'command_option')
+register_lazy_option('po_merge.po_dirs', 'po_dirs_option')
+register_lazy_option('po_merge.po_glob', 'po_glob_option')
+register_lazy_option('po_merge.pot_glob', 'pot_glob_option')
+
+
+def po_merge_hook(merger):
+ """Merger.merge_file_content hook for po files."""
+ from bzrlib.plugins.po_merge.po_merge import PoMerger
+ return PoMerger(merger)
+
+
+install_lazy_named_hook("bzrlib.merge", "Merger.hooks", "merge_file_content",
+ po_merge_hook, ".po file merge")
+
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'tests',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
+
diff --git a/bzrlib/plugins/po_merge/po_merge.py b/bzrlib/plugins/po_merge/po_merge.py
new file mode 100644
index 0000000..49b9a95
--- /dev/null
+++ b/bzrlib/plugins/po_merge/po_merge.py
@@ -0,0 +1,174 @@
+# Copyright (C) 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
+
+"""Merge logic for po_merge plugin."""
+
+from __future__ import absolute_import
+
+from bzrlib import (
+ config,
+ merge,
+ )
+
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import fnmatch
+import subprocess
+import tempfile
+import sys
+
+from bzrlib import (
+ cmdline,
+ osutils,
+ trace,
+ )
+""")
+
+
+command_option = config.Option(
+ 'po_merge.command',
+ default='msgmerge -N "{other}" "{pot_file}" -C "{this}" -o "{result}"',
+ help='''\
+Command used to create a conflict-free .po file during merge.
+
+The following parameters are provided by the hook:
+``this`` is the ``.po`` file content before the merge in the current branch,
+``other`` is the ``.po`` file content in the branch merged from,
+``pot_file`` is the path to the ``.pot`` file corresponding to the ``.po``
+file being merged.
+``result`` is the path where ``msgmerge`` will output its result. The hook will
+use the content of this file to produce the resulting ``.po`` file.
+
+All paths are absolute.
+''')
+
+
+po_dirs_option = config.ListOption(
+ 'po_merge.po_dirs', default='po,debian/po',
+ help='List of dirs containing .po files that the hook applies to.')
+
+
+po_glob_option = config.Option(
+ 'po_merge.po_glob', default='*.po',
+ help='Glob matching all ``.po`` files in one of ``po_merge.po_dirs``.')
+
+pot_glob_option = config.Option(
+ 'po_merge.pot_glob', default='*.pot',
+ help='Glob matching the ``.pot`` file in one of ``po_merge.po_dirs``.')
+
+
+class PoMerger(merge.PerFileMerger):
+ """Merge .po files."""
+
+ def __init__(self, merger):
+ super(merge.PerFileMerger, self).__init__(merger)
+ # config options are cached locally until config files are (see
+ # http://pad.lv/832042)
+
+ # FIXME: We use the branch config as there is no tree config
+ # -- vila 2011-11-23
+ self.conf = merger.this_branch.get_config_stack()
+ # Which dirs are targeted by the hook
+ self.po_dirs = self.conf.get('po_merge.po_dirs')
+ # Which files are targeted by the hook
+ self.po_glob = self.conf.get('po_merge.po_glob')
+ # Which .pot file should be used
+ self.pot_glob = self.conf.get('po_merge.pot_glob')
+ self.command = self.conf.get('po_merge.command', expand=False)
+ # file_matches() will set the following for merge_text()
+ self.pot_file_abspath = None
+ trace.mutter('PoMerger created')
+
+ def file_matches(self, params):
+ """Return True if merge_matching should be called on this file."""
+ if not self.po_dirs or not self.command:
+ # Return early if there is no options defined
+ return False
+ po_dir = None
+ po_path = self.get_filepath(params, self.merger.this_tree)
+ for po_dir in self.po_dirs:
+ glob = osutils.pathjoin(po_dir, self.po_glob)
+ if fnmatch.fnmatch(po_path, glob):
+ trace.mutter('po %s matches: %s' % (po_path, glob))
+ break
+ else:
+ trace.mutter('PoMerger did not match for %s and %s'
+ % (self.po_dirs, self.po_glob))
+ return False
+ # Do we have the corresponding .pot file
+ for inv_entry in self.merger.this_tree.list_files(from_dir=po_dir,
+ recursive=False):
+ trace.mutter('inv_entry: %r' % (inv_entry,))
+ pot_name, pot_file_id = inv_entry[0], inv_entry[3]
+ if fnmatch.fnmatch(pot_name, self.pot_glob):
+ relpath = osutils.pathjoin(po_dir, pot_name)
+ self.pot_file_abspath = self.merger.this_tree.abspath(relpath)
+ # FIXME: I can't find an easy way to know if the .pot file has
+ # conflicts *during* the merge itself. So either the actual
+ # content on disk is fine and msgmerge will work OR it's not
+ # and it will fail. Conversely, either the result is ok for the
+ # user and he's happy OR the user needs to resolve the
+ # conflicts in the .pot file and use remerge.
+ # -- vila 2011-11-24
+ trace.mutter('will msgmerge %s using %s'
+ % (po_path, self.pot_file_abspath))
+ return True
+ else:
+ return False
+
+ def _invoke(self, command):
+ trace.mutter('Will msgmerge: %s' % (command,))
+ # We use only absolute paths so we don't care about the cwd
+ proc = subprocess.Popen(cmdline.split(command),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=subprocess.PIPE)
+ out, err = proc.communicate()
+ return proc.returncode, out, err
+
+ def merge_matching(self, params):
+ return self.merge_text(params)
+
+ def merge_text(self, params):
+ """Calls msgmerge when .po files conflict.
+
+ This requires a valid .pot file to reconcile both sides.
+ """
+ # Create tmp files with the 'this' and 'other' content
+ tmpdir = tempfile.mkdtemp(prefix='po_merge')
+ env = {}
+ env['this'] = osutils.pathjoin(tmpdir, 'this')
+ env['other'] = osutils.pathjoin(tmpdir, 'other')
+ env['result'] = osutils.pathjoin(tmpdir, 'result')
+ env['pot_file'] = self.pot_file_abspath
+ try:
+ with osutils.open_file(env['this'], 'wb') as f:
+ f.writelines(params.this_lines)
+ with osutils.open_file(env['other'], 'wb') as f:
+ f.writelines(params.other_lines)
+ command = self.conf.expand_options(self.command, env)
+ retcode, out, err = self._invoke(command)
+ with osutils.open_file(env['result']) as f:
+ # FIXME: To avoid the list() construct below which means the
+ # whole 'result' file is kept in memory, there may be a way to
+ # use an iterator that will close the file when it's done, but
+ # there is still the issue of removing the tmp dir...
+ # -- vila 2011-11-24
+ return 'success', list(f.readlines())
+ finally:
+ osutils.rmtree(tmpdir)
+ return 'not applicable', []
diff --git a/bzrlib/plugins/po_merge/tests/__init__.py b/bzrlib/plugins/po_merge/tests/__init__.py
new file mode 100644
index 0000000..56a3985
--- /dev/null
+++ b/bzrlib/plugins/po_merge/tests/__init__.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2011 by 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
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_po_merge',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/po_merge/tests/test_po_merge.py b/bzrlib/plugins/po_merge/tests/test_po_merge.py
new file mode 100644
index 0000000..47fbc7c
--- /dev/null
+++ b/bzrlib/plugins/po_merge/tests/test_po_merge.py
@@ -0,0 +1,451 @@
+# -*- coding: utf-8
+# Copyright (C) 2011 by 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
+
+import os
+
+from bzrlib import (
+ merge,
+ tests,
+ )
+from bzrlib.tests import (
+ features,
+ script,
+ )
+
+from bzrlib.plugins import po_merge
+
+class BlackboxTestPoMerger(script.TestCaseWithTransportAndScript):
+
+ _test_needs_features = [features.msgmerge_feature]
+
+ def setUp(self):
+ super(BlackboxTestPoMerger, self).setUp()
+ self.builder = make_adduser_branch(self, 'adduser')
+ # We need to install our hook as the test framework cleared it as part
+ # of the initialization
+ merge.Merger.hooks.install_named_hook(
+ "merge_file_content", po_merge.po_merge_hook, ".po file merge")
+
+ def test_merge_with_hook_gives_unexpected_results(self):
+ # Since the conflicts in .pot are not seen *during* the merge, the .po
+ # merge triggers the hook and creates no conflicts for fr.po. But the
+ # .pot used is the one present in the tree *before* the merge.
+ self.run_script("""\
+$ bzr branch adduser -rrevid:this work
+2>Branched 2 revisions.
+$ cd work
+$ bzr merge ../adduser -rrevid:other
+2> M po/adduser.pot
+2> M po/fr.po
+2>Text conflict in po/adduser.pot
+2>1 conflicts encountered.
+""")
+
+ def test_called_on_remerge(self):
+ # Merge with no config for the hook to create the conflicts
+ self.run_script("""\
+$ bzr branch adduser -rrevid:this work
+2>Branched 2 revisions.
+$ cd work
+# set po_dirs to an empty list
+$ bzr merge ../adduser -rrevid:other -Opo_merge.po_dirs=
+2> M po/adduser.pot
+2> M po/fr.po
+2>Text conflict in po/adduser.pot
+2>Text conflict in po/fr.po
+2>2 conflicts encountered.
+""")
+ # Fix the conflicts in the .pot file
+ with open('po/adduser.pot', 'w') as f:
+ f.write(_Adduser['resolved_pot'])
+ # Tell bzr the conflict is resolved
+ self.run_script("""\
+$ bzr resolve po/adduser.pot
+2>1 conflict resolved, 1 remaining
+# Use remerge to trigger the hook, we use the default config options here
+$ bzr remerge po/*.po
+2>All changes applied successfully.
+# There should be no conflicts anymore
+$ bzr conflicts
+""")
+
+
+def make_adduser_branch(test, relpath):
+ """Helper for po_merge blackbox tests.
+
+ This creates a branch containing the needed base revisions so tests can
+ attempt merges and conflict resolutions.
+ """
+ builder = test.make_branch_builder(relpath)
+ builder.start_series()
+ builder.build_snapshot('base', None,
+ [('add', ('', 'root-id', 'directory', '')),
+ # Create empty files
+ ('add', ('po', 'dir-id', 'directory', None),),
+ ('add', ('po/adduser.pot', 'pot-id', 'file',
+ _Adduser['base_pot'])),
+ ('add', ('po/fr.po', 'po-id', 'file',
+ _Adduser['base_po'])),
+ ])
+ # The 'other' branch
+ builder.build_snapshot('other', ['base'],
+ [('modify', ('pot-id',
+ _Adduser['other_pot'])),
+ ('modify', ('po-id',
+ _Adduser['other_po'])),
+ ])
+ # The 'this' branch
+ builder.build_snapshot('this', ['base'],
+ [('modify', ('pot-id', _Adduser['this_pot'])),
+ ('modify', ('po-id', _Adduser['this_po'])),
+ ])
+ # builder.get_branch() tip is now 'this'
+ builder.finish_series()
+ return builder
+
+
+class TestAdduserBranch(script.TestCaseWithTransportAndScript):
+ """Sanity checks on the adduser branch content."""
+
+ def setUp(self):
+ super(TestAdduserBranch, self).setUp()
+ self.builder = make_adduser_branch(self, 'adduser')
+
+ def assertAdduserBranchContent(self, revid):
+ env = dict(revid=revid, branch_name=revid)
+ self.run_script("""\
+$ bzr branch adduser -rrevid:%(revid)s %(branch_name)s
+""" % env, null_output_matches_anything=True)
+ self.assertFileEqual(_Adduser['%(revid)s_pot' % env],
+ '%(branch_name)s/po/adduser.pot' % env)
+ self.assertFileEqual(_Adduser['%(revid)s_po' % env],
+ '%(branch_name)s/po/fr.po' % env )
+
+ def test_base(self):
+ self.assertAdduserBranchContent('base')
+
+ def test_this(self):
+ self.assertAdduserBranchContent('this')
+
+ def test_other(self):
+ self.assertAdduserBranchContent('other')
+
+
+# Real content from the adduser package so we don't have to guess about format
+# details. This is declared at the end of the file to avoid cluttering the
+# beginning of the file.
+
+_Adduser = dict(
+ base_pot = r"""# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2007-01-17 21:50+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@example.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:135
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+
+#: ../adduser:188
+msgid "Warning: The home dir you specified already exists.\n"
+msgstr ""
+
+""",
+ this_pot = r"""# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2011-01-06 21:06+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:152
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+
+#: ../adduser:208
+#, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+
+#: ../adduser:210
+#, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+
+""",
+ other_pot = r"""# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2010-11-21 17:13-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:150
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+
+#: ../adduser:206
+#, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+
+#: ../adduser:208
+#, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+
+""",
+ resolved_pot = r"""# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2011-10-19 12:50-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:152
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+
+#: ../adduser:208
+#, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+
+#: ../adduser:210
+#, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+
+""",
+ base_po = r"""# adduser's manpages translation to French
+# Copyright (C) 2004 Software in the Public Interest
+# This file is distributed under the same license as the adduser package
+#
+# Translators:
+# Jean-Baka Domelevo Entfellner <domelevo@example.com>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.111\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2007-01-17 21:50+0100\n"
+"PO-Revision-Date: 2010-01-21 10:36+0100\n"
+"Last-Translator: Jean-Baka Domelevo Entfellner <domelevo@example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: French\n"
+"X-Poedit-Country: FRANCE\n"
+
+# type: Plain text
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:135
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+"Seul le superutilisateur est autorisé à ajouter un utilisateur ou un groupe "
+"au système.\n"
+
+#: ../adduser:188
+msgid "Warning: The home dir you specified already exists.\n"
+msgstr ""
+"Attention ! Le répertoire personnel que vous avez indiqué existe déjà.\n"
+
+""",
+ this_po = r"""# adduser's manpages translation to French
+# Copyright (C) 2004 Software in the Public Interest
+# This file is distributed under the same license as the adduser package
+#
+# Translators:
+# Jean-Baka Domelevo Entfellner <domelevo@example.com>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.111\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2010-10-12 15:48+0200\n"
+"PO-Revision-Date: 2010-01-21 10:36+0100\n"
+"Last-Translator: Jean-Baka Domelevo Entfellner <domelevo@example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: French\n"
+"X-Poedit-Country: FRANCE\n"
+
+# type: Plain text
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:152
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+"Seul le superutilisateur est autorisé à ajouter un utilisateur ou un groupe "
+"au système.\n"
+
+#: ../adduser:208
+#, fuzzy, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+"Attention ! Le répertoire personnel que vous avez indiqué existe déjà.\n"
+
+#: ../adduser:210
+#, fuzzy, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+"Attention ! Le répertoire personnel que vous avez indiqué existe déjà.\n"
+
+""",
+ other_po = r"""# adduser's manpages translation to French
+# Copyright (C) 2004 Software in the Public Interest
+# This file is distributed under the same license as the adduser package
+#
+# Translators:
+# Jean-Baka Domelevo Entfellner <domelevo@example.com>, 2009, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.112+nmu2\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2010-11-21 17:13-0400\n"
+"PO-Revision-Date: 2010-11-10 11:08+0100\n"
+"Last-Translator: Jean-Baka Domelevo-Entfellner <domelevo@example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Country: FRANCE\n"
+
+# type: Plain text
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:150
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+"Seul le superutilisateur est autorisé à ajouter un utilisateur ou un groupe "
+"au système.\n"
+
+#: ../adduser:206
+#, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+"Attention ! Le répertoire personnel que vous avez indiqué (%s) existe déjà.\n"
+
+#: ../adduser:208
+#, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+"Attention ! Impossible d'accéder au répertoire personnel que vous avez "
+"indiqué (%s) : %s.\n"
+
+""",
+ resolved_po = r"""# adduser's manpages translation to French
+# Copyright (C) 2004 Software in the Public Interest
+# This file is distributed under the same license as the adduser package
+#
+# Translators:
+# Jean-Baka Domelevo Entfellner <domelevo@example.com>, 2009, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.112+nmu2\n"
+"Report-Msgid-Bugs-To: adduser-devel@example.com\n"
+"POT-Creation-Date: 2011-10-19 12:50-0700\n"
+"PO-Revision-Date: 2010-11-10 11:08+0100\n"
+"Last-Translator: Jean-Baka Domelevo-Entfellner <domelevo@example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french@example.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Country: FRANCE\n"
+
+# type: Plain text
+#. everyone can issue "--help" and "--version", but only root can go on
+#: ../adduser:152
+msgid "Only root may add a user or group to the system.\n"
+msgstr ""
+"Seul le superutilisateur est autorisé à ajouter un utilisateur ou un groupe "
+"au système.\n"
+
+#: ../adduser:208
+#, perl-format
+msgid "Warning: The home dir %s you specified already exists.\n"
+msgstr ""
+"Attention ! Le répertoire personnel que vous avez indiqué (%s) existe déjà.\n"
+
+#: ../adduser:210
+#, perl-format
+msgid "Warning: The home dir %s you specified can't be accessed: %s\n"
+msgstr ""
+"Attention ! Impossible d'accéder au répertoire personnel que vous avez "
+"indiqué (%s) : %s.\n"
+
+""",
+)
diff --git a/bzrlib/plugins/weave_fmt/__init__.py b/bzrlib/plugins/weave_fmt/__init__.py
new file mode 100644
index 0000000..e7d33d2
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/__init__.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2010 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
+
+"""Weave formats.
+
+These were formats present in pre-1.0 version of Bazaar.
+"""
+
+from __future__ import absolute_import
+
+# Since we are a built-in plugin we share the bzrlib version
+from bzrlib import version_info
+
+from bzrlib import (
+ branch as _mod_branch,
+ controldir,
+ repository as _mod_repository,
+ serializer,
+ workingtree as _mod_workingtree,
+ )
+from bzrlib.bzrdir import (
+ BzrProber,
+ register_metadir,
+ )
+
+# Pre-0.8 formats that don't have a disk format string (because they are
+# versioned by the matching control directory). We use the control directories
+# disk format string as a key for the network_name because they meet the
+# constraints (simple string, unique, immutable).
+_mod_repository.network_format_registry.register_lazy(
+ "Bazaar-NG branch, format 5\n",
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat5',
+)
+_mod_repository.network_format_registry.register_lazy(
+ "Bazaar-NG branch, format 6\n",
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat6',
+)
+
+# weave formats which has no format string and are not discoverable or independently
+# creatable on disk, so are not registered in format_registry. They're
+# all in bzrlib.plugins.weave_fmt.repository now. When an instance of one of these is
+# needed, it's constructed directly by the BzrDir. Non-native formats where
+# the repository is not separately opened are similar.
+
+_mod_repository.format_registry.register_lazy(
+ 'Bazaar-NG Repository format 7',
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat7'
+ )
+
+_mod_repository.format_registry.register_extra_lazy(
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat4')
+_mod_repository.format_registry.register_extra_lazy(
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat5')
+_mod_repository.format_registry.register_extra_lazy(
+ 'bzrlib.plugins.weave_fmt.repository',
+ 'RepositoryFormat6')
+
+
+# The pre-0.8 formats have their repository format network name registered in
+# repository.py. MetaDir formats have their repository format network name
+# inferred from their disk format string.
+controldir.format_registry.register_lazy('weave',
+ "bzrlib.plugins.weave_fmt.bzrdir", "BzrDirFormat6",
+ 'Pre-0.8 format. Slower than knit and does not'
+ ' support checkouts or shared repositories.',
+ hidden=True,
+ deprecated=True)
+register_metadir(controldir.format_registry, 'metaweave',
+ 'bzrlib.plugins.weave_fmt.repository.RepositoryFormat7',
+ 'Transitional format in 0.8. Slower than knit.',
+ branch_format='bzrlib.branchfmt.fullhistory.BzrBranchFormat5',
+ tree_format='bzrlib.workingtree_3.WorkingTreeFormat3',
+ hidden=True,
+ deprecated=True)
+
+
+BzrProber.formats.register_lazy(
+ "Bazaar-NG branch, format 0.0.4\n", "bzrlib.plugins.weave_fmt.bzrdir",
+ "BzrDirFormat4")
+BzrProber.formats.register_lazy(
+ "Bazaar-NG branch, format 5\n", "bzrlib.plugins.weave_fmt.bzrdir",
+ "BzrDirFormat5")
+BzrProber.formats.register_lazy(
+ "Bazaar-NG branch, format 6\n", "bzrlib.plugins.weave_fmt.bzrdir",
+ "BzrDirFormat6")
+
+
+_mod_branch.format_registry.register_extra_lazy(
+ 'bzrlib.plugins.weave_fmt.branch', 'BzrBranchFormat4')
+_mod_branch.network_format_registry.register_lazy(
+ "Bazaar-NG branch, format 6\n",
+ 'bzrlib.plugins.weave_fmt.branch', "BzrBranchFormat4")
+
+
+_mod_workingtree.format_registry.register_extra_lazy(
+ 'bzrlib.plugins.weave_fmt.workingtree',
+ 'WorkingTreeFormat2')
+
+serializer.format_registry.register_lazy('4', 'bzrlib.plugins.weave_fmt.xml4',
+ 'serializer_v4')
+
+def load_tests(basic_tests, module, loader):
+ testmod_names = [
+ 'test_bzrdir',
+ 'test_repository',
+ 'test_workingtree',
+ ]
+ basic_tests.addTest(loader.loadTestsFromModuleNames(
+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
+ return basic_tests
diff --git a/bzrlib/plugins/weave_fmt/branch.py b/bzrlib/plugins/weave_fmt/branch.py
new file mode 100644
index 0000000..f9852c2
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/branch.py
@@ -0,0 +1,219 @@
+# Copyright (C) 2010 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
+
+"""Weave-era branch implementations."""
+
+from __future__ import absolute_import
+
+from bzrlib import (
+ errors,
+ lockable_files,
+ )
+
+from bzrlib.decorators import (
+ needs_read_lock,
+ needs_write_lock,
+ only_raises,
+ )
+from bzrlib.lock import LogicalLockResult
+from bzrlib.trace import mutter
+
+from bzrlib.branch import (
+ BranchFormat,
+ BranchWriteLockResult,
+ )
+from bzrlib.branchfmt.fullhistory import (
+ FullHistoryBzrBranch,
+ )
+
+
+class BzrBranch4(FullHistoryBzrBranch):
+ """Branch format 4."""
+
+ def lock_write(self, token=None):
+ """Lock the branch for write operations.
+
+ :param token: A token to permit reacquiring a previously held and
+ preserved lock.
+ :return: A BranchWriteLockResult.
+ """
+ if not self.is_locked():
+ self._note_lock('w')
+ # All-in-one needs to always unlock/lock.
+ self.repository._warn_if_deprecated(self)
+ self.repository.lock_write()
+ try:
+ return BranchWriteLockResult(self.unlock,
+ self.control_files.lock_write(token=token))
+ except:
+ self.repository.unlock()
+ raise
+
+ def lock_read(self):
+ """Lock the branch for read operations.
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ if not self.is_locked():
+ self._note_lock('r')
+ # All-in-one needs to always unlock/lock.
+ self.repository._warn_if_deprecated(self)
+ self.repository.lock_read()
+ try:
+ self.control_files.lock_read()
+ return LogicalLockResult(self.unlock)
+ except:
+ self.repository.unlock()
+ raise
+
+ @only_raises(errors.LockNotHeld, errors.LockBroken)
+ def unlock(self):
+ if self.control_files._lock_count == 2 and self.conf_store is not None:
+ self.conf_store.save_changes()
+ try:
+ self.control_files.unlock()
+ finally:
+ # All-in-one needs to always unlock/lock.
+ self.repository.unlock()
+ if not self.control_files.is_locked():
+ # we just released the lock
+ self._clear_cached_state()
+
+ def _get_checkout_format(self, lightweight=False):
+ """Return the most suitable metadir for a checkout of this branch.
+ """
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat7
+ from bzrlib.bzrdir import BzrDirMetaFormat1
+ format = BzrDirMetaFormat1()
+ if lightweight:
+ format.set_branch_format(self._format)
+ format.repository_format = self.bzrdir._format.repository_format
+ else:
+ format.repository_format = RepositoryFormat7()
+ return format
+
+ def unbind(self):
+ raise errors.UpgradeRequired(self.user_url)
+
+ def bind(self, other):
+ raise errors.UpgradeRequired(self.user_url)
+
+ def set_bound_location(self, location):
+ raise NotImplementedError(self.set_bound_location)
+
+ def get_bound_location(self):
+ return None
+
+ def update(self):
+ return None
+
+ def get_master_branch(self, possible_transports=None):
+ return None
+
+
+class BzrBranchFormat4(BranchFormat):
+ """Bzr branch format 4.
+
+ This format has:
+ - a revision-history file.
+ - a branch-lock lock file [ to be shared with the bzrdir ]
+
+ It does not support binding.
+ """
+
+ def initialize(self, a_bzrdir, name=None, repository=None,
+ append_revisions_only=None):
+ """Create a branch of this format in a_bzrdir.
+
+ :param a_bzrdir: The bzrdir to initialize the branch in
+ :param name: Name of colocated branch to create, if any
+ :param repository: Repository for this branch (unused)
+ """
+ if append_revisions_only:
+ raise errors.UpgradeRequired(a_bzrdir.user_url)
+ if repository is not None:
+ raise NotImplementedError(
+ "initialize(repository=<not None>) on %r" % (self,))
+ if not [isinstance(a_bzrdir._format, format) for format in
+ self._compatible_bzrdirs]:
+ raise errors.IncompatibleFormat(self, a_bzrdir._format)
+ utf8_files = [('revision-history', ''),
+ ('branch-name', ''),
+ ]
+ mutter('creating branch %r in %s', self, a_bzrdir.user_url)
+ branch_transport = a_bzrdir.get_branch_transport(self, name=name)
+ control_files = lockable_files.LockableFiles(branch_transport,
+ 'branch-lock', lockable_files.TransportLock)
+ control_files.create_lock()
+ try:
+ control_files.lock_write()
+ except errors.LockContention:
+ lock_taken = False
+ else:
+ lock_taken = True
+ try:
+ for (filename, content) in utf8_files:
+ branch_transport.put_bytes(
+ filename, content,
+ mode=a_bzrdir._get_file_mode())
+ finally:
+ if lock_taken:
+ control_files.unlock()
+ branch = self.open(a_bzrdir, name, _found=True,
+ found_repository=None)
+ self._run_post_branch_init_hooks(a_bzrdir, name, branch)
+ return branch
+
+ def __init__(self):
+ super(BzrBranchFormat4, self).__init__()
+ from bzrlib.plugins.weave_fmt.bzrdir import (
+ BzrDirFormat4, BzrDirFormat5, BzrDirFormat6,
+ )
+ self._matchingbzrdir = BzrDirFormat6()
+ self._compatible_bzrdirs = [BzrDirFormat4, BzrDirFormat5,
+ BzrDirFormat6]
+
+ def network_name(self):
+ """The network name for this format is the control dirs disk label."""
+ return self._matchingbzrdir.get_format_string()
+
+ def get_format_description(self):
+ return "Branch format 4"
+
+ def open(self, a_bzrdir, name=None, _found=False, ignore_fallbacks=False,
+ found_repository=None, possible_transports=None):
+ """See BranchFormat.open()."""
+ if name is None:
+ name = a_bzrdir._get_selected_branch()
+ if name != "":
+ raise errors.NoColocatedBranchSupport(self)
+ if not _found:
+ # we are being called directly and must probe.
+ raise NotImplementedError
+ if found_repository is None:
+ found_repository = a_bzrdir.open_repository()
+ return BzrBranch4(_format=self,
+ _control_files=a_bzrdir._control_files,
+ a_bzrdir=a_bzrdir,
+ name=name,
+ _repository=found_repository,
+ possible_transports=possible_transports)
+
+ def __str__(self):
+ return "Bazaar-NG branch format 4"
+
+ def supports_leaving_lock(self):
+ return False
diff --git a/bzrlib/plugins/weave_fmt/bzrdir.py b/bzrlib/plugins/weave_fmt/bzrdir.py
new file mode 100644
index 0000000..5b593e8
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/bzrdir.py
@@ -0,0 +1,1006 @@
+# Copyright (C) 2006-2010 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
+
+"""Weave-era BzrDir formats."""
+
+from __future__ import absolute_import
+
+from bzrlib.bzrdir import (
+ BzrDir,
+ BzrDirFormat,
+ BzrDirMetaFormat1,
+ )
+from bzrlib.controldir import (
+ ControlDir,
+ Converter,
+ format_registry,
+ )
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import os
+import warnings
+
+from bzrlib import (
+ errors,
+ graph,
+ lockable_files,
+ lockdir,
+ osutils,
+ revision as _mod_revision,
+ trace,
+ ui,
+ urlutils,
+ versionedfile,
+ weave,
+ xml5,
+ )
+from bzrlib.i18n import gettext
+from bzrlib.store.versioned import VersionedFileStore
+from bzrlib.transactions import WriteTransaction
+from bzrlib.transport import (
+ get_transport,
+ local,
+ )
+from bzrlib.plugins.weave_fmt import xml4
+""")
+
+
+class BzrDirFormatAllInOne(BzrDirFormat):
+ """Common class for formats before meta-dirs."""
+
+ fixed_components = True
+
+ def initialize_on_transport_ex(self, transport, use_existing_dir=False,
+ create_prefix=False, force_new_repo=False, stacked_on=None,
+ stack_on_pwd=None, repo_format_name=None, make_working_trees=None,
+ shared_repo=False):
+ """See ControlDir.initialize_on_transport_ex."""
+ require_stacking = (stacked_on is not None)
+ # Format 5 cannot stack, but we've been asked to - actually init
+ # a Meta1Dir
+ if require_stacking:
+ format = BzrDirMetaFormat1()
+ return format.initialize_on_transport_ex(transport,
+ use_existing_dir=use_existing_dir, create_prefix=create_prefix,
+ force_new_repo=force_new_repo, stacked_on=stacked_on,
+ stack_on_pwd=stack_on_pwd, repo_format_name=repo_format_name,
+ make_working_trees=make_working_trees, shared_repo=shared_repo)
+ return BzrDirFormat.initialize_on_transport_ex(self, transport,
+ use_existing_dir=use_existing_dir, create_prefix=create_prefix,
+ force_new_repo=force_new_repo, stacked_on=stacked_on,
+ stack_on_pwd=stack_on_pwd, repo_format_name=repo_format_name,
+ make_working_trees=make_working_trees, shared_repo=shared_repo)
+
+ @classmethod
+ def from_string(cls, format_string):
+ if format_string != cls.get_format_string():
+ raise AssertionError("unexpected format string %r" % format_string)
+ return cls()
+
+
+class BzrDirFormat5(BzrDirFormatAllInOne):
+ """Bzr control format 5.
+
+ This format is a combined format for working tree, branch and repository.
+ It has:
+ - Format 2 working trees [always]
+ - Format 4 branches [always]
+ - Format 5 repositories [always]
+ Unhashed stores in the repository.
+ """
+
+ _lock_class = lockable_files.TransportLock
+
+ def __eq__(self, other):
+ return type(self) == type(other)
+
+ @classmethod
+ def get_format_string(cls):
+ """See BzrDirFormat.get_format_string()."""
+ return "Bazaar-NG branch, format 5\n"
+
+ def get_branch_format(self):
+ from bzrlib.plugins.weave_fmt.branch import BzrBranchFormat4
+ return BzrBranchFormat4()
+
+ def get_format_description(self):
+ """See ControlDirFormat.get_format_description()."""
+ return "All-in-one format 5"
+
+ def get_converter(self, format=None):
+ """See ControlDirFormat.get_converter()."""
+ # there is one and only one upgrade path here.
+ return ConvertBzrDir5To6()
+
+ def _initialize_for_clone(self, url):
+ return self.initialize_on_transport(get_transport(url), _cloning=True)
+
+ def initialize_on_transport(self, transport, _cloning=False):
+ """Format 5 dirs always have working tree, branch and repository.
+
+ Except when they are being cloned.
+ """
+ from bzrlib.plugins.weave_fmt.branch import BzrBranchFormat4
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat5
+ result = (super(BzrDirFormat5, self).initialize_on_transport(transport))
+ RepositoryFormat5().initialize(result, _internal=True)
+ if not _cloning:
+ branch = BzrBranchFormat4().initialize(result)
+ result._init_workingtree()
+ return result
+
+ def network_name(self):
+ return self.get_format_string()
+
+ def _open(self, transport):
+ """See BzrDirFormat._open."""
+ return BzrDir5(transport, self)
+
+ def __return_repository_format(self):
+ """Circular import protection."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat5
+ return RepositoryFormat5()
+ repository_format = property(__return_repository_format)
+
+
+class BzrDirFormat6(BzrDirFormatAllInOne):
+ """Bzr control format 6.
+
+ This format is a combined format for working tree, branch and repository.
+ It has:
+ - Format 2 working trees [always]
+ - Format 4 branches [always]
+ - Format 6 repositories [always]
+ """
+
+ _lock_class = lockable_files.TransportLock
+
+ def __eq__(self, other):
+ return type(self) == type(other)
+
+ @classmethod
+ def get_format_string(cls):
+ """See BzrDirFormat.get_format_string()."""
+ return "Bazaar-NG branch, format 6\n"
+
+ def get_format_description(self):
+ """See ControlDirFormat.get_format_description()."""
+ return "All-in-one format 6"
+
+ def get_branch_format(self):
+ from bzrlib.plugins.weave_fmt.branch import BzrBranchFormat4
+ return BzrBranchFormat4()
+
+ def get_converter(self, format=None):
+ """See ControlDirFormat.get_converter()."""
+ # there is one and only one upgrade path here.
+ return ConvertBzrDir6ToMeta()
+
+ def _initialize_for_clone(self, url):
+ return self.initialize_on_transport(get_transport(url), _cloning=True)
+
+ def initialize_on_transport(self, transport, _cloning=False):
+ """Format 6 dirs always have working tree, branch and repository.
+
+ Except when they are being cloned.
+ """
+ from bzrlib.plugins.weave_fmt.branch import BzrBranchFormat4
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat6
+ result = super(BzrDirFormat6, self).initialize_on_transport(transport)
+ RepositoryFormat6().initialize(result, _internal=True)
+ if not _cloning:
+ branch = BzrBranchFormat4().initialize(result)
+ result._init_workingtree()
+ return result
+
+ def network_name(self):
+ return self.get_format_string()
+
+ def _open(self, transport):
+ """See BzrDirFormat._open."""
+ return BzrDir6(transport, self)
+
+ def __return_repository_format(self):
+ """Circular import protection."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat6
+ return RepositoryFormat6()
+ repository_format = property(__return_repository_format)
+
+
+class ConvertBzrDir4To5(Converter):
+ """Converts format 4 bzr dirs to format 5."""
+
+ def __init__(self):
+ super(ConvertBzrDir4To5, self).__init__()
+ self.converted_revs = set()
+ self.absent_revisions = set()
+ self.text_count = 0
+ self.revisions = {}
+
+ def convert(self, to_convert, pb):
+ """See Converter.convert()."""
+ self.bzrdir = to_convert
+ if pb is not None:
+ warnings.warn(gettext("pb parameter to convert() is deprecated"))
+ self.pb = ui.ui_factory.nested_progress_bar()
+ try:
+ ui.ui_factory.note(gettext('starting upgrade from format 4 to 5'))
+ if isinstance(self.bzrdir.transport, local.LocalTransport):
+ self.bzrdir.get_workingtree_transport(None).delete('stat-cache')
+ self._convert_to_weaves()
+ return ControlDir.open(self.bzrdir.user_url)
+ finally:
+ self.pb.finished()
+
+ def _convert_to_weaves(self):
+ ui.ui_factory.note(gettext(
+ 'note: upgrade may be faster if all store files are ungzipped first'))
+ try:
+ # TODO permissions
+ stat = self.bzrdir.transport.stat('weaves')
+ if not S_ISDIR(stat.st_mode):
+ self.bzrdir.transport.delete('weaves')
+ self.bzrdir.transport.mkdir('weaves')
+ except errors.NoSuchFile:
+ self.bzrdir.transport.mkdir('weaves')
+ # deliberately not a WeaveFile as we want to build it up slowly.
+ self.inv_weave = weave.Weave('inventory')
+ # holds in-memory weaves for all files
+ self.text_weaves = {}
+ self.bzrdir.transport.delete('branch-format')
+ self.branch = self.bzrdir.open_branch()
+ self._convert_working_inv()
+ rev_history = self.branch._revision_history()
+ # to_read is a stack holding the revisions we still need to process;
+ # appending to it adds new highest-priority revisions
+ self.known_revisions = set(rev_history)
+ self.to_read = rev_history[-1:]
+ while self.to_read:
+ rev_id = self.to_read.pop()
+ if (rev_id not in self.revisions
+ and rev_id not in self.absent_revisions):
+ self._load_one_rev(rev_id)
+ self.pb.clear()
+ to_import = self._make_order()
+ for i, rev_id in enumerate(to_import):
+ self.pb.update(gettext('converting revision'), i, len(to_import))
+ self._convert_one_rev(rev_id)
+ self.pb.clear()
+ self._write_all_weaves()
+ self._write_all_revs()
+ ui.ui_factory.note(gettext('upgraded to weaves:'))
+ ui.ui_factory.note(' ' + gettext('%6d revisions and inventories') %
+ len(self.revisions))
+ ui.ui_factory.note(' ' + gettext('%6d revisions not present') %
+ len(self.absent_revisions))
+ ui.ui_factory.note(' ' + gettext('%6d texts') % self.text_count)
+ self._cleanup_spare_files_after_format4()
+ self.branch._transport.put_bytes(
+ 'branch-format',
+ BzrDirFormat5().get_format_string(),
+ mode=self.bzrdir._get_file_mode())
+
+ def _cleanup_spare_files_after_format4(self):
+ # FIXME working tree upgrade foo.
+ for n in 'merged-patches', 'pending-merged-patches':
+ try:
+ ## assert os.path.getsize(p) == 0
+ self.bzrdir.transport.delete(n)
+ except errors.NoSuchFile:
+ pass
+ self.bzrdir.transport.delete_tree('inventory-store')
+ self.bzrdir.transport.delete_tree('text-store')
+
+ def _convert_working_inv(self):
+ inv = xml4.serializer_v4.read_inventory(
+ self.branch._transport.get('inventory'))
+ new_inv_xml = xml5.serializer_v5.write_inventory_to_string(inv, working=True)
+ self.branch._transport.put_bytes('inventory', new_inv_xml,
+ mode=self.bzrdir._get_file_mode())
+
+ def _write_all_weaves(self):
+ controlweaves = VersionedFileStore(self.bzrdir.transport, prefixed=False,
+ versionedfile_class=weave.WeaveFile)
+ weave_transport = self.bzrdir.transport.clone('weaves')
+ weaves = VersionedFileStore(weave_transport, prefixed=False,
+ versionedfile_class=weave.WeaveFile)
+ transaction = WriteTransaction()
+
+ try:
+ i = 0
+ for file_id, file_weave in self.text_weaves.items():
+ self.pb.update(gettext('writing weave'), i,
+ len(self.text_weaves))
+ weaves._put_weave(file_id, file_weave, transaction)
+ i += 1
+ self.pb.update(gettext('inventory'), 0, 1)
+ controlweaves._put_weave('inventory', self.inv_weave, transaction)
+ self.pb.update(gettext('inventory'), 1, 1)
+ finally:
+ self.pb.clear()
+
+ def _write_all_revs(self):
+ """Write all revisions out in new form."""
+ self.bzrdir.transport.delete_tree('revision-store')
+ self.bzrdir.transport.mkdir('revision-store')
+ revision_transport = self.bzrdir.transport.clone('revision-store')
+ # TODO permissions
+ from bzrlib.xml5 import serializer_v5
+ from bzrlib.plugins.weave_fmt.repository import RevisionTextStore
+ revision_store = RevisionTextStore(revision_transport,
+ serializer_v5, False, versionedfile.PrefixMapper(),
+ lambda:True, lambda:True)
+ try:
+ for i, rev_id in enumerate(self.converted_revs):
+ self.pb.update(gettext('write revision'), i,
+ len(self.converted_revs))
+ text = serializer_v5.write_revision_to_string(
+ self.revisions[rev_id])
+ key = (rev_id,)
+ revision_store.add_lines(key, None, osutils.split_lines(text))
+ finally:
+ self.pb.clear()
+
+ def _load_one_rev(self, rev_id):
+ """Load a revision object into memory.
+
+ Any parents not either loaded or abandoned get queued to be
+ loaded."""
+ self.pb.update(gettext('loading revision'),
+ len(self.revisions),
+ len(self.known_revisions))
+ if not self.branch.repository.has_revision(rev_id):
+ self.pb.clear()
+ ui.ui_factory.note(gettext('revision {%s} not present in branch; '
+ 'will be converted as a ghost') %
+ rev_id)
+ self.absent_revisions.add(rev_id)
+ else:
+ rev = self.branch.repository.get_revision(rev_id)
+ for parent_id in rev.parent_ids:
+ self.known_revisions.add(parent_id)
+ self.to_read.append(parent_id)
+ self.revisions[rev_id] = rev
+
+ def _load_old_inventory(self, rev_id):
+ f = self.branch.repository.inventory_store.get(rev_id)
+ try:
+ old_inv_xml = f.read()
+ finally:
+ f.close()
+ inv = xml4.serializer_v4.read_inventory_from_string(old_inv_xml)
+ inv.revision_id = rev_id
+ rev = self.revisions[rev_id]
+ return inv
+
+ def _load_updated_inventory(self, rev_id):
+ inv_xml = self.inv_weave.get_text(rev_id)
+ inv = xml5.serializer_v5.read_inventory_from_string(inv_xml, rev_id)
+ return inv
+
+ def _convert_one_rev(self, rev_id):
+ """Convert revision and all referenced objects to new format."""
+ rev = self.revisions[rev_id]
+ inv = self._load_old_inventory(rev_id)
+ present_parents = [p for p in rev.parent_ids
+ if p not in self.absent_revisions]
+ self._convert_revision_contents(rev, inv, present_parents)
+ self._store_new_inv(rev, inv, present_parents)
+ self.converted_revs.add(rev_id)
+
+ def _store_new_inv(self, rev, inv, present_parents):
+ new_inv_xml = xml5.serializer_v5.write_inventory_to_string(inv)
+ new_inv_sha1 = osutils.sha_string(new_inv_xml)
+ self.inv_weave.add_lines(rev.revision_id,
+ present_parents,
+ new_inv_xml.splitlines(True))
+ rev.inventory_sha1 = new_inv_sha1
+
+ def _convert_revision_contents(self, rev, inv, present_parents):
+ """Convert all the files within a revision.
+
+ Also upgrade the inventory to refer to the text revision ids."""
+ rev_id = rev.revision_id
+ trace.mutter('converting texts of revision {%s}', rev_id)
+ parent_invs = map(self._load_updated_inventory, present_parents)
+ entries = inv.iter_entries()
+ entries.next()
+ for path, ie in entries:
+ self._convert_file_version(rev, ie, parent_invs)
+
+ def _convert_file_version(self, rev, ie, parent_invs):
+ """Convert one version of one file.
+
+ The file needs to be added into the weave if it is a merge
+ of >=2 parents or if it's changed from its parent.
+ """
+ file_id = ie.file_id
+ rev_id = rev.revision_id
+ w = self.text_weaves.get(file_id)
+ if w is None:
+ w = weave.Weave(file_id)
+ self.text_weaves[file_id] = w
+ text_changed = False
+ parent_candiate_entries = ie.parent_candidates(parent_invs)
+ heads = graph.Graph(self).heads(parent_candiate_entries.keys())
+ # XXX: Note that this is unordered - and this is tolerable because
+ # the previous code was also unordered.
+ previous_entries = dict((head, parent_candiate_entries[head]) for head
+ in heads)
+ self.snapshot_ie(previous_entries, ie, w, rev_id)
+
+ def get_parent_map(self, revision_ids):
+ """See graph.StackedParentsProvider.get_parent_map"""
+ return dict((revision_id, self.revisions[revision_id])
+ for revision_id in revision_ids
+ if revision_id in self.revisions)
+
+ def snapshot_ie(self, previous_revisions, ie, w, rev_id):
+ # TODO: convert this logic, which is ~= snapshot to
+ # a call to:. This needs the path figured out. rather than a work_tree
+ # a v4 revision_tree can be given, or something that looks enough like
+ # one to give the file content to the entry if it needs it.
+ # and we need something that looks like a weave store for snapshot to
+ # save against.
+ #ie.snapshot(rev, PATH, previous_revisions, REVISION_TREE, InMemoryWeaveStore(self.text_weaves))
+ if len(previous_revisions) == 1:
+ previous_ie = previous_revisions.values()[0]
+ if ie._unchanged(previous_ie):
+ ie.revision = previous_ie.revision
+ return
+ if ie.has_text():
+ f = self.branch.repository._text_store.get(ie.text_id)
+ try:
+ file_lines = f.readlines()
+ finally:
+ f.close()
+ w.add_lines(rev_id, previous_revisions, file_lines)
+ self.text_count += 1
+ else:
+ w.add_lines(rev_id, previous_revisions, [])
+ ie.revision = rev_id
+
+ def _make_order(self):
+ """Return a suitable order for importing revisions.
+
+ The order must be such that an revision is imported after all
+ its (present) parents.
+ """
+ todo = set(self.revisions.keys())
+ done = self.absent_revisions.copy()
+ order = []
+ while todo:
+ # scan through looking for a revision whose parents
+ # are all done
+ for rev_id in sorted(list(todo)):
+ rev = self.revisions[rev_id]
+ parent_ids = set(rev.parent_ids)
+ if parent_ids.issubset(done):
+ # can take this one now
+ order.append(rev_id)
+ todo.remove(rev_id)
+ done.add(rev_id)
+ return order
+
+
+class ConvertBzrDir5To6(Converter):
+ """Converts format 5 bzr dirs to format 6."""
+
+ def convert(self, to_convert, pb):
+ """See Converter.convert()."""
+ self.bzrdir = to_convert
+ pb = ui.ui_factory.nested_progress_bar()
+ try:
+ ui.ui_factory.note(gettext('starting upgrade from format 5 to 6'))
+ self._convert_to_prefixed()
+ return ControlDir.open(self.bzrdir.user_url)
+ finally:
+ pb.finished()
+
+ def _convert_to_prefixed(self):
+ from bzrlib.store import TransportStore
+ self.bzrdir.transport.delete('branch-format')
+ for store_name in ["weaves", "revision-store"]:
+ ui.ui_factory.note(gettext("adding prefixes to %s") % store_name)
+ store_transport = self.bzrdir.transport.clone(store_name)
+ store = TransportStore(store_transport, prefixed=True)
+ for urlfilename in store_transport.list_dir('.'):
+ filename = urlutils.unescape(urlfilename)
+ if (filename.endswith(".weave") or
+ filename.endswith(".gz") or
+ filename.endswith(".sig")):
+ file_id, suffix = os.path.splitext(filename)
+ else:
+ file_id = filename
+ suffix = ''
+ new_name = store._mapper.map((file_id,)) + suffix
+ # FIXME keep track of the dirs made RBC 20060121
+ try:
+ store_transport.move(filename, new_name)
+ except errors.NoSuchFile: # catches missing dirs strangely enough
+ store_transport.mkdir(osutils.dirname(new_name))
+ store_transport.move(filename, new_name)
+ self.bzrdir.transport.put_bytes(
+ 'branch-format',
+ BzrDirFormat6().get_format_string(),
+ mode=self.bzrdir._get_file_mode())
+
+
+class ConvertBzrDir6ToMeta(Converter):
+ """Converts format 6 bzr dirs to metadirs."""
+
+ def convert(self, to_convert, pb):
+ """See Converter.convert()."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat7
+ from bzrlib.branchfmt.fullhistory import BzrBranchFormat5
+ self.bzrdir = to_convert
+ self.pb = ui.ui_factory.nested_progress_bar()
+ self.count = 0
+ self.total = 20 # the steps we know about
+ self.garbage_inventories = []
+ self.dir_mode = self.bzrdir._get_dir_mode()
+ self.file_mode = self.bzrdir._get_file_mode()
+
+ ui.ui_factory.note(gettext('starting upgrade from format 6 to metadir'))
+ self.bzrdir.transport.put_bytes(
+ 'branch-format',
+ "Converting to format 6",
+ mode=self.file_mode)
+ # its faster to move specific files around than to open and use the apis...
+ # first off, nuke ancestry.weave, it was never used.
+ try:
+ self.step(gettext('Removing ancestry.weave'))
+ self.bzrdir.transport.delete('ancestry.weave')
+ except errors.NoSuchFile:
+ pass
+ # find out whats there
+ self.step(gettext('Finding branch files'))
+ last_revision = self.bzrdir.open_branch().last_revision()
+ bzrcontents = self.bzrdir.transport.list_dir('.')
+ for name in bzrcontents:
+ if name.startswith('basis-inventory.'):
+ self.garbage_inventories.append(name)
+ # create new directories for repository, working tree and branch
+ repository_names = [('inventory.weave', True),
+ ('revision-store', True),
+ ('weaves', True)]
+ self.step(gettext('Upgrading repository') + ' ')
+ self.bzrdir.transport.mkdir('repository', mode=self.dir_mode)
+ self.make_lock('repository')
+ # we hard code the formats here because we are converting into
+ # the meta format. The meta format upgrader can take this to a
+ # future format within each component.
+ self.put_format('repository', RepositoryFormat7())
+ for entry in repository_names:
+ self.move_entry('repository', entry)
+
+ self.step(gettext('Upgrading branch') + ' ')
+ self.bzrdir.transport.mkdir('branch', mode=self.dir_mode)
+ self.make_lock('branch')
+ self.put_format('branch', BzrBranchFormat5())
+ branch_files = [('revision-history', True),
+ ('branch-name', True),
+ ('parent', False)]
+ for entry in branch_files:
+ self.move_entry('branch', entry)
+
+ checkout_files = [('pending-merges', True),
+ ('inventory', True),
+ ('stat-cache', False)]
+ # If a mandatory checkout file is not present, the branch does not have
+ # a functional checkout. Do not create a checkout in the converted
+ # branch.
+ for name, mandatory in checkout_files:
+ if mandatory and name not in bzrcontents:
+ has_checkout = False
+ break
+ else:
+ has_checkout = True
+ if not has_checkout:
+ ui.ui_factory.note(gettext('No working tree.'))
+ # If some checkout files are there, we may as well get rid of them.
+ for name, mandatory in checkout_files:
+ if name in bzrcontents:
+ self.bzrdir.transport.delete(name)
+ else:
+ from bzrlib.workingtree_3 import WorkingTreeFormat3
+ self.step(gettext('Upgrading working tree'))
+ self.bzrdir.transport.mkdir('checkout', mode=self.dir_mode)
+ self.make_lock('checkout')
+ self.put_format(
+ 'checkout', WorkingTreeFormat3())
+ self.bzrdir.transport.delete_multi(
+ self.garbage_inventories, self.pb)
+ for entry in checkout_files:
+ self.move_entry('checkout', entry)
+ if last_revision is not None:
+ self.bzrdir.transport.put_bytes(
+ 'checkout/last-revision', last_revision)
+ self.bzrdir.transport.put_bytes(
+ 'branch-format',
+ BzrDirMetaFormat1().get_format_string(),
+ mode=self.file_mode)
+ self.pb.finished()
+ return ControlDir.open(self.bzrdir.user_url)
+
+ def make_lock(self, name):
+ """Make a lock for the new control dir name."""
+ self.step(gettext('Make %s lock') % name)
+ ld = lockdir.LockDir(self.bzrdir.transport,
+ '%s/lock' % name,
+ file_modebits=self.file_mode,
+ dir_modebits=self.dir_mode)
+ ld.create()
+
+ def move_entry(self, new_dir, entry):
+ """Move then entry name into new_dir."""
+ name = entry[0]
+ mandatory = entry[1]
+ self.step(gettext('Moving %s') % name)
+ try:
+ self.bzrdir.transport.move(name, '%s/%s' % (new_dir, name))
+ except errors.NoSuchFile:
+ if mandatory:
+ raise
+
+ def put_format(self, dirname, format):
+ self.bzrdir.transport.put_bytes('%s/format' % dirname,
+ format.get_format_string(),
+ self.file_mode)
+
+
+class BzrDirFormat4(BzrDirFormat):
+ """Bzr dir format 4.
+
+ This format is a combined format for working tree, branch and repository.
+ It has:
+ - Format 1 working trees [always]
+ - Format 4 branches [always]
+ - Format 4 repositories [always]
+
+ This format is deprecated: it indexes texts using a text it which is
+ removed in format 5; write support for this format has been removed.
+ """
+
+ _lock_class = lockable_files.TransportLock
+
+ def __eq__(self, other):
+ return type(self) == type(other)
+
+ @classmethod
+ def get_format_string(cls):
+ """See BzrDirFormat.get_format_string()."""
+ return "Bazaar-NG branch, format 0.0.4\n"
+
+ def get_format_description(self):
+ """See ControlDirFormat.get_format_description()."""
+ return "All-in-one format 4"
+
+ def get_converter(self, format=None):
+ """See ControlDirFormat.get_converter()."""
+ # there is one and only one upgrade path here.
+ return ConvertBzrDir4To5()
+
+ def initialize_on_transport(self, transport):
+ """Format 4 branches cannot be created."""
+ raise errors.UninitializableFormat(self)
+
+ def is_supported(self):
+ """Format 4 is not supported.
+
+ It is not supported because the model changed from 4 to 5 and the
+ conversion logic is expensive - so doing it on the fly was not
+ feasible.
+ """
+ return False
+
+ def network_name(self):
+ return self.get_format_string()
+
+ def _open(self, transport):
+ """See BzrDirFormat._open."""
+ return BzrDir4(transport, self)
+
+ def __return_repository_format(self):
+ """Circular import protection."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat4
+ return RepositoryFormat4()
+ repository_format = property(__return_repository_format)
+
+ @classmethod
+ def from_string(cls, format_string):
+ if format_string != cls.get_format_string():
+ raise AssertionError("unexpected format string %r" % format_string)
+ return cls()
+
+
+class BzrDirPreSplitOut(BzrDir):
+ """A common class for the all-in-one formats."""
+
+ def __init__(self, _transport, _format):
+ """See ControlDir.__init__."""
+ super(BzrDirPreSplitOut, self).__init__(_transport, _format)
+ self._control_files = lockable_files.LockableFiles(
+ self.get_branch_transport(None),
+ self._format._lock_file_name,
+ self._format._lock_class)
+
+ def break_lock(self):
+ """Pre-splitout bzrdirs do not suffer from stale locks."""
+ raise NotImplementedError(self.break_lock)
+
+ def cloning_metadir(self, require_stacking=False):
+ """Produce a metadir suitable for cloning with."""
+ if require_stacking:
+ return format_registry.make_bzrdir('1.6')
+ return self._format.__class__()
+
+ def clone(self, url, revision_id=None, force_new_repo=False,
+ preserve_stacking=False):
+ """See ControlDir.clone().
+
+ force_new_repo has no effect, since this family of formats always
+ require a new repository.
+ preserve_stacking has no effect, since no source branch using this
+ family of formats can be stacked, so there is no stacking to preserve.
+ """
+ self._make_tail(url)
+ result = self._format._initialize_for_clone(url)
+ self.open_repository().clone(result, revision_id=revision_id)
+ from_branch = self.open_branch()
+ from_branch.clone(result, revision_id=revision_id)
+ try:
+ tree = self.open_workingtree()
+ except errors.NotLocalUrl:
+ # make a new one, this format always has to have one.
+ result._init_workingtree()
+ else:
+ tree.clone(result)
+ return result
+
+ def create_branch(self, name=None, repository=None,
+ append_revisions_only=None):
+ """See ControlDir.create_branch."""
+ if repository is not None:
+ raise NotImplementedError(
+ "create_branch(repository=<not None>) on %r" % (self,))
+ return self._format.get_branch_format().initialize(self, name=name,
+ append_revisions_only=append_revisions_only)
+
+ def destroy_branch(self, name=None):
+ """See ControlDir.destroy_branch."""
+ raise errors.UnsupportedOperation(self.destroy_branch, self)
+
+ def create_repository(self, shared=False):
+ """See ControlDir.create_repository."""
+ if shared:
+ raise errors.IncompatibleFormat('shared repository', self._format)
+ return self.open_repository()
+
+ def destroy_repository(self):
+ """See ControlDir.destroy_repository."""
+ raise errors.UnsupportedOperation(self.destroy_repository, self)
+
+ def create_workingtree(self, revision_id=None, from_branch=None,
+ accelerator_tree=None, hardlink=False):
+ """See ControlDir.create_workingtree."""
+ # The workingtree is sometimes created when the bzrdir is created,
+ # but not when cloning.
+
+ # this looks buggy but is not -really-
+ # because this format creates the workingtree when the bzrdir is
+ # created
+ # clone and sprout will have set the revision_id
+ # and that will have set it for us, its only
+ # specific uses of create_workingtree in isolation
+ # that can do wonky stuff here, and that only
+ # happens for creating checkouts, which cannot be
+ # done on this format anyway. So - acceptable wart.
+ if hardlink:
+ warning("can't support hardlinked working trees in %r"
+ % (self,))
+ try:
+ result = self.open_workingtree(recommend_upgrade=False)
+ except errors.NoSuchFile:
+ result = self._init_workingtree()
+ if revision_id is not None:
+ if revision_id == _mod_revision.NULL_REVISION:
+ result.set_parent_ids([])
+ else:
+ result.set_parent_ids([revision_id])
+ return result
+
+ def _init_workingtree(self):
+ from bzrlib.plugins.weave_fmt.workingtree import WorkingTreeFormat2
+ try:
+ return WorkingTreeFormat2().initialize(self)
+ except errors.NotLocalUrl:
+ # Even though we can't access the working tree, we need to
+ # create its control files.
+ return WorkingTreeFormat2()._stub_initialize_on_transport(
+ self.transport, self._control_files._file_mode)
+
+ def destroy_workingtree(self):
+ """See ControlDir.destroy_workingtree."""
+ raise errors.UnsupportedOperation(self.destroy_workingtree, self)
+
+ def destroy_workingtree_metadata(self):
+ """See ControlDir.destroy_workingtree_metadata."""
+ raise errors.UnsupportedOperation(self.destroy_workingtree_metadata,
+ self)
+
+ def get_branch_transport(self, branch_format, name=None):
+ """See BzrDir.get_branch_transport()."""
+ if name is not None:
+ raise errors.NoColocatedBranchSupport(self)
+ if branch_format is None:
+ return self.transport
+ try:
+ branch_format.get_format_string()
+ except NotImplementedError:
+ return self.transport
+ raise errors.IncompatibleFormat(branch_format, self._format)
+
+ def get_repository_transport(self, repository_format):
+ """See BzrDir.get_repository_transport()."""
+ if repository_format is None:
+ return self.transport
+ try:
+ repository_format.get_format_string()
+ except NotImplementedError:
+ return self.transport
+ raise errors.IncompatibleFormat(repository_format, self._format)
+
+ def get_workingtree_transport(self, workingtree_format):
+ """See BzrDir.get_workingtree_transport()."""
+ if workingtree_format is None:
+ return self.transport
+ try:
+ workingtree_format.get_format_string()
+ except NotImplementedError:
+ return self.transport
+ raise errors.IncompatibleFormat(workingtree_format, self._format)
+
+ def needs_format_conversion(self, format=None):
+ """See ControlDir.needs_format_conversion()."""
+ # if the format is not the same as the system default,
+ # an upgrade is needed.
+ if format is None:
+ symbol_versioning.warn(symbol_versioning.deprecated_in((1, 13, 0))
+ % 'needs_format_conversion(format=None)')
+ format = BzrDirFormat.get_default_format()
+ return not isinstance(self._format, format.__class__)
+
+ def open_branch(self, name=None, unsupported=False,
+ ignore_fallbacks=False, possible_transports=None):
+ """See ControlDir.open_branch."""
+ from bzrlib.plugins.weave_fmt.branch import BzrBranchFormat4
+ format = BzrBranchFormat4()
+ format.check_support_status(unsupported)
+ return format.open(self, name, _found=True,
+ possible_transports=possible_transports)
+
+ def sprout(self, url, revision_id=None, force_new_repo=False,
+ possible_transports=None, accelerator_tree=None,
+ hardlink=False, stacked=False, create_tree_if_local=True,
+ source_branch=None):
+ """See ControlDir.sprout()."""
+ if source_branch is not None:
+ my_branch = self.open_branch()
+ if source_branch.base != my_branch.base:
+ raise AssertionError(
+ "source branch %r is not within %r with branch %r" %
+ (source_branch, self, my_branch))
+ if stacked:
+ raise errors.UnstackableBranchFormat(
+ self._format, self.root_transport.base)
+ if not create_tree_if_local:
+ raise errors.MustHaveWorkingTree(
+ self._format, self.root_transport.base)
+ from bzrlib.plugins.weave_fmt.workingtree import WorkingTreeFormat2
+ self._make_tail(url)
+ result = self._format._initialize_for_clone(url)
+ try:
+ self.open_repository().clone(result, revision_id=revision_id)
+ except errors.NoRepositoryPresent:
+ pass
+ try:
+ self.open_branch().sprout(result, revision_id=revision_id)
+ except errors.NotBranchError:
+ pass
+
+ # we always want a working tree
+ WorkingTreeFormat2().initialize(result,
+ accelerator_tree=accelerator_tree,
+ hardlink=hardlink)
+ return result
+
+ def set_branch_reference(self, target_branch, name=None):
+ from bzrlib.branch import BranchReferenceFormat
+ if name is not None:
+ raise errors.NoColocatedBranchSupport(self)
+ raise errors.IncompatibleFormat(BranchReferenceFormat, self._format)
+
+
+class BzrDir4(BzrDirPreSplitOut):
+ """A .bzr version 4 control object.
+
+ This is a deprecated format and may be removed after sept 2006.
+ """
+
+ def create_repository(self, shared=False):
+ """See ControlDir.create_repository."""
+ return self._format.repository_format.initialize(self, shared)
+
+ def needs_format_conversion(self, format=None):
+ """Format 4 dirs are always in need of conversion."""
+ if format is None:
+ symbol_versioning.warn(symbol_versioning.deprecated_in((1, 13, 0))
+ % 'needs_format_conversion(format=None)')
+ return True
+
+ def open_repository(self):
+ """See ControlDir.open_repository."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat4
+ return RepositoryFormat4().open(self, _found=True)
+
+
+class BzrDir5(BzrDirPreSplitOut):
+ """A .bzr version 5 control object.
+
+ This is a deprecated format and may be removed after sept 2006.
+ """
+
+ def has_workingtree(self):
+ """See ControlDir.has_workingtree."""
+ return True
+
+ def open_repository(self):
+ """See ControlDir.open_repository."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat5
+ return RepositoryFormat5().open(self, _found=True)
+
+ def open_workingtree(self, unsupported=False,
+ recommend_upgrade=True):
+ """See ControlDir.create_workingtree."""
+ from bzrlib.plugins.weave_fmt.workingtree import WorkingTreeFormat2
+ wt_format = WorkingTreeFormat2()
+ # we don't warn here about upgrades; that ought to be handled for the
+ # bzrdir as a whole
+ return wt_format.open(self, _found=True)
+
+
+class BzrDir6(BzrDirPreSplitOut):
+ """A .bzr version 6 control object.
+
+ This is a deprecated format and may be removed after sept 2006.
+ """
+
+ def has_workingtree(self):
+ """See ControlDir.has_workingtree."""
+ return True
+
+ def open_repository(self):
+ """See ControlDir.open_repository."""
+ from bzrlib.plugins.weave_fmt.repository import RepositoryFormat6
+ return RepositoryFormat6().open(self, _found=True)
+
+ def open_workingtree(self, unsupported=False, recommend_upgrade=True):
+ """See ControlDir.create_workingtree."""
+ # we don't warn here about upgrades; that ought to be handled for the
+ # bzrdir as a whole
+ from bzrlib.plugins.weave_fmt.workingtree import WorkingTreeFormat2
+ return WorkingTreeFormat2().open(self, _found=True)
diff --git a/bzrlib/plugins/weave_fmt/repository.py b/bzrlib/plugins/weave_fmt/repository.py
new file mode 100644
index 0000000..7af7ad1
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/repository.py
@@ -0,0 +1,883 @@
+# Copyright (C) 2007-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
+
+"""Deprecated weave-based repository formats.
+
+Weave based formats scaled linearly with history size and could not represent
+ghosts.
+"""
+
+from __future__ import absolute_import
+
+import gzip
+import os
+from cStringIO import StringIO
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import itertools
+
+from bzrlib import (
+ xml5,
+ graph as _mod_graph,
+ ui,
+ )
+""")
+from bzrlib import (
+ debug,
+ errors,
+ lockable_files,
+ lockdir,
+ osutils,
+ symbol_versioning,
+ trace,
+ tuned_gzip,
+ urlutils,
+ versionedfile,
+ weave,
+ weavefile,
+ )
+from bzrlib.decorators import needs_read_lock, needs_write_lock
+from bzrlib.repository import (
+ InterRepository,
+ RepositoryFormatMetaDir,
+ )
+from bzrlib.store.text import TextStore
+from bzrlib.versionedfile import (
+ AbsentContentFactory,
+ FulltextContentFactory,
+ VersionedFiles,
+ )
+from bzrlib.vf_repository import (
+ InterSameDataRepository,
+ VersionedFileCommitBuilder,
+ VersionedFileRepository,
+ VersionedFileRepositoryFormat,
+ MetaDirVersionedFileRepository,
+ MetaDirVersionedFileRepositoryFormat,
+ )
+
+from bzrlib.plugins.weave_fmt import bzrdir as weave_bzrdir
+
+
+class AllInOneRepository(VersionedFileRepository):
+ """Legacy support - the repository behaviour for all-in-one branches."""
+
+ @property
+ def _serializer(self):
+ return xml5.serializer_v5
+
+ def _escape(self, file_or_path):
+ if not isinstance(file_or_path, basestring):
+ file_or_path = '/'.join(file_or_path)
+ if file_or_path == '':
+ return u''
+ return urlutils.escape(osutils.safe_unicode(file_or_path))
+
+ def __init__(self, _format, a_bzrdir):
+ # we reuse one control files instance.
+ dir_mode = a_bzrdir._get_dir_mode()
+ file_mode = a_bzrdir._get_file_mode()
+
+ def get_store(name, compressed=True, prefixed=False):
+ # FIXME: This approach of assuming stores are all entirely compressed
+ # or entirely uncompressed is tidy, but breaks upgrade from
+ # some existing branches where there's a mixture; we probably
+ # still want the option to look for both.
+ relpath = self._escape(name)
+ store = TextStore(a_bzrdir.transport.clone(relpath),
+ prefixed=prefixed, compressed=compressed,
+ dir_mode=dir_mode,
+ file_mode=file_mode)
+ return store
+
+ # not broken out yet because the controlweaves|inventory_store
+ # and texts bits are still different.
+ if isinstance(_format, RepositoryFormat4):
+ # cannot remove these - there is still no consistent api
+ # which allows access to this old info.
+ self.inventory_store = get_store('inventory-store')
+ self._text_store = get_store('text-store')
+ super(AllInOneRepository, self).__init__(_format, a_bzrdir, a_bzrdir._control_files)
+
+ @needs_read_lock
+ def _all_possible_ids(self):
+ """Return all the possible revisions that we could find."""
+ if 'evil' in debug.debug_flags:
+ trace.mutter_callsite(
+ 3, "_all_possible_ids scales with size of history.")
+ return [key[-1] for key in self.inventories.keys()]
+
+ @needs_read_lock
+ def _all_revision_ids(self):
+ """Returns a list of all the revision ids in the repository.
+
+ These are in as much topological order as the underlying store can
+ present: for weaves ghosts may lead to a lack of correctness until
+ the reweave updates the parents list.
+ """
+ return [key[-1] for key in self.revisions.keys()]
+
+ def _activate_new_inventory(self):
+ """Put a replacement inventory.new into use as inventories."""
+ # Copy the content across
+ t = self.bzrdir._control_files._transport
+ t.copy('inventory.new.weave', 'inventory.weave')
+ # delete the temp inventory
+ t.delete('inventory.new.weave')
+ # Check we can parse the new weave properly as a sanity check
+ self.inventories.keys()
+
+ def _backup_inventory(self):
+ t = self.bzrdir._control_files._transport
+ t.copy('inventory.weave', 'inventory.backup.weave')
+
+ def _temp_inventories(self):
+ t = self.bzrdir._control_files._transport
+ return self._format._get_inventories(t, self, 'inventory.new')
+
+ def get_commit_builder(self, branch, parents, config, timestamp=None,
+ timezone=None, committer=None, revprops=None,
+ revision_id=None, lossy=False):
+ self._check_ascii_revisionid(revision_id, self.get_commit_builder)
+ result = VersionedFileCommitBuilder(self, parents, config, timestamp,
+ timezone, committer, revprops, revision_id, lossy=lossy)
+ self.start_write_group()
+ return result
+
+ @needs_read_lock
+ def get_revisions(self, revision_ids):
+ revs = self._get_revisions(revision_ids)
+ return revs
+
+ def _inventory_add_lines(self, revision_id, parents, lines,
+ check_content=True):
+ """Store lines in inv_vf and return the sha1 of the inventory."""
+ present_parents = self.get_graph().get_parent_map(parents)
+ final_parents = []
+ for parent in parents:
+ if parent in present_parents:
+ final_parents.append((parent,))
+ return self.inventories.add_lines((revision_id,), final_parents, lines,
+ check_content=check_content)[0]
+
+ def is_shared(self):
+ """AllInOne repositories cannot be shared."""
+ return False
+
+ @needs_write_lock
+ def set_make_working_trees(self, new_value):
+ """Set the policy flag for making working trees when creating branches.
+
+ This only applies to branches that use this repository.
+
+ The default is 'True'.
+ :param new_value: True to restore the default, False to disable making
+ working trees.
+ """
+ raise errors.RepositoryUpgradeRequired(self.user_url)
+
+ def make_working_trees(self):
+ """Returns the policy for making working trees on new branches."""
+ return True
+
+
+class WeaveMetaDirRepository(MetaDirVersionedFileRepository):
+ """A subclass of MetaDirRepository to set weave specific policy."""
+
+ def __init__(self, _format, a_bzrdir, control_files):
+ super(WeaveMetaDirRepository, self).__init__(_format, a_bzrdir, control_files)
+ self._serializer = _format._serializer
+
+ @needs_read_lock
+ def _all_possible_ids(self):
+ """Return all the possible revisions that we could find."""
+ if 'evil' in debug.debug_flags:
+ trace.mutter_callsite(
+ 3, "_all_possible_ids scales with size of history.")
+ return [key[-1] for key in self.inventories.keys()]
+
+ @needs_read_lock
+ def _all_revision_ids(self):
+ """Returns a list of all the revision ids in the repository.
+
+ These are in as much topological order as the underlying store can
+ present: for weaves ghosts may lead to a lack of correctness until
+ the reweave updates the parents list.
+ """
+ return [key[-1] for key in self.revisions.keys()]
+
+ def _activate_new_inventory(self):
+ """Put a replacement inventory.new into use as inventories."""
+ # Copy the content across
+ t = self._transport
+ t.copy('inventory.new.weave', 'inventory.weave')
+ # delete the temp inventory
+ t.delete('inventory.new.weave')
+ # Check we can parse the new weave properly as a sanity check
+ self.inventories.keys()
+
+ def _backup_inventory(self):
+ t = self._transport
+ t.copy('inventory.weave', 'inventory.backup.weave')
+
+ def _temp_inventories(self):
+ t = self._transport
+ return self._format._get_inventories(t, self, 'inventory.new')
+
+ def get_commit_builder(self, branch, parents, config, timestamp=None,
+ timezone=None, committer=None, revprops=None,
+ revision_id=None, lossy=False):
+ self._check_ascii_revisionid(revision_id, self.get_commit_builder)
+ result = VersionedFileCommitBuilder(self, parents, config, timestamp,
+ timezone, committer, revprops, revision_id, lossy=lossy)
+ self.start_write_group()
+ return result
+
+ @needs_read_lock
+ def get_revision(self, revision_id):
+ """Return the Revision object for a named revision"""
+ r = self.get_revision_reconcile(revision_id)
+ return r
+
+ def _inventory_add_lines(self, revision_id, parents, lines,
+ check_content=True):
+ """Store lines in inv_vf and return the sha1 of the inventory."""
+ present_parents = self.get_graph().get_parent_map(parents)
+ final_parents = []
+ for parent in parents:
+ if parent in present_parents:
+ final_parents.append((parent,))
+ return self.inventories.add_lines((revision_id,), final_parents, lines,
+ check_content=check_content)[0]
+
+
+class PreSplitOutRepositoryFormat(VersionedFileRepositoryFormat):
+ """Base class for the pre split out repository formats."""
+
+ rich_root_data = False
+ supports_tree_reference = False
+ supports_ghosts = False
+ supports_external_lookups = False
+ supports_chks = False
+ supports_nesting_repositories = True
+ _fetch_order = 'topological'
+ _fetch_reconcile = True
+ fast_deltas = False
+ supports_leaving_lock = False
+ # XXX: This is an old format that we don't support full checking on, so
+ # just claim that checking for this inconsistency is not required.
+ revision_graph_can_have_wrong_parents = False
+
+ def initialize(self, a_bzrdir, shared=False, _internal=False):
+ """Create a weave repository."""
+ if shared:
+ raise errors.IncompatibleFormat(self, a_bzrdir._format)
+
+ if not _internal:
+ # always initialized when the bzrdir is.
+ return self.open(a_bzrdir, _found=True)
+
+ # Create an empty weave
+ sio = StringIO()
+ weavefile.write_weave_v5(weave.Weave(), sio)
+ empty_weave = sio.getvalue()
+
+ trace.mutter('creating repository in %s.', a_bzrdir.transport.base)
+
+ # FIXME: RBC 20060125 don't peek under the covers
+ # NB: no need to escape relative paths that are url safe.
+ control_files = lockable_files.LockableFiles(a_bzrdir.transport,
+ 'branch-lock', lockable_files.TransportLock)
+ control_files.create_lock()
+ control_files.lock_write()
+ transport = a_bzrdir.transport
+ try:
+ transport.mkdir_multi(['revision-store', 'weaves'],
+ mode=a_bzrdir._get_dir_mode())
+ transport.put_bytes_non_atomic('inventory.weave', empty_weave,
+ mode=a_bzrdir._get_file_mode())
+ finally:
+ control_files.unlock()
+ repository = self.open(a_bzrdir, _found=True)
+ self._run_post_repo_init_hooks(repository, a_bzrdir, shared)
+ return repository
+
+ def open(self, a_bzrdir, _found=False):
+ """See RepositoryFormat.open()."""
+ if not _found:
+ # we are being called directly and must probe.
+ raise NotImplementedError
+
+ repo_transport = a_bzrdir.get_repository_transport(None)
+ result = AllInOneRepository(_format=self, a_bzrdir=a_bzrdir)
+ result.revisions = self._get_revisions(repo_transport, result)
+ result.signatures = self._get_signatures(repo_transport, result)
+ result.inventories = self._get_inventories(repo_transport, result)
+ result.texts = self._get_texts(repo_transport, result)
+ result.chk_bytes = None
+ return result
+
+ def is_deprecated(self):
+ return True
+
+
+class RepositoryFormat4(PreSplitOutRepositoryFormat):
+ """Bzr repository format 4.
+
+ This repository format has:
+ - flat stores
+ - TextStores for texts, inventories,revisions.
+
+ This format is deprecated: it indexes texts using a text id which is
+ removed in format 5; initialization and write support for this format
+ has been removed.
+ """
+
+ supports_funky_characters = False
+
+ _matchingbzrdir = weave_bzrdir.BzrDirFormat4()
+
+ def get_format_description(self):
+ """See RepositoryFormat.get_format_description()."""
+ return "Repository format 4"
+
+ def initialize(self, url, shared=False, _internal=False):
+ """Format 4 branches cannot be created."""
+ raise errors.UninitializableFormat(self)
+
+ def is_supported(self):
+ """Format 4 is not supported.
+
+ It is not supported because the model changed from 4 to 5 and the
+ conversion logic is expensive - so doing it on the fly was not
+ feasible.
+ """
+ return False
+
+ def _get_inventories(self, repo_transport, repo, name='inventory'):
+ # No inventories store written so far.
+ return None
+
+ def _get_revisions(self, repo_transport, repo):
+ from bzrlib.plugins.weave_fmt.xml4 import serializer_v4
+ return RevisionTextStore(repo_transport.clone('revision-store'),
+ serializer_v4, True, versionedfile.PrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_signatures(self, repo_transport, repo):
+ return SignatureTextStore(repo_transport.clone('revision-store'),
+ False, versionedfile.PrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_texts(self, repo_transport, repo):
+ return None
+
+
+class RepositoryFormat5(PreSplitOutRepositoryFormat):
+ """Bzr control format 5.
+
+ This repository format has:
+ - weaves for file texts and inventory
+ - flat stores
+ - TextStores for revisions and signatures.
+ """
+
+ _versionedfile_class = weave.WeaveFile
+ _matchingbzrdir = weave_bzrdir.BzrDirFormat5()
+ supports_funky_characters = False
+
+ @property
+ def _serializer(self):
+ return xml5.serializer_v5
+
+ def get_format_description(self):
+ """See RepositoryFormat.get_format_description()."""
+ return "Weave repository format 5"
+
+ def network_name(self):
+ """The network name for this format is the control dirs disk label."""
+ return self._matchingbzrdir.get_format_string()
+
+ def _get_inventories(self, repo_transport, repo, name='inventory'):
+ mapper = versionedfile.ConstantMapper(name)
+ return versionedfile.ThunkedVersionedFiles(repo_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+ def _get_revisions(self, repo_transport, repo):
+ return RevisionTextStore(repo_transport.clone('revision-store'),
+ xml5.serializer_v5, False, versionedfile.PrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_signatures(self, repo_transport, repo):
+ return SignatureTextStore(repo_transport.clone('revision-store'),
+ False, versionedfile.PrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_texts(self, repo_transport, repo):
+ mapper = versionedfile.PrefixMapper()
+ base_transport = repo_transport.clone('weaves')
+ return versionedfile.ThunkedVersionedFiles(base_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+
+class RepositoryFormat6(PreSplitOutRepositoryFormat):
+ """Bzr control format 6.
+
+ This repository format has:
+ - weaves for file texts and inventory
+ - hash subdirectory based stores.
+ - TextStores for revisions and signatures.
+ """
+
+ _versionedfile_class = weave.WeaveFile
+ _matchingbzrdir = weave_bzrdir.BzrDirFormat6()
+ supports_funky_characters = False
+ @property
+ def _serializer(self):
+ return xml5.serializer_v5
+
+ def get_format_description(self):
+ """See RepositoryFormat.get_format_description()."""
+ return "Weave repository format 6"
+
+ def network_name(self):
+ """The network name for this format is the control dirs disk label."""
+ return self._matchingbzrdir.get_format_string()
+
+ def _get_inventories(self, repo_transport, repo, name='inventory'):
+ mapper = versionedfile.ConstantMapper(name)
+ return versionedfile.ThunkedVersionedFiles(repo_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+ def _get_revisions(self, repo_transport, repo):
+ return RevisionTextStore(repo_transport.clone('revision-store'),
+ xml5.serializer_v5, False, versionedfile.HashPrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_signatures(self, repo_transport, repo):
+ return SignatureTextStore(repo_transport.clone('revision-store'),
+ False, versionedfile.HashPrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_texts(self, repo_transport, repo):
+ mapper = versionedfile.HashPrefixMapper()
+ base_transport = repo_transport.clone('weaves')
+ return versionedfile.ThunkedVersionedFiles(base_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+
+class RepositoryFormat7(MetaDirVersionedFileRepositoryFormat):
+ """Bzr repository 7.
+
+ This repository format has:
+ - weaves for file texts and inventory
+ - hash subdirectory based stores.
+ - TextStores for revisions and signatures.
+ - a format marker of its own
+ - an optional 'shared-storage' flag
+ - an optional 'no-working-trees' flag
+ """
+
+ _versionedfile_class = weave.WeaveFile
+ supports_ghosts = False
+ supports_chks = False
+ supports_funky_characters = False
+ revision_graph_can_have_wrong_parents = False
+
+ _fetch_order = 'topological'
+ _fetch_reconcile = True
+ fast_deltas = False
+ @property
+ def _serializer(self):
+ return xml5.serializer_v5
+
+ @classmethod
+ def get_format_string(cls):
+ """See RepositoryFormat.get_format_string()."""
+ return "Bazaar-NG Repository format 7"
+
+ def get_format_description(self):
+ """See RepositoryFormat.get_format_description()."""
+ return "Weave repository format 7"
+
+ def _get_inventories(self, repo_transport, repo, name='inventory'):
+ mapper = versionedfile.ConstantMapper(name)
+ return versionedfile.ThunkedVersionedFiles(repo_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+ def _get_revisions(self, repo_transport, repo):
+ return RevisionTextStore(repo_transport.clone('revision-store'),
+ xml5.serializer_v5, True, versionedfile.HashPrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_signatures(self, repo_transport, repo):
+ return SignatureTextStore(repo_transport.clone('revision-store'),
+ True, versionedfile.HashPrefixMapper(),
+ repo.is_locked, repo.is_write_locked)
+
+ def _get_texts(self, repo_transport, repo):
+ mapper = versionedfile.HashPrefixMapper()
+ base_transport = repo_transport.clone('weaves')
+ return versionedfile.ThunkedVersionedFiles(base_transport,
+ weave.WeaveFile, mapper, repo.is_locked)
+
+ def initialize(self, a_bzrdir, shared=False):
+ """Create a weave repository.
+
+ :param shared: If true the repository will be initialized as a shared
+ repository.
+ """
+ # Create an empty weave
+ sio = StringIO()
+ weavefile.write_weave_v5(weave.Weave(), sio)
+ empty_weave = sio.getvalue()
+
+ trace.mutter('creating repository in %s.', a_bzrdir.transport.base)
+ dirs = ['revision-store', 'weaves']
+ files = [('inventory.weave', StringIO(empty_weave)),
+ ]
+ utf8_files = [('format', self.get_format_string())]
+
+ self._upload_blank_content(a_bzrdir, dirs, files, utf8_files, shared)
+ return self.open(a_bzrdir=a_bzrdir, _found=True)
+
+ def open(self, a_bzrdir, _found=False, _override_transport=None):
+ """See RepositoryFormat.open().
+
+ :param _override_transport: INTERNAL USE ONLY. Allows opening the
+ repository at a slightly different url
+ than normal. I.e. during 'upgrade'.
+ """
+ if not _found:
+ format = RepositoryFormatMetaDir.find_format(a_bzrdir)
+ if _override_transport is not None:
+ repo_transport = _override_transport
+ else:
+ repo_transport = a_bzrdir.get_repository_transport(None)
+ control_files = lockable_files.LockableFiles(repo_transport,
+ 'lock', lockdir.LockDir)
+ result = WeaveMetaDirRepository(_format=self, a_bzrdir=a_bzrdir,
+ control_files=control_files)
+ result.revisions = self._get_revisions(repo_transport, result)
+ result.signatures = self._get_signatures(repo_transport, result)
+ result.inventories = self._get_inventories(repo_transport, result)
+ result.texts = self._get_texts(repo_transport, result)
+ result.chk_bytes = None
+ result._transport = repo_transport
+ return result
+
+ def is_deprecated(self):
+ return True
+
+
+class TextVersionedFiles(VersionedFiles):
+ """Just-a-bunch-of-files based VersionedFile stores."""
+
+ def __init__(self, transport, compressed, mapper, is_locked, can_write):
+ self._compressed = compressed
+ self._transport = transport
+ self._mapper = mapper
+ if self._compressed:
+ self._ext = '.gz'
+ else:
+ self._ext = ''
+ self._is_locked = is_locked
+ self._can_write = can_write
+
+ def add_lines(self, key, parents, lines):
+ """Add a revision to the store."""
+ if not self._is_locked():
+ raise errors.ObjectNotLocked(self)
+ if not self._can_write():
+ raise errors.ReadOnlyError(self)
+ if '/' in key[-1]:
+ raise ValueError('bad idea to put / in %r' % (key,))
+ text = ''.join(lines)
+ if self._compressed:
+ text = tuned_gzip.bytes_to_gzip(text)
+ path = self._map(key)
+ self._transport.put_bytes_non_atomic(path, text, create_parent_dir=True)
+
+ def insert_record_stream(self, stream):
+ adapters = {}
+ for record in stream:
+ # Raise an error when a record is missing.
+ if record.storage_kind == 'absent':
+ raise errors.RevisionNotPresent([record.key[0]], self)
+ # adapt to non-tuple interface
+ if record.storage_kind == 'fulltext':
+ self.add_lines(record.key, None,
+ osutils.split_lines(record.get_bytes_as('fulltext')))
+ else:
+ adapter_key = record.storage_kind, 'fulltext'
+ try:
+ adapter = adapters[adapter_key]
+ except KeyError:
+ adapter_factory = adapter_registry.get(adapter_key)
+ adapter = adapter_factory(self)
+ adapters[adapter_key] = adapter
+ lines = osutils.split_lines(adapter.get_bytes(
+ record, record.get_bytes_as(record.storage_kind)))
+ try:
+ self.add_lines(record.key, None, lines)
+ except errors.RevisionAlreadyPresent:
+ pass
+
+ def _load_text(self, key):
+ if not self._is_locked():
+ raise errors.ObjectNotLocked(self)
+ path = self._map(key)
+ try:
+ text = self._transport.get_bytes(path)
+ compressed = self._compressed
+ except errors.NoSuchFile:
+ if self._compressed:
+ # try without the .gz
+ path = path[:-3]
+ try:
+ text = self._transport.get_bytes(path)
+ compressed = False
+ except errors.NoSuchFile:
+ return None
+ else:
+ return None
+ if compressed:
+ text = gzip.GzipFile(mode='rb', fileobj=StringIO(text)).read()
+ return text
+
+ def _map(self, key):
+ return self._mapper.map(key) + self._ext
+
+
+class RevisionTextStore(TextVersionedFiles):
+ """Legacy thunk for format 4 repositories."""
+
+ def __init__(self, transport, serializer, compressed, mapper, is_locked,
+ can_write):
+ """Create a RevisionTextStore at transport with serializer."""
+ TextVersionedFiles.__init__(self, transport, compressed, mapper,
+ is_locked, can_write)
+ self._serializer = serializer
+
+ def _load_text_parents(self, key):
+ text = self._load_text(key)
+ if text is None:
+ return None, None
+ parents = self._serializer.read_revision_from_string(text).parent_ids
+ return text, tuple((parent,) for parent in parents)
+
+ def get_parent_map(self, keys):
+ result = {}
+ for key in keys:
+ parents = self._load_text_parents(key)[1]
+ if parents is None:
+ continue
+ result[key] = parents
+ return result
+
+ def get_known_graph_ancestry(self, keys):
+ """Get a KnownGraph instance with the ancestry of keys."""
+ keys = self.keys()
+ parent_map = self.get_parent_map(keys)
+ kg = _mod_graph.KnownGraph(parent_map)
+ return kg
+
+ def get_record_stream(self, keys, sort_order, include_delta_closure):
+ for key in keys:
+ text, parents = self._load_text_parents(key)
+ if text is None:
+ yield AbsentContentFactory(key)
+ else:
+ yield FulltextContentFactory(key, parents, None, text)
+
+ def keys(self):
+ if not self._is_locked():
+ raise errors.ObjectNotLocked(self)
+ relpaths = set()
+ for quoted_relpath in self._transport.iter_files_recursive():
+ relpath = urlutils.unquote(quoted_relpath)
+ path, ext = os.path.splitext(relpath)
+ if ext == '.gz':
+ relpath = path
+ if not relpath.endswith('.sig'):
+ relpaths.add(relpath)
+ paths = list(relpaths)
+ return set([self._mapper.unmap(path) for path in paths])
+
+
+class SignatureTextStore(TextVersionedFiles):
+ """Legacy thunk for format 4-7 repositories."""
+
+ def __init__(self, transport, compressed, mapper, is_locked, can_write):
+ TextVersionedFiles.__init__(self, transport, compressed, mapper,
+ is_locked, can_write)
+ self._ext = '.sig' + self._ext
+
+ def get_parent_map(self, keys):
+ result = {}
+ for key in keys:
+ text = self._load_text(key)
+ if text is None:
+ continue
+ result[key] = None
+ return result
+
+ def get_record_stream(self, keys, sort_order, include_delta_closure):
+ for key in keys:
+ text = self._load_text(key)
+ if text is None:
+ yield AbsentContentFactory(key)
+ else:
+ yield FulltextContentFactory(key, None, None, text)
+
+ def keys(self):
+ if not self._is_locked():
+ raise errors.ObjectNotLocked(self)
+ relpaths = set()
+ for quoted_relpath in self._transport.iter_files_recursive():
+ relpath = urlutils.unquote(quoted_relpath)
+ path, ext = os.path.splitext(relpath)
+ if ext == '.gz':
+ relpath = path
+ if not relpath.endswith('.sig'):
+ continue
+ relpaths.add(relpath[:-4])
+ paths = list(relpaths)
+ return set([self._mapper.unmap(path) for path in paths])
+
+
+class InterWeaveRepo(InterSameDataRepository):
+ """Optimised code paths between Weave based repositories.
+ """
+
+ @classmethod
+ def _get_repo_format_to_test(self):
+ return RepositoryFormat7()
+
+ @staticmethod
+ def is_compatible(source, target):
+ """Be compatible with known Weave formats.
+
+ We don't test for the stores being of specific types because that
+ could lead to confusing results, and there is no need to be
+ overly general.
+ """
+ try:
+ return (isinstance(source._format, (RepositoryFormat5,
+ RepositoryFormat6,
+ RepositoryFormat7)) and
+ isinstance(target._format, (RepositoryFormat5,
+ RepositoryFormat6,
+ RepositoryFormat7)))
+ except AttributeError:
+ return False
+
+ @needs_write_lock
+ def copy_content(self, revision_id=None):
+ """See InterRepository.copy_content()."""
+ # weave specific optimised path:
+ try:
+ self.target.set_make_working_trees(self.source.make_working_trees())
+ except (errors.RepositoryUpgradeRequired, NotImplemented):
+ pass
+ # FIXME do not peek!
+ if self.source._transport.listable():
+ pb = ui.ui_factory.nested_progress_bar()
+ try:
+ self.target.texts.insert_record_stream(
+ self.source.texts.get_record_stream(
+ self.source.texts.keys(), 'topological', False))
+ pb.update('Copying inventory', 0, 1)
+ self.target.inventories.insert_record_stream(
+ self.source.inventories.get_record_stream(
+ self.source.inventories.keys(), 'topological', False))
+ self.target.signatures.insert_record_stream(
+ self.source.signatures.get_record_stream(
+ self.source.signatures.keys(),
+ 'unordered', True))
+ self.target.revisions.insert_record_stream(
+ self.source.revisions.get_record_stream(
+ self.source.revisions.keys(),
+ 'topological', True))
+ finally:
+ pb.finished()
+ else:
+ self.target.fetch(self.source, revision_id=revision_id)
+
+ @needs_read_lock
+ def search_missing_revision_ids(self,
+ revision_id=symbol_versioning.DEPRECATED_PARAMETER,
+ find_ghosts=True, revision_ids=None, if_present_ids=None,
+ limit=None):
+ """See InterRepository.search_missing_revision_ids()."""
+ # we want all revisions to satisfy revision_id in source.
+ # but we don't want to stat every file here and there.
+ # we want then, all revisions other needs to satisfy revision_id
+ # checked, but not those that we have locally.
+ # so the first thing is to get a subset of the revisions to
+ # satisfy revision_id in source, and then eliminate those that
+ # we do already have.
+ # this is slow on high latency connection to self, but as this
+ # disk format scales terribly for push anyway due to rewriting
+ # inventory.weave, this is considered acceptable.
+ # - RBC 20060209
+ if symbol_versioning.deprecated_passed(revision_id):
+ symbol_versioning.warn(
+ 'search_missing_revision_ids(revision_id=...) was '
+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
+ DeprecationWarning, stacklevel=2)
+ if revision_ids is not None:
+ raise AssertionError(
+ 'revision_ids is mutually exclusive with revision_id')
+ if revision_id is not None:
+ revision_ids = [revision_id]
+ del revision_id
+ source_ids_set = self._present_source_revisions_for(
+ revision_ids, if_present_ids)
+ # source_ids is the worst possible case we may need to pull.
+ # now we want to filter source_ids against what we actually
+ # have in target, but don't try to check for existence where we know
+ # we do not have a revision as that would be pointless.
+ target_ids = set(self.target._all_possible_ids())
+ possibly_present_revisions = target_ids.intersection(source_ids_set)
+ actually_present_revisions = set(
+ self.target._eliminate_revisions_not_present(possibly_present_revisions))
+ required_revisions = source_ids_set.difference(actually_present_revisions)
+ if revision_ids is not None:
+ # we used get_ancestry to determine source_ids then we are assured all
+ # revisions referenced are present as they are installed in topological order.
+ # and the tip revision was validated by get_ancestry.
+ result_set = required_revisions
+ else:
+ # if we just grabbed the possibly available ids, then
+ # we only have an estimate of whats available and need to validate
+ # that against the revision records.
+ result_set = set(
+ self.source._eliminate_revisions_not_present(required_revisions))
+ if limit is not None:
+ topo_ordered = self.get_graph().iter_topo_order(result_set)
+ result_set = set(itertools.islice(topo_ordered, limit))
+ return self.source.revision_ids_to_search_result(result_set)
+
+
+InterRepository.register_optimiser(InterWeaveRepo)
+
+
+def get_extra_interrepo_test_combinations():
+ from bzrlib.repofmt import knitrepo
+ return [(InterRepository, RepositoryFormat5(),
+ knitrepo.RepositoryFormatKnit3())]
diff --git a/bzrlib/plugins/weave_fmt/test_bzrdir.py b/bzrlib/plugins/weave_fmt/test_bzrdir.py
new file mode 100644
index 0000000..2391aa4
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/test_bzrdir.py
@@ -0,0 +1,584 @@
+# Copyright (C) 2006-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
+
+"""Tests for the weave-era BzrDir formats.
+
+For interface contract tests, see tests/per_bzr_dir.
+"""
+
+from __future__ import absolute_import
+
+import os
+import sys
+
+from bzrlib import (
+ branch,
+ bzrdir,
+ controldir,
+ errors,
+ repository,
+ upgrade,
+ urlutils,
+ workingtree,
+ )
+from bzrlib.osutils import (
+ getcwd,
+ )
+from bzrlib.tests.test_bundle import V4BundleTester
+from bzrlib.tests.test_sftp_transport import TestCaseWithSFTPServer
+from bzrlib.tests import (
+ TestCaseWithTransport,
+ )
+
+from bzrlib.plugins.weave_fmt.branch import (
+ BzrBranchFormat4,
+ )
+from bzrlib.plugins.weave_fmt.bzrdir import (
+ BzrDirFormat5,
+ BzrDirFormat6,
+ )
+
+
+class TestFormat5(TestCaseWithTransport):
+ """Tests specific to the version 5 bzrdir format."""
+
+ def test_same_lockfiles_between_tree_repo_branch(self):
+ # this checks that only a single lockfiles instance is created
+ # for format 5 objects
+ dir = BzrDirFormat5().initialize(self.get_url())
+ def check_dir_components_use_same_lock(dir):
+ ctrl_1 = dir.open_repository().control_files
+ ctrl_2 = dir.open_branch().control_files
+ ctrl_3 = dir.open_workingtree()._control_files
+ self.assertTrue(ctrl_1 is ctrl_2)
+ self.assertTrue(ctrl_2 is ctrl_3)
+ check_dir_components_use_same_lock(dir)
+ # and if we open it normally.
+ dir = controldir.ControlDir.open(self.get_url())
+ check_dir_components_use_same_lock(dir)
+
+ def test_can_convert(self):
+ # format 5 dirs are convertable
+ dir = BzrDirFormat5().initialize(self.get_url())
+ self.assertTrue(dir.can_convert_format())
+
+ def test_needs_conversion(self):
+ # format 5 dirs need a conversion if they are not the default,
+ # and they aren't
+ dir = BzrDirFormat5().initialize(self.get_url())
+ # don't need to convert it to itself
+ self.assertFalse(dir.needs_format_conversion(BzrDirFormat5()))
+ # do need to convert it to the current default
+ self.assertTrue(dir.needs_format_conversion(
+ bzrdir.BzrDirFormat.get_default_format()))
+
+
+class TestFormat6(TestCaseWithTransport):
+ """Tests specific to the version 6 bzrdir format."""
+
+ def test_same_lockfiles_between_tree_repo_branch(self):
+ # this checks that only a single lockfiles instance is created
+ # for format 6 objects
+ dir = BzrDirFormat6().initialize(self.get_url())
+ def check_dir_components_use_same_lock(dir):
+ ctrl_1 = dir.open_repository().control_files
+ ctrl_2 = dir.open_branch().control_files
+ ctrl_3 = dir.open_workingtree()._control_files
+ self.assertTrue(ctrl_1 is ctrl_2)
+ self.assertTrue(ctrl_2 is ctrl_3)
+ check_dir_components_use_same_lock(dir)
+ # and if we open it normally.
+ dir = controldir.ControlDir.open(self.get_url())
+ check_dir_components_use_same_lock(dir)
+
+ def test_can_convert(self):
+ # format 6 dirs are convertable
+ dir = BzrDirFormat6().initialize(self.get_url())
+ self.assertTrue(dir.can_convert_format())
+
+ def test_needs_conversion(self):
+ # format 6 dirs need an conversion if they are not the default.
+ dir = BzrDirFormat6().initialize(self.get_url())
+ self.assertTrue(dir.needs_format_conversion(
+ bzrdir.BzrDirFormat.get_default_format()))
+
+
+class TestBreakLockOldBranch(TestCaseWithTransport):
+
+ def test_break_lock_format_5_bzrdir(self):
+ # break lock on a format 5 bzrdir should just return
+ self.make_branch_and_tree('foo', format=BzrDirFormat5())
+ out, err = self.run_bzr('break-lock foo')
+ self.assertEqual('', out)
+ self.assertEqual('', err)
+
+
+_upgrade1_template = \
+ [
+ ('foo', 'new contents\n'),
+ ('.bzr/',),
+ ('.bzr/README',
+ 'This is a Bazaar control directory.\n'
+ 'Do not change any files in this directory.\n'
+ 'See http://bazaar.canonical.com/ for more information about Bazaar.\n'),
+ ('.bzr/branch-format', 'Bazaar-NG branch, format 0.0.4\n'),
+ ('.bzr/revision-history',
+ 'mbp@sourcefrog.net-20051004035611-176b16534b086b3c\n'
+ 'mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd\n'),
+ ('.bzr/merged-patches', ''),
+ ('.bzr/pending-merged-patches', ''),
+ ('.bzr/branch-name', ''),
+ ('.bzr/branch-lock', ''),
+ ('.bzr/pending-merges', ''),
+ ('.bzr/inventory',
+ '<inventory>\n'
+ '<entry file_id="foo-20051004035605-91e788d1875603ae" kind="file" name="foo" />\n'
+ '</inventory>\n'),
+ ('.bzr/stat-cache',
+ '### bzr hashcache v5\n'
+ 'foo// be9f309239729f69a6309e970ef24941d31e042c 13 1128398176 1128398176 303464 770\n'),
+ ('.bzr/text-store/',),
+ ('.bzr/text-store/foo-20051004035611-1591048e9dc7c2d4.gz',
+ '\x1f\x8b\x08\x00[\xfdAC\x02\xff\xcb\xcc\xcb,\xc9L\xccQH\xce\xcf+I\xcd+)\xe6\x02\x00\xdd\xcc\xf90\x11\x00\x00\x00'),
+ ('.bzr/text-store/foo-20051004035756-4081373d897c3453.gz',
+ '\x1f\x8b\x08\x00\xc4\xfdAC\x02\xff\xcbK-WH\xce\xcf+I\xcd+)\xe6\x02\x00g\xc3\xdf\xc9\r\x00\x00\x00'),
+ ('.bzr/inventory-store/',),
+ ('.bzr/inventory-store/mbp@sourcefrog.net-20051004035611-176b16534b086b3c.gz',
+ '\x1f\x8b\x08\x00[\xfdAC\x02\xffm\x8f\xcd\n\xc20\x10\x84\xef>E\xc8\xbdt7?M\x02\xad\xaf"\xa1\x99`P[\xa8E\xacOo\x14\x05\x0f\xdef\xe1\xfbv\x98\xbeL7L\xeb\xbcl\xfb]_\xc3\xb2\x89\\\xce8\x944\xc8<\xcf\x8d"\xb2LdH\xdb\x8el\x13\x18\xce\xfb\xc4\xde\xd5SGHq*\xd3\x0b\xad\x8e\x14S\xbc\xe0\xadI\xb1\xe2\xbe\xfe}\xc2\xdc\xb0\rL\xc6#\xa4\xd1\x8d*\x99\x0f}=F\x1e$8G\x9d\xa0\x02\xa1rP9\x01c`FV\xda1qg\x98"\x02}\xa5\xf2\xa8\x95\xec\xa4h\xeb\x80\xf6g\xcd\x13\xb3\x01\xcc\x98\xda\x00\x00\x00'),
+ ('.bzr/inventory-store/mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd.gz',
+ '\x1f\x8b\x08\x00\xc4\xfdAC\x02\xffm\x8f\xc1\n\xc20\x10D\xef~E\xc8\xbd\xb8\x9bM\x9a,\xb4\xfe\x8a\xc4f\x83Am\xa1\x16\xb1~\xbdQ\x14<x\x9b\x81y3LW\xc6\x9b\x8c\xcb4\xaf\xbbMW\xc5\xbc\xaa\\\xce\xb2/\xa9\xd7y\x9a\x1a\x03\xe0\x10\xc0\x02\xb9\x16\\\xc3(>\x84\x84\xc1WKQ\xb4:\x95\xf1\x15\xad\x8cVc\xbc\xc8\x1b\xd3j\x91\xfb\xf2\xaf\xa4r\x8d\x85\x80\xe4)\x05\xf6\x03YG\x9f\xf4\xf5\x18\xb1\xd7\x07\xe1L\xc0\x86\xd8\x1b\xce-\xc7\xb6:a\x0f\x92\x8de\x8b\x89P\xc0\x9a\xe1\x0b\x95G\x9d\xc4\xda\xb1\xad\x07\xb6?o\x9e\xb5\xff\xf0\xf9\xda\x00\x00\x00'),
+ ('.bzr/revision-store/',),
+ ('.bzr/revision-store/mbp@sourcefrog.net-20051004035611-176b16534b086b3c.gz',
+ '\x1f\x8b\x08\x00[\xfdAC\x02\xff\x9d\x8eKj\xc30\x14E\xe7^\x85\xd0 \xb3$\xefI\xd1\x8f\xd8\xa6\x1b(t\x07E?\xbb\x82H\n\xb2\x1ahW\xdfB1\x14:\xeb\xf4r\xee\xbdgl\xf1\x91\xb6T\x0b\xf15\xe7\xd4{l\x13}\xb6\xad\xa7B^j\xbd\x91\xc3\xad_\xb3\xbb?m\xf5\xbd\xf9\xb8\xb4\xba\x9eJ\xec\x87\xb5_)I\xe5\x11K\xaf\xed\xe35\x85\x89\xfe\xa5\x8e\x0c@ \xc0\x05\xb8\x90\x88GT\xd2\xa1\x14\xfc\xe2@K\xc7\xfd\xef\x85\xed\xcd\xe2D\x95\x8d\x1a\xa47<\x02c2\xb0 \xbc\xd0\x8ay\xa3\xbcp\x8a\x83\x12A3\xb7XJv\xef\x7f_\xf7\x94\xe3\xd6m\xbeO\x14\x91in4*<\x812\x88\xc60\xfc\x01>k\x89\x13\xe5\x12\x00\xe8<\x8c\xdf\x8d\xcd\xaeq\xb6!\x90\xa5\xd6\xf1\xbc\x07\xc3x\xde\x85\xe6\xe1\x0b\xc8\x8a\x98\x03T\x01\x00\x00'),
+ ('.bzr/revision-store/mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd.gz',
+ '\x1f\x8b\x08\x00\xc4\xfdAC\x02\xff\x9d\x90Kj\x031\x0c\x86\xf79\xc5\xe0Ev\xe9\xc8o\x9b\xcc\x84^\xa0\xd0\x1b\x14\xbf&5d\xec`\xbb\x81\xf6\xf45\x84\xa4\x81\xaeZ\xa1\x85\x84^\xdf\xaf\xa9\x84K\xac1\xa7\xc1\xe5u\x8d\xad\x852\xa3\x17SZL\xc3k\xce\xa7a{j\xfb\xd5\x9e\x9fk\xfe(.,%\x1f\x9fRh\xdbc\xdb\xa3!\xa6KH-\x97\xcf\xb7\xe8g\xf4\xbbkG\x008\x06`@\xb9\xe4bG(_\x88\x95\xde\xf9n\xca\xfb\xc7\r\xf5\xdd\xe0\x19\xa9\x85)\x81\xf5"\xbd\x04j\xb8\x02b\xa8W\\\x0b\xc9\x14\xf4\xbc\xbb\xd7\xd6H4\xdc\xb8\xff}\xba\xc55\xd4f\xd6\xf3\x8c0&\x8ajE\xa4x\xe2@\xa5\xa6\x9a\xf3k\xc3WNaFT\x00\x00:l\xa6>Q\xcd1\x1cjp9\xf9;\xc34\xde\n\x9b\xe9lJWT{t\',a\xf9\x0b\xae\xc0x\x87\xa5\xb0Xp\xca,(a\xa9{\xd0{}\xd4\x12\x04(\xc5\xbb$\xc5$V\xceaI\x19\x01\xa2\x1dh\xed\x82d\x8c.\xccr@\xc3\xd8Q\xc6\x1f\xaa\xf1\xb6\xe8\xb0\xf9\x06QR\r\xf9\xfc\x01\x00\x00')]
+
+
+_ghost_template = [
+ ( './foo',
+ 'hello\n'
+ ),
+ ( './.bzr/', ),
+ ( './.bzr/README',
+ 'This is a Bazaar control directory.\n'
+ 'Do not change any files in this directory.\n'
+ 'See http://bazaar.canonical.com/ for more information about Bazaar.\n'
+ ),
+ ( './.bzr/branch-format',
+ 'Bazaar-NG branch, format 0.0.4\n'
+ ),
+ ( './.bzr/branch-lock',
+ ''
+ ),
+ ( './.bzr/branch-name',
+ ''
+ ),
+ ( './.bzr/inventory',
+ '<inventory>\n'
+ '<entry file_id="foo-20051004104918-0379cb7c76354cde" kind="file" name="foo" />\n'
+ '</inventory>\n'
+ ),
+ ( './.bzr/merged-patches',
+ ''
+ ),
+ ( './.bzr/pending-merged-patches',
+ ''
+ ),
+ ( './.bzr/pending-merges',
+ ''
+ ),
+ ( './.bzr/revision-history',
+ 'mbp@sourcefrog.net-20051004104921-a98be2278dd30b7b\n'
+ 'mbp@sourcefrog.net-20051004104937-c9b7a7bfcc0bb22d\n'
+ ),
+ ( './.bzr/stat-cache',
+ '### bzr hashcache v5\n'
+ 'foo// f572d396fae9206628714fb2ce00f72e94f2258f 6 1128422956 1128422956 306900 770\n'
+ ),
+ ( './.bzr/text-store/', ),
+ ( './.bzr/text-store/foo-20051004104921-8de8118a71be45ba.gz',
+ '\x1f\x8b\x08\x081^BC\x00\x03foo-20051004104921-8de8118a71be45ba\x00\xcbH\xcd\xc9\xc9\xe7\x02\x00 0:6\x06\x00\x00\x00'
+ ),
+ ( './.bzr/inventory-store/', ),
+ ( './.bzr/inventory-store/mbp@sourcefrog.net-20051004104921-a98be2278dd30b7b.gz',
+ '\x1f\x8b\x08\x081^BC\x00\x03mbp@sourcefrog.net-20051004104921-a98be2278dd30b7b\x00m\x8f\xcb\n'
+ '\xc20\x10E\xf7~E\xc8\xbe83\xcd\x13\xaa\xbf"yL0\xa8-\xd4"\xd6\xaf7\x8a\x82\x0bw\xb38\xe7\xde;C\x1do<.\xd3\xbc\xee7C;\xe6U\x94z\xe6C\xcd;Y\xa6\xa9#\x00\x8d\x00\n'
+ 'Ayt\x1d\xf4\xd6\xa7h\x935\xbdV)\xb3\x14\xa7:\xbe\xd0\xe6H1\x86\x0b\xbf5)\x16\xbe/\x7fC\x08;\x97\xd9!\xba`1\xb2\xd21|\xe8\xeb1`\xe3\xb5\xa5\xdc{S\x02{\x02c\xc8YT%Rb\x80b\x89\xbd*D\xda\x95\xafT\x1f\xad\xd2H\xb1m\xfb\xb7?\xcf<\x01W}\xb5\x8b\xd9\x00\x00\x00'
+ ),
+ ( './.bzr/inventory-store/mbp@sourcefrog.net-20051004104937-c9b7a7bfcc0bb22d.gz',
+ '\x1f\x8b\x08\x08A^BC\x00\x03mbp@sourcefrog.net-20051004104937-c9b7a7bfcc0bb22d\x00m\x8f\xcb\n'
+ '\xc20\x10E\xf7~E\xc8\xbe83\xcd\x13\xaa\xbf"yL0\xa8-\xd4"\xd6\xaf7\x8a\x82\x0bw\xb38\xe7\xde;C\x1do<.\xd3\xbc\xee7C;\xe6U\x94z\xe6C\xcd;Y\xa6\xa9#\x00\x8d\x00\n'
+ 'Ayt\x1d\xf4\xd6\xa7h\x935\xbdV)\xb3\x14\xa7:\xbe\xd0\xe6H1\x86\x0b\xbf5)\x16\xbe/\x7fC\x08;\x97\xd9!\xba`1\xb2\xd21|\xe8\xeb1`\xe3\xb5\xa5\xdc{S\x02{\x02c\xc8YT%Rb\x80b\x89\xbd*D\xda\x95\xafT\x1f\xad\xd2H\xb1m\xfb\xb7?\xcf<\x01W}\xb5\x8b\xd9\x00\x00\x00'
+ ),
+ ( './.bzr/revision-store/', ),
+ ( './.bzr/revision-store/mbp@sourcefrog.net-20051004104921-a98be2278dd30b7b.gz',
+ '\x1f\x8b\x08\x081^BC\x00\x03mbp@sourcefrog.net-20051004104921-a98be2278dd30b7b\x00\x9d\x8eMj\xc30\x14\x84\xf7>\x85\xd0"\xbb$\xef\xc9\xb6,\x11\xdb\xf4\x02\x85\xde\xa0\xe8\xe7\xd9\x11\xc4R\x90\xd4@{\xfa\x06\x8a\xa1\xd0]\x97\x03\xdf\xcc|c\xa6G(!E\xe6\xd2\xb6\x85Z)O\xfc\xd5\xe4\x1a"{K\xe9\xc6\x0e\xb7z\xd9\xec\xfd\xa5\xa4\x8f\xech\xc9i=E\xaa\x87\xb5^8\x0b\xf1A\xb1\xa6\xfc\xf9\x1e\xfc\xc4\xffRG\x01\xd0#@\x87\xd0i\x81G\xa3\x95%!\x06\xe5}\x0bv\xb0\xbf\x17\xca\xd5\xe0\xc4-\xa0\xb1\x8b\xb6`\xc0I\xa4\xc5\xf4\x9el\xef\x95v [\x94\xcf\x8e\xd5\xcay\xe4l\xf7\xfe\xf7u\r'
+ '\x1b\x95j\xb6\xfb\xc4\x11\x85\xea\x84\xd0\x12O\x03t\x83D\xad\xc4\x0f\xf0\x95"M\xbc\x95\x00\xc0\xe7f|6\x8aYi^B.u<\xef\xb1\x19\xcf\xbb\xce\xdc|\x038=\xc7\xe6R\x01\x00\x00'
+ ),
+ ( './.bzr/revision-store/mbp@sourcefrog.net-20051004104937-c9b7a7bfcc0bb22d.gz',
+ '\x1f\x8b\x08\x08A^BC\x00\x03mbp@sourcefrog.net-20051004104937-c9b7a7bfcc0bb22d\x00\x9d\x90\xc1j\xc30\x0c\x86\xef}\n'
+ "\xe3Coie'\xb1c\x9a\x94\xbe\xc0`o0,[N\x03M\\\x1c\xafe{\xfae\x94n\x85\xc1`;Y\x88O\xd2\xff\xb9Mt\x19\xe6!N\xcc\xc5q\x1cr\xa6\xd4\xf1'\x9b\xf20\xb1\xe7\x18Ol}\xca\xbb\x11\xcf\x879\xbe&G!\xc5~3Q^\xf7y\xc7\xd90]h\xca1\xbd\xbd\x0c\xbe\xe3?\xa9B\x02\xd4\x02\xa0\x12P\x99R\x17\xce\xa0\xb6\x1a\x83s\x80(\xa5\x7f\xdc0\x1f\xad\xe88\x82\xb0\x18\x0c\x82\x05\xa7\x04\x05[{\xc2\xda7\xc6\x81*\x85B\x8dh\x1a\xe7\x05g\xf7\xdc\xff>\x9d\x87\x91\xe6l\xc7s\xc7\x85\x90M%\xa5\xd1z#\x85\xa8\x9b\x1a\xaa\xfa\x06\xbc\xc7\x89:^*\x00\xe0\xfbU\xbbL\xcc\xb6\xa7\xfdH\xa9'\x16\x03\xeb\x8fq\xce\xed\xf6\xde_\xb5g\x9b\x16\xa1y\xa9\xbe\x02&\n"
+ '\x7fJ+EaM\x83$\xa5n\xbc/a\x91~\xd0\xbd\xfd\x135\n'
+ '\xd0\x9a`\x0c*W\x1aR\xc1\x94du\x08(\t\xb0\x91\xdeZ\xa3\x9cU\x9cm\x7f\x8dr\x1d\x10Ot\xb8\xc6\xcf\xa7\x907|\xfb-\xb1\xbd\xd3\xfb\xd5\x07\xeeD\xee\x08*\x02\x00\x00'
+ ),
+]
+
+_upgrade_dir_template = [
+ ( './.bzr/', ),
+ ( './.bzr/README',
+ 'This is a Bazaar control directory.\n'
+ 'Do not change any files in this directory.\n'
+ 'See http://bazaar.canonical.com/ for more information about Bazaar.\n'
+ ),
+ ( './.bzr/branch-format',
+ 'Bazaar-NG branch, format 0.0.4\n'
+ ),
+ ( './.bzr/branch-lock',
+ ''
+ ),
+ ( './.bzr/branch-name',
+ ''
+ ),
+ ( './.bzr/inventory',
+ '<inventory>\n'
+ '<entry file_id="dir-20051005095101-da1441ea3fa6917a" kind="directory" name="dir" />\n'
+ '</inventory>\n'
+ ),
+ ( './.bzr/merged-patches',
+ ''
+ ),
+ ( './.bzr/pending-merged-patches',
+ ''
+ ),
+ ( './.bzr/pending-merges',
+ ''
+ ),
+ ( './.bzr/revision-history',
+ 'robertc@robertcollins.net-20051005095108-6065fbd8e7d8617e\n'
+ ),
+ ( './.bzr/stat-cache',
+ '### bzr hashcache v5\n'
+ ),
+ ( './.bzr/text-store/', ),
+ ( './.bzr/inventory-store/', ),
+ ( './.bzr/inventory-store/robertc@robertcollins.net-20051005095108-6065fbd8e7d8617e.gz',
+ '\x1f\x8b\x08\x00\x0c\xa2CC\x02\xff\xb3\xc9\xcc+K\xcd+\xc9/\xaa\xb4\xe3\xb2\x012\x8a*\x15\xd22sR\xe33Sl\x95R2\x8bt\x8d\x0c\x0cL\r'
+ "\x81\xd8\xc0\x12H\x19\xea\xa6$\x1a\x9a\x98\x18\xa6&\x1a\xa7%\x9aY\x1a\x9a'*)dg\xe6A\x94\xa6&\x83LQR\xc8K\xccM\x05\x0b()\xe8\x03\xcd\xd4G\xb2\x00\x00\xc2<\x94\xb1m\x00\x00\x00"
+ ),
+ ( './.bzr/revision-store/', ),
+ ( './.bzr/revision-store/robertc@robertcollins.net-20051005095108-6065fbd8e7d8617e.gz',
+ '\x1f\x8b\x08\x00\x0c\xa2CC\x02\xff\xa5OKj\xc30\x14\xdc\xfb\x14B\x8b\xec\x92<I\xd6\xc7\xc42\x85\xde\xa0\x17(\xb6\xf4\x9c\n'
+ 'l\xa9H"\x90\x9c\xbe\xa6\xa9\xa1\x9b\xae\xbax\x0c\xcc\xe71\xd3g\xbc\x85\x12R$.\xadk\xa8\x15\xb3\xa5oi\xc2\\\xc9kZ\x96\x10\x0b9,\xf5\x92\xbf)\xf7\xf2\x83O\xe5\x14\xb1\x1e\xae\xf5BI\x887\x8c5\xe5\xfb{\xf0\x96\xfei>r\x00\xc9\xb6\x83n\x03sT\xa0\xe4<y\x83\xda\x1b\xc54\xfe~T>Ff\xe9\xcc:\xdd\x8e\xa6E\xc7@\xa2\x82I\xaaNL\xbas\\313)\x00\xb9\xe6\xe0(\xd9\x87\xfc\xb7A\r'
+ "+\x96:\xae\x9f\x962\xc6\x8d\x04i\x949\x01\x97R\xb7\x1d\x17O\xc3#E\xb4T(\x00\xa0C\xd3o\x892^q\x18\xbd'>\xe4\xfe\xbc\x13M\x7f\xde{\r"
+ '\xcd\x17\x85\xea\xba\x03l\x01\x00\x00'
+ ),
+ ( './dir/', ),
+]
+
+
+class TestUpgrade(TestCaseWithTransport):
+
+ def test_upgrade_v6_to_meta_no_workingtree(self):
+ # Some format6 branches do not have checkout files. Upgrading
+ # such a branch to metadir must not setup a working tree.
+ self.build_tree_contents(_upgrade1_template)
+ upgrade.upgrade('.', BzrDirFormat6())
+ t = self.get_transport('.')
+ t.delete_multi(['.bzr/pending-merges', '.bzr/inventory'])
+ self.assertFalse(t.has('.bzr/stat-cache'))
+ t.delete_tree('backup.bzr.~1~')
+ # At this point, we have a format6 branch without checkout files.
+ upgrade.upgrade('.', bzrdir.BzrDirMetaFormat1())
+ # The upgrade should not have set up a working tree.
+ control = controldir.ControlDir.open('.')
+ self.assertFalse(control.has_workingtree())
+ # We have covered the scope of this test, we may as well check that
+ # upgrade has not eaten our data, even if it's a bit redundant with
+ # other tests.
+ self.assertIsInstance(control._format, bzrdir.BzrDirMetaFormat1)
+ b = control.open_branch()
+ self.addCleanup(b.lock_read().unlock)
+ self.assertEquals(b._revision_history(),
+ ['mbp@sourcefrog.net-20051004035611-176b16534b086b3c',
+ 'mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd'])
+
+ def test_upgrade_simple(self):
+ """Upgrade simple v0.0.4 format to latest format"""
+ eq = self.assertEquals
+ self.build_tree_contents(_upgrade1_template)
+ upgrade.upgrade(u'.')
+ control = controldir.ControlDir.open('.')
+ b = control.open_branch()
+ # tsk, peeking under the covers.
+ self.assertIsInstance(
+ control._format,
+ bzrdir.BzrDirFormat.get_default_format().__class__)
+ self.addCleanup(b.lock_read().unlock)
+ rh = b._revision_history()
+ eq(rh,
+ ['mbp@sourcefrog.net-20051004035611-176b16534b086b3c',
+ 'mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd'])
+ rt = b.repository.revision_tree(rh[0])
+ foo_id = 'foo-20051004035605-91e788d1875603ae'
+ rt.lock_read()
+ try:
+ eq(rt.get_file_text(foo_id), 'initial contents\n')
+ finally:
+ rt.unlock()
+ rt = b.repository.revision_tree(rh[1])
+ rt.lock_read()
+ try:
+ eq(rt.get_file_text(foo_id), 'new contents\n')
+ finally:
+ rt.unlock()
+ # check a backup was made:
+ backup_dir = 'backup.bzr.~1~'
+ t = self.get_transport('.')
+ t.stat(backup_dir)
+ t.stat(backup_dir + '/README')
+ t.stat(backup_dir + '/branch-format')
+ t.stat(backup_dir + '/revision-history')
+ t.stat(backup_dir + '/merged-patches')
+ t.stat(backup_dir + '/pending-merged-patches')
+ t.stat(backup_dir + '/pending-merges')
+ t.stat(backup_dir + '/branch-name')
+ t.stat(backup_dir + '/branch-lock')
+ t.stat(backup_dir + '/inventory')
+ t.stat(backup_dir + '/stat-cache')
+ t.stat(backup_dir + '/text-store')
+ t.stat(backup_dir + '/text-store/foo-20051004035611-1591048e9dc7c2d4.gz')
+ t.stat(backup_dir + '/text-store/foo-20051004035756-4081373d897c3453.gz')
+ t.stat(backup_dir + '/inventory-store/')
+ t.stat(backup_dir + '/inventory-store/mbp@sourcefrog.net-20051004035611-176b16534b086b3c.gz')
+ t.stat(backup_dir + '/inventory-store/mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd.gz')
+ t.stat(backup_dir + '/revision-store/')
+ t.stat(backup_dir + '/revision-store/mbp@sourcefrog.net-20051004035611-176b16534b086b3c.gz')
+ t.stat(backup_dir + '/revision-store/mbp@sourcefrog.net-20051004035756-235f2b7dcdddd8dd.gz')
+
+ def test_upgrade_with_ghosts(self):
+ """Upgrade v0.0.4 tree containing ghost references.
+
+ That is, some of the parents of revisions mentioned in the branch
+ aren't present in the branch's storage.
+
+ This shouldn't normally happen in branches created entirely in
+ bzr, but can happen in branches imported from baz and arch, or from
+ other systems, where the importer knows about a revision but not
+ its contents."""
+ eq = self.assertEquals
+ self.build_tree_contents(_ghost_template)
+ upgrade.upgrade(u'.')
+ b = branch.Branch.open(u'.')
+ self.addCleanup(b.lock_read().unlock)
+ revision_id = b._revision_history()[1]
+ rev = b.repository.get_revision(revision_id)
+ eq(len(rev.parent_ids), 2)
+ eq(rev.parent_ids[1], 'wibble@wobble-2')
+
+ def test_upgrade_makes_dir_weaves(self):
+ self.build_tree_contents(_upgrade_dir_template)
+ old_repodir = controldir.ControlDir.open_unsupported('.')
+ old_repo_format = old_repodir.open_repository()._format
+ upgrade.upgrade('.')
+ # this is the path to the literal file. As format changes
+ # occur it needs to be updated. FIXME: ask the store for the
+ # path.
+ repo = repository.Repository.open('.')
+ # it should have changed the format
+ self.assertNotEqual(old_repo_format.__class__, repo._format.__class__)
+ # and we should be able to read the names for the file id
+ # 'dir-20051005095101-da1441ea3fa6917a'
+ repo.lock_read()
+ self.addCleanup(repo.unlock)
+ text_keys = repo.texts.keys()
+ dir_keys = [key for key in text_keys if key[0] ==
+ 'dir-20051005095101-da1441ea3fa6917a']
+ self.assertNotEqual([], dir_keys)
+
+ def test_upgrade_to_meta_sets_workingtree_last_revision(self):
+ self.build_tree_contents(_upgrade_dir_template)
+ upgrade.upgrade('.', bzrdir.BzrDirMetaFormat1())
+ tree = workingtree.WorkingTree.open('.')
+ self.addCleanup(tree.lock_read().unlock)
+ self.assertEqual([tree.branch._revision_history()[-1]],
+ tree.get_parent_ids())
+
+
+class SFTPBranchTest(TestCaseWithSFTPServer):
+ """Test some stuff when accessing a bzr Branch over sftp"""
+
+ def test_lock_file(self):
+ # old format branches use a special lock file on sftp.
+ b = self.make_branch('', format=BzrDirFormat6())
+ b = branch.Branch.open(self.get_url())
+ self.assertPathExists('.bzr/')
+ self.assertPathExists('.bzr/branch-format')
+ self.assertPathExists('.bzr/branch-lock')
+
+ self.assertPathDoesNotExist('.bzr/branch-lock.write-lock')
+ b.lock_write()
+ self.assertPathExists('.bzr/branch-lock.write-lock')
+ b.unlock()
+ self.assertPathDoesNotExist('.bzr/branch-lock.write-lock')
+
+
+class TestInfo(TestCaseWithTransport):
+
+ def test_info_locking_oslocks(self):
+ if sys.platform == "win32":
+ self.skip("don't use oslocks on win32 in unix manner")
+ # This test tests old (all-in-one, OS lock using) behaviour which
+ # simply cannot work on windows (and is indeed why we changed our
+ # design. As such, don't try to remove the thisFailsStrictLockCheck
+ # call here.
+ self.thisFailsStrictLockCheck()
+
+ tree = self.make_branch_and_tree('branch',
+ format=BzrDirFormat6())
+
+ # Test all permutations of locking the working tree, branch and repository
+ # XXX: Well not yet, as we can't query oslocks yet. Currently, it's
+ # implemented by raising NotImplementedError and get_physical_lock_status()
+ # always returns false. This makes bzr info hide the lock status. (Olaf)
+ # W B R
+
+ # U U U
+ out, err = self.run_bzr('info -v branch')
+ self.assertEqualDiff(
+"""Standalone tree (format: weave)
+Location:
+ branch root: %s
+
+Format:
+ control: All-in-one format 6
+ working tree: Working tree format 2
+ branch: Branch format 4
+ repository: %s
+
+In the working tree:
+ 0 unchanged
+ 0 modified
+ 0 added
+ 0 removed
+ 0 renamed
+ 0 unknown
+ 0 ignored
+ 0 versioned subdirectories
+
+Branch history:
+ 0 revisions
+
+Repository:
+ 0 revisions
+""" % ('branch', tree.branch.repository._format.get_format_description(),
+ ), out)
+ self.assertEqual('', err)
+ # L L L
+ tree.lock_write()
+ out, err = self.run_bzr('info -v branch')
+ self.assertEqualDiff(
+"""Standalone tree (format: weave)
+Location:
+ branch root: %s
+
+Format:
+ control: All-in-one format 6
+ working tree: Working tree format 2
+ branch: Branch format 4
+ repository: %s
+
+In the working tree:
+ 0 unchanged
+ 0 modified
+ 0 added
+ 0 removed
+ 0 renamed
+ 0 unknown
+ 0 ignored
+ 0 versioned subdirectories
+
+Branch history:
+ 0 revisions
+
+Repository:
+ 0 revisions
+""" % ('branch', tree.branch.repository._format.get_format_description(),
+ ), out)
+ self.assertEqual('', err)
+ tree.unlock()
+
+
+class TestBranchFormat4(TestCaseWithTransport):
+ """Tests specific to branch format 4"""
+
+ def test_no_metadir_support(self):
+ url = self.get_url()
+ bdir = bzrdir.BzrDirMetaFormat1().initialize(url)
+ bdir.create_repository()
+ self.assertRaises(errors.IncompatibleFormat,
+ BzrBranchFormat4().initialize, bdir)
+
+ def test_supports_bzrdir_6(self):
+ url = self.get_url()
+ bdir = BzrDirFormat6().initialize(url)
+ bdir.create_repository()
+ BzrBranchFormat4().initialize(bdir)
+
+
+class TestBoundBranch(TestCaseWithTransport):
+
+ def setUp(self):
+ super(TestBoundBranch, self).setUp()
+ self.build_tree(['master/', 'child/'])
+ self.make_branch_and_tree('master')
+ self.make_branch_and_tree('child',
+ format=controldir.format_registry.make_bzrdir('weave'))
+ os.chdir('child')
+
+ def test_bind_format_6_bzrdir(self):
+ # bind on a format 6 bzrdir should error
+ out,err = self.run_bzr('bind ../master', retcode=3)
+ self.assertEqual('', out)
+ # TODO: jam 20060427 Probably something like this really should
+ # print out the actual path, rather than the URL
+ cwd = urlutils.local_path_to_url(getcwd())
+ self.assertEqual('bzr: ERROR: To use this feature you must '
+ 'upgrade your branch at %s/.\n' % cwd, err)
+
+ def test_unbind_format_6_bzrdir(self):
+ # bind on a format 6 bzrdir should error
+ out,err = self.run_bzr('unbind', retcode=3)
+ self.assertEqual('', out)
+ cwd = urlutils.local_path_to_url(getcwd())
+ self.assertEqual('bzr: ERROR: To use this feature you must '
+ 'upgrade your branch at %s/.\n' % cwd, err)
+
+
+class TestInit(TestCaseWithTransport):
+
+ def test_init_weave(self):
+ # --format=weave should be accepted to allow interoperation with
+ # old releases when desired.
+ out, err = self.run_bzr('init --format=weave')
+ self.assertEqual("""Created a standalone tree (format: weave)\n""",
+ out)
+ self.assertEqual('', err)
+
+
+class V4WeaveBundleTester(V4BundleTester):
+
+ def bzrdir_format(self):
+ return 'metaweave'
diff --git a/bzrlib/plugins/weave_fmt/test_repository.py b/bzrlib/plugins/weave_fmt/test_repository.py
new file mode 100644
index 0000000..7453296
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/test_repository.py
@@ -0,0 +1,331 @@
+# Copyright (C) 2006-2010 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
+
+"""Tests for weave repositories.
+
+For interface tests see tests/per_repository/*.py.
+
+"""
+
+from __future__ import absolute_import
+
+from cStringIO import StringIO
+from stat import S_ISDIR
+import sys
+
+from bzrlib.bzrdir import (
+ BzrDirMetaFormat1,
+ )
+from bzrlib.errors import (
+ IllegalPath,
+ NoSuchFile,
+ )
+from bzrlib.repository import (
+ InterRepository,
+ Repository,
+ )
+from bzrlib.serializer import (
+ format_registry as serializer_format_registry,
+ )
+from bzrlib.tests import (
+ TestCase,
+ TestCaseWithTransport,
+ )
+
+from bzrlib.plugins.weave_fmt import xml4
+from bzrlib.plugins.weave_fmt.bzrdir import (
+ BzrDirFormat6,
+ )
+from bzrlib.plugins.weave_fmt.repository import (
+ InterWeaveRepo,
+ RepositoryFormat4,
+ RepositoryFormat5,
+ RepositoryFormat6,
+ RepositoryFormat7,
+ )
+
+
+class TestFormat6(TestCaseWithTransport):
+
+ def test_attribute__fetch_order(self):
+ """Weaves need topological data insertion."""
+ control = BzrDirFormat6().initialize(self.get_url())
+ repo = RepositoryFormat6().initialize(control)
+ self.assertEqual('topological', repo._format._fetch_order)
+
+ def test_attribute__fetch_uses_deltas(self):
+ """Weaves do not reuse deltas."""
+ control = BzrDirFormat6().initialize(self.get_url())
+ repo = RepositoryFormat6().initialize(control)
+ self.assertEqual(False, repo._format._fetch_uses_deltas)
+
+ def test_attribute__fetch_reconcile(self):
+ """Weave repositories need a reconcile after fetch."""
+ control = BzrDirFormat6().initialize(self.get_url())
+ repo = RepositoryFormat6().initialize(control)
+ self.assertEqual(True, repo._format._fetch_reconcile)
+
+ def test_no_ancestry_weave(self):
+ control = BzrDirFormat6().initialize(self.get_url())
+ repo = RepositoryFormat6().initialize(control)
+ # We no longer need to create the ancestry.weave file
+ # since it is *never* used.
+ self.assertRaises(NoSuchFile,
+ control.transport.get,
+ 'ancestry.weave')
+
+ def test_supports_external_lookups(self):
+ control = BzrDirFormat6().initialize(self.get_url())
+ repo = RepositoryFormat6().initialize(control)
+ self.assertFalse(repo._format.supports_external_lookups)
+
+
+
+
+class TestFormat7(TestCaseWithTransport):
+
+ def test_attribute__fetch_order(self):
+ """Weaves need topological data insertion."""
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control)
+ self.assertEqual('topological', repo._format._fetch_order)
+
+ def test_attribute__fetch_uses_deltas(self):
+ """Weaves do not reuse deltas."""
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control)
+ self.assertEqual(False, repo._format._fetch_uses_deltas)
+
+ def test_attribute__fetch_reconcile(self):
+ """Weave repositories need a reconcile after fetch."""
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control)
+ self.assertEqual(True, repo._format._fetch_reconcile)
+
+ def test_disk_layout(self):
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control)
+ # in case of side effects of locking.
+ repo.lock_write()
+ repo.unlock()
+ # we want:
+ # format 'Bazaar-NG Repository format 7'
+ # lock ''
+ # inventory.weave == empty_weave
+ # empty revision-store directory
+ # empty weaves directory
+ t = control.get_repository_transport(None)
+ self.assertEqualDiff('Bazaar-NG Repository format 7',
+ t.get('format').read())
+ self.assertTrue(S_ISDIR(t.stat('revision-store').st_mode))
+ self.assertTrue(S_ISDIR(t.stat('weaves').st_mode))
+ self.assertEqualDiff('# bzr weave file v5\n'
+ 'w\n'
+ 'W\n',
+ t.get('inventory.weave').read())
+ # Creating a file with id Foo:Bar results in a non-escaped file name on
+ # disk.
+ control.create_branch()
+ tree = control.create_workingtree()
+ tree.add(['foo'], ['Foo:Bar'], ['file'])
+ tree.put_file_bytes_non_atomic('Foo:Bar', 'content\n')
+ try:
+ tree.commit('first post', rev_id='first')
+ except IllegalPath:
+ if sys.platform != 'win32':
+ raise
+ self.knownFailure('Foo:Bar cannot be used as a file-id on windows'
+ ' in repo format 7')
+ return
+ self.assertEqualDiff(
+ '# bzr weave file v5\n'
+ 'i\n'
+ '1 7fe70820e08a1aac0ef224d9c66ab66831cc4ab1\n'
+ 'n first\n'
+ '\n'
+ 'w\n'
+ '{ 0\n'
+ '. content\n'
+ '}\n'
+ 'W\n',
+ t.get('weaves/74/Foo%3ABar.weave').read())
+
+ def test_shared_disk_layout(self):
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control, shared=True)
+ # we want:
+ # format 'Bazaar-NG Repository format 7'
+ # inventory.weave == empty_weave
+ # empty revision-store directory
+ # empty weaves directory
+ # a 'shared-storage' marker file.
+ # lock is not present when unlocked
+ t = control.get_repository_transport(None)
+ self.assertEqualDiff('Bazaar-NG Repository format 7',
+ t.get('format').read())
+ self.assertEqualDiff('', t.get('shared-storage').read())
+ self.assertTrue(S_ISDIR(t.stat('revision-store').st_mode))
+ self.assertTrue(S_ISDIR(t.stat('weaves').st_mode))
+ self.assertEqualDiff('# bzr weave file v5\n'
+ 'w\n'
+ 'W\n',
+ t.get('inventory.weave').read())
+ self.assertFalse(t.has('branch-lock'))
+
+ def test_creates_lockdir(self):
+ """Make sure it appears to be controlled by a LockDir existence"""
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control, shared=True)
+ t = control.get_repository_transport(None)
+ # TODO: Should check there is a 'lock' toplevel directory,
+ # regardless of contents
+ self.assertFalse(t.has('lock/held/info'))
+ repo.lock_write()
+ try:
+ self.assertTrue(t.has('lock/held/info'))
+ finally:
+ # unlock so we don't get a warning about failing to do so
+ repo.unlock()
+
+ def test_uses_lockdir(self):
+ """repo format 7 actually locks on lockdir"""
+ base_url = self.get_url()
+ control = BzrDirMetaFormat1().initialize(base_url)
+ repo = RepositoryFormat7().initialize(control, shared=True)
+ t = control.get_repository_transport(None)
+ repo.lock_write()
+ repo.unlock()
+ del repo
+ # make sure the same lock is created by opening it
+ repo = Repository.open(base_url)
+ repo.lock_write()
+ self.assertTrue(t.has('lock/held/info'))
+ repo.unlock()
+ self.assertFalse(t.has('lock/held/info'))
+
+ def test_shared_no_tree_disk_layout(self):
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control, shared=True)
+ repo.set_make_working_trees(False)
+ # we want:
+ # format 'Bazaar-NG Repository format 7'
+ # lock ''
+ # inventory.weave == empty_weave
+ # empty revision-store directory
+ # empty weaves directory
+ # a 'shared-storage' marker file.
+ t = control.get_repository_transport(None)
+ self.assertEqualDiff('Bazaar-NG Repository format 7',
+ t.get('format').read())
+ ## self.assertEqualDiff('', t.get('lock').read())
+ self.assertEqualDiff('', t.get('shared-storage').read())
+ self.assertEqualDiff('', t.get('no-working-trees').read())
+ repo.set_make_working_trees(True)
+ self.assertFalse(t.has('no-working-trees'))
+ self.assertTrue(S_ISDIR(t.stat('revision-store').st_mode))
+ self.assertTrue(S_ISDIR(t.stat('weaves').st_mode))
+ self.assertEqualDiff('# bzr weave file v5\n'
+ 'w\n'
+ 'W\n',
+ t.get('inventory.weave').read())
+
+ def test_supports_external_lookups(self):
+ control = BzrDirMetaFormat1().initialize(self.get_url())
+ repo = RepositoryFormat7().initialize(control)
+ self.assertFalse(repo._format.supports_external_lookups)
+
+
+class TestInterWeaveRepo(TestCaseWithTransport):
+
+ def test_is_compatible_and_registered(self):
+ # InterWeaveRepo is compatible when either side
+ # is a format 5/6/7 branch
+ from bzrlib.repofmt import knitrepo
+ formats = [RepositoryFormat5(),
+ RepositoryFormat6(),
+ RepositoryFormat7()]
+ incompatible_formats = [RepositoryFormat4(),
+ knitrepo.RepositoryFormatKnit1(),
+ ]
+ repo_a = self.make_repository('a')
+ repo_b = self.make_repository('b')
+ is_compatible = InterWeaveRepo.is_compatible
+ for source in incompatible_formats:
+ # force incompatible left then right
+ repo_a._format = source
+ repo_b._format = formats[0]
+ self.assertFalse(is_compatible(repo_a, repo_b))
+ self.assertFalse(is_compatible(repo_b, repo_a))
+ for source in formats:
+ repo_a._format = source
+ for target in formats:
+ repo_b._format = target
+ self.assertTrue(is_compatible(repo_a, repo_b))
+ self.assertEqual(InterWeaveRepo,
+ InterRepository.get(repo_a, repo_b).__class__)
+
+
+_working_inventory_v4 = """<inventory file_id="TREE_ROOT">
+<entry file_id="bar-20050901064931-73b4b1138abc9cd2" kind="file" name="bar" parent_id="TREE_ROOT" />
+<entry file_id="foo-20050801201819-4139aa4a272f4250" kind="directory" name="foo" parent_id="TREE_ROOT" />
+<entry file_id="bar-20050824000535-6bc48cfad47ed134" kind="file" name="bar" parent_id="foo-20050801201819-4139aa4a272f4250" />
+</inventory>"""
+
+
+_revision_v4 = """<revision committer="Martin Pool &lt;mbp@sourcefrog.net&gt;"
+ inventory_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9"
+ inventory_sha1="e79c31c1deb64c163cf660fdedd476dd579ffd41"
+ revision_id="mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9"
+ timestamp="1125907235.212"
+ timezone="36000">
+<message>- start splitting code for xml (de)serialization away from objects
+ preparatory to supporting multiple formats by a single library
+</message>
+<parents>
+<revision_ref revision_id="mbp@sourcefrog.net-20050905063503-43948f59fa127d92" revision_sha1="7bdf4cc8c5bdac739f8cf9b10b78cf4b68f915ff" />
+</parents>
+</revision>
+"""
+
+
+class TestSerializer(TestCase):
+ """Test serializer"""
+
+ def test_registry(self):
+ self.assertIs(xml4.serializer_v4,
+ serializer_format_registry.get('4'))
+
+ def test_canned_inventory(self):
+ """Test unpacked a canned inventory v4 file."""
+ inp = StringIO(_working_inventory_v4)
+ inv = xml4.serializer_v4.read_inventory(inp)
+ self.assertEqual(len(inv), 4)
+ self.assert_(inv.has_id('bar-20050901064931-73b4b1138abc9cd2'))
+
+ def test_unpack_revision(self):
+ """Test unpacking a canned revision v4"""
+ inp = StringIO(_revision_v4)
+ rev = xml4.serializer_v4.read_revision(inp)
+ eq = self.assertEqual
+ eq(rev.committer,
+ "Martin Pool <mbp@sourcefrog.net>")
+ eq(rev.inventory_id,
+ "mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9")
+ eq(len(rev.parent_ids), 1)
+ eq(rev.parent_ids[0],
+ "mbp@sourcefrog.net-20050905063503-43948f59fa127d92")
+
+
diff --git a/bzrlib/plugins/weave_fmt/test_workingtree.py b/bzrlib/plugins/weave_fmt/test_workingtree.py
new file mode 100644
index 0000000..cea8c6b
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/test_workingtree.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2005-2011 Canonical Ltd
+# Authors: Robert Collins <robert.collins@canonical.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Tests for weave-era working tree formats."""
+
+from __future__ import absolute_import
+
+import os
+
+from bzrlib import (
+ conflicts,
+ errors,
+ )
+
+from bzrlib.tests import (
+ TestCaseWithTransport,
+ )
+
+from bzrlib.plugins.weave_fmt.bzrdir import BzrDirFormat6
+
+
+class TestFormat2WorkingTree(TestCaseWithTransport):
+ """Tests that are specific to format 2 trees."""
+
+ def create_format2_tree(self, url):
+ return self.make_branch_and_tree(
+ url, format=BzrDirFormat6())
+
+ def test_conflicts(self):
+ # test backwards compatability
+ tree = self.create_format2_tree('.')
+ self.assertRaises(errors.UnsupportedOperation, tree.set_conflicts,
+ None)
+ file('lala.BASE', 'wb').write('labase')
+ expected = conflicts.ContentsConflict('lala')
+ self.assertEqual(list(tree.conflicts()), [expected])
+ file('lala', 'wb').write('la')
+ tree.add('lala', 'lala-id')
+ expected = conflicts.ContentsConflict('lala', file_id='lala-id')
+ self.assertEqual(list(tree.conflicts()), [expected])
+ file('lala.THIS', 'wb').write('lathis')
+ file('lala.OTHER', 'wb').write('laother')
+ # When "text conflict"s happen, stem, THIS and OTHER are text
+ expected = conflicts.TextConflict('lala', file_id='lala-id')
+ self.assertEqual(list(tree.conflicts()), [expected])
+ os.unlink('lala.OTHER')
+ os.mkdir('lala.OTHER')
+ expected = conflicts.ContentsConflict('lala', file_id='lala-id')
+ self.assertEqual(list(tree.conflicts()), [expected])
+
+ def test_detect_conflicts(self):
+ """Conflicts are detected properly"""
+ tree = self.create_format2_tree('.')
+ self.build_tree_contents([('hello', 'hello world4'),
+ ('hello.THIS', 'hello world2'),
+ ('hello.BASE', 'hello world1'),
+ ('hello.OTHER', 'hello world3'),
+ ('hello.sploo.BASE', 'yellowworld'),
+ ('hello.sploo.OTHER', 'yellowworld2'),
+ ])
+ tree.lock_read()
+ self.assertLength(6, list(tree.list_files()))
+ tree.unlock()
+ tree_conflicts = tree.conflicts()
+ self.assertLength(2, tree_conflicts)
+ self.assertTrue('hello' in tree_conflicts[0].path)
+ self.assertTrue('hello.sploo' in tree_conflicts[1].path)
+ conflicts.restore('hello')
+ conflicts.restore('hello.sploo')
+ self.assertLength(0, tree.conflicts())
+ self.assertFileEqual('hello world2', 'hello')
+ self.assertFalse(os.path.lexists('hello.sploo'))
+ self.assertRaises(errors.NotConflicted, conflicts.restore, 'hello')
+ self.assertRaises(errors.NotConflicted,
+ conflicts.restore, 'hello.sploo')
diff --git a/bzrlib/plugins/weave_fmt/workingtree.py b/bzrlib/plugins/weave_fmt/workingtree.py
new file mode 100644
index 0000000..2ec443c
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/workingtree.py
@@ -0,0 +1,243 @@
+# Copyright (C) 2005-2010 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
+
+"""Weave-era working tree objects."""
+
+from __future__ import absolute_import
+
+from cStringIO import StringIO
+
+from bzrlib import (
+ conflicts as _mod_conflicts,
+ errors,
+ inventory,
+ osutils,
+ revision as _mod_revision,
+ transform,
+ xml5,
+ )
+from bzrlib.decorators import needs_read_lock
+from bzrlib.mutabletree import MutableTree
+from bzrlib.transport.local import LocalTransport
+from bzrlib.workingtree import (
+ WorkingTreeFormat,
+ )
+from bzrlib.workingtree_3 import (
+ PreDirStateWorkingTree,
+ )
+
+
+def get_conflicted_stem(path):
+ for suffix in _mod_conflicts.CONFLICT_SUFFIXES:
+ if path.endswith(suffix):
+ return path[:-len(suffix)]
+
+
+class WorkingTreeFormat2(WorkingTreeFormat):
+ """The second working tree format.
+
+ This format modified the hash cache from the format 1 hash cache.
+ """
+
+ upgrade_recommended = True
+
+ requires_normalized_unicode_filenames = True
+
+ case_sensitive_filename = "Branch-FoRMaT"
+
+ missing_parent_conflicts = False
+
+ supports_versioned_directories = True
+
+ def get_format_description(self):
+ """See WorkingTreeFormat.get_format_description()."""
+ return "Working tree format 2"
+
+ def _stub_initialize_on_transport(self, transport, file_mode):
+ """Workaround: create control files for a remote working tree.
+
+ This ensures that it can later be updated and dealt with locally,
+ since BzrDirFormat6 and BzrDirFormat5 cannot represent dirs with
+ no working tree. (See bug #43064).
+ """
+ sio = StringIO()
+ inv = inventory.Inventory()
+ xml5.serializer_v5.write_inventory(inv, sio, working=True)
+ sio.seek(0)
+ transport.put_file('inventory', sio, file_mode)
+ transport.put_bytes('pending-merges', '', file_mode)
+
+ def initialize(self, a_bzrdir, revision_id=None, from_branch=None,
+ accelerator_tree=None, hardlink=False):
+ """See WorkingTreeFormat.initialize()."""
+ if not isinstance(a_bzrdir.transport, LocalTransport):
+ raise errors.NotLocalUrl(a_bzrdir.transport.base)
+ if from_branch is not None:
+ branch = from_branch
+ else:
+ branch = a_bzrdir.open_branch()
+ if revision_id is None:
+ revision_id = _mod_revision.ensure_null(branch.last_revision())
+ branch.lock_write()
+ try:
+ branch.generate_revision_history(revision_id)
+ finally:
+ branch.unlock()
+ inv = inventory.Inventory()
+ wt = WorkingTree2(a_bzrdir.root_transport.local_abspath('.'),
+ branch,
+ inv,
+ _internal=True,
+ _format=self,
+ _bzrdir=a_bzrdir,
+ _control_files=branch.control_files)
+ basis_tree = branch.repository.revision_tree(revision_id)
+ if basis_tree.get_root_id() is not None:
+ wt.set_root_id(basis_tree.get_root_id())
+ # set the parent list and cache the basis tree.
+ if _mod_revision.is_null(revision_id):
+ parent_trees = []
+ else:
+ parent_trees = [(revision_id, basis_tree)]
+ wt.set_parent_trees(parent_trees)
+ transform.build_tree(basis_tree, wt)
+ for hook in MutableTree.hooks['post_build_tree']:
+ hook(wt)
+ return wt
+
+ def __init__(self):
+ super(WorkingTreeFormat2, self).__init__()
+ from bzrlib.plugins.weave_fmt.bzrdir import BzrDirFormat6
+ self._matchingbzrdir = BzrDirFormat6()
+
+ def open(self, a_bzrdir, _found=False):
+ """Return the WorkingTree object for a_bzrdir
+
+ _found is a private parameter, do not use it. It is used to indicate
+ if format probing has already been done.
+ """
+ if not _found:
+ # we are being called directly and must probe.
+ raise NotImplementedError
+ if not isinstance(a_bzrdir.transport, LocalTransport):
+ raise errors.NotLocalUrl(a_bzrdir.transport.base)
+ wt = WorkingTree2(a_bzrdir.root_transport.local_abspath('.'),
+ _internal=True,
+ _format=self,
+ _bzrdir=a_bzrdir,
+ _control_files=a_bzrdir.open_branch().control_files)
+ return wt
+
+
+class WorkingTree2(PreDirStateWorkingTree):
+ """This is the Format 2 working tree.
+
+ This was the first weave based working tree.
+ - uses os locks for locking.
+ - uses the branch last-revision.
+ """
+
+ def __init__(self, basedir, *args, **kwargs):
+ super(WorkingTree2, self).__init__(basedir, *args, **kwargs)
+ # WorkingTree2 has more of a constraint that self._inventory must
+ # exist. Because this is an older format, we don't mind the overhead
+ # caused by the extra computation here.
+
+ # Newer WorkingTree's should only have self._inventory set when they
+ # have a read lock.
+ if self._inventory is None:
+ self.read_working_inventory()
+
+ def _get_check_refs(self):
+ """Return the references needed to perform a check of this tree."""
+ return [('trees', self.last_revision())]
+
+
+ def lock_tree_write(self):
+ """See WorkingTree.lock_tree_write().
+
+ In Format2 WorkingTrees we have a single lock for the branch and tree
+ so lock_tree_write() degrades to lock_write().
+
+ :return: An object with an unlock method which will release the lock
+ obtained.
+ """
+ self.branch.lock_write()
+ try:
+ self._control_files.lock_write()
+ return self
+ except:
+ self.branch.unlock()
+ raise
+
+ def unlock(self):
+ # we share control files:
+ if self._control_files._lock_count == 3:
+ # do non-implementation specific cleanup
+ self._cleanup()
+ # _inventory_is_modified is always False during a read lock.
+ if self._inventory_is_modified:
+ self.flush()
+ self._write_hashcache_if_dirty()
+
+ # reverse order of locking.
+ try:
+ return self._control_files.unlock()
+ finally:
+ self.branch.unlock()
+
+ def _iter_conflicts(self):
+ conflicted = set()
+ for info in self.list_files():
+ path = info[0]
+ stem = get_conflicted_stem(path)
+ if stem is None:
+ continue
+ if stem not in conflicted:
+ conflicted.add(stem)
+ yield stem
+
+ @needs_read_lock
+ def conflicts(self):
+ conflicts = _mod_conflicts.ConflictList()
+ for conflicted in self._iter_conflicts():
+ text = True
+ try:
+ if osutils.file_kind(self.abspath(conflicted)) != "file":
+ text = False
+ except errors.NoSuchFile:
+ text = False
+ if text is True:
+ for suffix in ('.THIS', '.OTHER'):
+ try:
+ kind = osutils.file_kind(self.abspath(conflicted+suffix))
+ if kind != "file":
+ text = False
+ except errors.NoSuchFile:
+ text = False
+ if text == False:
+ break
+ ctype = {True: 'text conflict', False: 'contents conflict'}[text]
+ conflicts.append(_mod_conflicts.Conflict.factory(ctype,
+ path=conflicted,
+ file_id=self.path2id(conflicted)))
+ return conflicts
+
+ def set_conflicts(self, arg):
+ raise errors.UnsupportedOperation(self.set_conflicts, self)
+
+ def add_conflicts(self, arg):
+ raise errors.UnsupportedOperation(self.add_conflicts, self)
diff --git a/bzrlib/plugins/weave_fmt/xml4.py b/bzrlib/plugins/weave_fmt/xml4.py
new file mode 100644
index 0000000..f1cd664
--- /dev/null
+++ b/bzrlib/plugins/weave_fmt/xml4.py
@@ -0,0 +1,190 @@
+# Copyright (C) 2005-2010 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
+
+from bzrlib.xml_serializer import (
+ Element,
+ SubElement,
+ XMLSerializer,
+ escape_invalid_chars,
+ )
+from bzrlib.inventory import ROOT_ID, Inventory
+import bzrlib.inventory as inventory
+from bzrlib.revision import Revision
+from bzrlib.errors import BzrError
+
+
+class _Serializer_v4(XMLSerializer):
+ """Version 0.0.4 serializer
+
+ You should use the serializer_v4 singleton.
+
+ v4 serialisation is no longer supported, only deserialisation.
+ """
+
+ __slots__ = []
+
+ def _pack_entry(self, ie):
+ """Convert InventoryEntry to XML element"""
+ e = Element('entry')
+ e.set('name', ie.name)
+ e.set('file_id', ie.file_id)
+ e.set('kind', ie.kind)
+
+ if ie.text_size is not None:
+ e.set('text_size', '%d' % ie.text_size)
+
+ for f in ['text_id', 'text_sha1', 'symlink_target']:
+ v = getattr(ie, f)
+ if v is not None:
+ e.set(f, v)
+
+ # to be conservative, we don't externalize the root pointers
+ # for now, leaving them as null in the xml form. in a future
+ # version it will be implied by nested elements.
+ if ie.parent_id != ROOT_ID:
+ e.set('parent_id', ie.parent_id)
+
+ e.tail = '\n'
+
+ return e
+
+
+ def _unpack_inventory(self, elt, revision_id=None, entry_cache=None,
+ return_from_cache=False):
+ """Construct from XML Element
+
+ :param revision_id: Ignored parameter used by xml5.
+ """
+ root_id = elt.get('file_id') or ROOT_ID
+ inv = Inventory(root_id)
+ for e in elt:
+ ie = self._unpack_entry(e, entry_cache=entry_cache,
+ return_from_cache=return_from_cache)
+ if ie.parent_id == ROOT_ID:
+ ie.parent_id = root_id
+ inv.add(ie)
+ return inv
+
+
+ def _unpack_entry(self, elt, entry_cache=None, return_from_cache=False):
+ ## original format inventories don't have a parent_id for
+ ## nodes in the root directory, but it's cleaner to use one
+ ## internally.
+ parent_id = elt.get('parent_id')
+ if parent_id is None:
+ parent_id = ROOT_ID
+
+ kind = elt.get('kind')
+ if kind == 'directory':
+ ie = inventory.InventoryDirectory(elt.get('file_id'),
+ elt.get('name'),
+ parent_id)
+ elif kind == 'file':
+ ie = inventory.InventoryFile(elt.get('file_id'),
+ elt.get('name'),
+ parent_id)
+ ie.text_id = elt.get('text_id')
+ ie.text_sha1 = elt.get('text_sha1')
+ v = elt.get('text_size')
+ ie.text_size = v and int(v)
+ elif kind == 'symlink':
+ ie = inventory.InventoryLink(elt.get('file_id'),
+ elt.get('name'),
+ parent_id)
+ ie.symlink_target = elt.get('symlink_target')
+ else:
+ raise BzrError("unknown kind %r" % kind)
+
+ ## mutter("read inventoryentry: %r", elt.attrib)
+
+ return ie
+
+
+ def _pack_revision(self, rev):
+ """Revision object -> xml tree"""
+ root = Element('revision',
+ committer = rev.committer,
+ timestamp = '%.9f' % rev.timestamp,
+ revision_id = rev.revision_id,
+ inventory_id = rev.inventory_id,
+ inventory_sha1 = rev.inventory_sha1,
+ )
+ if rev.timezone:
+ root.set('timezone', str(rev.timezone))
+ root.text = '\n'
+
+ msg = SubElement(root, 'message')
+ msg.text = escape_invalid_chars(rev.message)[0]
+ msg.tail = '\n'
+
+ if rev.parents:
+ pelts = SubElement(root, 'parents')
+ pelts.tail = pelts.text = '\n'
+ for i, parent_id in enumerate(rev.parents):
+ p = SubElement(pelts, 'revision_ref')
+ p.tail = '\n'
+ p.set('revision_id', parent_id)
+ if i < len(rev.parent_sha1s):
+ p.set('revision_sha1', rev.parent_sha1s[i])
+ return root
+
+
+ def _unpack_revision(self, elt):
+ """XML Element -> Revision object"""
+
+ # <changeset> is deprecated...
+ if elt.tag not in ('revision', 'changeset'):
+ raise BzrError("unexpected tag in revision file: %r" % elt)
+
+ rev = Revision(committer = elt.get('committer'),
+ timestamp = float(elt.get('timestamp')),
+ revision_id = elt.get('revision_id'),
+ inventory_id = elt.get('inventory_id'),
+ inventory_sha1 = elt.get('inventory_sha1')
+ )
+
+ precursor = elt.get('precursor')
+ precursor_sha1 = elt.get('precursor_sha1')
+
+ pelts = elt.find('parents')
+
+ if pelts:
+ for p in pelts:
+ rev.parent_ids.append(p.get('revision_id'))
+ rev.parent_sha1s.append(p.get('revision_sha1'))
+ if precursor:
+ # must be consistent
+ prec_parent = rev.parent_ids[0]
+ elif precursor:
+ # revisions written prior to 0.0.5 have a single precursor
+ # give as an attribute
+ rev.parent_ids.append(precursor)
+ rev.parent_sha1s.append(precursor_sha1)
+
+ v = elt.get('timezone')
+ rev.timezone = v and int(v)
+
+ rev.message = elt.findtext('message') # text of <message>
+ return rev
+
+
+
+
+"""singleton instance"""
+serializer_v4 = _Serializer_v4()
+