* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable @ 2013-12-30 12:43 Daniel Dehennin 2014-01-02 4:12 ` Stefan Monnier 2019-07-05 1:57 ` Noam Postavsky 0 siblings, 2 replies; 6+ messages in thread From: Daniel Dehennin @ 2013-12-30 12:43 UTC (permalink / raw) To: 16301 [-- Attachment #1: Type: text/plain, Size: 3153 bytes --] Hello, I just start to use magit in complement[1] of vc-git and was wondering why there is no feature like “add-change-log-entry(-other-window)” that insert in the commit log instead of creating/adding to a plain ChangeLog. I finally found[2] the “magit-commit-add-log” which behave like what I had in DVC[3]. All these functions do near the same thing but code is duplicated. I propose to split the “add-change-log-entry” function to make parts usable by third parties. You can consider the following as pure product of my damaged brain: A templating system could avoid much of third parties code by supporting more than one changelog format: 1. Provide the changelog file name: .git/COMMIT_EDITMSG for magit 2. Set buffer parameter “new entry template”: this could be useful to inform about best practice, for example, magit could use: #+begin_src git-commit Subject == Why this commit is necessary? Long description of what was wrong before this commit. * file (function): short description of the change. #+end_src If possible: - each part should vanish when user modify the line by some kind of “special property”: + user place point on first line + user hit “A” + “special property” delete the line, the “special property” is removed for this line (not the others) + “A” is inserted a pos 0 of first line - function calls should be usable in template, for example to fill the list of modified files instead of a static vanish-on-write example 3. Provide offset for items: 0 for magit as in the previous template I don't know the feasibility of a such feature, but I think it could be great. Regards. In GNU Emacs 24.3.50.1 (x86_64-pc-linux-gnu, GTK+ Version 3.8.6) of 2013-12-26 on prometheus, modified by Debian (emacs-snapshot package, version 2:20131226-1) Windowing system distributor `The X.Org Foundation', version 11.0.11403000 Configured using: `configure --build x86_64-linux-gnu --host x86_64-linux-gnu --prefix=/usr --sharedstatedir=/var/lib --libexecdir=/usr/lib --localstatedir=/var --infodir=/usr/share/info/emacs-snapshot --mandir=/usr/share/man --with-pop=yes --enable-locallisppath=/etc/emacs-snapshot:/etc/emacs:/usr/local/share/emacs/24.3.50/site-lisp:/usr/local/share/emacs/site-lisp:/usr/share/emacs/24.3.50/site-lisp:/usr/share/emacs/site-lisp --with-crt-dir=/usr/lib/x86_64-linux-gnu/ --with-x=yes --with-x-toolkit=gtk3 --with-imagemagick=yes 'CFLAGS=-DDEBIAN -DSITELOAD_PURESIZE_EXTRA=5000 -g -O2' CPPFLAGS=-D_FORTIFY_SOURCE=2 'LDFLAGS=-g -Wl,--as-needed -znocombreloc'' Important settings: value of $LANG: fr_FR.UTF-8 locale-coding-system: utf-8-unix Major mode: Magit Footnotes: [1] http://git.baby-gnu.net/gitweb/gitweb.cgi?p=user/dad/config/emacs.git;a=blob;f=lisp/startup.d/VC.el [2] https://github.com/magit/magit/issues/1130 [3] http://www.emacswiki.org/emacs/DistributedVersionControl -- Daniel Dehennin Récupérer ma clef GPG: gpg --keyserver pgp.mit.edu --recv-keys 0x7A6FE2DF [-- Attachment #2: Type: application/pgp-signature, Size: 229 bytes --] ^ permalink raw reply [flat|nested] 6+ messages in thread
* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable 2013-12-30 12:43 bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable Daniel Dehennin @ 2014-01-02 4:12 ` Stefan Monnier 2019-07-05 1:57 ` Noam Postavsky 1 sibling, 0 replies; 6+ messages in thread From: Stefan Monnier @ 2014-01-02 4:12 UTC (permalink / raw) To: Daniel Dehennin; +Cc: 16301 > I just start to use magit in complement[1] of vc-git and was wondering > why there is no feature like “add-change-log-entry(-other-window)” that > insert in the commit log instead of creating/adding to a plain > ChangeLog. Indeed, we need this feature. For VC as well. Stefan ^ permalink raw reply [flat|nested] 6+ messages in thread
* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable 2013-12-30 12:43 bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable Daniel Dehennin 2014-01-02 4:12 ` Stefan Monnier @ 2019-07-05 1:57 ` Noam Postavsky 2019-07-16 23:47 ` Noam Postavsky 1 sibling, 1 reply; 6+ messages in thread From: Noam Postavsky @ 2019-07-05 1:57 UTC (permalink / raw) To: Daniel Dehennin; +Cc: 16301 [-- Attachment #1: Type: text/plain, Size: 713 bytes --] Daniel Dehennin <daniel.dehennin@baby-gnu.org> writes: > I just start to use magit in complement[1] of vc-git and was wondering > why there is no feature like “add-change-log-entry(-other-window)” that > insert in the commit log instead of creating/adding to a plain > ChangeLog. > > I finally found[2] the “magit-commit-add-log” which behave like what I > had in DVC[3]. > > All these functions do near the same thing but code is duplicated. > > I propose to split the “add-change-log-entry” function to make parts > usable by third parties. I've started working on this, patch attached below. It's not entirely baked, but I thought I'd post it now to let people know about it. [-- Attachment #2: patch --] [-- Type: text/plain, Size: 13884 bytes --] From 00b2e60143b9ecfbe86e993d2c29b76514368fb4 Mon Sep 17 00:00:00 2001 From: Noam Postavsky <npostavs@gmail.com> Date: Thu, 4 Jul 2019 20:32:39 -0400 Subject: [PATCH] Improved ChangeLog generation for vc log (Bug#16301) * lisp/vc/add-log.el (change-log-unindented-file-names-re) (change-log-read-entries, change-log-read-defuns) (change-log-insert-entries): New functions. * lisp/vc/diff-mode.el (diff-find-source-location): Fix docstring. (diff-add-log-current-defuns): New function. * lisp/vc/log-edit.el (log-edit-generate-changelog): New command. (log-edit-mode-map): Bind it to C-c C-a. (log-edit-fill-entry): New function. (log-edit-mode): Set it as fill-paragraph-function. (log-edit-insert-filled-defuns): (change-log-no-margin-fill-forward-paragraph): New functions. --- lisp/vc/add-log.el | 18 ++++++++ lisp/vc/diff-mode.el | 115 +++++++++++++++++++++++++++++++++++++++++++++++++-- lisp/vc/log-edit.el | 81 +++++++++++++++++++++++++++++++++++- 3 files changed, 209 insertions(+), 5 deletions(-) diff --git a/lisp/vc/add-log.el b/lisp/vc/add-log.el index f9efd44c5c..4810c0dbe9 100644 --- a/lisp/vc/add-log.el +++ b/lisp/vc/add-log.el @@ -309,6 +309,24 @@ change-log-search-file-name (re-search-forward change-log-file-names-re nil t) (match-string-no-properties 2)))))) +(defconst change-log-unindented-file-names-re "^[*] \\([^ ,:([\n]+\\)") + +(defun change-log-read-entries (&optional end) + (cl-loop while (re-search-forward change-log-unindented-file-names-re end t) + collect (cons (match-string-no-properties 1) + (change-log-read-defuns end)))) + +(defun change-log-read-defuns (&optional end) + (cl-loop while (re-search-forward change-log-tag-re end t) + nconc (split-string (match-string-no-properties 1) + ",[[:blank:]]*" t))) + +(defun change-log-insert-entries (changelogs) + (cl-loop for (file . defuns) in changelogs do + (insert "* " file " ") + (cl-loop for def in defuns + do (insert "(" def "):\n")))) + (defun change-log-find-file () "Visit the file for the change under point." (interactive) diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el index 0d5dc0e1c0..229e901e0a 100644 --- a/lisp/vc/diff-mode.el +++ b/lisp/vc/diff-mode.el @@ -54,6 +54,7 @@ ;;; Code: (eval-when-compile (require 'cl-lib)) +(eval-when-compile (require 'subr-x)) (autoload 'vc-find-revision "vc") (autoload 'vc-find-revision-no-save "vc") @@ -1773,15 +1774,22 @@ diff-find-approx-text (defsubst diff-xor (a b) (if a (if (not b) a) b)) (defun diff-find-source-location (&optional other-file reverse noprompt) - "Find out (BUF LINE-OFFSET POS SRC DST SWITCHED). + "Find current diff location within the source file. +OTHER-FILE, if non-nil, means to look at the diff's name and line + numbers for the old file. Furthermore, use `diff-vc-revisions' + if it's available. If `diff-jump-to-old-file' is non-nil, the + sense of this parameter is reversed. If the prefix argument is + 8 or more, `diff-jump-to-old-file' is set to OTHER-FILE. +REVERSE, if non-nil, switches the sense of SRC and DST (see below). +NOPROMPT, if non-nil, means not to prompt the user. +Return a list (BUF LINE-OFFSET (BEG . END) SRC DST SWITCHED). BUF is the buffer corresponding to the source file. LINE-OFFSET is the offset between the expected and actual positions of the text of the hunk or nil if the text was not found. -POS is a pair (BEG . END) indicating the position of the text in the buffer. +\(BEG . END) is a pair indicating the position of the text in the buffer. SRC and DST are the two variants of text as returned by `diff-hunk-text'. SRC is the variant that was found in the buffer. -SWITCHED is non-nil if the patch is already applied. -NOPROMPT, if non-nil, means not to prompt the user." +SWITCHED is non-nil if the patch is already applied." (save-excursion (let* ((other (diff-xor other-file diff-jump-to-old-file)) (char-offset (- (point) (diff-beginning-of-hunk t))) @@ -2210,6 +2218,105 @@ diff-undo (let ((inhibit-read-only t)) (undo arg))) +(defun diff-add-log-current-defuns () + "Return an alist of defun names for the current diff. +The elements of the alist are of the form (FILE . (DEFUN...)), +where DEFUN... is a list of function names found in FILE." + (save-excursion + (goto-char (point-min)) + (let ((defuns nil) + (hunk-end nil) + (make-defun-context-follower + (lambda (goline) + (let ((eodefun nil) + (defname nil)) + (list + (lambda () ;; Check for end of current defun. + (when (and eodefun + (funcall goline) + (>= (point) eodefun)) + (setq defname nil) + (setq eodefun nil))) + (lambda (&optional get-current) ;; Check for new defun. + (if get-current + defname + (when-let* ((def (and (not eodefun) + (funcall goline) + (add-log-current-defun))) + (eof (save-excursion (end-of-defun) (point)))) + (setq eodefun eof) + (setq defname def))))))))) + (while + ;; Might need to skip over file headers between diff + ;; hunks (e.g., "diff --git ..." etc). + (re-search-forward diff-hunk-header-re nil t) + (setq hunk-end (save-excursion (diff-end-of-hunk))) + (pcase-let* ((filename (substring-no-properties (diff-find-file-name))) + (=lines 0) + (+lines 0) + (-lines 0) + (`(,buf ,_line-offset (,beg . ,end) + (,old-text . ,_old-offset) + (,new-text . ,_new-offset) + ,applied) + (diff-find-source-location t)) + (new-buf nil) + (goto-newbuf + ;; If APPLIED, we have NEW-TEXT in BUF, so we + ;; need to a buffer with OLD-TEXT to follow + ;; -lines. + (lambda () + (if new-buf (set-buffer new-buf) + (set-buffer (generate-new-buffer " *diff-new-text*")) + (insert (if applied old-text new-text)) + (funcall (buffer-local-value 'major-mode buf)) + (setq new-buf (current-buffer))) + (goto-char (point-min)) + (forward-line (+ =lines -1 + (if applied -lines +lines))))) + (gotobuf (lambda () + (set-buffer buf) + (goto-char beg) + (forward-line (+ =lines -1 + (if applied +lines -lines))))) + (`(,=ck-eodefun ,=ck-defun) + (funcall make-defun-context-follower gotobuf)) + (`(,-ck-eodefun ,-ck-defun) + (funcall make-defun-context-follower + (if applied goto-newbuf gotobuf))) + (`(,+ck-eodefun ,+ck-defun) + (funcall make-defun-context-follower + (if applied gotobuf goto-newbuf)))) + (unwind-protect + (while (progn (forward-line) + (< (point) hunk-end)) + (let ((patch-char (char-after))) + (pcase patch-char + (?+ (cl-incf +lines)) + (?- (cl-incf -lines)) + (?\s (cl-incf =lines))) + (save-current-buffer + (funcall =ck-eodefun) + (funcall +ck-eodefun) + (funcall -ck-eodefun) + (when-let* ((def (cond + ((eq patch-char ?\s) + ;; Just updating context defun. + (ignore (funcall =ck-defun))) + ;; + or - in existing defun. + ((funcall =ck-defun t)) + ;; Check added or removed defun. + (t (funcall (if (eq ?+ patch-char) + +ck-defun -ck-defun)))))) + (cl-pushnew def (alist-get filename defuns + nil nil #'equal) + :test #'equal))))) + (when (buffer-live-p new-buf) + (kill-buffer new-buf))))) + (dolist (file-defuns defuns) + (cl-callf nreverse (cdr file-defuns))) + (nreverse defuns)))) + (defun diff-add-change-log-entries-other-window () "Iterate through the current diff and create ChangeLog entries. I.e. like `add-change-log-entry-other-window' but applied to all hunks." diff --git a/lisp/vc/log-edit.el b/lisp/vc/log-edit.el index 91e18c1ec5..736bcaf0fa 100644 --- a/lisp/vc/log-edit.el +++ b/lisp/vc/log-edit.el @@ -53,7 +53,8 @@ cvs-buffer (easy-mmode-defmap log-edit-mode-map '(("\C-c\C-c" . log-edit-done) - ("\C-c\C-a" . log-edit-insert-changelog) + ;;("\C-c\C-a" . log-edit-insert-changelog) + ("\C-c\C-a" . log-edit-generate-changelog) ("\C-c\C-d" . log-edit-show-diff) ("\C-c\C-f" . log-edit-show-files) ("\C-c\C-k" . log-edit-kill-buffer) @@ -488,10 +489,73 @@ log-edit-mode (set (make-local-variable 'font-lock-defaults) '(log-edit-font-lock-keywords t)) (setq-local jit-lock-contextually t) ;For the "first line is summary". + (setq-local fill-paragraph-function #'log-edit-fill-entry) (make-local-variable 'log-edit-comment-ring-index) (add-hook 'kill-buffer-hook 'log-edit-remember-comment nil t) (hack-dir-local-variables-non-file-buffer)) +(defun log-edit-insert-filled-defuns (defuns) + (cl-loop for def in defuns do + (when (> (+ (current-column) (string-width def)) fill-column) + (insert (if (memq (char-before) '(?\n ?\s)) + "\n" ")\n"))) + (insert (if (memq (char-before) '(?\n ?\s)) + "(" ", ") + def)) + (insert "): ")) + +(defun log-edit-fill-entry (&optional _justify _region) + ;; TODO: Use arguments! + (pcase-let ((`(,beg ,entry-end) (log-edit-changelog-subparagraph))) + (if (= beg entry-end) + ;; Not a ChangeLog entry, fill as normal. + nil + (cl-callf copy-marker entry-end) + (cl-loop + do (goto-char beg) + (let ((end (if (re-search-forward "): ?\\(\\).*[^:[:blank:]\n].*$" + entry-end t) + (match-beginning 1) + entry-end))) + (goto-char beg) + (re-search-forward (concat "\\(?1:" change-log-unindented-file-names-re + " \\)\\|^\\(?1:\\)(") + entry-end) + (goto-char (setq beg (match-end 1))) + (log-edit-insert-filled-defuns + (prog1 (change-log-read-defuns end) + (delete-region beg end)))) + while (setq beg (and (re-search-forward "^(" entry-end t) + (match-beginning 0)))) + (set-marker entry-end nil) + t))) + +(defun change-log-no-margin-fill-forward-paragraph (n) + "Move N change log entries forward. +Delete redundant parens along the way." + (let ((end-marker (make-marker)) + (dir (cl-signum n))) + (cl-callf abs n) + (catch 'paragraphs-left + (dotimes (i n) + (pcase-let ((`(,beg ,end) (log-edit-changelog-subparagraph))) + (when (= end beg) + (throw 'paragraphs-left (- n i))) + (goto-char beg) + (set-marker end-marker end) + (cl-loop + do (progn (when (and (re-search-forward "):?$" (line-end-position) 'move) + (eq ?\( (char-after (1+ (match-end 0))))) + (replace-match "," t t)) + (forward-line 1)) + while (< (point) end-marker) + do (when (looking-at "^(") + (replace-match "" t t))) + (goto-char (if (< dir 0) beg (1- end-marker))) + (set-marker end-marker nil) + )) + 0))) + (defun log-edit-hide-buf (&optional buf where) (when (setq buf (get-buffer (or buf log-edit-files-buf))) ;; FIXME: Should use something like `quit-windows-on' here, but @@ -726,6 +790,21 @@ log-edit-add-field (replace-match (concat " " value) t t nil 1) (insert field ": " value "\n" (if (looking-at "\n") "" "\n")))) +(defun log-edit-generate-changelog () + (interactive) + (let* ((diff-buf nil) + ;; Unfortunately, `log-edit-show-diff' doesn't have a NO-SHOW + ;; option, so we try to work around it via display-buffer + ;; machinery. + (display-buffer-overriding-action + `(,(lambda (buf alist) + (setq diff-buf buf) + (display-buffer-no-window buf alist)) + . ((allow-no-window . t))))) + (change-log-insert-entries + (with-current-buffer (progn (log-edit-show-diff) diff-buf) + (diff-add-log-current-defuns))))) + (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 -- 2.11.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable 2019-07-05 1:57 ` Noam Postavsky @ 2019-07-16 23:47 ` Noam Postavsky 2019-07-28 14:13 ` Noam Postavsky 0 siblings, 1 reply; 6+ messages in thread From: Noam Postavsky @ 2019-07-16 23:47 UTC (permalink / raw) To: Daniel Dehennin; +Cc: 16301 [-- Attachment #1: Type: text/plain, Size: 634 bytes --] tags 16301 + patch quit >> I propose to split the “add-change-log-entry” function to make parts >> usable by third parties. > > I've started working on this, patch attached below. It's not entirely > baked, but I thought I'd post it now to let people know about it. I've made enough progress that this should be usable by now. One problem I noticed is that add-log-current-defun returns "foo" for lines like (require 'foo) Which is usually not wanted. So we might try to make add-log-current-defun a bit more discriminating, although missing definitions might be more annoying than false positives. [-- Attachment #2: patch --] [-- Type: text/plain, Size: 20523 bytes --] From 74f28ab974fd1690381f0a4f3e47f95c8ab83ce0 Mon Sep 17 00:00:00 2001 From: Noam Postavsky <npostavs@gmail.com> Date: Thu, 4 Jul 2019 20:32:39 -0400 Subject: [PATCH] Improved ChangeLog generation for vc log (Bug#16301) * lisp/vc/diff-mode.el (diff-find-source-location): Fix docstring. * lisp/vc/add-log.el (change-log-unindented-file-names-re) (change-log-read-entries, change-log-read-defuns) (change-log-insert-entries): * lisp/vc/diff-mode.el (diff-add-log-current-defuns): * lisp/vc/log-edit.el (log-edit--insert-filled-defuns) (log-edit-fill-entry): New functions. (log-edit-generate-changelog-from-diff): New command. (log-edit-mode-map): Bind it to C-c C-w. (log-edit-mode): Set it as `fill-paragraph-function'. * doc/emacs/maintaining.texi (Types of Log File, Log Buffer): Document it. * etc/NEWS: Announce it. * test/lisp/vc/log-edit-tests.el (log-edit-fill-entry): New test. --- doc/emacs/maintaining.texi | 13 ++++- etc/NEWS | 4 ++ lisp/vc/add-log.el | 36 ++++++++++++ lisp/vc/diff-mode.el | 127 +++++++++++++++++++++++++++++++++++++++-- lisp/vc/log-edit.el | 67 ++++++++++++++++++++++ test/lisp/vc/log-edit-tests.el | 90 +++++++++++++++++++++++++++++ 6 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 test/lisp/vc/log-edit-tests.el diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index 4986c11103..da500338a9 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -396,8 +396,9 @@ Types of Log File for each change just once, then put it into both logs. You can write the entry in @file{ChangeLog}, then copy it to the log buffer with @kbd{C-c C-a} when committing the change (@pxref{Log Buffer}). Or you -can write the entry in the log buffer while committing the change, and -later use the @kbd{C-x v a} command to copy it to @file{ChangeLog} +can write the entry in the log buffer while committing the change +(with the help of @kbd{C-c C-w}), and later use the @kbd{C-x v a} +command to copy it to @file{ChangeLog} @iftex (@pxref{Change Logs and VC,,,emacs-xtra, Specialized Emacs Features}). @end iftex @@ -677,6 +678,14 @@ Log Buffer started editing (@pxref{Old Revisions}), type @kbd{C-c C-d} (@code{log-edit-show-diff}). +@kindex C-c C-w @r{(Log Edit mode)} +@findex log-edit-generate-changelog + To help generate ChangeLog entries, type @kbd{C-c C-w} +(@code{log-edit-generate-changelog}), to generate skeleton ChangeLog +entries, listing all changed file and function names based on the diff +of the VC fileset. Consecutive entries left empty will be combined by +@kbd{C-q} (@code{fill-paragraph}). + @kindex C-c C-a @r{(Log Edit mode)} @findex log-edit-insert-changelog If the VC fileset includes one or more @file{ChangeLog} files diff --git a/etc/NEWS b/etc/NEWS index 06d5e93d02..f7b4f041f4 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -649,6 +649,10 @@ The default value is 'find-dired-sort-by-filename'. ** Change Logs and VC ++++ +*** New command 'log-edit-generate-changelog', bound to C-c C-w. +This generates ChangeLog entries from the VC fileset diff. + *** Recording ChangeLog entries doesn't require an actual file. If a ChangeLog file doesn't exist, and if the new variable 'add-log-dont-create-changelog-file' is non-nil (which is the diff --git a/lisp/vc/add-log.el b/lisp/vc/add-log.el index f9efd44c5c..df665887f1 100644 --- a/lisp/vc/add-log.el +++ b/lisp/vc/add-log.el @@ -36,6 +36,8 @@ ;;; Code: +(eval-when-compile (require 'cl-lib)) + (defgroup change-log nil "Change log maintenance." :group 'tools @@ -309,6 +311,40 @@ change-log-search-file-name (re-search-forward change-log-file-names-re nil t) (match-string-no-properties 2)))))) +(defconst change-log-unindented-file-names-re "^[*] \\([^ ,:([\n]+\\)") + +(defun change-log-read-entries (&optional end) + "Read ChangeLog entries at point until END. +Move point to the end of entries that were read. Return a list +in the same form as `diff-add-log-current-defuns'." + (cl-loop while (and (or (not end) (< (point) end)) + (looking-at change-log-unindented-file-names-re)) + do (goto-char (match-end 0)) + collect (cons (match-string-no-properties 1) + (change-log-read-defuns end)))) + +(defvar change-log-tag-re) ; add-log.el +(defun change-log-read-defuns (&optional end) + "Read ChangeLog formatted function names at point until END. +Move point to the end of names read and return the function names +as a list of strings." + (cl-loop while (and (skip-chars-forward ":\n[:blank:]" end) + (or (not end) (< (point) end)) + (looking-at change-log-tag-re)) + do (goto-char (match-end 0)) + nconc (split-string (match-string-no-properties 1) + ",[[:blank:]]*" t) + finally do (skip-chars-backward "\n[:blank:]"))) + +(defun change-log-insert-entries (changelogs) + "Format and insert CHANGELOGS into current buffer. +CHANGELOGS is a list in the form returned by +`diff-add-log-current-defuns'." + (cl-loop for (file . defuns) in changelogs do + (insert "* " file " ") + (cl-loop for def in defuns + do (insert "(" def "):\n")))) + (defun change-log-find-file () "Visit the file for the change under point." (interactive) diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el index 0d5dc0e1c0..2397d43e16 100644 --- a/lisp/vc/diff-mode.el +++ b/lisp/vc/diff-mode.el @@ -54,6 +54,7 @@ ;;; Code: (eval-when-compile (require 'cl-lib)) +(eval-when-compile (require 'subr-x)) (autoload 'vc-find-revision "vc") (autoload 'vc-find-revision-no-save "vc") @@ -1773,15 +1774,22 @@ diff-find-approx-text (defsubst diff-xor (a b) (if a (if (not b) a) b)) (defun diff-find-source-location (&optional other-file reverse noprompt) - "Find out (BUF LINE-OFFSET POS SRC DST SWITCHED). + "Find current diff location within the source file. +OTHER-FILE, if non-nil, means to look at the diff's name and line + numbers for the old file. Furthermore, use `diff-vc-revisions' + if it's available. If `diff-jump-to-old-file' is non-nil, the + sense of this parameter is reversed. If the prefix argument is + 8 or more, `diff-jump-to-old-file' is set to OTHER-FILE. +REVERSE, if non-nil, switches the sense of SRC and DST (see below). +NOPROMPT, if non-nil, means not to prompt the user. +Return a list (BUF LINE-OFFSET (BEG . END) SRC DST SWITCHED). BUF is the buffer corresponding to the source file. LINE-OFFSET is the offset between the expected and actual positions of the text of the hunk or nil if the text was not found. -POS is a pair (BEG . END) indicating the position of the text in the buffer. +\(BEG . END) is a pair indicating the position of the text in the buffer. SRC and DST are the two variants of text as returned by `diff-hunk-text'. SRC is the variant that was found in the buffer. -SWITCHED is non-nil if the patch is already applied. -NOPROMPT, if non-nil, means not to prompt the user." +SWITCHED is non-nil if the patch is already applied." (save-excursion (let* ((other (diff-xor other-file diff-jump-to-old-file)) (char-offset (- (point) (diff-beginning-of-hunk t))) @@ -2210,6 +2218,117 @@ diff-undo (let ((inhibit-read-only t)) (undo arg))) +(defun diff-add-log-current-defuns () + "Return an alist of defun names for the current diff. +The elements of the alist are of the form (FILE . (DEFUN...)), +where DEFUN... is a list of function names found in FILE." + (save-excursion + (goto-char (point-min)) + (let ((defuns nil) + (hunk-end nil) + (hunk-mismatch-files nil) + (make-defun-context-follower + (lambda (goline) + (let ((eodefun nil) + (defname nil)) + (list + (lambda () ;; Check for end of current defun. + (when (and eodefun + (funcall goline) + (>= (point) eodefun)) + (setq defname nil) + (setq eodefun nil))) + (lambda (&optional get-current) ;; Check for new defun. + (if get-current + defname + (when-let* ((def (and (not eodefun) + (funcall goline) + (add-log-current-defun))) + (eof (save-excursion (end-of-defun) (point)))) + (setq eodefun eof) + (setq defname def))))))))) + (while + ;; Might need to skip over file headers between diff + ;; hunks (e.g., "diff --git ..." etc). + (re-search-forward diff-hunk-header-re nil t) + (setq hunk-end (save-excursion (diff-end-of-hunk))) + (pcase-let* ((filename (substring-no-properties (diff-find-file-name))) + (=lines 0) + (+lines 0) + (-lines 0) + (`(,buf ,line-offset (,beg . ,_end) + (,old-text . ,_old-offset) + (,new-text . ,_new-offset) + ,applied) + ;; Try to use the vc integration of + ;; `diff-find-source-location', unless it + ;; would look for non-existent files like + ;; /dev/null. + (diff-find-source-location + (not (equal "/dev/null" + (car (diff-hunk-file-names t)))))) + (other-buf nil) + (goto-otherbuf + ;; If APPLIED, we have NEW-TEXT in BUF, so we + ;; need to a buffer with OLD-TEXT to follow + ;; -lines. + (lambda () + (if other-buf (set-buffer other-buf) + (set-buffer (generate-new-buffer " *diff-other-text*")) + (insert (if applied old-text new-text)) + (funcall (buffer-local-value 'major-mode buf)) + (setq other-buf (current-buffer))) + (goto-char (point-min)) + (forward-line (+ =lines -1 + (if applied -lines +lines))))) + (gotobuf (lambda () + (set-buffer buf) + (goto-char beg) + (forward-line (+ =lines -1 + (if applied +lines -lines))))) + (`(,=ck-eodefun ,=ck-defun) + (funcall make-defun-context-follower gotobuf)) + (`(,-ck-eodefun ,-ck-defun) + (funcall make-defun-context-follower + (if applied goto-otherbuf gotobuf))) + (`(,+ck-eodefun ,+ck-defun) + (funcall make-defun-context-follower + (if applied gotobuf goto-otherbuf)))) + (unless (eql line-offset 0) + (cl-pushnew filename hunk-mismatch-files :test #'equal)) + (unwind-protect + (while (progn (forward-line) + (< (point) hunk-end)) + (let ((patch-char (char-after))) + (pcase patch-char + (?+ (cl-incf +lines)) + (?- (cl-incf -lines)) + (?\s (cl-incf =lines))) + (save-current-buffer + (funcall =ck-eodefun) + (funcall +ck-eodefun) + (funcall -ck-eodefun) + (when-let* ((def (cond + ((eq patch-char ?\s) + ;; Just updating context defun. + (ignore (funcall =ck-defun))) + ;; + or - in existing defun. + ((funcall =ck-defun t)) + ;; Check added or removed defun. + (t (funcall (if (eq ?+ patch-char) + +ck-defun -ck-defun)))))) + (cl-pushnew def (alist-get filename defuns + nil nil #'equal) + :test #'equal))))) + (when (buffer-live-p other-buf) + (kill-buffer other-buf))))) + (when hunk-mismatch-files + (message "Diff didn't match for %s." + (mapconcat #'identity hunk-mismatch-files ", "))) + (dolist (file-defuns defuns) + (cl-callf nreverse (cdr file-defuns))) + (nreverse defuns)))) + (defun diff-add-change-log-entries-other-window () "Iterate through the current diff and create ChangeLog entries. I.e. like `add-change-log-entry-other-window' but applied to all hunks." diff --git a/lisp/vc/log-edit.el b/lisp/vc/log-edit.el index 91e18c1ec5..f375e10d79 100644 --- a/lisp/vc/log-edit.el +++ b/lisp/vc/log-edit.el @@ -54,6 +54,7 @@ cvs-buffer (easy-mmode-defmap log-edit-mode-map '(("\C-c\C-c" . log-edit-done) ("\C-c\C-a" . log-edit-insert-changelog) + ("\C-c\C-w" . log-edit-generate-changelog-from-diff) ("\C-c\C-d" . log-edit-show-diff) ("\C-c\C-f" . log-edit-show-files) ("\C-c\C-k" . log-edit-kill-buffer) @@ -488,10 +489,55 @@ log-edit-mode (set (make-local-variable 'font-lock-defaults) '(log-edit-font-lock-keywords t)) (setq-local jit-lock-contextually t) ;For the "first line is summary". + (setq-local fill-paragraph-function #'log-edit-fill-entry) (make-local-variable 'log-edit-comment-ring-index) (add-hook 'kill-buffer-hook 'log-edit-remember-comment nil t) (hack-dir-local-variables-non-file-buffer)) +(defun log-edit--insert-filled-defuns (func-names) + "Insert FUNC-NAMES, following ChangeLog formatting." + (cl-loop for def in func-names do + (when (> (+ (current-column) (string-width def)) fill-column) + (insert (if (memq (char-before) '(?\n ?\s)) + "\n" ")\n"))) + (insert (if (memq (char-before) '(?\n ?\s)) + "(" ", ") + def)) + (insert "):")) + +(defun log-edit-fill-entry (&optional justify) + "Like \\[fill-paragraph], but handle ChangeLog entries. +Consecutive function entries without prose (i.e., lines of the +form \"(FUNCTION):\") will be combined into \"(FUNC1, FUNC2):\" +according to `fill-column'." + (save-excursion + (pcase-let ((`(,beg ,end) (log-edit-changelog-paragraph))) + (if (= beg end) + ;; Not a ChangeLog entry, fill as normal. + nil + (cl-callf copy-marker end) + (goto-char beg) + (cl-loop + for defuns-beg = + (and (< beg end) + (re-search-forward + (concat "\\(?1:" change-log-unindented-file-names-re + " \\)\\|^\\(?1:\\)(") + end t) + (copy-marker (match-end 1))) + ;; Fill prose normally, but don't pick up indentation. + do (let ((fill-indent-according-to-mode t)) + (fill-region (progn (goto-char beg) (line-beginning-position)) + (if defuns-beg (match-beginning 0) end) + justify)) + while defuns-beg + for defuns = (progn (goto-char defuns-beg) + (change-log-read-defuns end)) + do (progn (delete-region defuns-beg (point)) + (log-edit--insert-filled-defuns defuns) + (setq beg (point)))) + t)))) + (defun log-edit-hide-buf (&optional buf where) (when (setq buf (get-buffer (or buf log-edit-files-buf))) ;; FIXME: Should use something like `quit-windows-on' here, but @@ -726,6 +772,27 @@ log-edit-add-field (replace-match (concat " " value) t t nil 1) (insert field ": " value "\n" (if (looking-at "\n") "" "\n")))) +(declare-function diff-add-log-current-defuns "diff-mode" ()) + +(defun log-edit-generate-changelog-from-diff () + "Insert a log message by looking at the current diff. +This command will generate a ChangeLog entries listing the +functions. You can then add a description where needed, and use +\\[fill-paragraph] to join consecutive function names." + (interactive) + (let* ((diff-buf nil) + ;; Unfortunately, `log-edit-show-diff' doesn't have a NO-SHOW + ;; option, so we try to work around it via display-buffer + ;; machinery. + (display-buffer-overriding-action + `(,(lambda (buf alist) + (setq diff-buf buf) + (display-buffer-no-window buf alist)) + . ((allow-no-window . t))))) + (change-log-insert-entries + (with-current-buffer (progn (log-edit-show-diff) diff-buf) + (diff-add-log-current-defuns))))) + (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 diff --git a/test/lisp/vc/log-edit-tests.el b/test/lisp/vc/log-edit-tests.el new file mode 100644 index 0000000000..e48ad4f610 --- /dev/null +++ b/test/lisp/vc/log-edit-tests.el @@ -0,0 +1,90 @@ +;;; log-edit-tests.el --- Unit tests for log-edit.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Unit tests for lisp/vc/log-edit.el. + +;;; Code: + +(require 'log-edit) +(require 'ert) + +(ert-deftest log-edit-fill-entry () + (with-temp-buffer + (insert "\ +* dir/file.ext (fun1): +\(fun2): +\(fun3): +* file2.txt (fun4): +\(fun5): +\(fun6): +\(fun7): Some prose. +\(fun8): A longer description of a complicated change.\ + Spread over a couple of sentencences.\ + Long enough to be filled for several lines. +\(fun9): Etc.") + (goto-char (point-min)) + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1, fun2, fun3): +* file2.txt (fun4, fun5, fun6, fun7): Some prose. +\(fun8): A longer description of a complicated change. Spread over a +couple of sentencences. Long enough to be filled for several lines. +\(fun9): Etc.")) + (let ((fill-column 20)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1) +\(fun2, fun3): +* file2.txt (fun4) +\(fun5, fun6, fun7): +Some prose. +\(fun8): A longer +description of a +complicated change. +Spread over a couple +of sentencences. +Long enough to be +filled for several +lines. +\(fun9): Etc.")) + (let ((fill-column 40)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1, fun2, fun3): +* file2.txt (fun4, fun5, fun6, fun7): +Some prose. +\(fun8): A longer description of a +complicated change. Spread over a +couple of sentencences. Long enough to +be filled for several lines. +\(fun9): Etc.")))) + +(ert-deftest log-edit-fill-entry-trailing-prose () + (with-temp-buffer + (insert "\ +* dir/file.ext (fun1): A longer description of a complicated change.\ + Spread over a couple of sentencences.\ + Long enough to be filled for several lines.") + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1): A longer description of a complicated change. +Spread over a couple of sentencences. Long enough to be filled for +several lines.")))) + +;;; log-edit-tests.el ends here -- 2.11.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable 2019-07-16 23:47 ` Noam Postavsky @ 2019-07-28 14:13 ` Noam Postavsky 2019-08-04 0:16 ` Noam Postavsky 0 siblings, 1 reply; 6+ messages in thread From: Noam Postavsky @ 2019-07-28 14:13 UTC (permalink / raw) To: Daniel Dehennin; +Cc: 16301 [-- Attachment #1: Type: text/plain, Size: 141 bytes --] > I've made enough progress that this should be usable by now. I found a few more corner cases while using it. I'll push to master soon. [-- Attachment #2: patch --] [-- Type: text/plain, Size: 24174 bytes --] From a593ab35656c89e2850bbff283963c00a595a74b Mon Sep 17 00:00:00 2001 From: Noam Postavsky <npostavs@gmail.com> Date: Thu, 4 Jul 2019 20:32:39 -0400 Subject: [PATCH] Improved ChangeLog generation for vc log (Bug#16301) * lisp/vc/diff-mode.el (diff-find-source-location): Fix docstring. * lisp/vc/add-log.el (change-log-unindented-file-names-re) (change-log-read-entries, change-log-read-defuns) (change-log-insert-entries): * lisp/vc/diff-mode.el (diff-add-log-current-defuns): * lisp/vc/log-edit.el (log-edit--insert-filled-defuns) (log-edit-fill-entry): New functions. (log-edit-mode): Set `log-edit-fill-entry' as `fill-paragraph-function'. (log-edit-generate-changelog-from-diff): New command. (log-edit-mode-map): Bind it to C-c C-w. * doc/emacs/maintaining.texi (Types of Log File, Log Buffer): * CONTRIBUTE: Document it. * etc/NEWS: Announce it. * test/lisp/vc/log-edit-tests.el (log-edit-fill-entry) (log-edit-fill-entry-joining): New tests. --- CONTRIBUTE | 20 ++++--- doc/emacs/maintaining.texi | 13 +++- etc/NEWS | 4 ++ lisp/vc/add-log.el | 39 ++++++++++++ lisp/vc/diff-mode.el | 131 +++++++++++++++++++++++++++++++++++++++-- lisp/vc/log-edit.el | 75 +++++++++++++++++++++++ test/lisp/vc/log-edit-tests.el | 113 +++++++++++++++++++++++++++++++++++ 7 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 test/lisp/vc/log-edit-tests.el diff --git a/CONTRIBUTE b/CONTRIBUTE index f257fc57f0..f480ffec9b 100644 --- a/CONTRIBUTE +++ b/CONTRIBUTE @@ -263,18 +263,22 @@ them right the first time, so here are guidelines for formatting them: ** Generating ChangeLog entries -- You can use Emacs functions to write ChangeLog entries; see +- If you use Emacs VC, you can use 'C-c C-w' to generate formatted + blank ChangeLog entries from the diff being committed, then use + 'M-q' to combine and fill them. See 'info "(emacs) Log Buffer"'. + +- Alternatively, you can use Emacs functions for ChangeLog files; see https://www.gnu.org/software/emacs/manual/html_node/emacs/Change-Log-Commands.html or run 'info "(emacs)Change Log Commands"'. -- If you use Emacs VC, one way to format ChangeLog entries is to create - a top-level ChangeLog file manually, and update it with 'C-x 4 a' as - usual. Do not register the ChangeLog file under git; instead, use - 'C-c C-a' to insert its contents into your *vc-log* buffer. - Or if 'log-edit-hook' includes 'log-edit-insert-changelog' (which it - does by default), they will be filled in for you automatically. + To format ChangeLog entries with Emacs VC, create a top-level + ChangeLog file manually, and update it with 'C-x 4 a' as usual. Do + not register the ChangeLog file under git; instead, use 'C-c C-a' to + insert its contents into your *vc-log* buffer. Or if + 'log-edit-hook' includes 'log-edit-insert-changelog' (which it does + by default), they will be filled in for you automatically. -- Alternatively, you can use the vc-dwim command to maintain commit +- Instead of Emacs VC, you can use the vc-dwim command to maintain commit messages. When you create a source directory, run the shell command 'git-changelog-symlink-init' to create a symbolic link from ChangeLog to .git/c/ChangeLog. Edit this ChangeLog via its symlink diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index c3895bffb5..c6fe29ed27 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -396,8 +396,9 @@ Types of Log File for each change just once, then put it into both logs. You can write the entry in @file{ChangeLog}, then copy it to the log buffer with @kbd{C-c C-a} when committing the change (@pxref{Log Buffer}). Or you -can write the entry in the log buffer while committing the change, and -later use the @kbd{C-x v a} command to copy it to @file{ChangeLog} +can write the entry in the log buffer while committing the change +(with the help of @kbd{C-c C-w}), and later use the @kbd{C-x v a} +command to copy it to @file{ChangeLog} @iftex (@pxref{Change Logs and VC,,,emacs-xtra, Specialized Emacs Features}). @end iftex @@ -677,6 +678,14 @@ Log Buffer started editing (@pxref{Old Revisions}), type @kbd{C-c C-d} (@code{log-edit-show-diff}). +@kindex C-c C-w @r{(Log Edit mode)} +@findex log-edit-generate-changelog + To help generate ChangeLog entries, type @kbd{C-c C-w} +(@code{log-edit-generate-changelog}), to generate skeleton ChangeLog +entries, listing all changed file and function names based on the diff +of the VC fileset. Consecutive entries left empty will be combined by +@kbd{C-q} (@code{fill-paragraph}). + @kindex C-c C-a @r{(Log Edit mode)} @findex log-edit-insert-changelog If the VC fileset includes one or more @file{ChangeLog} files diff --git a/etc/NEWS b/etc/NEWS index 13de6bb0f8..f5e13ea88c 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -653,6 +653,10 @@ The default value is 'find-dired-sort-by-filename'. ** Change Logs and VC ++++ +*** New command 'log-edit-generate-changelog', bound to C-c C-w. +This generates ChangeLog entries from the VC fileset diff. + *** Recording ChangeLog entries doesn't require an actual file. If a ChangeLog file doesn't exist, and if the new variable 'add-log-dont-create-changelog-file' is non-nil (which is the diff --git a/lisp/vc/add-log.el b/lisp/vc/add-log.el index f9efd44c5c..47a68167fb 100644 --- a/lisp/vc/add-log.el +++ b/lisp/vc/add-log.el @@ -36,6 +36,8 @@ ;;; Code: +(eval-when-compile (require 'cl-lib)) + (defgroup change-log nil "Change log maintenance." :group 'tools @@ -309,6 +311,43 @@ change-log-search-file-name (re-search-forward change-log-file-names-re nil t) (match-string-no-properties 2)))))) +(defconst change-log-unindented-file-names-re "^[*] \\([^ ,:([\n]+\\)") + +(defun change-log-read-entries (&optional end) + "Read ChangeLog entries at point until END. +Move point to the end of entries that were read. Return a list +in the same form as `diff-add-log-current-defuns'." + (cl-loop while (and (or (not end) (< (point) end)) + (looking-at change-log-unindented-file-names-re)) + do (goto-char (match-end 0)) + collect (cons (match-string-no-properties 1) + (change-log-read-defuns end)))) + +(defvar change-log-tag-re) ; add-log.el +(defun change-log-read-defuns (&optional end) + "Read ChangeLog formatted function names at point until END. +Move point to the end of names read and return the function names +as a list of strings." + (cl-loop while (and (skip-chars-forward ":\n[:blank:]" end) + (or (not end) (< (point) end)) + (looking-at change-log-tag-re)) + do (goto-char (match-end 0)) + nconc (split-string (match-string-no-properties 1) + ",[[:blank:]]*" t) + finally do (skip-chars-backward "\n[:blank:]"))) + +(defun change-log-insert-entries (changelogs) + "Format and insert CHANGELOGS into current buffer. +CHANGELOGS is a list in the form returned by +`diff-add-log-current-defuns'." + (cl-loop for (file . defuns) in changelogs do + (insert "* " file) + (if (not defuns) + (insert ":\n") + (insert " ") + (cl-loop for def in defuns + do (insert "(" def "):\n"))))) + (defun change-log-find-file () "Visit the file for the change under point." (interactive) diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el index 0d5dc0e1c0..81662cafed 100644 --- a/lisp/vc/diff-mode.el +++ b/lisp/vc/diff-mode.el @@ -54,6 +54,7 @@ ;;; Code: (eval-when-compile (require 'cl-lib)) +(eval-when-compile (require 'subr-x)) (autoload 'vc-find-revision "vc") (autoload 'vc-find-revision-no-save "vc") @@ -1773,15 +1774,22 @@ diff-find-approx-text (defsubst diff-xor (a b) (if a (if (not b) a) b)) (defun diff-find-source-location (&optional other-file reverse noprompt) - "Find out (BUF LINE-OFFSET POS SRC DST SWITCHED). + "Find current diff location within the source file. +OTHER-FILE, if non-nil, means to look at the diff's name and line + numbers for the old file. Furthermore, use `diff-vc-revisions' + if it's available. If `diff-jump-to-old-file' is non-nil, the + sense of this parameter is reversed. If the prefix argument is + 8 or more, `diff-jump-to-old-file' is set to OTHER-FILE. +REVERSE, if non-nil, switches the sense of SRC and DST (see below). +NOPROMPT, if non-nil, means not to prompt the user. +Return a list (BUF LINE-OFFSET (BEG . END) SRC DST SWITCHED). BUF is the buffer corresponding to the source file. LINE-OFFSET is the offset between the expected and actual positions of the text of the hunk or nil if the text was not found. -POS is a pair (BEG . END) indicating the position of the text in the buffer. +\(BEG . END) is a pair indicating the position of the text in the buffer. SRC and DST are the two variants of text as returned by `diff-hunk-text'. SRC is the variant that was found in the buffer. -SWITCHED is non-nil if the patch is already applied. -NOPROMPT, if non-nil, means not to prompt the user." +SWITCHED is non-nil if the patch is already applied." (save-excursion (let* ((other (diff-xor other-file diff-jump-to-old-file)) (char-offset (- (point) (diff-beginning-of-hunk t))) @@ -2210,6 +2218,121 @@ diff-undo (let ((inhibit-read-only t)) (undo arg))) +(defun diff-add-log-current-defuns () + "Return an alist of defun names for the current diff. +The elements of the alist are of the form (FILE . (DEFUN...)), +where DEFUN... is a list of function names found in FILE." + (save-excursion + (goto-char (point-min)) + (let ((defuns nil) + (hunk-end nil) + (hunk-mismatch-files nil) + (make-defun-context-follower + (lambda (goline) + (let ((eodefun nil) + (defname nil)) + (list + (lambda () ;; Check for end of current defun. + (when (and eodefun + (funcall goline) + (>= (point) eodefun)) + (setq defname nil) + (setq eodefun nil))) + (lambda (&optional get-current) ;; Check for new defun. + (if get-current + defname + (when-let* ((def (and (not eodefun) + (funcall goline) + (add-log-current-defun))) + (eof (save-excursion (end-of-defun) (point)))) + (setq eodefun eof) + (setq defname def))))))))) + (while + ;; Might need to skip over file headers between diff + ;; hunks (e.g., "diff --git ..." etc). + (re-search-forward diff-hunk-header-re nil t) + (setq hunk-end (save-excursion (diff-end-of-hunk))) + (pcase-let* ((filename (substring-no-properties (diff-find-file-name))) + (=lines 0) + (+lines 0) + (-lines 0) + (`(,buf ,line-offset (,beg . ,_end) + (,old-text . ,_old-offset) + (,new-text . ,_new-offset) + ,applied) + ;; Try to use the vc integration of + ;; `diff-find-source-location', unless it + ;; would look for non-existent files like + ;; /dev/null. + (diff-find-source-location + (not (equal "/dev/null" + (car (diff-hunk-file-names t)))))) + (other-buf nil) + (goto-otherbuf + ;; If APPLIED, we have NEW-TEXT in BUF, so we + ;; need to a buffer with OLD-TEXT to follow + ;; -lines. + (lambda () + (if other-buf (set-buffer other-buf) + (set-buffer (generate-new-buffer " *diff-other-text*")) + (insert (if applied old-text new-text)) + (funcall (buffer-local-value 'major-mode buf)) + (setq other-buf (current-buffer))) + (goto-char (point-min)) + (forward-line (+ =lines -1 + (if applied -lines +lines))))) + (gotobuf (lambda () + (set-buffer buf) + (goto-char beg) + (forward-line (+ =lines -1 + (if applied +lines -lines))))) + (`(,=ck-eodefun ,=ck-defun) + (funcall make-defun-context-follower gotobuf)) + (`(,-ck-eodefun ,-ck-defun) + (funcall make-defun-context-follower + (if applied goto-otherbuf gotobuf))) + (`(,+ck-eodefun ,+ck-defun) + (funcall make-defun-context-follower + (if applied gotobuf goto-otherbuf)))) + (unless (eql line-offset 0) + (cl-pushnew filename hunk-mismatch-files :test #'equal)) + ;; Some modes always return nil for `add-log-current-defun', + ;; make sure at least the filename is included. + (unless (assoc filename defuns) + (push (cons filename nil) defuns)) + (unwind-protect + (while (progn (forward-line) + (< (point) hunk-end)) + (let ((patch-char (char-after))) + (pcase patch-char + (?+ (cl-incf +lines)) + (?- (cl-incf -lines)) + (?\s (cl-incf =lines))) + (save-current-buffer + (funcall =ck-eodefun) + (funcall +ck-eodefun) + (funcall -ck-eodefun) + (when-let* ((def (cond + ((eq patch-char ?\s) + ;; Just updating context defun. + (ignore (funcall =ck-defun))) + ;; + or - in existing defun. + ((funcall =ck-defun t)) + ;; Check added or removed defun. + (t (funcall (if (eq ?+ patch-char) + +ck-defun -ck-defun)))))) + (cl-pushnew def (alist-get filename defuns + nil nil #'equal) + :test #'equal))))) + (when (buffer-live-p other-buf) + (kill-buffer other-buf))))) + (when hunk-mismatch-files + (message "Diff didn't match for %s." + (mapconcat #'identity hunk-mismatch-files ", "))) + (dolist (file-defuns defuns) + (cl-callf nreverse (cdr file-defuns))) + (nreverse defuns)))) + (defun diff-add-change-log-entries-other-window () "Iterate through the current diff and create ChangeLog entries. I.e. like `add-change-log-entry-other-window' but applied to all hunks." diff --git a/lisp/vc/log-edit.el b/lisp/vc/log-edit.el index 91e18c1ec5..8d47d66ac3 100644 --- a/lisp/vc/log-edit.el +++ b/lisp/vc/log-edit.el @@ -54,6 +54,7 @@ cvs-buffer (easy-mmode-defmap log-edit-mode-map '(("\C-c\C-c" . log-edit-done) ("\C-c\C-a" . log-edit-insert-changelog) + ("\C-c\C-w" . log-edit-generate-changelog-from-diff) ("\C-c\C-d" . log-edit-show-diff) ("\C-c\C-f" . log-edit-show-files) ("\C-c\C-k" . log-edit-kill-buffer) @@ -488,10 +489,63 @@ log-edit-mode (set (make-local-variable 'font-lock-defaults) '(log-edit-font-lock-keywords t)) (setq-local jit-lock-contextually t) ;For the "first line is summary". + (setq-local fill-paragraph-function #'log-edit-fill-entry) (make-local-variable 'log-edit-comment-ring-index) (add-hook 'kill-buffer-hook 'log-edit-remember-comment nil t) (hack-dir-local-variables-non-file-buffer)) +(defun log-edit--insert-filled-defuns (func-names) + "Insert FUNC-NAMES, following ChangeLog formatting." + (if (not func-names) + (insert ":") + (unless (or (memq (char-before) '(?\n ?\s)) + (> (current-column) fill-column)) + (insert " ")) + (cl-loop for first-fun = t then nil + for def in func-names do + (when (> (+ (current-column) (string-width def)) fill-column) + (unless first-fun + (insert ")")) + (insert "\n")) + (insert (if (memq (char-before) '(?\n ?\s)) + "(" ", ") + def)) + (insert "):"))) + +(defun log-edit-fill-entry (&optional justify) + "Like \\[fill-paragraph], but handle ChangeLog entries. +Consecutive function entries without prose (i.e., lines of the +form \"(FUNCTION):\") will be combined into \"(FUNC1, FUNC2):\" +according to `fill-column'." + (save-excursion + (pcase-let ((`(,beg ,end) (log-edit-changelog-paragraph))) + (if (= beg end) + ;; Not a ChangeLog entry, fill as normal. + nil + (cl-callf copy-marker end) + (goto-char beg) + (cl-loop + for defuns-beg = + (and (< beg end) + (re-search-forward + (concat "\\(?1:" change-log-unindented-file-names-re + "\\)\\|^\\(?1:\\)(") + end t) + (copy-marker (match-end 1))) + ;; Fill prose between log entries. + do (let ((fill-indent-according-to-mode t) + (end (if defuns-beg (match-beginning 0) end)) + (beg (progn (goto-char beg) (line-beginning-position)))) + (when (<= (line-end-position) end) + (fill-region beg end justify))) + while defuns-beg + for defuns = (progn (goto-char defuns-beg) + (change-log-read-defuns end)) + do (progn (delete-region defuns-beg (point)) + (log-edit--insert-filled-defuns defuns) + (setq beg (point)))) + t)))) + (defun log-edit-hide-buf (&optional buf where) (when (setq buf (get-buffer (or buf log-edit-files-buf))) ;; FIXME: Should use something like `quit-windows-on' here, but @@ -726,6 +780,27 @@ log-edit-add-field (replace-match (concat " " value) t t nil 1) (insert field ": " value "\n" (if (looking-at "\n") "" "\n")))) +(declare-function diff-add-log-current-defuns "diff-mode" ()) + +(defun log-edit-generate-changelog-from-diff () + "Insert a log message by looking at the current diff. +This command will generate a ChangeLog entries listing the +functions. You can then add a description where needed, and use +\\[fill-paragraph] to join consecutive function names." + (interactive) + (let* ((diff-buf nil) + ;; Unfortunately, `log-edit-show-diff' doesn't have a NO-SHOW + ;; option, so we try to work around it via display-buffer + ;; machinery. + (display-buffer-overriding-action + `(,(lambda (buf alist) + (setq diff-buf buf) + (display-buffer-no-window buf alist)) + . ((allow-no-window . t))))) + (change-log-insert-entries + (with-current-buffer (progn (log-edit-show-diff) diff-buf) + (diff-add-log-current-defuns))))) + (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 diff --git a/test/lisp/vc/log-edit-tests.el b/test/lisp/vc/log-edit-tests.el new file mode 100644 index 0000000000..7d77eca87d --- /dev/null +++ b/test/lisp/vc/log-edit-tests.el @@ -0,0 +1,113 @@ +;;; log-edit-tests.el --- Unit tests for log-edit.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Unit tests for lisp/vc/log-edit.el. + +;;; Code: + +(require 'log-edit) +(require 'ert) + +(ert-deftest log-edit-fill-entry () + (with-temp-buffer + (insert "\ +* dir/file.ext (fun1): +\(fun2): +\(fun3): +* file2.txt (fun4): +\(fun5): +\(fun6): +\(fun7): Some prose. +\(fun8): A longer description of a complicated change.\ + Spread over a couple of sentencences.\ + Long enough to be filled for several lines. +\(fun9): Etc.") + (goto-char (point-min)) + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1, fun2, fun3): +* file2.txt (fun4, fun5, fun6, fun7): Some prose. +\(fun8): A longer description of a complicated change. Spread over a +couple of sentencences. Long enough to be filled for several lines. +\(fun9): Etc.")) + (let ((fill-column 20)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1) +\(fun2, fun3): +* file2.txt (fun4) +\(fun5, fun6, fun7): +Some prose. +\(fun8): A longer +description of a +complicated change. +Spread over a couple +of sentencences. +Long enough to be +filled for several +lines. +\(fun9): Etc.")) + (let ((fill-column 40)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1, fun2, fun3): +* file2.txt (fun4, fun5, fun6, fun7): +Some prose. +\(fun8): A longer description of a +complicated change. Spread over a +couple of sentencences. Long enough to +be filled for several lines. +\(fun9): Etc.")))) + +(ert-deftest log-edit-fill-entry-trailing-prose () + (with-temp-buffer + (insert "\ +* dir/file.ext (fun1): A longer description of a complicated change.\ + Spread over a couple of sentencences.\ + Long enough to be filled for several lines.") + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* dir/file.ext (fun1): A longer description of a complicated change. +Spread over a couple of sentencences. Long enough to be filled for +several lines.")))) + +(ert-deftest log-edit-fill-entry-joining () + ;; Join short enough function names on the same line. + (with-temp-buffer + (insert "* dir/file.ext (fun1):\n(fun2):") + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "* dir/file.ext (fun1, fun2):"))) + ;; Don't combine them if they're too long. + (with-temp-buffer + (insert "* dir/long-file-name.ext (a-really-long-function-name): +\(another-very-long-function-name):") + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "* dir/long-file-name.ext (a-really-long-function-name) +\(another-very-long-function-name):"))) + ;; Put function name on next line, if the file name is too long. + (with-temp-buffer + (insert "\ +* a-very-long-directory-name/another-long-directory-name/and-a-long-file-name.ext\ + (a-really-long-function-name):") + (let ((fill-column 72)) (log-edit-fill-entry)) + (should (equal (buffer-string) "\ +* a-very-long-directory-name/another-long-directory-name/and-a-long-file-name.ext +\(a-really-long-function-name):")))) + +;;; log-edit-tests.el ends here -- 2.11.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable 2019-07-28 14:13 ` Noam Postavsky @ 2019-08-04 0:16 ` Noam Postavsky 0 siblings, 0 replies; 6+ messages in thread From: Noam Postavsky @ 2019-08-04 0:16 UTC (permalink / raw) To: Daniel Dehennin; +Cc: 16301 tags 16301 fixed close 16301 27.1 quit Noam Postavsky <npostavs@gmail.com> writes: >> I've made enough progress that this should be usable by now. > > I found a few more corner cases while using it. I'll push to master > soon. Done. 01661f33c1 2019-08-03T20:14:52-04:00 "Improved ChangeLog generation for vc log (Bug#16301)" https://git.savannah.gnu.org/cgit/emacs.git/commit/?id=01661f33c11654d1fe5fe1013332db2500b7f449 ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2019-08-04 0:16 UTC | newest] Thread overview: 6+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2013-12-30 12:43 bug#16301: 24.3.50; [Feature] Split add-change-log-entry to make it reusable Daniel Dehennin 2014-01-02 4:12 ` Stefan Monnier 2019-07-05 1:57 ` Noam Postavsky 2019-07-16 23:47 ` Noam Postavsky 2019-07-28 14:13 ` Noam Postavsky 2019-08-04 0:16 ` Noam Postavsky
Code repositories for project(s) associated with this public inbox https://git.savannah.gnu.org/cgit/emacs.git This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).