/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

  • Committer: John Arbash Meinel
  • Date: 2006-04-25 15:05:42 UTC
  • mfrom: (1185.85.85 bzr-encoding)
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060425150542-c7b518dca9928691
[merge] the old bzr-encoding changes, reparenting them on bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
2
 
#
 
1
# Bazaar-NG -- distributed version control
 
2
 
 
3
# Copyright (C) 2005 by Canonical Ltd
 
4
 
3
5
# This program is free software; you can redistribute it and/or modify
4
6
# it under the terms of the GNU General Public License as published by
5
7
# the Free Software Foundation; either version 2 of the License, or
6
8
# (at your option) any later version.
7
 
#
 
9
 
8
10
# This program is distributed in the hope that it will be useful,
9
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
13
# GNU General Public License for more details.
12
 
#
 
14
 
13
15
# You should have received a copy of the GNU General Public License
14
16
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
16
19
 
17
20
"""Commit message editor support."""
18
21
 
19
 
from __future__ import absolute_import
20
 
 
21
22
import codecs
22
23
import os
 
24
import errno
23
25
from subprocess import call
24
 
import sys
25
 
 
26
 
from . import (
27
 
    bedding,
28
 
    cmdline,
29
 
    config,
30
 
    osutils,
31
 
    trace,
32
 
    transport,
33
 
    ui,
34
 
    )
35
 
from .errors import BzrError
36
 
from .hooks import Hooks
37
 
from .sixish import (
38
 
    BytesIO,
39
 
    StringIO,
40
 
    )
41
 
 
42
 
 
43
 
class BadCommitMessageEncoding(BzrError):
44
 
 
45
 
    _fmt = 'The specified commit message contains characters unsupported by '\
46
 
        'the current encoding.'
47
 
 
 
26
 
 
27
import bzrlib
 
28
import bzrlib.config as config
 
29
from bzrlib.errors import BzrError
48
30
 
49
31
def _get_editor():
50
 
    """Return sequence of possible editor binaries for the current platform"""
 
32
    """Return a sequence of possible editor binaries for the current platform"""
51
33
    try:
52
 
        yield os.environ["BRZ_EDITOR"], '$BRZ_EDITOR'
 
34
        yield os.environ["BZR_EDITOR"]
53
35
    except KeyError:
54
36
        pass
55
37
 
56
 
    e = config.GlobalStack().get('editor')
 
38
    e = config.GlobalConfig().get_editor()
57
39
    if e is not None:
58
 
        yield e, bedding.config_path()
59
 
 
60
 
    for varname in 'VISUAL', 'EDITOR':
61
 
        if varname in os.environ:
62
 
            yield os.environ[varname], '$' + varname
63
 
 
64
 
    if sys.platform == 'win32':
65
 
        for editor in 'wordpad.exe', 'notepad.exe':
66
 
            yield editor, None
67
 
    else:
68
 
        for editor in ['/usr/bin/editor', 'vi', 'pico', 'nano', 'joe']:
69
 
            yield editor, None
 
40
        yield e
 
41
        
 
42
    try:
 
43
        yield os.environ["EDITOR"]
 
44
    except KeyError:
 
45
        pass
 
46
 
 
47
    if os.name == "nt":
 
48
        yield "notepad.exe"
 
49
    elif os.name == "posix":
 
50
        yield "/usr/bin/vi"
70
51
 
71
52
 
72
53
def _run_editor(filename):
73
54
    """Try to execute an editor to edit the commit message."""
74
 
    for candidate, candidate_source in _get_editor():
75
 
        edargs = cmdline.split(candidate)
 
55
    for e in _get_editor():
 
56
        edargs = e.split(' ')
76
57
        try:
77
58
            x = call(edargs + [filename])
78
 
        except OSError as e:
79
 
            if candidate_source is not None:
80
 
                # We tried this editor because some user configuration (an
81
 
                # environment variable or config file) said to try it.  Let
82
 
                # the user know their configuration is broken.
83
 
                trace.warning(
84
 
                    'Could not start editor "%s" (specified by %s): %s\n'
85
 
                    % (candidate, candidate_source, str(e)))
86
 
            continue
87
 
            raise
 
59
        except OSError, e:
 
60
           # ENOENT means no such editor
 
61
           if e.errno == errno.ENOENT:
 
62
               continue
 
63
           raise
88
64
        if x == 0:
89
65
            return True
90
66
        elif x == 127:
91
67
            continue
92
68
        else:
93
69
            break
94
 
    raise BzrError("Could not start any editor.\nPlease specify one with:\n"
95
 
                   " - $BRZ_EDITOR\n - editor=/some/path in %s\n"
96
 
                   " - $VISUAL\n - $EDITOR" %
97
 
                   bedding.config_path())
 
70
    raise BzrError("Could not start any editor. "
 
71
                   "Please specify $EDITOR or use ~/.bzr.conf/editor")
98
72
 
99
73
 
100
74
DEFAULT_IGNORE_LINE = "%(bar)s %(msg)s %(bar)s" % \
101
 
    {'bar': '-' * 14, 'msg': 'This line and the following will be ignored'}
102
 
 
103
 
 
104
 
def edit_commit_message(infotext, ignoreline=DEFAULT_IGNORE_LINE,
105
 
                        start_message=None):
106
 
    """Let the user edit a commit message in a temp file.
107
 
 
108
 
    This is run if they don't give a message or
109
 
    message-containing file on the command line.
110
 
 
111
 
    :param infotext:    Text to be displayed at bottom of message
112
 
                        for the user's reference;
113
 
                        currently similar to 'bzr status'.
114
 
 
115
 
    :param ignoreline:  The separator to use above the infotext.
116
 
 
117
 
    :param start_message:   The text to place above the separator, if any.
118
 
                            This will not be removed from the message
119
 
                            after the user has edited it.
120
 
 
121
 
    :return:    commit message or None.
122
 
    """
123
 
 
124
 
    if start_message is not None:
125
 
        start_message = start_message.encode(osutils.get_user_encoding())
126
 
    infotext = infotext.encode(osutils.get_user_encoding(), 'replace')
127
 
    return edit_commit_message_encoded(infotext, ignoreline, start_message)
128
 
 
129
 
 
130
 
def edit_commit_message_encoded(infotext, ignoreline=DEFAULT_IGNORE_LINE,
131
 
                                start_message=None):
132
 
    """Let the user edit a commit message in a temp file.
133
 
 
134
 
    This is run if they don't give a message or
135
 
    message-containing file on the command line.
136
 
 
137
 
    :param infotext:    Text to be displayed at bottom of message
138
 
                        for the user's reference;
139
 
                        currently similar to 'bzr status'.
140
 
                        The string is already encoded
141
 
 
142
 
    :param ignoreline:  The separator to use above the infotext.
143
 
 
144
 
    :param start_message:   The text to place above the separator, if any.
145
 
                            This will not be removed from the message
146
 
                            after the user has edited it.
147
 
                            The string is already encoded
148
 
 
149
 
    :return:    commit message or None.
150
 
    """
151
 
    msgfilename = None
 
75
    { 'bar' : '-' * 14, 'msg' : 'This line and the following will be ignored' }
 
76
 
 
77
 
 
78
def edit_commit_message(infotext, ignoreline=DEFAULT_IGNORE_LINE):
 
79
    """Let the user edit a commit message in a temp file.
 
80
 
 
81
    This is run if they don't give a message or
 
82
    message-containing file on the command line.
 
83
 
 
84
    infotext:
 
85
        Text to be displayed at bottom of message for
 
86
        the user's reference; currently similar to
 
87
        'bzr status'.
 
88
    """
 
89
    import tempfile
 
90
 
152
91
    try:
153
 
        msgfilename, hasinfo = _create_temp_file_with_commit_template(
154
 
            infotext, ignoreline, start_message)
155
 
        if not msgfilename:
156
 
            return None
157
 
        basename = osutils.basename(msgfilename)
158
 
        msg_transport = transport.get_transport_from_path(
159
 
            osutils.dirname(msgfilename))
160
 
        reference_content = msg_transport.get_bytes(basename)
 
92
        tmp_fileno, msgfilename = tempfile.mkstemp(prefix='bzr_log.', dir=u'.')
 
93
        msgfile = os.close(tmp_fileno)
 
94
        if infotext is not None and infotext != "":
 
95
            hasinfo = True
 
96
            msgfile = file(msgfilename, "w")
 
97
            msgfile.write("\n\n%s\n\n%s" % (ignoreline,
 
98
                infotext.encode(bzrlib.user_encoding, 'replace')))
 
99
            msgfile.close()
 
100
        else:
 
101
            hasinfo = False
 
102
 
161
103
        if not _run_editor(msgfilename):
162
104
            return None
163
 
        edited_content = msg_transport.get_bytes(basename)
164
 
        if edited_content == reference_content:
165
 
            if not ui.ui_factory.confirm_action(
166
 
                u"Commit message was not edited, use anyway",
167
 
                "breezy.msgeditor.unchanged",
168
 
                    {}):
169
 
                # Returning "" makes cmd_commit raise 'empty commit message
170
 
                # specified' which is a reasonable error, given the user has
171
 
                # rejected using the unedited template.
172
 
                return ""
 
105
        
173
106
        started = False
174
107
        msg = []
175
108
        lastline, nlines = 0, 0
176
 
        with codecs.open(msgfilename, mode='rb', encoding=osutils.get_user_encoding()) as f:
177
 
            try:
178
 
                for line in f:
179
 
                    stripped_line = line.strip()
180
 
                    # strip empty line before the log message starts
181
 
                    if not started:
182
 
                        if stripped_line != "":
183
 
                            started = True
184
 
                        else:
185
 
                            continue
186
 
                    # check for the ignore line only if there
187
 
                    # is additional information at the end
188
 
                    if hasinfo and stripped_line == ignoreline:
189
 
                        break
190
 
                    nlines += 1
191
 
                    # keep track of the last line that had some content
192
 
                    if stripped_line != "":
193
 
                        lastline = nlines
194
 
                    msg.append(line)
195
 
            except UnicodeDecodeError:
196
 
                raise BadCommitMessageEncoding()
197
 
 
 
109
        for line in codecs.open(msgfilename, 'r', bzrlib.user_encoding):
 
110
            stripped_line = line.strip()
 
111
            # strip empty line before the log message starts
 
112
            if not started:
 
113
                if stripped_line != "":
 
114
                    started = True
 
115
                else:
 
116
                    continue
 
117
            # check for the ignore line only if there
 
118
            # is additional information at the end
 
119
            if hasinfo and stripped_line == ignoreline:
 
120
                break
 
121
            nlines += 1
 
122
            # keep track of the last line that had some content
 
123
            if stripped_line != "":
 
124
                lastline = nlines
 
125
            msg.append(line)
 
126
            
198
127
        if len(msg) == 0:
199
128
            return ""
200
129
        # delete empty lines at the end
206
135
            return "".join(msg)
207
136
    finally:
208
137
        # delete the msg file in any case
209
 
        if msgfilename is not None:
210
 
            try:
211
 
                os.unlink(msgfilename)
212
 
            except IOError as e:
213
 
                trace.warning(
214
 
                    "failed to unlink %s: %s; ignored", msgfilename, e)
215
 
 
216
 
 
217
 
def _create_temp_file_with_commit_template(infotext,
218
 
                                           ignoreline=DEFAULT_IGNORE_LINE,
219
 
                                           start_message=None,
220
 
                                           tmpdir=None):
221
 
    """Create temp file and write commit template in it.
222
 
 
223
 
    :param infotext: Text to be displayed at bottom of message for the
224
 
        user's reference; currently similar to 'bzr status'.  The text is
225
 
        already encoded.
226
 
 
227
 
    :param ignoreline:  The separator to use above the infotext.
228
 
 
229
 
    :param start_message: The text to place above the separator, if any.
230
 
        This will not be removed from the message after the user has edited
231
 
        it.  The string is already encoded
232
 
 
233
 
    :return:    2-tuple (temp file name, hasinfo)
234
 
    """
235
 
    import tempfile
236
 
    tmp_fileno, msgfilename = tempfile.mkstemp(prefix='bzr_log.',
237
 
                                               dir=tmpdir, text=True)
238
 
    with os.fdopen(tmp_fileno, 'wb') as msgfile:
239
 
        if start_message is not None:
240
 
            msgfile.write(b"%s\n" % start_message)
241
 
 
242
 
        if infotext is not None and infotext != "":
243
 
            hasinfo = True
244
 
            trailer = b"\n\n%s\n\n%s" % (
245
 
                ignoreline.encode(osutils.get_user_encoding()), infotext)
246
 
            msgfile.write(trailer)
247
 
        else:
248
 
            hasinfo = False
249
 
 
250
 
    return (msgfilename, hasinfo)
 
138
        try: os.unlink(msgfilename)
 
139
        except (IOError, OSError), e:
 
140
            if (not hasattr(e, 'errno')
 
141
                or e.errno not in (errno.ENOENT, errno.ENOTDIR,
 
142
                                   errno.EPERM, errno.EACCES)):
 
143
                raise
251
144
 
252
145
 
253
146
def make_commit_message_template(working_tree, specific_files):
255
148
 
256
149
    Returns a unicode string containing the template.
257
150
    """
 
151
    # TODO: Should probably be given the WorkingTree not the branch
 
152
    #
258
153
    # TODO: make provision for this to be overridden or modified by a hook
259
154
    #
260
155
    # TODO: Rather than running the status command, should prepare a draft of
261
156
    # the revision to be committed, then pause and ask the user to
262
157
    # confirm/write a message.
263
 
    from .status import show_tree_status
 
158
    from StringIO import StringIO       # must be unicode-safe
 
159
    from bzrlib.status import show_tree_status
264
160
    status_tmp = StringIO()
265
 
    show_tree_status(working_tree, specific_files=specific_files,
266
 
                     to_file=status_tmp, verbose=True)
 
161
    show_tree_status(working_tree, specific_files=specific_files, 
 
162
                     to_file=status_tmp)
267
163
    return status_tmp.getvalue()
268
 
 
269
 
 
270
 
def make_commit_message_template_encoded(working_tree, specific_files,
271
 
                                         diff=None, output_encoding='utf-8'):
272
 
    """Prepare a template file for a commit into a branch.
273
 
 
274
 
    Returns an encoded string.
275
 
    """
276
 
    # TODO: make provision for this to be overridden or modified by a hook
277
 
    #
278
 
    # TODO: Rather than running the status command, should prepare a draft of
279
 
    # the revision to be committed, then pause and ask the user to
280
 
    # confirm/write a message.
281
 
    from .diff import show_diff_trees
282
 
 
283
 
    template = make_commit_message_template(working_tree, specific_files)
284
 
    template = template.encode(output_encoding, "replace")
285
 
 
286
 
    if diff:
287
 
        stream = BytesIO()
288
 
        show_diff_trees(working_tree.basis_tree(),
289
 
                        working_tree, stream, specific_files,
290
 
                        path_encoding=output_encoding)
291
 
        template = template + b'\n' + stream.getvalue()
292
 
 
293
 
    return template
294
 
 
295
 
 
296
 
class MessageEditorHooks(Hooks):
297
 
    """A dictionary mapping hook name to a list of callables for message editor
298
 
    hooks.
299
 
 
300
 
    e.g. ['commit_message_template'] is the list of items to be called to
301
 
    generate a commit message template
302
 
    """
303
 
 
304
 
    def __init__(self):
305
 
        """Create the default hooks.
306
 
 
307
 
        These are all empty initially.
308
 
        """
309
 
        Hooks.__init__(self, "breezy.msgeditor", "hooks")
310
 
        self.add_hook(
311
 
            'set_commit_message',
312
 
            "Set a fixed commit message. "
313
 
            "set_commit_message is called with the "
314
 
            "breezy.commit.Commit object (so you can also change e.g. "
315
 
            "revision properties by editing commit.builder._revprops) and the "
316
 
            "message so far. set_commit_message must return the message to "
317
 
            "use or None if it should use the message editor as normal.",
318
 
            (2, 4))
319
 
        self.add_hook(
320
 
            'commit_message_template',
321
 
            "Called when a commit message is being generated. "
322
 
            "commit_message_template is called with the breezy.commit.Commit "
323
 
            "object and the message that is known so far. "
324
 
            "commit_message_template must return a new message to use (which "
325
 
            "could be the same as it was given). When there are multiple "
326
 
            "hooks registered for commit_message_template, they are chained "
327
 
            "with the result from the first passed into the second, and so "
328
 
            "on.", (1, 10))
329
 
 
330
 
 
331
 
hooks = MessageEditorHooks()
332
 
 
333
 
 
334
 
def set_commit_message(commit, start_message=None):
335
 
    """Sets the commit message.
336
 
    :param commit: Commit object for the active commit.
337
 
    :return: The commit message or None to continue using the message editor
338
 
    """
339
 
    start_message = None
340
 
    for hook in hooks['set_commit_message']:
341
 
        start_message = hook(commit, start_message)
342
 
    return start_message
343
 
 
344
 
 
345
 
def generate_commit_message_template(commit, start_message=None):
346
 
    """Generate a commit message template.
347
 
 
348
 
    :param commit: Commit object for the active commit.
349
 
    :param start_message: Message to start with.
350
 
    :return: A start commit message or None for an empty start commit message.
351
 
    """
352
 
    start_message = None
353
 
    for hook in hooks['commit_message_template']:
354
 
        start_message = hook(commit, start_message)
355
 
    return start_message