/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: Robert Collins
  • Date: 2005-10-19 10:11:57 UTC
  • mfrom: (1185.16.78)
  • mto: This revision was merged to the branch mainline in revision 1470.
  • Revision ID: robertc@robertcollins.net-20051019101157-17438d311e746b4f
mergeĀ fromĀ upstream

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
 
import codecs
22
22
import os
23
23
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
 
 
 
24
 
 
25
import bzrlib.config as config
 
26
from bzrlib.errors import BzrError
48
27
 
49
28
def _get_editor():
50
 
    """Return sequence of possible editor binaries for the current platform"""
 
29
    """Return a sequence of possible editor binaries for the current platform"""
51
30
    try:
52
 
        yield os.environ["BRZ_EDITOR"], '$BRZ_EDITOR'
 
31
        yield os.environ["BZR_EDITOR"]
53
32
    except KeyError:
54
33
        pass
55
34
 
56
 
    e = config.GlobalStack().get('editor')
 
35
    e = config.GlobalConfig().get_editor()
57
36
    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
 
37
        yield e
 
38
        
 
39
    try:
 
40
        yield os.environ["EDITOR"]
 
41
    except KeyError:
 
42
        pass
 
43
 
 
44
    if os.name == "nt":
 
45
        yield "notepad.exe"
 
46
    elif os.name == "posix":
 
47
        yield "/usr/bin/vi"
70
48
 
71
49
 
72
50
def _run_editor(filename):
73
51
    """Try to execute an editor to edit the commit message."""
74
 
    for candidate, candidate_source in _get_editor():
75
 
        edargs = cmdline.split(candidate)
76
 
        try:
77
 
            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
 
52
    for e in _get_editor():
 
53
        edargs = e.split(' ')
 
54
        x = call(edargs + [filename])
88
55
        if x == 0:
89
56
            return True
90
57
        elif x == 127:
91
58
            continue
92
59
        else:
93
60
            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())
98
 
 
99
 
 
100
 
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
 
61
    raise BzrError("Could not start any editor. "
 
62
                   "Please specify $EDITOR or use ~/.bzr.conf/editor")
 
63
                          
 
64
 
 
65
def edit_commit_message(infotext, ignoreline=None):
 
66
    """Let the user edit a commit message in a temp file.
 
67
 
 
68
    This is run if they don't give a message or
 
69
    message-containing file on the command line.
 
70
 
 
71
    infotext:
 
72
        Text to be displayed at bottom of message for
 
73
        the user's reference; currently similar to
 
74
        'bzr status'.
 
75
    """
 
76
    import tempfile
 
77
    
 
78
    if ignoreline is None:
 
79
        ignoreline = "-- This line and the following will be ignored --"
 
80
        
152
81
    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)
 
82
        tmp_fileno, msgfilename = tempfile.mkstemp()
 
83
        msgfile = os.close(tmp_fileno)
 
84
        if infotext is not None and infotext != "":
 
85
            hasinfo = True
 
86
            msgfile = file(msgfilename, "w")
 
87
            msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext))
 
88
            msgfile.close()
 
89
        else:
 
90
            hasinfo = False
 
91
 
161
92
        if not _run_editor(msgfilename):
162
93
            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 ""
 
94
        
173
95
        started = False
174
96
        msg = []
175
97
        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
 
 
 
98
        for line in file(msgfilename, "r"):
 
99
            stripped_line = line.strip()
 
100
            # strip empty line before the log message starts
 
101
            if not started:
 
102
                if stripped_line != "":
 
103
                    started = True
 
104
                else:
 
105
                    continue
 
106
            # check for the ignore line only if there
 
107
            # is additional information at the end
 
108
            if hasinfo and stripped_line == ignoreline:
 
109
                break
 
110
            nlines += 1
 
111
            # keep track of the last line that had some content
 
112
            if stripped_line != "":
 
113
                lastline = nlines
 
114
            msg.append(line)
 
115
            
198
116
        if len(msg) == 0:
199
117
            return ""
200
118
        # delete empty lines at the end
206
124
            return "".join(msg)
207
125
    finally:
208
126
        # 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)
251
 
 
252
 
 
253
 
def make_commit_message_template(working_tree, specific_files):
254
 
    """Prepare a template file for a commit into a branch.
255
 
 
256
 
    Returns a unicode string containing the template.
257
 
    """
258
 
    # TODO: make provision for this to be overridden or modified by a hook
259
 
    #
260
 
    # TODO: Rather than running the status command, should prepare a draft of
261
 
    # the revision to be committed, then pause and ask the user to
262
 
    # confirm/write a message.
263
 
    from .status import show_tree_status
264
 
    status_tmp = StringIO()
265
 
    show_tree_status(working_tree, specific_files=specific_files,
266
 
                     to_file=status_tmp, verbose=True)
267
 
    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
 
127
        try: os.unlink(msgfilename)
 
128
        except IOError: pass
 
129