Merge from emacs-24; up to 2012-05-08T14:11:47Z!monnier@iro.umontreal.ca
[bpt/emacs.git] / lisp / vc / log-edit.el
CommitLineData
ba83908c 1;;; log-edit.el --- Major mode for editing CVS commit messages -*- lexical-binding: t -*-
5b467bf4 2
acaf905b 3;; Copyright (C) 1999-2012 Free Software Foundation, Inc.
5b467bf4 4
cc1eecfd 5;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
9766adfb 6;; Keywords: pcl-cvs cvs commit log vc
5b467bf4
SM
7
8;; This file is part of GNU Emacs.
9
eb3fa2cf 10;; GNU Emacs is free software: you can redistribute it and/or modify
5b467bf4 11;; it under the terms of the GNU General Public License as published by
eb3fa2cf
GM
12;; the Free Software Foundation, either version 3 of the License, or
13;; (at your option) any later version.
5b467bf4
SM
14
15;; GNU Emacs is distributed in the hope that it will be useful,
16;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;; GNU General Public License for more details.
19
20;; You should have received a copy of the GNU General Public License
eb3fa2cf 21;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
5b467bf4
SM
22
23;;; Commentary:
24
25;; Todo:
26
5b467bf4
SM
27;; - Move in VC's code
28;; - Add compatibility for VC's hook variables
5b467bf4
SM
29
30;;; Code:
31
5b467bf4
SM
32(require 'add-log) ; for all the ChangeLog goodies
33(require 'pcvs-util)
34(require 'ring)
5b467bf4 35
f1180544 36;;;;
5b467bf4 37;;;; Global Variables
f1180544 38;;;;
5b467bf4
SM
39
40(defgroup log-edit nil
bc35d341 41 "Major mode for editing RCS and CVS commit messages."
5b467bf4 42 :group 'pcl-cvs
bc35d341
DL
43 :group 'vc ; It's used by VC.
44 :version "21.1"
5b467bf4
SM
45 :prefix "log-edit-")
46
47;; compiler pacifiers
48(defvar cvs-buffer)
49
12dd83de
SM
50\f
51;; The main keymap
52
5b467bf4
SM
53(easy-mmode-defmap log-edit-mode-map
54 `(("\C-c\C-c" . log-edit-done)
55 ("\C-c\C-a" . log-edit-insert-changelog)
93a142e1 56 ("\C-c\C-d" . log-edit-show-diff)
5b467bf4 57 ("\C-c\C-f" . log-edit-show-files)
9fe89a26
SM
58 ("\M-n" . log-edit-next-comment)
59 ("\M-p" . log-edit-previous-comment)
60 ("\M-r" . log-edit-comment-search-backward)
61 ("\M-s" . log-edit-comment-search-forward)
0916e956 62 ("\C-c?" . log-edit-mode-help))
cdf54749 63 "Keymap for the `log-edit-mode' (to edit version control log messages)."
0916e956
SM
64 :group 'log-edit)
65
66;; Compatibility with old names. Should we bother ?
67(defvar vc-log-mode-map log-edit-mode-map)
68(defvar vc-log-entry-mode vc-log-mode-map)
5b467bf4 69
d247e32d
SM
70(easy-menu-define log-edit-menu log-edit-mode-map
71 "Menu used for `log-edit-mode'."
72 '("Log-Edit"
73 ["Done" log-edit-done
74 :help "Exit log-edit and proceed with the actual action."]
75 "--"
799224fe
DN
76 ["Insert ChangeLog" log-edit-insert-changelog
77 :help "Insert a log message by looking at the ChangeLog"]
78 ["Add to ChangeLog" log-edit-add-to-changelog
79 :help "Insert this log message into the appropriate ChangeLog file"]
d247e32d 80 "--"
93a142e1
DN
81 ["Show diff" log-edit-show-diff
82 :help "Show the diff for the files to be committed."]
d247e32d
SM
83 ["List files" log-edit-show-files
84 :help "Show the list of relevant files."]
85 "--"
799224fe
DN
86 ["Previous comment" log-edit-previous-comment
87 :help "Cycle backwards through comment history"]
88 ["Next comment" log-edit-next-comment
89 :help "Cycle forwards through comment history."]
90 ["Search comment forward" log-edit-comment-search-forward
91 :help "Search forwards through comment history for a substring match of str"]
92 ["Search comment backward" log-edit-comment-search-backward
93 :help "Search backwards through comment history for substring match of str"]))
d247e32d 94
f077c462 95(defcustom log-edit-confirm 'changed
9201cc28 96 "If non-nil, `log-edit-done' will request confirmation.
5b467bf4
SM
97If 'changed, only request confirmation if the list of files has
98 changed since the beginning of the log-edit session."
99 :group 'log-edit
100 :type '(choice (const changed) (const t) (const nil)))
101
102(defcustom log-edit-keep-buffer nil
9201cc28 103 "If non-nil, don't hide the buffer after `log-edit-done'."
5b467bf4
SM
104 :group 'log-edit
105 :type 'boolean)
106
0c765e5f 107(defcustom log-edit-require-final-newline t
9201cc28 108 "Enforce a newline at the end of commit log messages.
5b467bf4
SM
109Enforce it silently if t, query if non-nil and don't do anything if nil."
110 :group 'log-edit
111 :type '(choice (const ask) (const t) (const nil)))
112
113(defcustom log-edit-setup-invert nil
9201cc28 114 "Non-nil means `log-edit' should invert the meaning of its SETUP arg.
5b467bf4
SM
115If SETUP is 'force, this variable has no effect."
116 :group 'log-edit
117 :type 'boolean)
118
119(defcustom log-edit-hook '(log-edit-insert-cvs-template
e97a42c1 120 log-edit-show-files
5b467bf4 121 log-edit-insert-changelog)
9201cc28 122 "Hook run at the end of `log-edit'."
5b467bf4 123 :group 'log-edit
f7eeab0d
SM
124 :type '(hook :options (log-edit-insert-changelog
125 log-edit-insert-cvs-rcstemplate
126 log-edit-insert-cvs-template
127 log-edit-insert-filenames)))
5b467bf4 128
3831af62 129(defcustom log-edit-mode-hook (if (boundp 'vc-log-mode-hook) vc-log-mode-hook)
9201cc28 130 "Hook run when entering `log-edit-mode'."
5b467bf4
SM
131 :group 'log-edit
132 :type 'hook)
133
134(defcustom log-edit-done-hook nil
9201cc28 135 "Hook run before doing the actual commit.
5b467bf4
SM
136This hook can be used to cleanup the message, enforce various
137conventions, or to allow recording the message in some other database,
138such as a bug-tracking system. The list of files about to be committed
139can be obtained from `log-edit-files'."
140 :group 'log-edit
43a9a0c4 141 :type '(hook :options (log-edit-set-common-indentation
5b467bf4
SM
142 log-edit-add-to-changelog)))
143
bef4957b 144(defcustom log-edit-strip-single-file-name nil
31764e15 145 "If non-nil, remove file name from single-file log entries."
c6265c10
GM
146 :type 'boolean
147 :safe 'booleanp
148 :group 'log-edit
bef4957b 149 :version "24.1")
31764e15 150
0c765e5f 151(defvar log-edit-changelog-full-paragraphs t
fb7ada5f 152 "If non-nil, include full ChangeLog paragraphs in the log.
5b467bf4
SM
153This may be set in the ``local variables'' section of a ChangeLog, to
154indicate the policy for that ChangeLog.
155
156A ChangeLog paragraph is a bunch of log text containing no blank lines;
157a paragraph usually describes a set of changes with a single purpose,
158but perhaps spanning several functions in several files. Changes in
159different paragraphs are unrelated.
160
14188021 161You could argue that the log entry for a file should contain the
5b467bf4
SM
162full ChangeLog paragraph mentioning the change to the file, even though
163it may mention other files, because that gives you the full context you
5ab405e4 164need to understand the change. This is the behavior you get when this
5b467bf4
SM
165variable is set to t.
166
14188021 167On the other hand, you could argue that the log entry for a change
5b467bf4 168should contain only the text for the changes which occurred in that
5ab405e4 169file, because the log is per-file. This is the behavior you get
5b467bf4
SM
170when this variable is set to nil.")
171
172;;;; Internal global or buffer-local vars
173
174(defconst log-edit-files-buf "*log-edit-files*")
175(defvar log-edit-initial-files nil)
176(defvar log-edit-callback nil)
93a142e1 177(defvar log-edit-diff-function nil)
5b467bf4 178(defvar log-edit-listfun nil)
495b517c 179
cdf54749 180(defvar log-edit-parent-buffer nil)
5b467bf4 181
9af57756
CY
182(defvar log-edit-vc-backend nil
183 "VC fileset corresponding to the current log.")
184
9fe89a26 185;;; Originally taken from VC-Log mode
12dd83de 186
9fe89a26 187(defconst log-edit-maximum-comment-ring-size 32
12dd83de 188 "Maximum number of saved comments in the comment ring.")
e5bd0a28 189(define-obsolete-variable-alias 'vc-comment-ring 'log-edit-comment-ring "22.1")
9fe89a26 190(defvar log-edit-comment-ring (make-ring log-edit-maximum-comment-ring-size))
e5bd0a28
SM
191(define-obsolete-variable-alias 'vc-comment-ring-index
192 'log-edit-comment-ring-index "22.1")
9fe89a26
SM
193(defvar log-edit-comment-ring-index nil)
194(defvar log-edit-last-comment-match "")
12dd83de 195
9fe89a26 196(defun log-edit-new-comment-index (stride len)
12dd83de 197 "Return the comment index STRIDE elements from the current one.
9fe89a26 198LEN is the length of `log-edit-comment-ring'."
12dd83de 199 (mod (cond
9fe89a26 200 (log-edit-comment-ring-index (+ log-edit-comment-ring-index stride))
12dd83de
SM
201 ;; Initialize the index on the first use of this command
202 ;; so that the first M-p gets index 0, and the first M-n gets
203 ;; index -1.
204 ((> stride 0) (1- stride))
205 (t stride))
206 len))
207
9fe89a26 208(defun log-edit-previous-comment (arg)
12dd83de
SM
209 "Cycle backwards through comment history.
210With a numeric prefix ARG, go back ARG comments."
211 (interactive "*p")
9fe89a26 212 (let ((len (ring-length log-edit-comment-ring)))
12dd83de
SM
213 (if (<= len 0)
214 (progn (message "Empty comment ring") (ding))
0916e956
SM
215 ;; Don't use `erase-buffer' because we don't want to `widen'.
216 (delete-region (point-min) (point-max))
9fe89a26
SM
217 (setq log-edit-comment-ring-index (log-edit-new-comment-index arg len))
218 (message "Comment %d" (1+ log-edit-comment-ring-index))
219 (insert (ring-ref log-edit-comment-ring log-edit-comment-ring-index)))))
12dd83de 220
9fe89a26 221(defun log-edit-next-comment (arg)
12dd83de
SM
222 "Cycle forwards through comment history.
223With a numeric prefix ARG, go forward ARG comments."
224 (interactive "*p")
9fe89a26 225 (log-edit-previous-comment (- arg)))
12dd83de 226
9fe89a26 227(defun log-edit-comment-search-backward (str &optional stride)
12dd83de
SM
228 "Search backwards through comment history for substring match of STR.
229If the optional argument STRIDE is present, that is a step-width to use
230when going through the comment ring."
231 ;; Why substring rather than regexp ? -sm
232 (interactive
9fe89a26 233 (list (read-string "Comment substring: " nil nil log-edit-last-comment-match)))
12dd83de
SM
234 (unless stride (setq stride 1))
235 (if (string= str "")
9fe89a26
SM
236 (setq str log-edit-last-comment-match)
237 (setq log-edit-last-comment-match str))
12dd83de 238 (let* ((str (regexp-quote str))
9fe89a26
SM
239 (len (ring-length log-edit-comment-ring))
240 (n (log-edit-new-comment-index stride len)))
12dd83de 241 (while (progn (when (or (>= n len) (< n 0)) (error "Not found"))
9fe89a26 242 (not (string-match str (ring-ref log-edit-comment-ring n))))
12dd83de 243 (setq n (+ n stride)))
9fe89a26
SM
244 (setq log-edit-comment-ring-index n)
245 (log-edit-previous-comment 0)))
12dd83de 246
9fe89a26 247(defun log-edit-comment-search-forward (str)
12dd83de
SM
248 "Search forwards through comment history for a substring match of STR."
249 (interactive
9fe89a26
SM
250 (list (read-string "Comment substring: " nil nil log-edit-last-comment-match)))
251 (log-edit-comment-search-backward str -1))
12dd83de 252
9fe89a26 253(defun log-edit-comment-to-change-log (&optional whoami file-name)
12dd83de
SM
254 "Enter last VC comment into the change log for the current file.
255WHOAMI (interactive prefix) non-nil means prompt for user name
256and site. FILE-NAME is the name of the change log; if nil, use
257`change-log-default-name'.
258
9fe89a26 259This may be useful as a `log-edit-checkin-hook' to update change logs
12dd83de
SM
260automatically."
261 (interactive (if current-prefix-arg
262 (list current-prefix-arg
263 (prompt-for-change-log-name))))
12dd83de 264 (let (;; Extract the comment first so we get any error before doing anything.
9fe89a26 265 (comment (ring-ref log-edit-comment-ring 0))
12dd83de
SM
266 ;; Don't let add-change-log-entry insert a defun name.
267 (add-log-current-defun-function 'ignore)
268 end)
269 ;; Call add-log to do half the work.
270 (add-change-log-entry whoami file-name t t)
271 ;; Insert the VC comment, leaving point before it.
272 (setq end (save-excursion (insert comment) (point-marker)))
273 (if (looking-at "\\s *\\s(")
274 ;; It starts with an open-paren, as in "(foo): Frobbed."
275 ;; So remove the ": " add-log inserted.
276 (delete-char -2))
277 ;; Canonicalize the white space between the file name and comment.
278 (just-one-space)
279 ;; Indent rest of the text the same way add-log indented the first line.
280 (let ((indentation (current-indentation)))
281 (save-excursion
282 (while (< (point) end)
283 (forward-line 1)
284 (indent-to indentation))
285 (setq end (point))))
286 ;; Fill the inserted text, preserving open-parens at bol.
0916e956 287 (let ((paragraph-start (concat paragraph-start "\\|\\s *\\s(")))
12dd83de
SM
288 (beginning-of-line)
289 (fill-region (point) end))
290 ;; Canonicalize the white space at the end of the entry so it is
291 ;; separated from the next entry by a single blank line.
292 (skip-syntax-forward " " end)
293 (delete-char (- (skip-syntax-backward " ")))
294 (or (eobp) (looking-at "\n\n")
295 (insert "\n"))))
296
9fe89a26 297;; Compatibility with old names.
f7eeab0d
SM
298(define-obsolete-function-alias 'vc-previous-comment 'log-edit-previous-comment "22.1")
299(define-obsolete-function-alias 'vc-next-comment 'log-edit-next-comment "22.1")
300(define-obsolete-function-alias 'vc-comment-search-reverse 'log-edit-comment-search-backward "22.1")
301(define-obsolete-function-alias 'vc-comment-search-forward 'log-edit-comment-search-forward "22.1")
302(define-obsolete-function-alias 'vc-comment-to-change-log 'log-edit-comment-to-change-log "22.1")
9fe89a26 303
cdf54749
SM
304;;;
305;;; Actual code
306;;;
5b467bf4 307
e97a42c1
SM
308(defface log-edit-summary '((t :inherit font-lock-function-name-face))
309 "Face for the summary in `log-edit-mode' buffers.")
310
311(defface log-edit-header '((t :inherit font-lock-keyword-face))
312 "Face for the headers in `log-edit-mode' buffers.")
313
314(defface log-edit-unknown-header '((t :inherit font-lock-comment-face))
315 "Face for unknown headers in `log-edit-mode' buffers.")
316
317(defvar log-edit-headers-alist '(("Summary" . log-edit-summary)
318 ("Fixes") ("Author"))
319 "AList of known headers and the face to use to highlight them.")
320
321(defconst log-edit-header-contents-regexp
322 "[ \t]*\\(.*\\(\n[ \t].*\\)*\\)\n?")
323
ba83908c 324(defun log-edit-match-to-eoh (_limit)
e97a42c1
SM
325 ;; FIXME: copied from message-match-to-eoh.
326 (let ((start (point)))
327 (rfc822-goto-eoh)
328 ;; Typical situation: some temporary change causes the header to be
329 ;; incorrect, so EOH comes earlier than intended: the last lines of the
330 ;; intended headers are now not considered part of the header any more,
331 ;; so they don't have the multiline property set. When the change is
332 ;; completed and the header has its correct shape again, the lack of the
333 ;; multiline property means we won't rehighlight the last lines of
334 ;; the header.
335 (if (< (point) start)
336 nil ;No header within start..limit.
337 ;; Here we disregard LIMIT so that we may extend the area again.
338 (set-match-data (list start (point)))
339 (point))))
340
63fbe552 341(defvar log-edit-font-lock-keywords
e97a42c1
SM
342 ;; Copied/inspired by message-font-lock-keywords.
343 `((log-edit-match-to-eoh
9f7b98f8 344 (,(concat "^\\(\\([[:alpha:]-]+\\):\\)" log-edit-header-contents-regexp)
e97a42c1 345 (progn (goto-char (match-beginning 0)) (match-end 0)) nil
02661b3a 346 (1 (if (assoc-string (match-string 2) log-edit-headers-alist t)
e97a42c1
SM
347 'log-edit-header
348 'log-edit-unknown-header)
349 nil lax)
402c8a49 350 ;; From `log-edit-header-contents-regexp':
02661b3a 351 (3 (or (cdr (assoc-string (match-string 2) log-edit-headers-alist t))
e97a42c1 352 'log-edit-header)
02661b3a
SM
353 nil lax))
354 ("^\n"
355 (progn (goto-char (match-end 0)) (1+ (match-end 0))) nil
356 (0 '(:height 0.1 :inverse-video t))))))
165a7fbe 357
49ed9c8e
SM
358(defvar log-edit-font-lock-gnu-style nil
359 "If non-nil, highlight common failures to follow the GNU coding standards.")
360(put 'log-edit-font-lock-gnu-style 'safe-local-variable 'booleanp)
361
362(defconst log-edit-font-lock-gnu-keywords
363 ;; Use
364 ;; * foo.el (bla, bli)
365 ;; (blo, blu): Toto.
366 ;; Rather than
367 ;; * foo.el (bla, bli,
368 ;; blo, blu): Toto.
369 '(("^[ \t]*\\(?:\\* .*\\)?\\(([^\n)]*,\\s-*\\)$"
370 (1 '(face font-lock-warning-face
371 help-echo "Continue function lists with \")\\n(\".") t))
372 ;; Don't leave a lone word on a single line.
373 ;;("^\\s-*\\(\\S-*[^\n:)]\\)\\s-*$" (1 font-lock-warning-face t))
374 ;; Don't cut a sentence right after the first word (better to move
375 ;; the sentence on the next line, then).
376 ;;("[.:]\\s-+\\(\\sw+\\)\\s-*$" (1 font-lock-warning-face t))
377 ;; Change Log entries should use present tense.
378 ("):[ \t\n]*[[:alpha:]]+\\(ed\\)\\>"
379 (1 '(face font-lock-warning-face help-echo "Use present tense.") t))
380 ;; Change log entries start with a capital letter.
381 ("): [a-z]" (0 '(face font-lock-warning-face help-echo "Capitalize.") t))
382 ("[^[:upper:]]\\(\\. [[:upper:]]\\)"
383 (1 '(face font-lock-warning-face
384 help-echo "Use two spaces to end a sentence") t))
385 ("^("
386 (0 (let ((beg (max (point-min) (- (match-beginning 0) 2))))
387 (put-text-property beg (match-end 0) 'font-lock-multiline t)
388 (if (eq (char-syntax (char-after beg)) ?w)
389 '(face font-lock-warning-face
390 help-echo "Punctuate previous line.")))
391 t))
392 ))
393
394(defun log-edit-font-lock-keywords ()
395 (if log-edit-font-lock-gnu-style
396 (append log-edit-font-lock-keywords
397 log-edit-font-lock-gnu-keywords)
398 log-edit-font-lock-keywords))
399
5b467bf4 400;;;###autoload
ba83908c 401(defun log-edit (callback &optional setup params buffer mode &rest _ignore)
5b467bf4 402 "Setup a buffer to enter a log message.
9af57756
CY
403The buffer is put in mode MODE or `log-edit-mode' if MODE is nil.
404\\<log-edit-mode-map>
405If SETUP is non-nil, erase the buffer and run `log-edit-hook'.
406Set mark and point around the entire contents of the buffer, so
407that it is easy to kill the contents of the buffer with
408\\[kill-region]. Once the user is done editing the message,
409invoking the command \\[log-edit-done] (`log-edit-done') will
410call CALLBACK to do the actual commit.
411
412PARAMS if non-nil is an alist of variables and buffer-local
413values to give them in the Log Edit buffer. Possible keys and
414associated values:
2507310c
TTN
415 `log-edit-listfun' -- function taking no arguments that returns the list of
416 files that are concerned by the current operation (using relative names);
417 `log-edit-diff-function' -- function taking no arguments that
418 displays a diff of the files concerned by the current operation.
9af57756 419 `vc-log-fileset' -- the VC fileset to be committed (if any).
2507310c 420
9af57756
CY
421If BUFFER is non-nil `log-edit' will jump to that buffer, use it
422to edit the log message and go back to the current buffer when
423done. Otherwise, it uses the current buffer."
cdf54749
SM
424 (let ((parent (current-buffer)))
425 (if buffer (pop-to-buffer buffer))
426 (when (and log-edit-setup-invert (not (eq setup 'force)))
427 (setq setup (not setup)))
e97a42c1
SM
428 (when setup
429 (erase-buffer)
6978a151 430 (insert "Summary: \nAuthor: ")
e97a42c1 431 (save-excursion (insert "\n\n")))
09158997
DN
432 (if mode
433 (funcall mode)
434 (log-edit-mode))
cdf54749 435 (set (make-local-variable 'log-edit-callback) callback)
93a142e1
DN
436 (if (listp params)
437 (dolist (crt params)
438 (set (make-local-variable (car crt)) (cdr crt)))
439 ;; For backward compatibility with log-edit up to version 22.2
440 ;; accept non-list PARAMS to mean `log-edit-list'.
441 (set (make-local-variable 'log-edit-listfun) params))
442
cdf54749 443 (if buffer (set (make-local-variable 'log-edit-parent-buffer) parent))
70c2a484 444 (set (make-local-variable 'log-edit-initial-files) (log-edit-files))
cdf54749
SM
445 (when setup (run-hooks 'log-edit-hook))
446 (goto-char (point-min)) (push-mark (point-max))
8a26c165 447 (message "%s" (substitute-command-keys
cdf54749 448 "Press \\[log-edit-done] when you are done editing."))))
5b467bf4
SM
449
450(define-derived-mode log-edit-mode text-mode "Log-Edit"
54877f36
SM
451 "Major mode for editing version-control log messages.
452When done editing the log entry, just type \\[log-edit-done] which
453will trigger the actual commit of the file(s).
454Several other handy support commands are provided of course and
455the package from which this is used might also provide additional
456commands (under C-x v for VC, for example).
457
1be77002 458\\{log-edit-mode-map}"
165a7fbe 459 (set (make-local-variable 'font-lock-defaults)
49ed9c8e 460 '(log-edit-font-lock-keywords t))
8117868f
DN
461 (make-local-variable 'log-edit-comment-ring-index)
462 (hack-dir-local-variables-non-file-buffer))
5b467bf4
SM
463
464(defun log-edit-hide-buf (&optional buf where)
465 (when (setq buf (get-buffer (or buf log-edit-files-buf)))
466 (let ((win (get-buffer-window buf where)))
467 (if win (ignore-errors (delete-window win))))
468 (bury-buffer buf)))
469
470(defun log-edit-done ()
471 "Finish editing the log message and commit the files.
5b467bf4
SM
472If you want to abort the commit, simply delete the buffer."
473 (interactive)
e97a42c1
SM
474 ;; Clean up empty headers.
475 (goto-char (point-min))
476 (while (looking-at (concat "^[a-z]*:" log-edit-header-contents-regexp))
477 (let ((beg (match-beginning 0)))
478 (goto-char (match-end 0))
479 (if (string-match "\\`[ \n\t]*\\'" (match-string 1))
480 (delete-region beg (point)))))
481 ;; Get rid of leading empty lines.
482 (goto-char (point-min))
483 (when (looking-at "\\([ \t]*\n\\)+")
484 (delete-region (match-beginning 0) (match-end 0)))
ffe7dc64
SM
485 ;; Get rid of trailing empty lines
486 (goto-char (point-max))
487 (skip-syntax-backward " ")
488 (when (equal (char-after) ?\n) (forward-char 1))
489 (delete-region (point) (point-max))
490 ;; Check for final newline
cdf54749
SM
491 (if (and (> (point-max) (point-min))
492 (/= (char-before (point-max)) ?\n)
5b467bf4
SM
493 (or (eq log-edit-require-final-newline t)
494 (and log-edit-require-final-newline
495 (y-or-n-p
496 (format "Buffer %s does not end in newline. Add one? "
497 (buffer-name))))))
498 (save-excursion
499 (goto-char (point-max))
500 (insert ?\n)))
1be77002 501 (let ((comment (buffer-string)))
9fe89a26
SM
502 (when (or (ring-empty-p log-edit-comment-ring)
503 (not (equal comment (ring-ref log-edit-comment-ring 0))))
504 (ring-insert log-edit-comment-ring comment)))
5b467bf4
SM
505 (let ((win (get-buffer-window log-edit-files-buf)))
506 (if (and log-edit-confirm
507 (not (and (eq log-edit-confirm 'changed)
508 (equal (log-edit-files) log-edit-initial-files)))
509 (progn
510 (log-edit-show-files)
ce5a3ac0 511 (not (y-or-n-p "Really commit? "))))
5b467bf4
SM
512 (progn (when (not win) (log-edit-hide-buf))
513 (message "Oh, well! Later maybe?"))
514 (run-hooks 'log-edit-done-hook)
515 (log-edit-hide-buf)
cdf54749
SM
516 (unless (or log-edit-keep-buffer (not log-edit-parent-buffer))
517 (cvs-bury-buffer (current-buffer) log-edit-parent-buffer))
5b467bf4
SM
518 (call-interactively log-edit-callback))))
519
520(defun log-edit-files ()
521 "Return the list of files that are about to be committed."
522 (ignore-errors (funcall log-edit-listfun)))
523
5b467bf4
SM
524(defun log-edit-mode-help ()
525 "Provide help for the `log-edit-mode-map'."
526 (interactive)
527 (if (eq last-command 'log-edit-mode-help)
528 (describe-function major-mode)
8a26c165 529 (message "%s"
5b467bf4
SM
530 (substitute-command-keys
531 "Type `\\[log-edit-done]' to finish commit. Try `\\[describe-function] log-edit-done' for more help."))))
532
43a9a0c4
SM
533(defcustom log-edit-common-indent 0
534 "Minimum indentation to use in `log-edit-set-common-indentation'."
535 :group 'log-edit
536 :type 'integer)
537
538(defun log-edit-set-common-indentation ()
539 "(Un)Indent the current buffer rigidly to `log-edit-common-indent'."
5b467bf4
SM
540 (save-excursion
541 (let ((common (point-max)))
e97a42c1 542 (rfc822-goto-eoh)
5b467bf4
SM
543 (while (< (point) (point-max))
544 (if (not (looking-at "^[ \t]*$"))
545 (setq common (min common (current-indentation))))
546 (forward-line 1))
e97a42c1
SM
547 (rfc822-goto-eoh)
548 (indent-rigidly (point) (point-max)
43a9a0c4 549 (- log-edit-common-indent common)))))
5b467bf4 550
93a142e1
DN
551(defun log-edit-show-diff ()
552 "Show the diff for the files to be committed."
553 (interactive)
37b72bf5
DN
554 (if (functionp log-edit-diff-function)
555 (funcall log-edit-diff-function)
556 (error "Diff functionality has not been setup")))
93a142e1 557
5b467bf4
SM
558(defun log-edit-show-files ()
559 "Show the list of files to be committed."
560 (interactive)
561 (let* ((files (log-edit-files))
cdf54749 562 (buf (get-buffer-create log-edit-files-buf)))
5b467bf4
SM
563 (with-current-buffer buf
564 (log-edit-hide-buf buf 'all)
565 (setq buffer-read-only nil)
566 (erase-buffer)
a88e99b5 567 (cvs-insert-strings files)
5b467bf4
SM
568 (setq buffer-read-only t)
569 (goto-char (point-min))
570 (save-selected-window
571 (cvs-pop-to-buffer-same-frame buf)
572 (shrink-window-if-larger-than-buffer)
573 (selected-window)))))
574
b605679c
GM
575(defun log-edit-empty-buffer-p ()
576 "Return non-nil if the buffer is \"empty\"."
577 (or (= (point-min) (point-max))
578 (save-excursion
579 (goto-char (point-min))
02661b3a 580 (while (and (looking-at "^\\([a-zA-Z]+: ?\\)?$")
b605679c
GM
581 (zerop (forward-line 1))))
582 (eobp))))
583
5b467bf4 584(defun log-edit-insert-cvs-template ()
f7eeab0d
SM
585 "Insert the template specified by the CVS administrator, if any.
586This simply uses the local CVS/Template file."
5b467bf4 587 (interactive)
32226619 588 (when (or (called-interactively-p 'interactive)
b605679c
GM
589 (log-edit-empty-buffer-p))
590 ;; Should the template take precedence over an empty Summary:,
591 ;; ie should we first erase the buffer?
f7eeab0d 592 (when (file-readable-p "CVS/Template")
b605679c 593 (goto-char (point-max))
f7eeab0d
SM
594 (insert-file-contents "CVS/Template"))))
595
596(defun log-edit-insert-cvs-rcstemplate ()
597 "Insert the rcstemplate from the CVS repository.
598This contacts the repository to get the rcstemplate file and
599can thus take some time."
600 (interactive)
32226619 601 (when (or (called-interactively-p 'interactive)
b605679c 602 (log-edit-empty-buffer-p))
21227135 603 (when (file-readable-p "CVS/Root")
b605679c 604 (goto-char (point-max))
21227135
SM
605 ;; Ignore the stderr stuff, even if it's an error.
606 (call-process "cvs" nil '(t nil) nil
607 "checkout" "-p" "CVSROOT/rcstemplate"))))
f1180544 608
f7eeab0d
SM
609(defun log-edit-insert-filenames ()
610 "Insert the list of files that are to be committed."
611 (interactive)
612 (insert "Affected files: \n"
613 (mapconcat 'identity (log-edit-files) " \n")))
5b467bf4
SM
614
615(defun log-edit-add-to-changelog ()
616 "Insert this log message into the appropriate ChangeLog file."
617 (interactive)
618 ;; Yuck!
9fe89a26
SM
619 (unless (string= (buffer-string) (ring-ref log-edit-comment-ring 0))
620 (ring-insert log-edit-comment-ring (buffer-string)))
5b467bf4
SM
621 (dolist (f (log-edit-files))
622 (let ((buffer-file-name (expand-file-name f)))
623 (save-excursion
9fe89a26 624 (log-edit-comment-to-change-log)))))
5b467bf4 625
f7eeab0d 626(defvar log-edit-changelog-use-first nil)
ce8794df
SM
627
628(defvar log-edit-rewrite-fixes nil
629 "Rule to rewrite bug numbers into Fixes: headers.
630The value should be of the form (REGEXP . REPLACEMENT)
631where REGEXP should match the expression referring to a bug number
632in the text, and REPLACEMENT is an expression to pass to `replace-match'
633to build the Fixes: header.")
a62b88d4
SM
634(put 'log-edit-rewrite-fixes 'safe-local-variable
635 (lambda (v) (and (stringp (car-safe v)) (stringp (cdr v)))))
ce8794df 636
7a6c0941
SM
637(defun log-edit-add-field (field value)
638 (rfc822-goto-eoh)
639 (if (save-excursion (re-search-backward (concat "^" field ":\\([ \t]*\\)$")
640 nil t))
641 (replace-match (concat " " value) t t nil 1)
642 (insert field ": " value "\n" (if (looking-at "\n") "" "\n"))))
643
f7eeab0d
SM
644(defun log-edit-insert-changelog (&optional use-first)
645 "Insert a log message by looking at the ChangeLog.
646The idea is to write your ChangeLog entries first, and then use this
647command to commit your changes.
648
649To select default log text, we:
650- find the ChangeLog entries for the files to be checked in,
651- verify that the top entry in the ChangeLog is on the current date
652 and by the current user; if not, we don't provide any default text,
653- search the ChangeLog entry for paragraphs containing the names of
654 the files we're checking in, and finally
655- use those paragraphs as the log text.
656
657If the optional prefix arg USE-FIRST is given (via \\[universal-argument]),
658or if the command is repeated a second time in a row, use the first log entry
659regardless of user name or time."
660 (interactive "P")
e97a42c1
SM
661 (let ((eoh (save-excursion (rfc822-goto-eoh) (point))))
662 (when (<= (point) eoh)
663 (goto-char eoh)
664 (if (looking-at "\n") (forward-char 1))))
ce8794df
SM
665 (let ((author
666 (let ((log-edit-changelog-use-first
667 (or use-first (eq last-command 'log-edit-insert-changelog))))
668 (log-edit-insert-changelog-entries (log-edit-files)))))
669 (log-edit-set-common-indentation)
670 ;; Add an Author: field if appropriate.
7a6c0941 671 (when author (log-edit-add-field "Author" author))
ce8794df
SM
672 ;; Add a Fixes: field if applicable.
673 (when (consp log-edit-rewrite-fixes)
674 (rfc822-goto-eoh)
675 (when (re-search-forward (car log-edit-rewrite-fixes) nil t)
676 (let ((start (match-beginning 0))
677 (end (match-end 0))
678 (fixes (match-substitute-replacement
679 (cdr log-edit-rewrite-fixes))))
680 (delete-region start end)
7a6c0941 681 (log-edit-add-field "Fixes" fixes))))
b8bd9908
GM
682 (and log-edit-strip-single-file-name
683 (progn (rfc822-goto-eoh)
684 (if (looking-at "\n") (forward-char 1))
685 (looking-at "\\*\\s-+"))
686 (let ((start (point)))
687 (forward-line 1)
688 (when (not (re-search-forward "^\\*\\s-+" nil t))
689 (goto-char start)
690 (skip-chars-forward "^():")
691 (skip-chars-forward ": ")
692 (delete-region start (point)))))
693 (goto-char (point-min))))
f7eeab0d 694
f1180544 695;;;;
5b467bf4
SM
696;;;; functions for getting commit message from ChangeLog a file...
697;;;; Courtesy Jim Blandy
f1180544 698;;;;
5b467bf4 699
14188021 700(defun log-edit-narrow-changelog ()
5b467bf4
SM
701 "Narrow to the top page of the current buffer, a ChangeLog file.
702Actually, the narrowed region doesn't include the date line.
703A \"page\" in a ChangeLog file is the area between two dates."
704 (or (eq major-mode 'change-log-mode)
14188021 705 (error "log-edit-narrow-changelog: current buffer isn't a ChangeLog"))
5b467bf4
SM
706
707 (goto-char (point-min))
708
709 ;; Skip date line and subsequent blank lines.
710 (forward-line 1)
711 (if (looking-at "[ \t\n]*\n")
712 (goto-char (match-end 0)))
713
714 (let ((start (point)))
715 (forward-page 1)
716 (narrow-to-region start (point))
717 (goto-char (point-min))))
718
14188021 719(defun log-edit-changelog-paragraph ()
5b467bf4
SM
720 "Return the bounds of the ChangeLog paragraph containing point.
721If we are between paragraphs, return the previous paragraph."
8390fb80
SM
722 (beginning-of-line)
723 (if (looking-at "^[ \t]*$")
724 (skip-chars-backward " \t\n" (point-min)))
725 (list (progn
726 (if (re-search-backward "^[ \t]*\n" nil 'or-to-limit)
727 (goto-char (match-end 0)))
728 (point))
729 (if (re-search-forward "^[ \t\n]*$" nil t)
730 (match-beginning 0)
731 (point-max))))
5b467bf4 732
14188021 733(defun log-edit-changelog-subparagraph ()
5b467bf4
SM
734 "Return the bounds of the ChangeLog subparagraph containing point.
735A subparagraph is a block of non-blank lines beginning with an asterisk.
736If we are between sub-paragraphs, return the previous subparagraph."
5b467bf4
SM
737 (end-of-line)
738 (if (search-backward "*" nil t)
739 (list (progn (beginning-of-line) (point))
bc35d341 740 (progn
5b467bf4
SM
741 (forward-line 1)
742 (if (re-search-forward "^[ \t]*[\n*]" nil t)
743 (match-beginning 0)
744 (point-max))))
8390fb80 745 (list (point) (point))))
5b467bf4 746
14188021 747(defun log-edit-changelog-entry ()
5b467bf4 748 "Return the bounds of the ChangeLog entry containing point.
14188021 749The variable `log-edit-changelog-full-paragraphs' decides whether an
5b467bf4
SM
750\"entry\" is a paragraph or a subparagraph; see its documentation string
751for more details."
8390fb80
SM
752 (save-excursion
753 (if log-edit-changelog-full-paragraphs
754 (log-edit-changelog-paragraph)
755 (log-edit-changelog-subparagraph))))
5b467bf4
SM
756
757(defvar user-full-name)
758(defvar user-mail-address)
ce8794df
SM
759
760(defvar log-edit-author) ;Dynamically scoped.
761
14188021 762(defun log-edit-changelog-ours-p ()
5b467bf4 763 "See if ChangeLog entry at point is for the current user, today.
4837b516 764Return non-nil if it is."
5b467bf4
SM
765 ;; Code adapted from add-change-log-entry.
766 (let ((name (or (and (boundp 'add-log-full-name) add-log-full-name)
767 (and (fboundp 'user-full-name) (user-full-name))
768 (and (boundp 'user-full-name) user-full-name)))
769 (mail (or (and (boundp 'add-log-mailing-address) add-log-mailing-address)
770 ;;(and (fboundp 'user-mail-address) (user-mail-address))
771 (and (boundp 'user-mail-address) user-mail-address)))
772 (time (or (and (boundp 'add-log-time-format)
773 (functionp add-log-time-format)
774 (funcall add-log-time-format))
775 (format-time-string "%Y-%m-%d"))))
ce8794df
SM
776 (if (null log-edit-changelog-use-first)
777 (looking-at (regexp-quote (format "%s %s <%s>" time name mail)))
778 ;; Check the author, to potentially add it as a "Author: " header.
779 (when (looking-at "[^ \t]")
780 (when (and (boundp 'log-edit-author)
781 (not (looking-at (format ".+ .+ <%s>"
782 (regexp-quote mail))))
783 (looking-at ".+ \\(.+ <.+>\\)"))
784 (let ((author (replace-regexp-in-string " " " "
785 (match-string 1))))
786 (unless (and log-edit-author
787 (string-match (regexp-quote author) log-edit-author))
788 (setq log-edit-author
789 (if log-edit-author
790 (concat log-edit-author ", " author)
791 author)))))
792 t))))
5b467bf4 793
14188021 794(defun log-edit-changelog-entries (file)
5b467bf4
SM
795 "Return the ChangeLog entries for FILE, and the ChangeLog they came from.
796The return value looks like this:
bef4957b 797 (LOGBUFFER (ENTRYSTART ENTRYEND) ...)
5b467bf4
SM
798where LOGBUFFER is the name of the ChangeLog buffer, and each
799\(ENTRYSTART . ENTRYEND\) pair is a buffer region."
d944ee49
SM
800 (let ((changelog-file-name
801 (let ((default-directory
802 (file-name-directory (expand-file-name file)))
803 (visiting-buffer (find-buffer-visiting file)))
804 ;; If there is a buffer visiting FILE, and it has a local
805 ;; value for `change-log-default-name', use that.
806 (if (and visiting-buffer
807 (local-variable-p 'change-log-default-name
808 visiting-buffer))
809 (with-current-buffer visiting-buffer
810 change-log-default-name)
811 ;; `find-change-log' uses `change-log-default-name' if set
812 ;; and sets it before exiting, so we need to work around
02661b3a 813 ;; that memoizing which is undesired here.
d944ee49
SM
814 (setq change-log-default-name nil)
815 (find-change-log)))))
816 (with-current-buffer (find-file-noselect changelog-file-name)
5b467bf4
SM
817 (unless (eq major-mode 'change-log-mode) (change-log-mode))
818 (goto-char (point-min))
819 (if (looking-at "\\s-*\n") (goto-char (match-end 0)))
14188021 820 (if (not (log-edit-changelog-ours-p))
5b467bf4
SM
821 (list (current-buffer))
822 (save-restriction
14188021 823 (log-edit-narrow-changelog)
5b467bf4 824 (goto-char (point-min))
f1180544 825
5b467bf4
SM
826 ;; Search for the name of FILE relative to the ChangeLog. If that
827 ;; doesn't occur anywhere, they're not using full relative
828 ;; filenames in the ChangeLog, so just look for FILE; we'll accept
829 ;; some false positives.
830 (let ((pattern (file-relative-name
831 file (file-name-directory changelog-file-name))))
832 (if (or (string= pattern "")
833 (not (save-excursion
834 (search-forward pattern nil t))))
835 (setq pattern (file-name-nondirectory file)))
836
b543ff57 837 (setq pattern (concat "\\(^\\|[^[:alnum:]]\\)"
9b2a758a 838 (regexp-quote pattern)
b543ff57
NR
839 "\\($\\|[^[:alnum:]]\\)"))
840
8390fb80
SM
841 (let (texts
842 (pos (point)))
843 (while (and (not (eobp)) (re-search-forward pattern nil t))
14188021 844 (let ((entry (log-edit-changelog-entry)))
8390fb80
SM
845 (if (< (elt entry 1) (max (1+ pos) (point)))
846 ;; This is not relevant, actually.
847 nil
848 (push entry texts))
849 ;; Make sure we make progress.
850 (setq pos (max (1+ pos) (elt entry 1)))
851 (goto-char pos)))
5b467bf4
SM
852
853 (cons (current-buffer) texts))))))))
854
bef4957b
CY
855(defun log-edit-changelog-insert-entries (buffer beg end &rest files)
856 "Insert the text from BUFFER between BEG and END.
857Rename relative filenames in the ChangeLog entry as FILES."
858 (let ((opoint (point))
859 (log-name (buffer-file-name buffer))
860 (case-fold-search nil)
861 bound)
862 (insert-buffer-substring buffer beg end)
863 (setq bound (point-marker))
864 (when log-name
865 (dolist (f files)
866 (save-excursion
867 (goto-char opoint)
868 (when (re-search-forward
869 (concat "\\(^\\|[ \t]\\)\\("
870 (file-relative-name f (file-name-directory log-name))
871 "\\)[, :\n]")
872 bound t)
873 (replace-match f t t nil 2)))))
874 ;; Eliminate tabs at the beginning of the line.
875 (save-excursion
876 (goto-char opoint)
877 (while (re-search-forward "^\\(\t+\\)" bound t)
878 (replace-match "")))))
5b467bf4 879
14188021 880(defun log-edit-insert-changelog-entries (files)
5b467bf4 881 "Given a list of files FILES, insert the ChangeLog entries for them."
ce8794df
SM
882 (let ((log-entries nil)
883 (log-edit-author nil))
bef4957b
CY
884 ;; Note that any ChangeLog entry can apply to more than one file.
885 ;; Here we construct a log-entries list with elements of the form
886 ;; ((LOGBUFFER ENTRYSTART ENTRYEND) FILE1 FILE2...)
5b467bf4 887 (dolist (file files)
14188021 888 (let* ((entries (log-edit-changelog-entries file))
bef4957b
CY
889 (buf (car entries))
890 key entry)
891 (dolist (region (cdr entries))
892 (setq key (cons buf region))
893 (if (setq entry (assoc key log-entries))
894 (setcdr entry (append (cdr entry) (list file)))
895 (push (list key file) log-entries)))))
896 ;; Now map over log-entries, and extract the strings.
897 (dolist (log-entry (nreverse log-entries))
898 (apply 'log-edit-changelog-insert-entries
899 (append (car log-entry) (cdr log-entry)))
ce8794df
SM
900 (insert "\n"))
901 log-edit-author))
5b467bf4 902
9f7b98f8
DG
903(defun log-edit-toggle-header (header value)
904 "Toggle a boolean-type header in the current buffer.
905If the value of HEADER is VALUE, clear it. Otherwise, add the
906header if it's not present and set it to VALUE. Then make sure
907there is an empty line after the headers. Return t if toggled
908on, otherwise nil."
909 (let ((val t)
910 (line (concat header ": " value "\n")))
911 (save-excursion
912 (save-restriction
913 (rfc822-goto-eoh)
914 (narrow-to-region (point-min) (point))
915 (goto-char (point-min))
916 (if (re-search-forward (concat "^" header ":"
917 log-edit-header-contents-regexp)
918 nil t)
919 (if (setq val (not (string= (match-string 1) value)))
920 (replace-match line t t)
921 (replace-match "" t t nil 1))
922 (insert line)))
923 (rfc822-goto-eoh)
924 (delete-horizontal-space)
925 (unless (looking-at "\n")
926 (insert "\n")))
927 val))
928
e97a42c1
SM
929(defun log-edit-extract-headers (headers comment)
930 "Extract headers from COMMENT to form command line arguments.
9f7b98f8
DG
931HEADERS should be an alist with elements (HEADER . CMDARG)
932or (HEADER . FUNCTION) associating headers to command line
933options and the result is then a list of the form (MSG ARGUMENTS...)
934where MSG is the remaining text from COMMENT.
935FUNCTION should be a function of one argument that takes the
936header value and returns the list of strings to be appended to
937ARGUMENTS. CMDARG will be added to ARGUMENTS followed by the
938header value. If \"Summary\" is not in HEADERS, then the
939\"Summary\" header is extracted anyway and put back as the first
940line of MSG."
e97a42c1
SM
941 (with-temp-buffer
942 (insert comment)
943 (rfc822-goto-eoh)
944 (narrow-to-region (point-min) (point))
945 (let ((case-fold-search t)
946 (summary ())
947 (res ()))
948 (dolist (header (if (assoc "Summary" headers) headers
949 (cons '("Summary" . t) headers)))
950 (goto-char (point-min))
951 (while (re-search-forward (concat "^" (car header)
952 ":" log-edit-header-contents-regexp)
953 nil t)
954 (if (eq t (cdr header))
955 (setq summary (match-string 1))
9f7b98f8
DG
956 (if (functionp (cdr header))
957 (setq res (nconc res (funcall (cdr header) (match-string 1))))
958 (push (match-string 1) res)
959 (push (or (cdr header) (car header)) res)))
e97a42c1
SM
960 (replace-match "" t t)))
961 ;; Remove header separator if the header is empty.
962 (widen)
09158997 963 (goto-char (point-min))
e97a42c1
SM
964 (when (looking-at "\\([ \t]*\n\\)+")
965 (delete-region (match-beginning 0) (match-end 0)))
966 (if summary (insert summary "\n"))
967 (cons (buffer-string) res))))
09158997 968
5b467bf4 969(provide 'log-edit)
54877f36 970
5b467bf4 971;;; log-edit.el ends here