From 84ce235f133459dd06c2d1a0a1c6e0d90f5770f1 Mon Sep 17 00:00:00 2001 From: Yoni Rabkin Date: Wed, 5 Aug 2020 17:22:31 -0400 Subject: Syncronize with Stefan's changes --- doc/gpl.texi | 1 - doc/rt-liberation.texinfo | 43 ++++++++++-- maint.el | 2 + rt-liberation-gnus.el | 29 ++++----- rt-liberation-multi.el | 2 +- rt-liberation-report.el | 9 ++- rt-liberation-rest.el | 65 +++++++++++++++---- rt-liberation-storage.el | 2 +- rt-liberation-update.el | 2 +- rt-liberation.el | 162 +++++++++++++++++++++++++++++++--------------- 10 files changed, 221 insertions(+), 96 deletions(-) diff --git a/doc/gpl.texi b/doc/gpl.texi index 48d43f9..dc22d67 100644 --- a/doc/gpl.texi +++ b/doc/gpl.texi @@ -1,5 +1,4 @@ @node Copying, The GNU FDL, Local Storage, Top -@chapter The GNU General Public License. @c The GNU General Public License. @center Version 3, 29 June 2007 diff --git a/doc/rt-liberation.texinfo b/doc/rt-liberation.texinfo index 8b63fc0..f5fa128 100644 --- a/doc/rt-liberation.texinfo +++ b/doc/rt-liberation.texinfo @@ -14,11 +14,11 @@ @copying - @copyright{} 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2020 Free Software Foundation + @copyright{} 2009, 2010, 2011, 2012, 2013, 2014, 2015 Free Software Foundation @quotation Permission is granted to copy, distribute and/or modify this document -under the terms of the GNU Free Documentation License, Version 1.2 or +under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled ``GNU Free @@ -97,6 +97,8 @@ Ticket Browser + + @c -------------------------------------------------- @node Introduction @@ -115,6 +117,8 @@ the resulting tickets, viewing the tickets' contents and performing operations on the tickets. + + @c -------------------------------------------------- @node Installation @@ -136,10 +140,12 @@ Tell GNU/Emacs to load the package with: (require 'rt-liberation) @end lisp -Tell rt-liberation where to find the RT server's REST interface: +Tell rt-liberation where to find the RT server's REST interface and +which version of RT the server is running: @lisp -(setq rt-liber-rest-url "rt.example.org") +(setq rt-liber-rest-url "rt.example.org" + rt-liber-rt-version "4.2.4") @end lisp rt-liberation can issue a command to ``take'' a ticket (that is, @@ -178,6 +184,8 @@ TicketSQL language. + + @c -------------------------------------------------- @node Query Compiler @@ -198,6 +206,8 @@ number of TicketSQL tokens. + + @c -------------------------------------------------- @node Query Language @@ -277,6 +287,8 @@ in function calls: @end lisp + + @c -------------------------------------------------- @node Ticket Browser @@ -412,6 +424,8 @@ Set the numerical priority level of the ticket at point. + + @c -------------------------------------------------- @node Ticket Browser Display @@ -478,6 +492,8 @@ considered high priority if its value is strictly higher than @var{rt-liber-browser-priority-cutoff} + + @c -------------------------------------------------- @node Ticket Browser Sorting @@ -619,6 +635,8 @@ buffers will be created displaying the query results and named + + @c -------------------------------------------------- @node Ticket Viewer @@ -736,6 +754,8 @@ Display the associated ticket in the ticket browser. + + @c -------------------------------------------------- @node Gnus Integration @@ -804,6 +824,8 @@ the Viewer will be able to call into it, @xref{Ticket Viewer}. + + @c -------------------------------------------------- @node Tracking Updates @@ -839,6 +861,8 @@ time-stamp so that the next invocation will produce the same result. @end defun + + @c -------------------------------------------------- @node Batch Operations @@ -884,6 +908,9 @@ Set the status of all the marked tickets to ``is-spam'' and delete. @end defun + + + @c -------------------------------------------------- @node Local Storage @@ -933,6 +960,8 @@ can be extended to associate any arbitrary data with any ticket. + + @c -------------------------------------------------- @node Concept Index @@ -940,6 +969,8 @@ can be extended to associate any arbitrary data with any ticket. @printindex cp + + @c -------------------------------------------------- @node Function Index @@ -947,6 +978,8 @@ can be extended to associate any arbitrary data with any ticket. @printindex fn + + @c -------------------------------------------------- @node Variable Index @@ -954,6 +987,8 @@ can be extended to associate any arbitrary data with any ticket. @printindex vr + + @c -------------------------------------------------- @node Keybinding Index diff --git a/maint.el b/maint.el index b89538e..4b1d978 100644 --- a/maint.el +++ b/maint.el @@ -1,2 +1,4 @@ +;; Copyright (C) 2020 Free Software Foundation, Inc. +;; FIXME: Remove this file (add-to-list 'load-path ".") diff --git a/rt-liberation-gnus.el b/rt-liberation-gnus.el index 012ad24..eb0b262 100644 --- a/rt-liberation-gnus.el +++ b/rt-liberation-gnus.el @@ -1,6 +1,6 @@ ;;; rt-liberation-gnus.el --- Gnus integration for rt-liberation -;; Copyright (C) 2009, 2012, 2014 Free Software Foundation +;; Copyright (C) 2009-2014 Free Software Foundation, Inc. ;; ;; Authors: Yoni Rabkin ;; @@ -21,21 +21,12 @@ ;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, ;; MA 02111-1307, USA. - ;;; Installation: ;; ;; For installation instructions and detailed help please see the ;; wonderful rt-liberation manual located in the "doc/" directory of ;; the rt-liberation distribution. - -;; Code: - -(require 'rt-liberation) -(require 'nnir) -(require 'gnus-msg) - - (defgroup rt-liber-gnus nil "*Gnus integration for rt-liberation." :prefix "rt-liber-gnus-" @@ -74,15 +65,13 @@ line of an email. For example: \\[company.com #\\([0-9].+?\\)\\]" :type 'string :group 'rt-liber-gnus) -(defvar rt-liber-gnus-p nil - "Non-nil when rt-liberation-gnus is composing a Gnus buffer.") +(require 'rt-liberation) +(require 'nnir) +(require 'gnus-msg) -(defmacro rt-liber-gnus-with-ticket-buffer (&rest body) - `(progn - (when (not (boundp 'rt-liber-ticket-local)) - (error "rt-liberation ticket view buffer not present")) - ,@body)) +(defvar rt-liber-gnus-p nil + "Non-nil when rt-liberation-gnus is composing a Gnus buffer.") (defun rt-liber-gnus-compose (addr ticket-alist options) @@ -126,6 +115,12 @@ OPTIONS association list of options. (save-excursion (insert message-text)))) +(defmacro rt-liber-gnus-with-ticket-buffer (&rest body) + `(progn + (when (not (boundp 'rt-liber-ticket-local)) + (error "rt-liberation ticket view buffer not present")) + ,@body)) + (defun rt-liber-gnus-content-to-string () "Return the current content section as a string" (rt-liber-gnus-with-ticket-buffer diff --git a/rt-liberation-multi.el b/rt-liberation-multi.el index 1b2d275..8e4665a 100644 --- a/rt-liberation-multi.el +++ b/rt-liberation-multi.el @@ -1,6 +1,6 @@ ;;; rt-liberation-multi.el --- Emacs interface to RT -;; Copyright (C) 2010, 2014 Free Software Foundation +;; Copyright (C) 2010, 2014 Free Software Foundation, Inc. ;; ;; Authors: Yoni Rabkin ;; diff --git a/rt-liberation-report.el b/rt-liberation-report.el index 07b83ee..11d20c2 100644 --- a/rt-liberation-report.el +++ b/rt-liberation-report.el @@ -1,6 +1,6 @@ ;;; rt-liberation-report.el --- Emacs interface to RT -;; Copyright (C) 2015 Free Software Foundation +;; Copyright (C) 2015 Free Software Foundation, Inc. ;; ;; Authors: Yoni Rabkin ;; @@ -32,9 +32,8 @@ ;;; Code: -(require 'rt-liberation) (require 'rt-liberation-rest) - +(require 'rt-liberation) (defvar rt-liber-report-csv-header '("date" "tickets resolved") @@ -126,8 +125,8 @@ return `nil'." (rt-liber-report-get-interval rt-queue start-date end-date))) by-date by-owner - by-date-out - by-owner-out + ;; by-date-out + ;; by-owner-out total) (when (not tickets) (error (concat "no tickets in interval between " diff --git a/rt-liberation-rest.el b/rt-liberation-rest.el index 0b52211..7782665 100644 --- a/rt-liberation-rest.el +++ b/rt-liberation-rest.el @@ -1,6 +1,6 @@ ;;; rt-liberation-rest.el --- Interface to the RT REST API -;; Copyright (C) 2014, 2015 Free Software Foundation +;; Copyright (C) 2014-2015 Free Software Foundation, Inc. ;; ;; Authors: Yoni Rabkin ;; @@ -20,7 +20,9 @@ ;; License along with this program; if not, write to the Free ;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, ;; MA 02111-1307, USA. - +;; +;; Note: Licensed under GPLv2+ and not GPLv3+ in order to be +;; compatible with the license of RT. ;;; History: ;; @@ -31,7 +33,9 @@ (require 'url) (require 'url-util) - +;; (require 'rt-liberation) ; FIXME: Circular dependency +(declare-function rt-liber-parse-answer "rt-liberation" (answer-string parser-f)) +(declare-function rt-liber-ticket-base-retriever-parser-f "rt-liberation" ()) (defvar rt-liber-rest-debug-buffer-name "*rt-liber-rest debug log*" "Buffer name of debug capture.") @@ -54,9 +58,6 @@ (defvar rt-liber-rest-verbose-p t "If non-nil, be verbose about what's happening.") -(defvar rt-liber-rest-response-buffer nil - "Buffer for manipulating server responses.") - (defun rt-liber-rest-write-debug (str) "Write to debug buffer." @@ -82,7 +83,7 @@ "format=i" "&" "orderby=+Created"))) -(defun rt-liber-rest-show-string (scheme url ticket-id-list username password query) +(defun rt-liber-rest-show-string (scheme url ticket-id-list username password _query) "Return the ticket show string." (let ((user (url-encode-url username)) (pass (url-encode-url password))) @@ -169,11 +170,29 @@ "Parse the HTTP header from the server." (let ((http-ok-regexp "^HTTP.*200 OK$") (rt-ok-regexp "^rt/.*200 ok$")) - (condition-case excep + (condition-case nil (progn (re-search-forward http-ok-regexp (point-max)) (re-search-forward rt-ok-regexp (point-max))) - (error "bad HTTP response from server")))) + (error "bad HTTP response from server")))) ;FIXME: Unused string! + +(defun rt-liber-rest-ticketsql-runner-parser-f () + "Parser function for a textual list of tickets." + (let (idsub-list) + (rt-liber-rest-parse-http-header) + (while (re-search-forward "ticket/\\([0-9].+\\)" (point-max) t) + ;; the output should be compatible with the input to + ;; `rt-liber-create-tickets-string' + (push (list (match-string-no-properties 1) + ".") + idsub-list)) + idsub-list)) + +(defun rt-liber-rest-run-ls-query (query) + "Run an \"ls\" type query against the server with QUERY." + (rt-liber-parse-answer + (rt-liber-rest-query-runner "ls" query) + 'rt-liber-rest-ticketsql-runner-parser-f)) (defun rt-liber-rest-show-process (response) "Process and return the show query response." @@ -209,9 +228,23 @@ (message "done retrieving %d tickets" l))) (buffer-substring (point-min) (point-max)))) +(defun rt-liber-rest-run-show-base-query (idsublist) + "Run \"show\" type query against the server with IDSUBLIST." + (rt-liber-parse-answer + (rt-liber-rest-show-query-runner idsublist) + #'rt-liber-ticket-base-retriever-parser-f)) + +(defun rt-liber-rest-run-ticket-history-base-query (ticket-id) + "Run history query against server for TICKET-ID." + (rt-liber-parse-answer + (rt-liber-rest-query-runner "history" ticket-id) + #'(lambda () + (rt-liber-rest-parse-http-header) + (buffer-substring (point) (point-max))))) + (defun rt-liber-rest-handle-response (buffer) "Handle the response provided in BUFFER." - (with-current-buffer rt-liber-rest-response-buffer + (with-current-buffer buffer (rt-liber-rest-write-debug (buffer-string)))) (defun rt-liber-rest-edit-runner (ticket-id field value) @@ -225,8 +258,8 @@ (rt-liber-rest-write-debug (concat request-data "\n")) (let ((url-request-method "POST") (url-request-data request-data) - rt-liber-rest-response-buffer) - (setq rt-liber-rest-response-buffer + response-buffer) + (setq response-buffer (url-retrieve-synchronously (rt-liber-rest-command-edit-string rt-liber-rest-scheme @@ -234,9 +267,15 @@ ticket-id rt-liber-rest-username rt-liber-rest-password))) - (rt-liber-rest-handle-response rt-liber-rest-response-buffer))) + (rt-liber-rest-handle-response response-buffer))) (message "edit command ended at %s" (current-time-string))) +(defun rt-liber-rest-command-set (id field status) + "Set ticket ID status to be STATUS." + (rt-liber-parse-answer + (rt-liber-rest-edit-runner id field status) + 'rt-liber-command-runner-parser-f)) + (provide 'rt-liberation-rest) diff --git a/rt-liberation-storage.el b/rt-liberation-storage.el index 3018430..df82ce1 100644 --- a/rt-liberation-storage.el +++ b/rt-liberation-storage.el @@ -1,6 +1,6 @@ ;;; rt-liberation-storage.el --- Storage backend for rt-liberation -;; Copyright (C) 2010 Free Software Foundation +;; Copyright (C) 2010 Free Software Foundation, Inc. ;; ;; Author: Yoni Rabkin ;; diff --git a/rt-liberation-update.el b/rt-liberation-update.el index 903a84a..bdd0f75 100644 --- a/rt-liberation-update.el +++ b/rt-liberation-update.el @@ -1,6 +1,6 @@ ;;; rt-liberation-update.el --- check updated tickets -;; Copyright (C) 2009 Free Software Foundation +;; Copyright (C) 2009 Free Software Foundation, Inc. ;; ;; Authors: Yoni Rabkin ;; diff --git a/rt-liberation.el b/rt-liberation.el index 9dd4550..c1a7e76 100644 --- a/rt-liberation.el +++ b/rt-liberation.el @@ -1,7 +1,6 @@ ;;; rt-liberation.el --- Emacs interface to RT -;; Copyright (C) 2008, 2009, 2010, 2011, 2014, 2015, 2020 Free -;; Software Foundation +;; Copyright (C) 2008-2020 Free Software Foundation, Inc. ;; Author: Yoni Rabkin ;; Authors: Aaron S. Hawley , John Sullivan @@ -42,7 +41,7 @@ (require 'browse-url) (require 'time-date) -(require 'seq) +(require 'cl-lib) (require 'rt-liberation-rest) @@ -262,11 +261,10 @@ This variable is made buffer local for the ticket history") (defun rt-liber-reduce (op seq) "Reduce-OP with SEQ to a string of \"s0 op s1 op s2..\"." (if seq - (seq-reduce + (cl-reduce #'(lambda (a b) (format "%s %s %s" a op b)) - (cdr seq) - (car seq)) + seq) "")) (defun rt-liber-make-interval (pred before after) @@ -385,53 +383,32 @@ AFTER date after predicate." (re-search-forward "^id:" (point-max) t)) (while (and continue (re-search-forward - "^\\(\\([\.{} #[:alpha:]]+\\): \\(.*\\)\\)$\\|^--$" + "^\\(\\([.{} #[:alpha:]]+\\): \\(.*\\)\\)$\\|^--$" (point-max) t)) (if (string= (match-string-no-properties 0) "--") (setq continue nil) (push (cons (match-string-no-properties 2) (match-string-no-properties 3)) ticketbase))) - (push (copy-tree ticketbase) ticketbase-list) + (push (copy-sequence ticketbase) ticketbase-list) (setq ticketbase nil continue t)) ticketbase-list)) -(defun rt-liber-rest-ticketsql-runner-parser-f () - "Parser function for a textual list of tickets." - (let (idsub-list) - (rt-liber-rest-parse-http-header) - (while (re-search-forward "ticket/\\([0-9].+\\)" (point-max) t) - (push (list (match-string-no-properties 1) - ".") - idsub-list)) - idsub-list)) - -(defun rt-liber-rest-run-ls-query (query) - "Run an \"ls\" type query against the server with QUERY." - (rt-liber-parse-answer - (rt-liber-rest-query-runner "ls" query) - 'rt-liber-rest-ticketsql-runner-parser-f)) - -(defun rt-liber-rest-run-show-base-query (idsublist) - "Run \"show\" type query against the server with IDSUBLIST." - (rt-liber-parse-answer - (rt-liber-rest-show-query-runner idsublist) - #'rt-liber-ticket-base-retriever-parser-f)) - -(defun rt-liber-rest-run-ticket-history-base-query (ticket-id) - "Run history query against server for TICKET-ID." - (rt-liber-parse-answer - (rt-liber-rest-query-runner "history" ticket-id) - #'(lambda () - (rt-liber-rest-parse-http-header) - (buffer-substring (point) (point-max))))) - -(defun rt-liber-rest-command-set (id field status) - "Set ticket ID status to be STATUS." - (rt-liber-parse-answer - (rt-liber-rest-edit-runner id field status) - 'rt-liber-command-runner-parser-f)) +;; accept the output of `rt-liber-ticketsql-runner-parser-f' and +;; return a string suitable for an RT "show" query +(defun rt-liber-create-tickets-string (idsublist) + "Create a RT CLI ticket \"show\" string from IDSUBLIST." + (let ((ticket-list (mapcar #'(lambda (e) (car e)) idsublist))) + (if ticket-list + (concat "ticket/" + (if (= (length ticket-list) 1) + (format "%s" (car ticket-list)) + (cl-reduce + #'(lambda (a b) + (format "%s,%s" a b)) + ticket-list))) + (signal 'rt-liber-no-result-from-query-error nil)))) ;;; -------------------------------------------------------- @@ -568,9 +545,9 @@ AFTER date after predicate." (make-local-variable 'font-lock-defaults) '((rt-liber-viewer-font-lock-keywords))) (set (make-local-variable 'revert-buffer-function) - 'rt-liber-refresh-ticket-history) + #'rt-liber-refresh-ticket-history) (set (make-local-variable 'buffer-stale-function) - (lambda (&optional noconfirm) 'slow)) + (lambda (&optional _noconfirm) 'slow)) (when rt-liber-jump-to-latest (rt-liber-jump-to-latest-correspondence)) (run-hooks 'rt-liber-viewer-hook)) @@ -601,7 +578,7 @@ ASSOC-BROWSER if non-nil should be a ticket browser." (setq buffer-read-only t))) (switch-to-buffer new-ticket-buffer))) -(defun rt-liber-refresh-ticket-history (&optional ignore-auto noconfirm) +(defun rt-liber-refresh-ticket-history (&optional _ignore-auto _noconfirm) (interactive) (if rt-liber-ticket-local (rt-liber-display-ticket-history rt-liber-ticket-local @@ -807,7 +784,7 @@ The ticket's priority is compared to the variable (when (< 0 filtered-count) (insert (format "%d tickets not shown (filtered)" filtered-count)))))) -(defun rt-liber-browser-refresh (&optional ignore-auto noconfirm) +(defun rt-liber-browser-refresh (&optional _ignore-auto noconfirm) (interactive) (if rt-liber-query (when (or rt-liber-browser-do-refresh @@ -917,7 +894,7 @@ If POINT is nil then called on (point)." (defun rt-liber-sort-ticket-list (ticket-list sort-f) "Return a copy of TICKET-LIST sorted by SORT-F." - (let ((seq (copy-tree ticket-list))) + (let ((seq (copy-sequence ticket-list))) (sort seq sort-f))) (defun rt-liber-sort-by-owner (ticket-list) @@ -943,7 +920,7 @@ If POINT is nil then called on (point)." ;; See the fine manual for example code. -(defun rt-liber-default-filter-f (ticket) +(defun rt-liber-default-filter-f (_ticket) "The default filtering function for the ticket browser This function is really a placeholder for user custom functions, @@ -951,6 +928,79 @@ and as such always return t." t) +;;; -------------------------------------------------------- +;;; Version comparison functions +;;; -------------------------------------------------------- + +;; rt-liber-version-<: string * string -> t-or-nil +(defun rt-liber-version-< (vnum1 vnum2) + "Test whehther version number VNUM1 is less than VNUM2. +Arguments must be strings Lisp objects, and not numbers. + +Examples: + (rt-liber-version-< \"1.01\" \"1.11\") + => t + + (rt-liber-version-< \"1.1\" \"1.0.1\") + => nil" + (rt-liber-version-<- (rt-liber-version-value + (rt-liber-version-read vnum1)) + (rt-liber-version-value + (rt-liber-version-read vnum2)))) + +;; rt-liber-version-read: string -> list string +(defun rt-liber-version-read (str) + "Tokenize version number STR whenever the syntax class changes. + + Example: + \"1.043.0-1_=+\" \ +==> (\"1\" \".\" \"043\" \".\" \"0\" \"-\" \"1\" \"_=+\")" + (let ((tokens nil) + (start 0) + (re (mapconcat 'identity '("[[:digit:]]+" "[[:punct:]]+") "\\|"))) + (while (and (string-match re (substring str start)) + (> (length str) start)) + (setq tokens (cons (match-string 0 (substring str start)) tokens)) + (setq start (+ start (match-end 0)))) + (if (< start (length str)) + (error "Unknown character: %s" (substring str start (1+ start)))) + (reverse tokens))) + +;; rt-liber-version-value: list string -> list number +(defun rt-liber-version-value (tokens) + "Convert list of TOKENS to a comparable number list." + (mapcar #'(lambda (tk) + (if (string-match "^0+$" tk) + 1 + (if (string-match "^[[:digit:]]+$" tk) + (if (string-match "^0+" tk) + (1+ (* (string-to-number tk) + (expt 10 + (- (length + (match-string 0 tk)))))) + (1+ (string-to-number tk))) + (if (string-match "^[[:punct:]]+$" tk) + 0 + ;; else (string-match "[^[:digit:][:punct:]]" tk) + -1)))) + tokens)) + +;; rt-liber-version-<-: list number -> t-or-nil +(defun rt-liber-version-<- (vals1 vals2) + "Test whether version representation VALS1 is less than VALS2." + (if (and (null vals1) (null vals2)) + nil + (if (null vals2) + nil + (if (null vals1) + t + (if (= (car vals1) (car vals2)) + (rt-liber-version-<- (cdr vals1) (cdr vals2)) + (if (< (car vals1) (car vals2)) + t + nil)))))) + + ;;; -------------------------------------------------------- ;;; Entry points ;;; -------------------------------------------------------- @@ -961,7 +1011,7 @@ and as such always return t." NEW if non-nil create additional browser buffer. If NEW is a string then that will be the name of the new buffer." (interactive "Mquery: ") - (condition-case excep + (condition-case nil (rt-liber-browser-startup (rt-liber-rest-run-show-base-query (rt-liber-rest-run-ls-query query)) @@ -981,7 +1031,7 @@ returned as no associated text properties." (or ticket-redraw-f rt-liber-custom-ticket-redraw-function)) (out "")) - (condition-case excep + (condition-case nil (with-temp-buffer (rt-liber-ticketlist-browser-redraw (rt-liber-rest-run-show-base-query @@ -1029,9 +1079,9 @@ returned as no associated text properties." "Major Mode for browsing RT tickets. \\{rt-liber-browser-mode-map}" (set (make-local-variable 'revert-buffer-function) - 'rt-liber-browser-refresh) + #'rt-liber-browser-refresh) (set (make-local-variable 'buffer-stale-function) - (lambda (&optional noconfirm) 'slow)) + (lambda (&optional _noconfirm) 'slow)) (run-hooks 'rt-liber-browser-hook)) (defun rt-liber-setup-browser-name (new) @@ -1118,6 +1168,12 @@ returned as no associated text properties." status-symbol rt-liber-status-dictionary)) +(defun rt-liber-command-get-custom-field-string (custom-field-symbol) + "Return value associated with key CUSTOM-FIELD-SYMBOL." + (rt-liber-command-get-dictionary-value + custom-field-symbol + rt-liber-custom-field-dictionary)) + (defun rt-liber-command-runner-parser-f () "Display command return status from the server to the user." (message (buffer-string))) -- cgit v1.2.3