1
# Copyright (C) 2005, 2006 Canonical Ltd
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Commit message editor support."""
23
from subprocess import call
30
from bzrlib.errors import BzrError, BadCommitMessageEncoding
31
from bzrlib.hooks import Hooks
32
from bzrlib.trace import warning, mutter
36
"""Return a sequence of possible editor binaries for the current platform"""
38
yield os.environ["BZR_EDITOR"]
42
e = config.GlobalConfig().get_editor()
46
for varname in 'VISUAL', 'EDITOR':
47
if varname in os.environ:
48
yield os.environ[varname]
50
if sys.platform == 'win32':
51
for editor in 'wordpad.exe', 'notepad.exe':
54
for editor in ['/usr/bin/editor', 'vi', 'pico', 'nano', 'joe']:
58
def _run_editor(filename):
59
"""Try to execute an editor to edit the commit message."""
60
for e in _get_editor():
63
## mutter("trying editor: %r", (edargs +[filename]))
64
x = call(edargs + [filename])
66
# We're searching for an editor, so catch safe errors and continue
67
if e.errno in (errno.ENOENT, ):
76
raise BzrError("Could not start any editor.\nPlease specify one with:\n"
77
" - $BZR_EDITOR\n - editor=/some/path in %s\n"
78
" - $VISUAL\n - $EDITOR" % \
79
config.config_filename())
82
DEFAULT_IGNORE_LINE = "%(bar)s %(msg)s %(bar)s" % \
83
{ 'bar' : '-' * 14, 'msg' : 'This line and the following will be ignored' }
86
def edit_commit_message(infotext, ignoreline=DEFAULT_IGNORE_LINE,
88
"""Let the user edit a commit message in a temp file.
90
This is run if they don't give a message or
91
message-containing file on the command line.
93
:param infotext: Text to be displayed at bottom of message
94
for the user's reference;
95
currently similar to 'bzr status'.
97
:param ignoreline: The separator to use above the infotext.
99
:param start_message: The text to place above the separator, if any.
100
This will not be removed from the message
101
after the user has edited it.
103
:return: commit message or None.
106
if not start_message is None:
107
start_message = start_message.encode(osutils.get_user_encoding())
108
infotext = infotext.encode(osutils.get_user_encoding(), 'replace')
109
return edit_commit_message_encoded(infotext, ignoreline, start_message)
112
def edit_commit_message_encoded(infotext, ignoreline=DEFAULT_IGNORE_LINE,
114
"""Let the user edit a commit message in a temp file.
116
This is run if they don't give a message or
117
message-containing file on the command line.
119
:param infotext: Text to be displayed at bottom of message
120
for the user's reference;
121
currently similar to 'bzr status'.
122
The string is already encoded
124
:param ignoreline: The separator to use above the infotext.
126
:param start_message: The text to place above the separator, if any.
127
This will not be removed from the message
128
after the user has edited it.
129
The string is already encoded
131
:return: commit message or None.
135
msgfilename, hasinfo = _create_temp_file_with_commit_template(
136
infotext, ignoreline, start_message)
138
if not msgfilename or not _run_editor(msgfilename):
143
lastline, nlines = 0, 0
144
# codecs.open() ALWAYS opens file in binary mode but we need text mode
145
# 'rU' mode useful when bzr.exe used on Cygwin (bialix 20070430)
146
f = file(msgfilename, 'rU')
149
for line in codecs.getreader(osutils.get_user_encoding())(f):
150
stripped_line = line.strip()
151
# strip empty line before the log message starts
153
if stripped_line != "":
157
# check for the ignore line only if there
158
# is additional information at the end
159
if hasinfo and stripped_line == ignoreline:
162
# keep track of the last line that had some content
163
if stripped_line != "":
166
except UnicodeDecodeError:
167
raise BadCommitMessageEncoding()
173
# delete empty lines at the end
175
# add a newline at the end, if needed
176
if not msg[-1].endswith("\n"):
177
return "%s%s" % ("".join(msg), "\n")
181
# delete the msg file in any case
182
if msgfilename is not None:
184
os.unlink(msgfilename)
186
warning("failed to unlink %s: %s; ignored", msgfilename, e)
189
def _create_temp_file_with_commit_template(infotext,
190
ignoreline=DEFAULT_IGNORE_LINE,
192
"""Create temp file and write commit template in it.
194
:param infotext: Text to be displayed at bottom of message
195
for the user's reference;
196
currently similar to 'bzr status'.
197
The text is already encoded.
199
:param ignoreline: The separator to use above the infotext.
201
:param start_message: The text to place above the separator, if any.
202
This will not be removed from the message
203
after the user has edited it.
204
The string is already encoded
206
:return: 2-tuple (temp file name, hasinfo)
209
tmp_fileno, msgfilename = tempfile.mkstemp(prefix='bzr_log.',
212
msgfilename = osutils.basename(msgfilename)
213
msgfile = os.fdopen(tmp_fileno, 'w')
215
if start_message is not None:
216
msgfile.write("%s\n" % start_message)
218
if infotext is not None and infotext != "":
220
msgfile.write("\n\n%s\n\n%s" %(ignoreline, infotext))
226
return (msgfilename, hasinfo)
229
def make_commit_message_template(working_tree, specific_files):
230
"""Prepare a template file for a commit into a branch.
232
Returns a unicode string containing the template.
234
# TODO: make provision for this to be overridden or modified by a hook
236
# TODO: Rather than running the status command, should prepare a draft of
237
# the revision to be committed, then pause and ask the user to
238
# confirm/write a message.
239
from StringIO import StringIO # must be unicode-safe
240
from bzrlib.status import show_tree_status
241
status_tmp = StringIO()
242
show_tree_status(working_tree, specific_files=specific_files,
244
return status_tmp.getvalue()
247
def make_commit_message_template_encoded(working_tree, specific_files,
248
diff=None, output_encoding='utf-8'):
249
"""Prepare a template file for a commit into a branch.
251
Returns an encoded string.
253
# TODO: make provision for this to be overridden or modified by a hook
255
# TODO: Rather than running the status command, should prepare a draft of
256
# the revision to be committed, then pause and ask the user to
257
# confirm/write a message.
258
from StringIO import StringIO # must be unicode-safe
259
from bzrlib.diff import show_diff_trees
261
template = make_commit_message_template(working_tree, specific_files)
262
template = template.encode(output_encoding, "replace")
266
show_diff_trees(working_tree.basis_tree(),
267
working_tree, stream, specific_files,
268
path_encoding=output_encoding)
269
template = template + '\n' + stream.getvalue()
274
class MessageEditorHooks(Hooks):
275
"""A dictionary mapping hook name to a list of callables for message editor
278
e.g. ['commit_message_template'] is the list of items to be called to
279
generate a commit message template
283
"""Create the default hooks.
285
These are all empty initially.
288
# Introduced in 1.10:
289
# Invoked to generate the commit message template shown in the editor
290
# The api signature is:
291
# (commit, message), and the function should return the new message
292
# There is currently no way to modify the order in which
293
# template hooks are invoked
294
self['commit_message_template'] = []
297
hooks = MessageEditorHooks()
300
def generate_commit_message_template(commit, start_message=None):
301
"""Generate a commit message template.
303
:param commit: Commit object for the active commit.
304
:param start_message: Message to start with.
305
:return: A start commit message or None for an empty start commit message.
308
for hook in hooks['commit_message_template']:
309
start_message = hook(commit, start_message)