Add 2011 to FSF/AIST copyright years.
[bpt/emacs.git] / lisp / log-edit.el
index d19f984..865b1f3 100644 (file)
@@ -1,7 +1,7 @@
 ;;; log-edit.el --- Major mode for editing CVS commit messages
 
-;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004,
-;;   2005, 2006, 2007, 2008 Free Software Foundation, Inc.
+;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007,
+;;   2008, 2009, 2010, 2011  Free Software Foundation, Inc.
 
 ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
 ;; Keywords: pcl-cvs cvs commit log
      :help "Search backwards through comment history for substring match of str"]))
 
 (defcustom log-edit-confirm 'changed
-  "*If non-nil, `log-edit-done' will request confirmation.
+  "If non-nil, `log-edit-done' will request confirmation.
 If 'changed, only request confirmation if the list of files has
   changed since the beginning of the log-edit session."
   :group 'log-edit
   :type '(choice (const changed) (const t) (const nil)))
 
 (defcustom log-edit-keep-buffer nil
-  "*If non-nil, don't hide the buffer after `log-edit-done'."
+  "If non-nil, don't hide the buffer after `log-edit-done'."
   :group 'log-edit
   :type 'boolean)
 
@@ -113,20 +113,21 @@ If 'changed, only request confirmation if the list of files has
 
 (defcustom log-edit-require-final-newline
   cvs-commit-buffer-require-final-newline
-  "*Enforce a newline at the end of commit log messages.
+  "Enforce a newline at the end of commit log messages.
 Enforce it silently if t, query if non-nil and don't do anything if nil."
   :group 'log-edit
   :type '(choice (const ask) (const t) (const nil)))
 
 (defcustom log-edit-setup-invert nil
-  "*Non-nil means `log-edit' should invert the meaning of its SETUP arg.
+  "Non-nil means `log-edit' should invert the meaning of its SETUP arg.
 If SETUP is 'force, this variable has no effect."
   :group 'log-edit
   :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'."
+  "Hook run at the end of `log-edit'."
   :group 'log-edit
   :type '(hook :options (log-edit-insert-changelog
                          log-edit-insert-cvs-rcstemplate
@@ -134,12 +135,12 @@ If SETUP is 'force, this variable has no effect."
                         log-edit-insert-filenames)))
 
 (defcustom log-edit-mode-hook (if (boundp 'vc-log-mode-hook) vc-log-mode-hook)
-  "*Hook run when entering `log-edit-mode'."
+  "Hook run when entering `log-edit-mode'."
   :group 'log-edit
   :type 'hook)
 
 (defcustom log-edit-done-hook nil
-  "*Hook run before doing the actual commit.
+  "Hook run before doing the actual commit.
 This hook can be used to cleanup the message, enforce various
 conventions, or to allow recording the message in some other database,
 such as a bug-tracking system.  The list of files about to be committed
@@ -148,6 +149,13 @@ can be obtained from `log-edit-files'."
   :type '(hook :options (log-edit-set-common-indentation
                         log-edit-add-to-changelog)))
 
+(defcustom log-edit-strip-single-file-name t
+  "If non-nil, remove file name from single-file log entries."
+  :type 'boolean
+  :safe 'booleanp
+  :group 'log-edit
+  :version "23.2")
+
 (defvar cvs-changelog-full-paragraphs t)
 (make-obsolete-variable 'cvs-changelog-full-paragraphs
                         'log-edit-changelog-full-paragraphs
@@ -181,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
@@ -305,15 +314,58 @@ 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)
+      ;; From `log-edit-header-contents-regexp':
+      (3 (or (cdr (assoc (match-string 2) log-edit-headers-alist))
+             'log-edit-header)
+         nil lax)))))
 
 ;;;###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].
@@ -333,8 +385,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)
@@ -360,7 +417,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)
@@ -373,6 +430,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 " ")
@@ -430,12 +498,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 ()
@@ -466,7 +535,8 @@ If you want to abort the commit, simply delete the buffer."
   "Insert the template specified by the CVS administrator, if any.
 This simply uses the local CVS/Template file."
   (interactive)
-  (when (or (interactive-p) (= (point-min) (point-max)))
+  (when (or (called-interactively-p 'interactive)
+           (= (point-min) (point-max)))
     (when (file-readable-p "CVS/Template")
       (insert-file-contents "CVS/Template"))))
 
@@ -475,7 +545,8 @@ This simply uses the local CVS/Template file."
 This contacts the repository to get the rcstemplate file and
 can thus take some time."
   (interactive)
-  (when (or (interactive-p) (= (point-min) (point-max)))
+  (when (or (called-interactively-p 'interactive)
+           (= (point-min) (point-max)))
     (when (file-readable-p "CVS/Root")
       ;; Ignore the stderr stuff, even if it's an error.
       (call-process "cvs" nil '(t nil) nil
@@ -499,6 +570,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
@@ -516,18 +597,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 (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...
@@ -556,23 +657,21 @@ A \"page\" in a ChangeLog file is the area between two dates."
 (defun log-edit-changelog-paragraph ()
   "Return the bounds of the ChangeLog paragraph containing point.
 If we are between paragraphs, return the previous paragraph."
-  (save-excursion
-    (beginning-of-line)
-    (if (looking-at "^[ \t]*$")
-        (skip-chars-backward " \t\n" (point-min)))
-    (list (progn
-            (if (re-search-backward "^[ \t]*\n" nil 'or-to-limit)
-                (goto-char (match-end 0)))
-            (point))
-          (if (re-search-forward "^[ \t\n]*$" nil t)
-              (match-beginning 0)
-            (point-max)))))
+  (beginning-of-line)
+  (if (looking-at "^[ \t]*$")
+      (skip-chars-backward " \t\n" (point-min)))
+  (list (progn
+          (if (re-search-backward "^[ \t]*\n" nil 'or-to-limit)
+              (goto-char (match-end 0)))
+          (point))
+        (if (re-search-forward "^[ \t\n]*$" nil t)
+            (match-beginning 0)
+          (point-max))))
 
 (defun log-edit-changelog-subparagraph ()
   "Return the bounds of the ChangeLog subparagraph containing point.
 A subparagraph is a block of non-blank lines beginning with an asterisk.
 If we are between sub-paragraphs, return the previous subparagraph."
-  (save-excursion
     (end-of-line)
     (if (search-backward "*" nil t)
         (list (progn (beginning-of-line) (point))
@@ -581,19 +680,23 @@ If we are between sub-paragraphs, return the previous subparagraph."
                 (if (re-search-forward "^[ \t]*[\n*]" nil t)
                     (match-beginning 0)
                   (point-max))))
-      (list (point) (point)))))
+    (list (point) (point))))
 
 (defun log-edit-changelog-entry ()
   "Return the bounds of the ChangeLog entry containing point.
 The variable `log-edit-changelog-full-paragraphs' decides whether an
 \"entry\" is a paragraph or a subparagraph; see its documentation string
 for more details."
-  (if log-edit-changelog-full-paragraphs
-      (log-edit-changelog-paragraph)
-    (log-edit-changelog-subparagraph)))
+  (save-excursion
+    (if log-edit-changelog-full-paragraphs
+        (log-edit-changelog-paragraph)
+      (log-edit-changelog-subparagraph))))
 
 (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."
@@ -608,14 +711,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
@@ -656,45 +773,104 @@ where LOGBUFFER is the name of the ChangeLog buffer, and each
                (setq pattern (file-name-nondirectory file)))
 
             (setq pattern (concat "\\(^\\|[^[:alnum:]]\\)"
-                                  pattern
+                                  (regexp-quote pattern)
                                   "\\($\\|[^[:alnum:]]\\)"))
 
-           (let (texts)
-             (while (re-search-forward pattern nil t)
+           (let (texts
+                  (pos (point)))
+             (while (and (not (eobp)) (re-search-forward pattern nil t))
                (let ((entry (log-edit-changelog-entry)))
-                 (push entry texts)
-                 (goto-char (elt entry 1))))
+                  (if (< (elt entry 1) (max (1+ pos) (point)))
+                      ;; This is not relevant, actually.
+                      nil
+                    (push entry texts))
+                  ;; Make sure we make progress.
+                  (setq pos (max (1+ pos) (elt entry 1)))
+                 (goto-char pos)))
 
              (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)