~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugins/po_merge/po_merge.py

  • Committer: Vincent Ladeuil
  • Date: 2011-11-24 10:47:43 UTC
  • mto: (6321.1.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 6322.
  • Revision ID: v.ladeuil+lp@free.fr-20111124104743-rxqwhmzqu5k17f24
First cut at a working plugin to avoid conflicts in .po files by shelling out to msgmerge.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Merge logic for po_merge plugin."""
 
18
 
 
19
 
 
20
from bzrlib import (
 
21
    config,
 
22
    merge,
 
23
    )
 
24
 
 
25
 
 
26
from bzrlib.lazy_import import lazy_import
 
27
lazy_import(globals(), """
 
28
import fnmatch
 
29
import subprocess
 
30
import tempfile
 
31
import sys
 
32
 
 
33
from bzrlib import (
 
34
    cmdline,
 
35
    osutils,
 
36
    trace,
 
37
    )
 
38
""")
 
39
 
 
40
 
 
41
config.option_registry.register(config.Option(
 
42
        'po_merge.command',
 
43
        default='msgmerge -N "{other}" "{pot_file}" -C "{this}" -o "{result}"',
 
44
        help='''\
 
45
Command used to create a conflict-free .po file during merge.
 
46
 
 
47
The following parameters are provided by the hook:
 
48
``this`` is the ``.po`` file content before the merge in the current branch,
 
49
``other`` is the ``.po`` file content in the branch merged from,
 
50
``pot_file`` is the path to the ``.pot`` file corresponding to the ``.po``
 
51
file being merged.
 
52
``result`` is the path where ``msgmerge`` will output its result. The hook will
 
53
use the content of this file to produce the resulting ``.po`` file.
 
54
 
 
55
The command is invoked at the root of the working tree so all paths are
 
56
relative.
 
57
'''))
 
58
 
 
59
 
 
60
config.option_registry.register(config.Option(
 
61
        'po_merge.po_files', default=[],
 
62
        from_unicode=config.list_from_store,
 
63
        help='List of globs the po_merge hook applies to.'))
 
64
 
 
65
 
 
66
config.option_registry.register(config.Option(
 
67
        'po_merge.pot_file', default=[],
 
68
        from_unicode=config.list_from_store,
 
69
        help='List of ``.pot`` filenames related to ``po_merge.po_files``.'))
 
70
 
 
71
 
 
72
class PoMerger(merge.PerFileMerger):
 
73
    """Merge .po files."""
 
74
 
 
75
    def __init__(self, merger):
 
76
        super(merge.PerFileMerger, self).__init__(merger)
 
77
        # config options are cached locally until config files are (see
 
78
        # http://pad.lv/832042)
 
79
 
 
80
        # FIXME: We use the branch config as there is no tree config
 
81
        # -- vila 2011-11-23
 
82
        self.conf = merger.this_branch.get_config_stack()
 
83
        # Which files are targeted by the hook 
 
84
        self.po_files = self.conf.get('po_merge.po_files')
 
85
        # Which .pot file should be used
 
86
        self.pot_file = self.conf.get('po_merge.pot_file')
 
87
        self.command = self.conf.get('po_merge.command', expand=False)
 
88
        # file_matches() will set the following for merge_text()
 
89
        self.selected_po_file = None
 
90
        self.selected_pot_file = None
 
91
 
 
92
    def file_matches(self, params):
 
93
        """Return True if merge_matching should be called on this file."""
 
94
        if not self.po_files or not self.pot_file:
 
95
            # Return early if there is no options defined
 
96
            return False
 
97
        match = False
 
98
        po_path = self.get_filepath(params, self.merger.this_tree)
 
99
        # Does the merged file match one of the globs
 
100
        for idx, glob in enumerate(self.po_files):
 
101
            if fnmatch.fnmatch(po_path, glob):
 
102
                match = True
 
103
                break
 
104
        if not match:
 
105
            return False
 
106
        # Do we have the corresponding .pot file
 
107
        try:
 
108
            pot_path = self.pot_file[idx]
 
109
        except KeyError:
 
110
            trace.note('po_merge.po_files and po_merge.pot_file mismatch'
 
111
                       ' for index %d' %d)
 
112
            return False
 
113
        if self.merger.this_tree.has_filename(pot_path):
 
114
            self.selected_pot_file = pot_path
 
115
            self.selected_po_file = po_path
 
116
            # FIXME: I can't find a way to know if the .pot file has conflicts
 
117
            # *during* the merge itself. So either the actual content on disk
 
118
            # is fine and msgmerge will work OR it's not and it will
 
119
            # fail. Conversely, either the result is ok for the user and he's
 
120
            # happy OR the user needs to resolve the conflicts in the .pot file
 
121
            # and use remerge. -- vila 2011-11-24
 
122
            return True
 
123
        return False
 
124
 
 
125
    def _invoke(self, command):
 
126
        proc = subprocess.Popen(cmdline.split(command),
 
127
                                # FIXME: cwd= ? -- vila 2011-11-24
 
128
                                stdout=subprocess.PIPE,
 
129
                                stderr=subprocess.PIPE,
 
130
                                stdin=subprocess.PIPE)
 
131
        out, err = proc.communicate()
 
132
        return proc.returncode, out, err
 
133
 
 
134
    def merge_matching(self, params):
 
135
        return self.merge_text(params)
 
136
 
 
137
    def merge_text(self, params):
 
138
        """Calls msgmerge when .po files conflict.
 
139
 
 
140
        This requires a valid .pot file to reconcile both sides.
 
141
        """
 
142
        # Create tmp files with the 'this' and 'other' content
 
143
        tmpdir = tempfile.mkdtemp(prefix='po_merge')
 
144
        env = {}
 
145
        env['this'] = osutils.pathjoin(tmpdir, 'this')
 
146
        env['other'] = osutils.pathjoin(tmpdir, 'other')
 
147
        env['result'] = osutils.pathjoin(tmpdir, 'result')
 
148
        env['pot_file'] = self.selected_pot_file
 
149
        try:
 
150
            with osutils.open_file(env['this'], 'wb') as f:
 
151
                f.writelines(params.this_lines)
 
152
            with osutils.open_file(env['other'], 'wb') as f:
 
153
                f.writelines(params.other_lines)
 
154
            command = self.conf.expand_options(self.command, env)
 
155
            retcode, out, err = self._invoke(command)
 
156
            with osutils.open_file(env['result']) as f:
 
157
                # FIXME: To avoid the list() construct below which means the
 
158
                # whole 'result' file is kept in memory, there may be a way to
 
159
                # use an iterator that will close the file when it's done, but
 
160
                # there is still the issue of removing the tmp dir...
 
161
                # -- vila 2011-11-24
 
162
                return 'success', list(f.readlines())
 
163
        finally:
 
164
            osutils.rmtree(tmpdir)
 
165
        return 'not applicable', []