1
# Copyright (C) 2005-2011 Canonical Ltd
1
# Bazaar-NG -- distributed version control
3
# Copyright (C) 2005 by Canonical Ltd
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.
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.
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
17
20
"""Commit message editor support."""
19
from __future__ import absolute_import
23
23
from subprocess import call
35
from .errors import BzrError
36
from .hooks import Hooks
43
class BadCommitMessageEncoding(BzrError):
45
_fmt = 'The specified commit message contains characters unsupported by '\
46
'the current encoding.'
25
import bzrlib.config as config
26
from bzrlib.errors import BzrError
50
"""Return sequence of possible editor binaries for the current platform"""
29
"""Return a sequence of possible editor binaries for the current platform"""
52
yield os.environ["BRZ_EDITOR"], '$BRZ_EDITOR'
31
yield os.environ["BZR_EDITOR"]
56
e = config.GlobalStack().get('editor')
35
e = config.GlobalConfig().get_editor()
58
yield e, bedding.config_path()
60
for varname in 'VISUAL', 'EDITOR':
61
if varname in os.environ:
62
yield os.environ[varname], '$' + varname
64
if sys.platform == 'win32':
65
for editor in 'wordpad.exe', 'notepad.exe':
68
for editor in ['/usr/bin/editor', 'vi', 'pico', 'nano', 'joe']:
40
yield os.environ["EDITOR"]
46
elif os.name == "posix":
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)
77
x = call(edargs + [filename])
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.
84
'Could not start editor "%s" (specified by %s): %s\n'
85
% (candidate, candidate_source, str(e)))
52
for e in _get_editor():
54
x = call(edargs + [filename])
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())
100
DEFAULT_IGNORE_LINE = "%(bar)s %(msg)s %(bar)s" % \
101
{'bar': '-' * 14, 'msg': 'This line and the following will be ignored'}
104
def edit_commit_message(infotext, ignoreline=DEFAULT_IGNORE_LINE,
106
"""Let the user edit a commit message in a temp file.
108
This is run if they don't give a message or
109
message-containing file on the command line.
111
:param infotext: Text to be displayed at bottom of message
112
for the user's reference;
113
currently similar to 'bzr status'.
115
:param ignoreline: The separator to use above the infotext.
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.
121
:return: commit message or None.
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)
130
def edit_commit_message_encoded(infotext, ignoreline=DEFAULT_IGNORE_LINE,
132
"""Let the user edit a commit message in a temp file.
134
This is run if they don't give a message or
135
message-containing file on the command line.
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
142
:param ignoreline: The separator to use above the infotext.
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
149
:return: commit message or None.
61
raise BzrError("Could not start any editor. "
62
"Please specify $EDITOR or use ~/.bzr.conf/editor")
65
def edit_commit_message(infotext, ignoreline=None):
66
"""Let the user edit a commit message in a temp file.
68
This is run if they don't give a message or
69
message-containing file on the command line.
72
Text to be displayed at bottom of message for
73
the user's reference; currently similar to
78
if ignoreline is None:
79
ignoreline = "-- This line and the following will be ignored --"
153
msgfilename, hasinfo = _create_temp_file_with_commit_template(
154
infotext, ignoreline, start_message)
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 != "":
86
msgfile = file(msgfilename, "w")
87
msgfile.write("\n\n%s\n\n%s" % (ignoreline, infotext))
161
92
if not _run_editor(msgfilename):
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",
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.
175
97
lastline, nlines = 0, 0
176
with codecs.open(msgfilename, mode='rb', encoding=osutils.get_user_encoding()) as f:
179
stripped_line = line.strip()
180
# strip empty line before the log message starts
182
if stripped_line != "":
186
# check for the ignore line only if there
187
# is additional information at the end
188
if hasinfo and stripped_line == ignoreline:
191
# keep track of the last line that had some content
192
if stripped_line != "":
195
except UnicodeDecodeError:
196
raise BadCommitMessageEncoding()
98
for line in file(msgfilename, "r"):
99
stripped_line = line.strip()
100
# strip empty line before the log message starts
102
if stripped_line != "":
106
# check for the ignore line only if there
107
# is additional information at the end
108
if hasinfo and stripped_line == ignoreline:
111
# keep track of the last line that had some content
112
if stripped_line != "":
198
116
if len(msg) == 0:
200
118
# delete empty lines at the end
206
124
return "".join(msg)
208
126
# delete the msg file in any case
209
if msgfilename is not None:
211
os.unlink(msgfilename)
214
"failed to unlink %s: %s; ignored", msgfilename, e)
217
def _create_temp_file_with_commit_template(infotext,
218
ignoreline=DEFAULT_IGNORE_LINE,
221
"""Create temp file and write commit template in it.
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
227
:param ignoreline: The separator to use above the infotext.
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
233
:return: 2-tuple (temp file name, hasinfo)
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)
242
if infotext is not None and infotext != "":
244
trailer = b"\n\n%s\n\n%s" % (
245
ignoreline.encode(osutils.get_user_encoding()), infotext)
246
msgfile.write(trailer)
250
return (msgfilename, hasinfo)
253
def make_commit_message_template(working_tree, specific_files):
254
"""Prepare a template file for a commit into a branch.
256
Returns a unicode string containing the template.
258
# TODO: make provision for this to be overridden or modified by a hook
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()
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.
274
Returns an encoded string.
276
# TODO: make provision for this to be overridden or modified by a hook
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
283
template = make_commit_message_template(working_tree, specific_files)
284
template = template.encode(output_encoding, "replace")
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()
296
class MessageEditorHooks(Hooks):
297
"""A dictionary mapping hook name to a list of callables for message editor
300
e.g. ['commit_message_template'] is the list of items to be called to
301
generate a commit message template
305
"""Create the default hooks.
307
These are all empty initially.
309
Hooks.__init__(self, "breezy.msgeditor", "hooks")
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.",
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 "
331
hooks = MessageEditorHooks()
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
340
for hook in hooks['set_commit_message']:
341
start_message = hook(commit, start_message)
345
def generate_commit_message_template(commit, start_message=None):
346
"""Generate a commit message template.
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.
353
for hook in hooks['commit_message_template']:
354
start_message = hook(commit, start_message)
127
try: os.unlink(msgfilename)