Update docstrings for last change.
[bpt/emacs.git] / lisp / vc / log-edit.el
index 27290ee..7ee000a 100644 (file)
@@ -1,7 +1,6 @@
-;;; log-edit.el --- Major mode for editing CVS commit messages
+;;; log-edit.el --- Major mode for editing CVS commit messages -*- lexical-binding: t -*-
 
-;; Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007,
-;;   2008, 2009, 2010  Free Software Foundation, Inc.
+;; Copyright (C) 1999-2012  Free Software Foundation, Inc.
 
 ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
 ;; Keywords: pcl-cvs cvs commit log vc
@@ -30,7 +29,6 @@
 
 ;;; Code:
 
-(eval-when-compile (require 'cl))
 (require 'add-log)                     ; for all the ChangeLog goodies
 (require 'pcvs-util)
 (require 'ring)
@@ -162,7 +160,7 @@ can be obtained from `log-edit-files'."
                        "21.1")
 
 (defvar log-edit-changelog-full-paragraphs cvs-changelog-full-paragraphs
-  "*If non-nil, include full ChangeLog paragraphs in the log.
+  "If non-nil, include full ChangeLog paragraphs in the log.
 This may be set in the ``local variables'' section of a ChangeLog, to
 indicate the policy for that ChangeLog.
 
@@ -192,11 +190,17 @@ when this variable is set to nil.")
 
 (defvar log-edit-parent-buffer nil)
 
+(defvar log-edit-vc-backend nil
+  "VC fileset corresponding to the current log.")
+
 ;;; Originally taken from VC-Log mode
 
 (defconst log-edit-maximum-comment-ring-size 32
   "Maximum number of saved comments in the comment ring.")
+(define-obsolete-variable-alias 'vc-comment-ring 'log-edit-comment-ring "22.1")
 (defvar log-edit-comment-ring (make-ring log-edit-maximum-comment-ring-size))
+(define-obsolete-variable-alias 'vc-comment-ring-index
+  'log-edit-comment-ring-index "22.1")
 (defvar log-edit-comment-ring-index nil)
 (defvar log-edit-last-comment-match "")
 
@@ -302,8 +306,6 @@ automatically."
        (insert "\n"))))
 
 ;; Compatibility with old names.
-(define-obsolete-variable-alias 'vc-comment-ring 'log-edit-comment-ring "22.1")
-(define-obsolete-variable-alias 'vc-comment-ring-index 'log-edit-comment-ring-index "22.1")
 (define-obsolete-function-alias 'vc-previous-comment 'log-edit-previous-comment "22.1")
 (define-obsolete-function-alias 'vc-next-comment 'log-edit-next-comment "22.1")
 (define-obsolete-function-alias 'vc-comment-search-reverse 'log-edit-comment-search-backward "22.1")
@@ -330,7 +332,7 @@ automatically."
 (defconst log-edit-header-contents-regexp
   "[ \t]*\\(.*\\(\n[ \t].*\\)*\\)\n?")
 
-(defun log-edit-match-to-eoh (limit)
+(defun log-edit-match-to-eoh (_limit)
   ;; FIXME: copied from message-match-to-eoh.
   (let ((start (point)))
     (rfc822-goto-eoh)
@@ -350,45 +352,90 @@ automatically."
 (defvar log-edit-font-lock-keywords
   ;; Copied/inspired by message-font-lock-keywords.
   `((log-edit-match-to-eoh
-     (,(concat "^\\(\\([a-z]+\\):\\)" log-edit-header-contents-regexp
-               "\\|\\(.*\\)")
+     (,(concat "^\\(\\([[:alpha:]]+\\):\\)" 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)
-      (4 font-lock-warning-face)))))
+         nil lax)))))
+
+(defvar log-edit-font-lock-gnu-style nil
+  "If non-nil, highlight common failures to follow the GNU coding standards.")
+(put 'log-edit-font-lock-gnu-style 'safe-local-variable 'booleanp)
+
+(defconst log-edit-font-lock-gnu-keywords
+    ;; Use
+    ;;   * foo.el (bla, bli)
+    ;;   (blo, blu): Toto.
+    ;; Rather than
+    ;;   * foo.el (bla, bli,
+    ;;   blo, blu): Toto.
+  '(("^[ \t]*\\(?:\\* .*\\)?\\(([^\n)]*,\\s-*\\)$"
+     (1 '(face font-lock-warning-face
+          help-echo "Continue function lists with \")\\n(\".") t))
+    ;; Don't leave a lone word on a single line.
+    ;;("^\\s-*\\(\\S-*[^\n:)]\\)\\s-*$" (1 font-lock-warning-face t))
+    ;; Don't cut a sentence right after the first word (better to move
+    ;; the sentence on the next line, then).
+    ;;("[.:]\\s-+\\(\\sw+\\)\\s-*$" (1 font-lock-warning-face t))
+    ;; Change Log entries should use present tense.
+    ("):[ \t\n]*[[:alpha:]]+\\(ed\\)\\>"
+     (1 '(face font-lock-warning-face help-echo "Use present tense.") t))
+    ;; Change log entries start with a capital letter.
+    ("): [a-z]" (0 '(face font-lock-warning-face help-echo "Capitalize.") t))
+    ("[^[:upper:]]\\(\\. [[:upper:]]\\)"
+     (1 '(face font-lock-warning-face
+          help-echo "Use two spaces to end a sentence") t))
+    ("^("
+     (0 (let ((beg (max (point-min) (- (match-beginning 0) 2))))
+          (put-text-property beg (match-end 0) 'font-lock-multiline t)
+          (if (eq (char-syntax (char-after beg)) ?w)
+              '(face font-lock-warning-face
+                help-echo "Punctuate previous line.")))
+        t))
+    ))
+
+(defun log-edit-font-lock-keywords ()
+  (if log-edit-font-lock-gnu-style
+      (append log-edit-font-lock-keywords
+              log-edit-font-lock-gnu-keywords)
+    log-edit-font-lock-keywords))
 
 ;;;###autoload
-(defun log-edit (callback &optional setup params buffer mode &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 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].
-Once you're done editing the message, pressing \\[log-edit-done] will call
-`log-edit-done' which will end up calling CALLBACK to do the actual commit.
-
-PARAMS if non-nil is an alist.  Possible keys and associated values:
+The buffer is put in mode MODE or `log-edit-mode' if MODE is nil.
+\\<log-edit-mode-map>
+If SETUP is non-nil, erase the buffer and run `log-edit-hook'.
+Set mark and point around the entire contents of the buffer, so
+that it is easy to kill the contents of the buffer with
+\\[kill-region].  Once the user is done editing the message,
+invoking the command \\[log-edit-done] (`log-edit-done') will
+call CALLBACK to do the actual commit.
+
+PARAMS if non-nil is an alist of variables and buffer-local
+values to give them in the Log Edit buffer.  Possible keys and
+associated values:
  `log-edit-listfun' -- function taking no arguments that returns the list of
  files that are concerned by the current operation (using relative names);
  `log-edit-diff-function' -- function taking no arguments that
  displays a diff of the files concerned by the current operation.
+ `vc-log-fileset' -- the VC fileset to be committed (if any).
 
-If BUFFER is non-nil `log-edit' will jump to that buffer, use it to edit the
-log message and go back to the current buffer when done.  Otherwise, it
-uses the current buffer."
+If BUFFER is non-nil `log-edit' will jump to that buffer, use it
+to edit the log message and go back to the current buffer when
+done.  Otherwise, it uses the current buffer."
   (let ((parent (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)
-      (insert "Summary: ")
+      (insert "Summary: \nAuthor: ")
       (save-excursion (insert "\n\n")))
     (if mode
        (funcall mode)
@@ -418,7 +465,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 t))
+       '(log-edit-font-lock-keywords t))
   (make-local-variable 'log-edit-comment-ring-index)
   (hack-dir-local-variables-non-file-buffer))
 
@@ -533,13 +580,25 @@ If you want to abort the commit, simply delete the buffer."
        (shrink-window-if-larger-than-buffer)
        (selected-window)))))
 
+(defun log-edit-empty-buffer-p ()
+  "Return non-nil if the buffer is \"empty\"."
+  (or (= (point-min) (point-max))
+      (save-excursion
+        (goto-char (point-min))
+        (while (and (looking-at "^\\([a-zA-Z]+: \\)?$")
+                    (zerop (forward-line 1))))
+        (eobp))))
+
 (defun log-edit-insert-cvs-template ()
   "Insert the template specified by the CVS administrator, if any.
 This simply uses the local CVS/Template file."
   (interactive)
   (when (or (called-interactively-p 'interactive)
-           (= (point-min) (point-max)))
+            (log-edit-empty-buffer-p))
+    ;; Should the template take precedence over an empty Summary:,
+    ;; ie should we first erase the buffer?
     (when (file-readable-p "CVS/Template")
+      (goto-char (point-max))
       (insert-file-contents "CVS/Template"))))
 
 (defun log-edit-insert-cvs-rcstemplate ()
@@ -548,8 +607,9 @@ This contacts the repository to get the rcstemplate file and
 can thus take some time."
   (interactive)
   (when (or (called-interactively-p 'interactive)
-           (= (point-min) (point-max)))
+            (log-edit-empty-buffer-p))
     (when (file-readable-p "CVS/Root")
+      (goto-char (point-max))
       ;; Ignore the stderr stuff, even if it's an error.
       (call-process "cvs" nil '(t nil) nil
                     "checkout" "-p" "CVSROOT/rcstemplate"))))
@@ -572,6 +632,23 @@ 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-add-field (field value)
+  (rfc822-goto-eoh)
+  (if (save-excursion (re-search-backward (concat "^" field ":\\([ \t]*\\)$")
+                                          nil t))
+      (replace-match (concat " " value) t t nil 1)
+    (insert field ": " value "\n" (if (looking-at "\n") "" "\n"))))
+
 (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
@@ -593,18 +670,35 @@ regardless of user name or time."
     (when (<= (point) eoh)
       (goto-char eoh)
       (if (looking-at "\n") (forward-char 1))))
-  (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 ((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 (log-edit-add-field "Author" author))
+    ;; 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)
+          (log-edit-add-field "Fixes" fixes))))
+    (and log-edit-strip-single-file-name
+         (progn (rfc822-goto-eoh)
+                (if (looking-at "\n") (forward-char 1))
+                (looking-at "\\*\\s-+"))
+         (let ((start (point)))
+           (forward-line 1)
+           (when (not (re-search-forward "^\\*\\s-+" nil t))
+             (goto-char start)
+             (skip-chars-forward "^():")
+             (skip-chars-forward ": ")
+             (delete-region start (point)))))
+    (goto-char (point-min))))
 
 ;;;;
 ;;;; functions for getting commit message from ChangeLog a file...
@@ -670,6 +764,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."
@@ -684,9 +781,23 @@ 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.
@@ -732,7 +843,7 @@ 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
@@ -776,7 +887,8 @@ Rename relative filenames in the ChangeLog entry as FILES."
 
 (defun log-edit-insert-changelog-entries (files)
   "Given a list of files FILES, insert the ChangeLog entries for them."
-  (let ((log-entries nil))
+  (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...)
@@ -793,7 +905,8 @@ Rename relative filenames in the ChangeLog entry as FILES."
     (dolist (log-entry (nreverse log-entries))
       (apply 'log-edit-changelog-insert-entries
             (append (car log-entry) (cdr log-entry)))
-      (insert "\n"))))
+      (insert "\n"))
+    log-edit-author))
 
 (defun log-edit-extract-headers (headers comment)
   "Extract headers from COMMENT to form command line arguments.
@@ -831,5 +944,4 @@ anyway and put back as the first line of MSG."
 
 (provide 'log-edit)
 
-;; arch-tag: 8089b39c-983b-4e83-93cd-ed0a64c7fdcc
 ;;; log-edit.el ends here