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