/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/msgeditor.py

Return mapping in revision_id_bzr_to_foreign() as required by the interface.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 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
 
 
18
 
"""Commit message editor support."""
19
 
 
20
 
import codecs
21
 
import errno
22
 
import os
23
 
from subprocess import call
24
 
import sys
25
 
 
26
 
from bzrlib import (
27
 
    config,
28
 
    osutils,
29
 
    trace,
30
 
    )
31
 
from bzrlib.errors import BzrError, BadCommitMessageEncoding
32
 
from bzrlib.hooks import HookPoint, Hooks
33
 
 
34
 
 
35
 
def _get_editor():
36
 
    """Return a sequence of possible editor binaries for the current platform"""
37
 
    try:
38
 
        yield os.environ["BZR_EDITOR"], '$BZR_EDITOR'
39
 
    except KeyError:
40
 
        pass
41
 
 
42
 
    e = config.GlobalConfig().get_editor()
43
 
    if e is not None:
44
 
        yield e, config.config_filename()
45
 
 
46
 
    for varname in 'VISUAL', 'EDITOR':
47
 
        if varname in os.environ:
48
 
            yield os.environ[varname], '$' + varname
49
 
 
50
 
    if sys.platform == 'win32':
51
 
        for editor in 'wordpad.exe', 'notepad.exe':
52
 
            yield editor, None
53
 
    else:
54
 
        for editor in ['/usr/bin/editor', 'vi', 'pico', 'nano', 'joe']:
55
 
            yield editor, None
56
 
 
57
 
 
58
 
def _run_editor(filename):
59
 
    """Try to execute an editor to edit the commit message."""
60
 
    for candidate, candidate_source in _get_editor():
61
 
        edargs = candidate.split(' ')
62
 
        try:
63
 
            ## mutter("trying editor: %r", (edargs +[filename]))
64
 
            x = call(edargs + [filename])
65
 
        except OSError, e:
66
 
            # We're searching for an editor, so catch safe errors and continue
67
 
            # errno 193 is ERROR_BAD_EXE_FORMAT on Windows. Python2.4 uses the
68
 
            # winerror for errno. Python2.5+ use errno ENOEXEC and set winerror
69
 
            # to 193. However, catching 193 here should be fine. Other
70
 
            # platforms aren't likely to have that high of an error. And even
71
 
            # if they do, it is still reasonable to fall back to the next
72
 
            # editor.
73
 
            if e.errno in (errno.ENOENT, errno.EACCES, errno.ENOEXEC, 193):
74
 
                if candidate_source is not None:
75
 
                    # We tried this editor because some user configuration (an
76
 
                    # environment variable or config file) said to try it.  Let
77
 
                    # the user know their configuration is broken.
78
 
                    trace.warning(
79
 
                        'Could not start editor "%s" (specified by %s): %s\n'
80
 
                        % (candidate, candidate_source, str(e)))
81
 
                continue
82
 
            raise
83
 
        if x == 0:
84
 
            return True
85
 
        elif x == 127:
86
 
            continue
87
 
        else:
88
 
            break
89
 
    raise BzrError("Could not start any editor.\nPlease specify one with:\n"
90
 
                   " - $BZR_EDITOR\n - editor=/some/path in %s\n"
91
 
                   " - $VISUAL\n - $EDITOR" % \
92
 
                    config.config_filename())
93
 
 
94
 
 
95
 
DEFAULT_IGNORE_LINE = "%(bar)s %(msg)s %(bar)s" % \
96
 
    { 'bar' : '-' * 14, 'msg' : 'This line and the following will be ignored' }
97
 
 
98
 
 
99
 
def edit_commit_message(infotext, ignoreline=DEFAULT_IGNORE_LINE,
100
 
                        start_message=None):
101
 
    """Let the user edit a commit message in a temp file.
102
 
 
103
 
    This is run if they don't give a message or
104
 
    message-containing file on the command line.
105
 
 
106
 
    :param infotext:    Text to be displayed at bottom of message
107
 
                        for the user's reference;
108
 
                        currently similar to 'bzr status'.
109
 
 
110
 
    :param ignoreline:  The separator to use above the infotext.
111
 
 
112
 
    :param start_message:   The text to place above the separator, if any.
113
 
                            This will not be removed from the message
114
 
                            after the user has edited it.
115
 
 
116
 
    :return:    commit message or None.
117
 
    """
118
 
 
119
 
    if not start_message is None:
120
 
        start_message = start_message.encode(osutils.get_user_encoding())
121
 
    infotext = infotext.encode(osutils.get_user_encoding(), 'replace')
122
 
    return edit_commit_message_encoded(infotext, ignoreline, start_message)
123
 
 
124
 
 
125
 
def edit_commit_message_encoded(infotext, ignoreline=DEFAULT_IGNORE_LINE,
126
 
                                start_message=None):
127
 
    """Let the user edit a commit message in a temp file.
128
 
 
129
 
    This is run if they don't give a message or
130
 
    message-containing file on the command line.
131
 
 
132
 
    :param infotext:    Text to be displayed at bottom of message
133
 
                        for the user's reference;
134
 
                        currently similar to 'bzr status'.
135
 
                        The string is already encoded
136
 
 
137
 
    :param ignoreline:  The separator to use above the infotext.
138
 
 
139
 
    :param start_message:   The text to place above the separator, if any.
140
 
                            This will not be removed from the message
141
 
                            after the user has edited it.
142
 
                            The string is already encoded
143
 
 
144
 
    :return:    commit message or None.
145
 
    """
146
 
    msgfilename = None
147
 
    try:
148
 
        msgfilename, hasinfo = _create_temp_file_with_commit_template(
149
 
                                    infotext, ignoreline, start_message)
150
 
 
151
 
        if not msgfilename or not _run_editor(msgfilename):
152
 
            return None
153
 
 
154
 
        started = False
155
 
        msg = []
156
 
        lastline, nlines = 0, 0
157
 
        # codecs.open() ALWAYS opens file in binary mode but we need text mode
158
 
        # 'rU' mode useful when bzr.exe used on Cygwin (bialix 20070430)
159
 
        f = file(msgfilename, 'rU')
160
 
        try:
161
 
            try:
162
 
                for line in codecs.getreader(osutils.get_user_encoding())(f):
163
 
                    stripped_line = line.strip()
164
 
                    # strip empty line before the log message starts
165
 
                    if not started:
166
 
                        if stripped_line != "":
167
 
                            started = True
168
 
                        else:
169
 
                            continue
170
 
                    # check for the ignore line only if there
171
 
                    # is additional information at the end
172
 
                    if hasinfo and stripped_line == ignoreline:
173
 
                        break
174
 
                    nlines += 1
175
 
                    # keep track of the last line that had some content
176
 
                    if stripped_line != "":
177
 
                        lastline = nlines
178
 
                    msg.append(line)
179
 
            except UnicodeDecodeError:
180
 
                raise BadCommitMessageEncoding()
181
 
        finally:
182
 
            f.close()
183
 
 
184
 
        if len(msg) == 0:
185
 
            return ""
186
 
        # delete empty lines at the end
187
 
        del msg[lastline:]
188
 
        # add a newline at the end, if needed
189
 
        if not msg[-1].endswith("\n"):
190
 
            return "%s%s" % ("".join(msg), "\n")
191
 
        else:
192
 
            return "".join(msg)
193
 
    finally:
194
 
        # delete the msg file in any case
195
 
        if msgfilename is not None:
196
 
            try:
197
 
                os.unlink(msgfilename)
198
 
            except IOError, e:
199
 
                trace.warning(
200
 
                    "failed to unlink %s: %s; ignored", msgfilename, e)
201
 
 
202
 
 
203
 
def _create_temp_file_with_commit_template(infotext,
204
 
                                           ignoreline=DEFAULT_IGNORE_LINE,
205
 
                                           start_message=None):
206
 
    """Create temp file and write commit template in it.
207
 
 
208
 
    :param infotext:    Text to be displayed at bottom of message
209
 
                        for the user's reference;
210
 
                        currently similar to 'bzr status'.
211
 
                        The text is already encoded.
212
 
 
213
 
    :param ignoreline:  The separator to use above the infotext.
214
 
 
215
 
    :param start_message:   The text to place above the separator, if any.
216
 
                            This will not be removed from the message
217
 
                            after the user has edited it.
218
 
                            The string is already encoded
219
 
 
220
 
    :return:    2-tuple (temp file name, hasinfo)
221
 
    """
222
 
    import tempfile
223
 
    tmp_fileno, msgfilename = tempfile.mkstemp(prefix='bzr_log.',
224
 
                                               dir='.',
225
 
                                               text=True)
226
 
    msgfilename = osutils.basename(msgfilename)
227
 
    msgfile = os.fdopen(tmp_fileno, 'w')
228
 
    try:
229
 
        if start_message is not None:
230
 
            msgfile.write("%s\n" % start_message)
231
 
 
232
 
        if infotext is not None and infotext != "":
233
 
            hasinfo = True
234
 
            msgfile.write("\n\n%s\n\n%s" %(ignoreline, infotext))
235
 
        else:
236
 
            hasinfo = False
237
 
    finally:
238
 
        msgfile.close()
239
 
 
240
 
    return (msgfilename, hasinfo)
241
 
 
242
 
 
243
 
def make_commit_message_template(working_tree, specific_files):
244
 
    """Prepare a template file for a commit into a branch.
245
 
 
246
 
    Returns a unicode string containing the template.
247
 
    """
248
 
    # TODO: make provision for this to be overridden or modified by a hook
249
 
    #
250
 
    # TODO: Rather than running the status command, should prepare a draft of
251
 
    # the revision to be committed, then pause and ask the user to
252
 
    # confirm/write a message.
253
 
    from StringIO import StringIO       # must be unicode-safe
254
 
    from bzrlib.status import show_tree_status
255
 
    status_tmp = StringIO()
256
 
    show_tree_status(working_tree, specific_files=specific_files,
257
 
                     to_file=status_tmp, verbose=True)
258
 
    return status_tmp.getvalue()
259
 
 
260
 
 
261
 
def make_commit_message_template_encoded(working_tree, specific_files,
262
 
                                         diff=None, output_encoding='utf-8'):
263
 
    """Prepare a template file for a commit into a branch.
264
 
 
265
 
    Returns an encoded string.
266
 
    """
267
 
    # TODO: make provision for this to be overridden or modified by a hook
268
 
    #
269
 
    # TODO: Rather than running the status command, should prepare a draft of
270
 
    # the revision to be committed, then pause and ask the user to
271
 
    # confirm/write a message.
272
 
    from StringIO import StringIO       # must be unicode-safe
273
 
    from bzrlib.diff import show_diff_trees
274
 
 
275
 
    template = make_commit_message_template(working_tree, specific_files)
276
 
    template = template.encode(output_encoding, "replace")
277
 
 
278
 
    if diff:
279
 
        stream = StringIO()
280
 
        show_diff_trees(working_tree.basis_tree(),
281
 
                        working_tree, stream, specific_files,
282
 
                        path_encoding=output_encoding)
283
 
        template = template + '\n' + stream.getvalue()
284
 
 
285
 
    return template
286
 
 
287
 
 
288
 
class MessageEditorHooks(Hooks):
289
 
    """A dictionary mapping hook name to a list of callables for message editor
290
 
    hooks.
291
 
 
292
 
    e.g. ['commit_message_template'] is the list of items to be called to
293
 
    generate a commit message template
294
 
    """
295
 
 
296
 
    def __init__(self):
297
 
        """Create the default hooks.
298
 
 
299
 
        These are all empty initially.
300
 
        """
301
 
        Hooks.__init__(self)
302
 
        self.create_hook(HookPoint('commit_message_template',
303
 
            "Called when a commit message is being generated. "
304
 
            "commit_message_template is called with the bzrlib.commit.Commit "
305
 
            "object and the message that is known so far. "
306
 
            "commit_message_template must return a new message to use (which "
307
 
            "could be the same as it was given. When there are multiple "
308
 
            "hooks registered for commit_message_template, they are chained "
309
 
            "with the result from the first passed into the second, and so "
310
 
            "on.", (1, 10), None))
311
 
 
312
 
 
313
 
hooks = MessageEditorHooks()
314
 
 
315
 
 
316
 
def generate_commit_message_template(commit, start_message=None):
317
 
    """Generate a commit message template.
318
 
 
319
 
    :param commit: Commit object for the active commit.
320
 
    :param start_message: Message to start with.
321
 
    :return: A start commit message or None for an empty start commit message.
322
 
    """
323
 
    start_message = None
324
 
    for hook in hooks['commit_message_template']:
325
 
        start_message = hook(commit, start_message)
326
 
    return start_message