Rev 6322: Smarter .po file merging with merge hook as a plugin: po_merge in http://bazaar.launchpad.net/~vila/bzr/integration/
Vincent Ladeuil
v.ladeuil+lp at free.fr
Tue Nov 29 10:43:03 UTC 2011
At http://bazaar.launchpad.net/~vila/bzr/integration/
------------------------------------------------------------
revno: 6322 [merge]
revision-id: v.ladeuil+lp at free.fr-20111129104303-s556m163ckcbhd1g
parent: pqm at pqm.ubuntu.com-20111129065054-1657z2i4r8ceauz4
parent: v.ladeuil+lp at free.fr-20111129104136-wmfo389ngcsy6jmr
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: trunk
timestamp: Tue 2011-11-29 11:43:03 +0100
message:
Smarter .po file merging with merge hook as a plugin: po_merge
added:
bzrlib/plugins/po_merge/ po_merge-20111123180440-la918t6t068pzacx-1
bzrlib/plugins/po_merge/README readme-20111123180440-la918t6t068pzacx-2
bzrlib/plugins/po_merge/__init__.py __init__.py-20111123180440-la918t6t068pzacx-3
bzrlib/plugins/po_merge/po_merge.py po_merge.py-20111123180440-la918t6t068pzacx-4
bzrlib/plugins/po_merge/tests/ tests-20111123180440-la918t6t068pzacx-5
bzrlib/plugins/po_merge/tests/__init__.py __init__.py-20111123180440-la918t6t068pzacx-6
bzrlib/plugins/po_merge/tests/test_po_merge.py test_blackbox_po_mer-20111123180440-la918t6t068pzacx-7
modified:
bzrlib/merge.py merge.py-20050513021216-953b65a438527106
bzrlib/tests/features.py features.py-20090820042958-jglgza3wrn03ha9e-1
doc/en/release-notes/bzr-2.5.txt bzr2.5.txt-20110708125756-587p0hpw7oke4h05-1
-------------- next part --------------
=== modified file 'bzrlib/merge.py'
--- a/bzrlib/merge.py 2011-11-28 19:07:58 +0000
+++ b/bzrlib/merge.py 2011-11-29 10:43:03 +0000
@@ -140,7 +140,7 @@
params.winner == 'other' or
# THIS and OTHER aren't both files.
not params.is_file_merge() or
- # The filename doesn't match *.xml
+ # The filename doesn't match
not self.file_matches(params)):
return 'not_applicable', None
return self.merge_matching(params)
@@ -855,14 +855,18 @@
else:
entries = self._entries_lca()
resolver = self._lca_multi_way
+ # Prepare merge hooks
+ factories = Merger.hooks['merge_file_content']
+ # One hook for each registered one plus our default merger
+ hooks = [factory(self) for factory in factories] + [self]
+ self.active_hooks = [hook for hook in hooks if hook is not None]
child_pb = ui.ui_factory.nested_progress_bar()
try:
- factories = Merger.hooks['merge_file_content']
- hooks = [factory(self) for factory in factories] + [self]
- self.active_hooks = [hook for hook in hooks if hook is not None]
for num, (file_id, changed, parents3, names3,
executable3) in enumerate(entries):
- child_pb.update(gettext('Preparing file merge'), num, len(entries))
+ # Try merging each entry
+ child_pb.update(gettext('Preparing file merge'),
+ num, len(entries))
self._merge_names(file_id, parents3, names3, resolver=resolver)
if changed:
file_status = self._do_merge_contents(file_id)
=== added directory 'bzrlib/plugins/po_merge'
=== added file 'bzrlib/plugins/po_merge/README'
--- a/bzrlib/plugins/po_merge/README 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/po_merge/README 2011-11-29 10:13:56 +0000
@@ -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.
=== added file 'bzrlib/plugins/po_merge/__init__.py'
--- a/bzrlib/plugins/po_merge/__init__.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/po_merge/__init__.py 2011-11-29 10:41:36 +0000
@@ -0,0 +1,90 @@
+# 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
+
+__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 bzr-format NEWS 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
+
=== added file 'bzrlib/plugins/po_merge/po_merge.py'
--- a/bzrlib/plugins/po_merge/po_merge.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/po_merge/po_merge.py 2011-11-29 10:41:36 +0000
@@ -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 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.Option(
+ 'po_merge.po_dirs', default='po,debian/po',
+ from_unicode=config.list_from_store,
+ 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', []
=== added directory 'bzrlib/plugins/po_merge/tests'
=== added file 'bzrlib/plugins/po_merge/tests/__init__.py'
--- a/bzrlib/plugins/po_merge/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/po_merge/tests/__init__.py 2011-11-28 16:31:24 +0000
@@ -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
=== added file 'bzrlib/plugins/po_merge/tests/test_po_merge.py'
--- a/bzrlib/plugins/po_merge/tests/test_po_merge.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/plugins/po_merge/tests/test_po_merge.py 2011-11-29 08:24:10 +0000
@@ -0,0 +1,451 @@
+# -*- coding: utf8
+# 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 at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at 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 at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at 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 at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at 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 at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at 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 at example.com>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.111\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french at 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 at example.com>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.111\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french at 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 at example.com>, 2009, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.112+nmu2\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french at 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 at example.com>, 2009, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: adduser 3.112+nmu2\n"
+"Report-Msgid-Bugs-To: adduser-devel at 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 at example.com>\n"
+"Language-Team: Debian French Team <debian-l10n-french at 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"
+
+""",
+)
=== modified file 'bzrlib/tests/features.py'
--- a/bzrlib/tests/features.py 2011-11-21 00:06:30 +0000
+++ b/bzrlib/tests/features.py 2011-11-24 10:47:43 +0000
@@ -408,8 +408,9 @@
bash_feature = ExecutableFeature('bash')
+diff_feature = ExecutableFeature('diff')
sed_feature = ExecutableFeature('sed')
-diff_feature = ExecutableFeature('diff')
+msgmerge_feature = ExecutableFeature('msgmerge')
class _PosixPermissionsFeature(Feature):
=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- a/doc/en/release-notes/bzr-2.5.txt 2011-11-29 06:50:54 +0000
+++ b/doc/en/release-notes/bzr-2.5.txt 2011-11-29 10:43:03 +0000
@@ -20,6 +20,10 @@
.. New commands, options, etc that users may wish to try out.
+* Provides a ``po_merge`` plugin to automatically merge ``.po`` files with
+ ``msgmerge``. See ``bzr help po_merge`` for details.
+ (Vincent Ladeuil, #884270)
+
Improvements
************
More information about the bazaar-commits
mailing list