summaryrefslogtreecommitdiff
path: root/lisp/log-edit.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/log-edit.el')
-rw-r--r--lisp/log-edit.el261
1 files changed, 212 insertions, 49 deletions
diff --git a/lisp/log-edit.el b/lisp/log-edit.el
index 10c6d480d23..0d3061ad2df 100644
--- a/lisp/log-edit.el
+++ b/lisp/log-edit.el
@@ -125,6 +125,7 @@ If SETUP is 'force, this variable has no effect."
:type 'boolean)
(defcustom log-edit-hook '(log-edit-insert-cvs-template
+ log-edit-show-files
log-edit-insert-changelog)
"Hook run at the end of `log-edit'."
:group 'log-edit
@@ -188,6 +189,7 @@ when this variable is set to nil.")
(defvar log-edit-callback nil)
(defvar log-edit-diff-function nil)
(defvar log-edit-listfun nil)
+
(defvar log-edit-parent-buffer nil)
;;; Originally taken from VC-Log mode
@@ -312,15 +314,59 @@ automatically."
;;; Actual code
;;;
+(defface log-edit-summary '((t :inherit font-lock-function-name-face))
+ "Face for the summary in `log-edit-mode' buffers.")
+
+(defface log-edit-header '((t :inherit font-lock-keyword-face))
+ "Face for the headers in `log-edit-mode' buffers.")
+
+(defface log-edit-unknown-header '((t :inherit font-lock-comment-face))
+ "Face for unknown headers in `log-edit-mode' buffers.")
+
+(defvar log-edit-headers-alist '(("Summary" . log-edit-summary)
+ ("Fixes") ("Author"))
+ "AList of known headers and the face to use to highlight them.")
+
+(defconst log-edit-header-contents-regexp
+ "[ \t]*\\(.*\\(\n[ \t].*\\)*\\)\n?")
+
+(defun log-edit-match-to-eoh (limit)
+ ;; FIXME: copied from message-match-to-eoh.
+ (let ((start (point)))
+ (rfc822-goto-eoh)
+ ;; Typical situation: some temporary change causes the header to be
+ ;; incorrect, so EOH comes earlier than intended: the last lines of the
+ ;; intended headers are now not considered part of the header any more,
+ ;; so they don't have the multiline property set. When the change is
+ ;; completed and the header has its correct shape again, the lack of the
+ ;; multiline property means we won't rehighlight the last lines of
+ ;; the header.
+ (if (< (point) start)
+ nil ;No header within start..limit.
+ ;; Here we disregard LIMIT so that we may extend the area again.
+ (set-match-data (list start (point)))
+ (point))))
+
(defvar log-edit-font-lock-keywords
- '(("\\`\\(Summary:\\)\\(.*\\)"
- (1 font-lock-keyword-face)
- (2 font-lock-function-name-face))))
+ ;; Copied/inspired by message-font-lock-keywords.
+ `((log-edit-match-to-eoh
+ (,(concat "^\\(\\([a-z]+\\):\\)" log-edit-header-contents-regexp
+ "\\|\\(.*\\)")
+ (progn (goto-char (match-beginning 0)) (match-end 0)) nil
+ (1 (if (assoc (match-string 2) log-edit-headers-alist)
+ 'log-edit-header
+ 'log-edit-unknown-header)
+ nil lax)
+ (3 (or (cdr (assoc (match-string 2) log-edit-headers-alist))
+ 'log-edit-header)
+ nil lax)
+ (4 font-lock-warning-face)))))
;;;###autoload
-(defun log-edit (callback &optional setup params buffer &rest ignore)
+(defun log-edit (callback &optional setup params buffer mode &rest ignore)
"Setup a buffer to enter a log message.
-\\<log-edit-mode-map>The buffer will be put in `log-edit-mode'.
+\\<log-edit-mode-map>The buffer will be put in mode MODE or `log-edit-mode'
+if MODE is nil.
If SETUP is non-nil, the buffer is then erased and `log-edit-hook' is run.
Mark and point will be set around the entire contents of the buffer so
that it is easy to kill the contents of the buffer with \\[kill-region].
@@ -340,8 +386,13 @@ uses the current buffer."
(if buffer (pop-to-buffer buffer))
(when (and log-edit-setup-invert (not (eq setup 'force)))
(setq setup (not setup)))
- (when setup (erase-buffer))
- (log-edit-mode)
+ (when setup
+ (erase-buffer)
+ (insert "Summary: ")
+ (save-excursion (insert "\n\n")))
+ (if mode
+ (funcall mode)
+ (log-edit-mode))
(set (make-local-variable 'log-edit-callback) callback)
(if (listp params)
(dolist (crt params)
@@ -367,7 +418,7 @@ commands (under C-x v for VC, for example).
\\{log-edit-mode-map}"
(set (make-local-variable 'font-lock-defaults)
- '(log-edit-font-lock-keywords t))
+ '(log-edit-font-lock-keywords t t))
(make-local-variable 'log-edit-comment-ring-index))
(defun log-edit-hide-buf (&optional buf where)
@@ -380,6 +431,17 @@ commands (under C-x v for VC, for example).
"Finish editing the log message and commit the files.
If you want to abort the commit, simply delete the buffer."
(interactive)
+ ;; Clean up empty headers.
+ (goto-char (point-min))
+ (while (looking-at (concat "^[a-z]*:" log-edit-header-contents-regexp))
+ (let ((beg (match-beginning 0)))
+ (goto-char (match-end 0))
+ (if (string-match "\\`[ \n\t]*\\'" (match-string 1))
+ (delete-region beg (point)))))
+ ;; Get rid of leading empty lines.
+ (goto-char (point-min))
+ (when (looking-at "\\([ \t]*\n\\)+")
+ (delete-region (match-beginning 0) (match-end 0)))
;; Get rid of trailing empty lines
(goto-char (point-max))
(skip-syntax-backward " ")
@@ -437,12 +499,13 @@ If you want to abort the commit, simply delete the buffer."
"(Un)Indent the current buffer rigidly to `log-edit-common-indent'."
(save-excursion
(let ((common (point-max)))
- (goto-char (point-min))
+ (rfc822-goto-eoh)
(while (< (point) (point-max))
(if (not (looking-at "^[ \t]*$"))
(setq common (min common (current-indentation))))
(forward-line 1))
- (indent-rigidly (point-min) (point-max)
+ (rfc822-goto-eoh)
+ (indent-rigidly (point) (point-max)
(- log-edit-common-indent common)))))
(defun log-edit-show-diff ()
@@ -508,6 +571,16 @@ can thus take some time."
(log-edit-comment-to-change-log)))))
(defvar log-edit-changelog-use-first nil)
+
+(defvar log-edit-rewrite-fixes nil
+ "Rule to rewrite bug numbers into Fixes: headers.
+The value should be of the form (REGEXP . REPLACEMENT)
+where REGEXP should match the expression referring to a bug number
+in the text, and REPLACEMENT is an expression to pass to `replace-match'
+to build the Fixes: header.")
+(put 'log-edit-rewrite-fixes 'safe-local-variable
+ (lambda (v) (and (stringp (car-safe v)) (stringp (cdr v)))))
+
(defun log-edit-insert-changelog (&optional use-first)
"Insert a log message by looking at the ChangeLog.
The idea is to write your ChangeLog entries first, and then use this
@@ -525,18 +598,38 @@ If the optional prefix arg USE-FIRST is given (via \\[universal-argument]),
or if the command is repeated a second time in a row, use the first log entry
regardless of user name or time."
(interactive "P")
- (let ((log-edit-changelog-use-first
- (or use-first (eq last-command 'log-edit-insert-changelog))))
- (log-edit-insert-changelog-entries (log-edit-files)))
- (log-edit-set-common-indentation)
- (goto-char (point-min))
- (when (and log-edit-strip-single-file-name (looking-at "\\*\\s-+"))
- (forward-line 1)
- (when (not (re-search-forward "^\\*\\s-+" nil t))
- (goto-char (point-min))
- (skip-chars-forward "^():")
- (skip-chars-forward ": ")
- (delete-region (point-min) (point)))))
+ (let ((eoh (save-excursion (rfc822-goto-eoh) (point))))
+ (when (<= (point) eoh)
+ (goto-char eoh)
+ (if (looking-at "\n") (forward-char 1))))
+ (let ((author
+ (let ((log-edit-changelog-use-first
+ (or use-first (eq last-command 'log-edit-insert-changelog))))
+ (log-edit-insert-changelog-entries (log-edit-files)))))
+ (log-edit-set-common-indentation)
+ ;; Add an Author: field if appropriate.
+ (when author
+ (rfc822-goto-eoh)
+ (insert "Author: " author "\n" (if (looking-at "\n") "" "\n")))
+ ;; Add a Fixes: field if applicable.
+ (when (consp log-edit-rewrite-fixes)
+ (rfc822-goto-eoh)
+ (when (re-search-forward (car log-edit-rewrite-fixes) nil t)
+ (let ((start (match-beginning 0))
+ (end (match-end 0))
+ (fixes (match-substitute-replacement
+ (cdr log-edit-rewrite-fixes))))
+ (delete-region start end)
+ (rfc822-goto-eoh)
+ (insert "Fixes: " fixes "\n" (if (looking-at "\n") "" "\n")))))
+ (goto-char (point-min))
+ (when (and log-edit-strip-single-file-name (looking-at "\\*\\s-+"))
+ (forward-line 1)
+ (when (not (re-search-forward "^\\*\\s-+" nil t))
+ (goto-char (point-min))
+ (skip-chars-forward "^():")
+ (skip-chars-forward ": ")
+ (delete-region (point-min) (point))))))
;;;;
;;;; functions for getting commit message from ChangeLog a file...
@@ -602,6 +695,9 @@ for more details."
(defvar user-full-name)
(defvar user-mail-address)
+
+(defvar log-edit-author) ;Dynamically scoped.
+
(defun log-edit-changelog-ours-p ()
"See if ChangeLog entry at point is for the current user, today.
Return non-nil if it is."
@@ -616,14 +712,28 @@ Return non-nil if it is."
(functionp add-log-time-format)
(funcall add-log-time-format))
(format-time-string "%Y-%m-%d"))))
- (looking-at (if log-edit-changelog-use-first
- "[^ \t]"
- (regexp-quote (format "%s %s <%s>" time name mail))))))
+ (if (null log-edit-changelog-use-first)
+ (looking-at (regexp-quote (format "%s %s <%s>" time name mail)))
+ ;; Check the author, to potentially add it as a "Author: " header.
+ (when (looking-at "[^ \t]")
+ (when (and (boundp 'log-edit-author)
+ (not (looking-at (format ".+ .+ <%s>"
+ (regexp-quote mail))))
+ (looking-at ".+ \\(.+ <.+>\\)"))
+ (let ((author (replace-regexp-in-string " " " "
+ (match-string 1))))
+ (unless (and log-edit-author
+ (string-match (regexp-quote author) log-edit-author))
+ (setq log-edit-author
+ (if log-edit-author
+ (concat log-edit-author ", " author)
+ author)))))
+ t))))
(defun log-edit-changelog-entries (file)
"Return the ChangeLog entries for FILE, and the ChangeLog they came from.
The return value looks like this:
- (LOGBUFFER (ENTRYSTART . ENTRYEND) ...)
+ (LOGBUFFER (ENTRYSTART ENTRYEND) ...)
where LOGBUFFER is the name of the ChangeLog buffer, and each
\(ENTRYSTART . ENTRYEND\) pair is a buffer region."
(let ((changelog-file-name
@@ -681,34 +791,87 @@ where LOGBUFFER is the name of the ChangeLog buffer, and each
(cons (current-buffer) texts))))))))
-(defun log-edit-changelog-insert-entries (buffer regions)
- "Insert those regions in BUFFER specified in REGIONS.
-Sort REGIONS front-to-back first."
- (let ((regions (sort regions 'car-less-than-car))
- (last))
- (dolist (region regions)
- (when (and last (< last (car region))) (newline))
- (setq last (elt region 1))
- (apply 'insert-buffer-substring buffer region))))
+(defun log-edit-changelog-insert-entries (buffer beg end &rest files)
+ "Insert the text from BUFFER between BEG and END.
+Rename relative filenames in the ChangeLog entry as FILES."
+ (let ((opoint (point))
+ (log-name (buffer-file-name buffer))
+ (case-fold-search nil)
+ bound)
+ (insert-buffer-substring buffer beg end)
+ (setq bound (point-marker))
+ (when log-name
+ (dolist (f files)
+ (save-excursion
+ (goto-char opoint)
+ (when (re-search-forward
+ (concat "\\(^\\|[ \t]\\)\\("
+ (file-relative-name f (file-name-directory log-name))
+ "\\)[, :\n]")
+ bound t)
+ (replace-match f t t nil 2)))))
+ ;; Eliminate tabs at the beginning of the line.
+ (save-excursion
+ (goto-char opoint)
+ (while (re-search-forward "^\\(\t+\\)" bound t)
+ (replace-match "")))))
(defun log-edit-insert-changelog-entries (files)
"Given a list of files FILES, insert the ChangeLog entries for them."
- (let ((buffer-entries nil))
-
- ;; Add each buffer to buffer-entries, and associate it with the list
- ;; of entries we want from that file.
+ (let ((log-entries nil)
+ (log-edit-author nil))
+ ;; Note that any ChangeLog entry can apply to more than one file.
+ ;; Here we construct a log-entries list with elements of the form
+ ;; ((LOGBUFFER ENTRYSTART ENTRYEND) FILE1 FILE2...)
(dolist (file files)
(let* ((entries (log-edit-changelog-entries file))
- (pair (assq (car entries) buffer-entries)))
- (if pair
- (setcdr pair (cvs-union (cdr pair) (cdr entries)))
- (push entries buffer-entries))))
-
- ;; Now map over each buffer in buffer-entries, sort the entries for
- ;; each buffer, and extract them as strings.
- (dolist (buffer-entry buffer-entries)
- (log-edit-changelog-insert-entries (car buffer-entry) (cdr buffer-entry))
- (when (cdr buffer-entry) (newline)))))
+ (buf (car entries))
+ key entry)
+ (dolist (region (cdr entries))
+ (setq key (cons buf region))
+ (if (setq entry (assoc key log-entries))
+ (setcdr entry (append (cdr entry) (list file)))
+ (push (list key file) log-entries)))))
+ ;; Now map over log-entries, and extract the strings.
+ (dolist (log-entry (nreverse log-entries))
+ (apply 'log-edit-changelog-insert-entries
+ (append (car log-entry) (cdr log-entry)))
+ (insert "\n"))
+ log-edit-author))
+
+(defun log-edit-extract-headers (headers comment)
+ "Extract headers from COMMENT to form command line arguments.
+HEADERS should be an alist with elements of the form (HEADER . CMDARG)
+associating header names to the corresponding cmdline option name and the
+result is then a list of the form (MSG CMDARG1 HDRTEXT1 CMDARG2 HDRTEXT2...).
+where MSG is the remaining text from STRING.
+If \"Summary\" is not in HEADERS, then the \"Summary\" header is extracted
+anyway and put back as the first line of MSG."
+ (with-temp-buffer
+ (insert comment)
+ (rfc822-goto-eoh)
+ (narrow-to-region (point-min) (point))
+ (let ((case-fold-search t)
+ (summary ())
+ (res ()))
+ (dolist (header (if (assoc "Summary" headers) headers
+ (cons '("Summary" . t) headers)))
+ (goto-char (point-min))
+ (while (re-search-forward (concat "^" (car header)
+ ":" log-edit-header-contents-regexp)
+ nil t)
+ (if (eq t (cdr header))
+ (setq summary (match-string 1))
+ (push (match-string 1) res)
+ (push (or (cdr header) (car header)) res))
+ (replace-match "" t t)))
+ ;; Remove header separator if the header is empty.
+ (widen)
+ (goto-char (point-min))
+ (when (looking-at "\\([ \t]*\n\\)+")
+ (delete-region (match-beginning 0) (match-end 0)))
+ (if summary (insert summary "\n"))
+ (cons (buffer-string) res))))
(provide 'log-edit)