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