;;; dvc-log.el --- Manipulation of the log before committing ;; Copyright (C) 2005-2008 by all contributors ;; Author: Matthieu Moy ;; Contributions from: ;; Stefan Reichoer, ;; This file is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;;; Code: (require 'dvc-unified) (require 'ediff) (require 'vc) (defcustom dvc-log-edit-other-frame nil "If non-nil, dvc-log-edit defaults to other-frame." :type 'boolean :group 'dvc) ;; ;; Log edit mode ;; (defvar dvc-log-edit-font-lock-keywords `(("^\t?\\* \\([^ ,:([\n]+\\)" (1 'change-log-file-face) ("\\=, \\([^ ,:([\n]+\\)" nil nil (1 'change-log-file-face)) ("\\= (\\([^) ,:\n]+\\)" nil nil (1 'change-log-list-face)) ("\\=, *\\([^) ,:\n]+\\)" nil nil (1 'change-log-list-face))) ;; (,(concat "^" (regexp-quote dvc-log-edit-file-list-marker) "$") ;; . 'dvc-header) ) "Keywords in dvc-log-edit mode.") (defvar dvc-log-edit-flush-prefix "## ") (defvar dvc-log-edit-file-list-marker "--This line, and those below, will be ignored--" "A marker separating the actual log message from the list of files to commit.") (defvar dvc-log-edit-init-functions (make-hash-table :test 'equal) "A hash table that holds the mapping from work directory roots to functions that provide the initial content for a commit.") ;; -------------------------------------------------------------------------------- ;; Menus ;; -------------------------------------------------------------------------------- ;;;###autoload (define-derived-mode dvc-log-edit-mode text-mode "dvc-log-edit" "Major Mode to edit DVC log messages. Commands: \\{dvc-log-edit-mode-map} " (setq dvc-buffer-current-active-dvc (dvc-current-active-dvc)) (use-local-map dvc-log-edit-mode-map) (easy-menu-add dvc-log-edit-mode-menu) (dvc-install-buffer-menu) (set (make-local-variable 'font-lock-defaults) '(dvc-log-edit-font-lock-keywords t)) (set (make-local-variable 'fill-paragraph-function) 'dvc-log-fill-paragraph) (setq fill-column 73) (when (eq (point-min) (point-max)) (dvc-log-edit-insert-initial-commit-message)) (run-hooks 'dvc-log-edit-mode-hook)) (define-key dvc-log-edit-mode-map [(control ?c) (control ?c)] 'dvc-log-edit-done) (define-key dvc-log-edit-mode-map [(control ?c) (control ?d)] 'dvc-diff) (define-key dvc-log-edit-mode-map [(control ?c) (control ?l)] 'dvc-log) (define-key dvc-log-edit-mode-map [(control ?c) (control ?f)] 'dvc-log-insert-commit-file-list) (define-key dvc-log-edit-mode-map [(control ?c) (control ?p)] 'dvc-buffer-pop-to-partner-buffer) (define-key dvc-log-edit-mode-map [(control ?c) (control ?m)] 'dvc-log-edit-insert-memorized-log) (define-key dvc-log-edit-mode-map [(control ?c) (control ?i)] 'dvc-log-edit-insert-initial-commit-message) (easy-menu-define dvc-log-edit-mode-menu dvc-log-edit-mode-map "`dvc-log-edit-mode' menu" '("Log Edit" ["Show changes" dvc-diff t] ["Commit" dvc-log-edit-done t] ["Show Changelog" dvc-log t] ["Pop to partner buffer" dvc-buffer-pop-to-partner-buffer t] ["Insert/Flush commit file list" dvc-log-insert-commit-file-list t] ["Insert memorized log" dvc-log-edit-insert-memorized-log t] "--" ["Abort" dvc-log-edit-abort t])) ;; Internal variables (defvar dvc-pre-commit-window-configuration nil) ;;;###autoload (defun dvc-dvc-log-edit (root other-frame no-init) "Edit the log file for tree ROOT before a commit. OTHER_FRAME if non-nil puts log edit buffer in a separate frame. NO-INIT if non-nil suppresses initialization of the buffer if one is reused." (setq dvc-pre-commit-window-configuration (current-window-configuration)) (let ((start-buffer (current-buffer))) (dvc-switch-to-buffer (dvc-get-buffer-create (dvc-current-active-dvc) 'log-edit) other-frame) ;; `no-init' is somewhat misleading here. It is set to t in ;; dvc-add-log-entry, and nil in dvc-log-edit. That prevents ;; changing dvc-partner-buffer when we shouldn't. But the user ;; might call dvc-log-edit multiple times from the same diff or ;; status buffer, and expect edits in the log-edit buffer to be ;; preserved. (unless no-init (let ((buffer-name (buffer-name)) (file-name (dvc-log-edit-file-name))) (set-visited-file-name file-name t t) ;; `set-visited-file-name' modifies default-directory (setq default-directory root) ;; Read in the current log file, unless the user has already ;; edited the buffer. (when (and (= (point-min) (point-max)) (file-readable-p file-name)) (insert-file-contents file-name) (set-buffer-modified-p nil)) (rename-buffer buffer-name) (setq dvc-partner-buffer start-buffer) (dvc-call "log-edit-mode"))))) (defun dvc-log-edit-abort () "Abort the current log edit." (interactive) (bury-buffer) (set-window-configuration dvc-pre-commit-window-configuration)) (defun dvc-log-close (buffer) "Close the log buffer, and delete the file." (if vc-delete-logbuf-window (kill-buffer buffer) (quit-window)) (delete-file (dvc-log-edit-file-name))) (defun dvc-log-flush-commit-file-list () "Remove the list of the files to commit. All lines starting with `dvc-log-edit-flush-prefix' are deleted." (interactive) (save-excursion (goto-char (point-min)) (flush-lines (concat "^" dvc-log-edit-flush-prefix)))) (defun dvc-log-fill-paragraph (&optional justify) "Fill the paragraph, but preserve open parentheses at beginning of lines. Prefix arg means justify as well." (interactive "P") (let ((end (progn (forward-paragraph) (point))) (beg (progn (backward-paragraph) (point))) (paragraph-start (concat paragraph-start "\\|\\s *\\s("))) (fill-region beg end justify) t)) (defun dvc-log-insert-commit-file-list (arg) "Insert the file list that will be committed. With a negative prefix argument just remove the file list by calling `dvc-log-flush-commit-file-list'." (interactive "p") (if (< arg 0) (dvc-log-flush-commit-file-list) (let ((file-list (funcall (dvc-function (dvc-current-active-dvc) "dvc-files-to-commit"))) (mark)) (dvc-trace "Files to commit: %S" file-list) (save-excursion (goto-char (point-min)) (dvc-log-flush-commit-file-list) (insert dvc-log-edit-flush-prefix) (insert (format "Lines beginning with '%s' will be deleted from this buffer before committing\n" dvc-log-edit-flush-prefix)) (insert dvc-log-edit-flush-prefix) (insert "Files to commit:\n") (dolist (f file-list) (setq mark (cdr (assoc (car f) '( (dvc-modified . "M ") (dvc-added . "A ") (dvc-deleted . "R ") )))) (insert dvc-log-edit-flush-prefix) (insert (dvc-face-add (concat mark (cdr f)) (car f))) (newline)))))) (defun dvc-log-edit-insert-memorized-log () "Insert a memorized log message." (interactive) (when dvc-memorized-log-header (goto-char (point-min)) (delete-region (point) (line-end-position)) (insert dvc-memorized-log-header)) (when dvc-memorized-log-message (goto-char (point-min)) (end-of-line) (newline) (newline) (when dvc-memorized-patch-sender (if (looking-at "Patch from ") (forward-line 1) (progn (undo-boundary) (insert (format "Patch from %s\n" dvc-memorized-patch-sender))))) (when (looking-at "\* .+: ") ;; e.g.: "* lisp/dvc.el: " (end-of-line) (newline)) (insert dvc-memorized-log-message))) ;;;###autoload (defun dvc-add-log-entry (&optional other-frame) "Add new ChangeLog style entry to the current DVC log-edit buffer. If OTHER-FRAME xor `dvc-log-edit-other-frame' is non-nil, show log-edit buffer in other frame." (interactive "P") (save-restriction (dvc-add-log-entry-internal other-frame))) (defun dvc-add-log-file-name (buffer-file) "Return a file name for a log entry for BUFFER-FILE; including path from tree root. For use as add-log-file-name-function." ;; This is better than the default algorithm in add-log-file-name, ;; when the log file is not in the workspace root (as is true for ;; monotone) (if (string-match (concat "^" (regexp-quote (dvc-tree-root))) buffer-file) (substring buffer-file (match-end 0)) (file-name-nondirectory buffer-file))) (defun dvc-ediff-add-log-entry (&optional other-frame) "Add new DVC log ChangeLog style entry; intended to be invoked from the ediff control buffer." (interactive "P") (let ((dvc-temp-current-active-dvc dvc-buffer-current-active-dvc)) (set-buffer ediff-buffer-B) ; DVC puts workspace version here (dvc-add-log-entry-internal other-frame))) (defun dvc-ediff-setup () (define-key 'ediff-mode-map "t" 'dvc-ediff-add-log-entry)) ; matches dvc-diff-mode-map ;; ediff hooks that run after ediff-mode-map is created: ;; ediff-prepare-buffer-hook, ediff-startup-hook (add-hook 'ediff-startup-hook 'dvc-ediff-setup) (defun dvc-add-log-entry-internal (other-frame) "Similar to `add-change-log-entry'. Inserts the entry in the dvc log-edit buffer instead of the ChangeLog." ;; This is mostly copied from add-log.el. Perhaps it would be better to ;; split add-change-log-entry into several functions and then use them, but ;; that wouldn't work with older versions of Emacs. ;; ;; We don't set add-log-file-name-function globally because ;; dvc-diff-mode needs a different one. (if (not (featurep 'add-log)) (require 'add-log)) (let* ((dvc-temp-current-active-dvc (dvc-current-active-dvc)) (add-log-file-name-function 'dvc-add-log-file-name) (defun (add-log-current-defun)) (buf-file-name (if (and (boundp 'add-log-buffer-file-name-function) add-log-buffer-file-name-function) (funcall add-log-buffer-file-name-function) buffer-file-name)) (buffer-file (if buf-file-name (expand-file-name buf-file-name))) (file-name (dvc-log-edit-file-name)) ;; Set ENTRY to the file name to use in the new entry. (entry (add-log-file-name buffer-file file-name)) beg bound narrowing) (dvc-log-edit other-frame t) (undo-boundary) (goto-char (point-min)) (when (re-search-forward (regexp-opt (list "^Patches applied:" (regexp-quote ;; TODO dvc-log-edit-file-list-marker))) nil t) (narrow-to-region (point-min) (match-beginning 0)) (setq narrowing t) (goto-char (point-min))) (re-search-forward "\n\n\\|\\'") (setq beg (point)) (if (looking-at "\n*[^\n* \t]") (progn (skip-chars-forward "\n") (setq bound (point))) (goto-char (point-max)) (setq bound (point)) (unless (and (boundp 'add-log-keep-changes-together) add-log-keep-changes-together) (backward-paragraph) ; paragraph delimits entries for file (forward-line 1) (setq beg (point)))) (goto-char beg) (forward-line -1) ;; Now insert the new line for this entry. (cond ((re-search-forward "^\\s *\\*\\s *$" bound t) ;; Put this file name into the existing empty entry. (if entry (insert entry))) ((let (case-fold-search) (re-search-forward (concat (regexp-quote (concat "* " entry)) ;; Don't accept `foo.bar' when ;; looking for `foo': "\\(\\s \\|[(),:]\\)") bound t)) ;; Add to the existing entry for the same file. (if (re-search-forward "^\\s *$\\|^\\s \\*" nil t) (goto-char (match-beginning 0)) (goto-char (point-max)) (insert-char ?\n 1)) ;; Delete excess empty lines; make just 2. (while (and (not (eobp)) (looking-at "^\\s *$")) (delete-region (point) (line-beginning-position 2))) (insert-char ?\n 2) (forward-line -2) (indent-relative)) (t ;; Make a new entry. (if dvc-log-insert-last (progn (goto-char (point-max)) (re-search-backward "^." nil t) (end-of-line) (insert "\n\n* ") ) (forward-line 1) (while (looking-at "\\sW") (forward-line 1)) (while (and (not (eobp)) (looking-at "^\\s *$")) (delete-region (point) (line-beginning-position 2))) (insert-char ?\n 3) (forward-line -2) (indent-to left-margin) (insert "* ")) (if entry (insert entry)))) (if narrowing (widen)) ;; Now insert the function name, if we have one. ;; Point is at the entry for this file, ;; either at the end of the line or at the first blank line. (if defun (progn ;; Make it easy to get rid of the function name. (undo-boundary) (unless (save-excursion (beginning-of-line 1) (looking-at "\\s *$")) (insert ?\ )) ;; See if the prev function name has a message yet or not ;; If not, merge the two entries. (let ((pos (point-marker))) (if (and (skip-syntax-backward " ") (skip-chars-backward "):") (looking-at "):") (progn (delete-region (+ 1 (point)) (+ 2 (point))) t) (> fill-column (+ (current-column) (length defun) 3))) (progn (delete-region (point) pos) (insert ", ")) (goto-char pos) (insert "(")) (set-marker pos nil)) ;; Check for previous function name using re-search-backward ;; instead of looking-back, because looking-back is not ;; implemented in all variants of (X)Emacs. We could create ;; a compatibility function for it, but nobody else seems to ;; use it yet, so there is no point. (when (re-search-backward (concat (regexp-quote defun) ",\\s *\\=") nil t) (replace-match "")) (insert defun "): ")) ;; No function name, so put in a colon unless we have just a star. (unless (save-excursion (beginning-of-line 1) (looking-at "\\s *\\(\\*\\s *\\)?$")) (insert ": "))))) (defun dvc-log-edit-register-initial-content-function (working-copy-root the-function) "Register a mapping from a work directory root to a function that provide the initial content for a commit." (puthash (dvc-uniquify-file-name working-copy-root) the-function dvc-log-edit-init-functions)) (defun dvc-log-edit-insert-initial-commit-message () "Insert the initial commit message at point. See `dvc-log-edit-register-initial-content-function' to register functions that provide the message text." (interactive) (let ((initial-content-function (gethash (dvc-uniquify-file-name (dvc-tree-root)) dvc-log-edit-init-functions))) (when initial-content-function (insert (funcall initial-content-function))))) (provide 'dvc-log) ;;; dvc-log.el ends here