Rev 6322: (vila) Smarter .po file merging with merge hook as a plugin: po_merge. in file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/

Patch Queue Manager pqm at pqm.ubuntu.com
Tue Nov 29 10:47:48 UTC 2011


At file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 6322 [merge]
revision-id: pqm at pqm.ubuntu.com-20111129104747-32qen0v8yxbxeoov
parent: pqm at pqm.ubuntu.com-20111129065054-1657z2i4r8ceauz4
parent: v.ladeuil+lp at free.fr-20111129102118-pfk6ju6eydbknpxm
committer: Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2011-11-29 10:47:47 +0000
message:
  (vila) Smarter .po file merging with merge hook as a plugin: po_merge.
   (Vincent Ladeuil)
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
=== modified file 'bzrlib/merge.py'
--- a/bzrlib/merge.py	2011-11-28 19:07:58 +0000
+++ b/bzrlib/merge.py	2011-11-29 10:47:47 +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:21:18 +0000
@@ -0,0 +1,113 @@
+# 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
+
+
+config.option_registry.register(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.
+
+The command is invoked at the root of the working tree so all paths are
+relative.
+'''))
+
+
+config.option_registry.register(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.'))
+
+
+config.option_registry.register(config.Option(
+        'po_merge.po_glob', default='*.po',
+        help='Glob matching all ``.po`` files in one of ``po_merge.po_dirs``.'))
+
+config.option_registry.register(config.Option(
+        'po_merge.pot_glob', default='*.pot',
+        help='Glob matching the ``.pot`` file in one of ``po_merge.po_dirs``.'))
+
+
+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:21:18 +0000
@@ -0,0 +1,141 @@
+# 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,
+    )
+""")
+
+
+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:47:47 +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