X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/bf6098d462783a2ee75d031d80d3d296e4ff138a..aea4c6ae3a0b470ca16b2b209f8ee344c7b21a94:/packages/debbugs/debbugs.el diff --git a/packages/debbugs/debbugs.el b/packages/debbugs/debbugs.el index de10305f6..ad5147444 100644 --- a/packages/debbugs/debbugs.el +++ b/packages/debbugs/debbugs.el @@ -1,11 +1,12 @@ -;;; debbugs.el --- SOAP library to access debbugs servers +;;; debbugs.el --- SOAP library to access debbugs servers -*- lexical-binding:t -*- ;; Copyright (C) 2011-2016 Free Software Foundation, Inc. ;; Author: Michael Albinus ;; Keywords: comm, hypermedia ;; Package: debbugs -;; Version: 0.8 +;; Version: 0.9.6 +;; Package-Requires: ((soap-client "3.1.1") (cl-lib "0.5")) ;; This file is not part of GNU Emacs. @@ -34,11 +35,7 @@ ;(setq soap-debug t message-log-max t) (require 'soap-client) -(eval-when-compile (require 'cl)) - -(declare-function soap-invoke-async "soap-client") -(declare-function async-start "async") -(declare-function async-get "async") +(eval-when-compile (require 'cl-lib)) (defgroup debbugs nil "Debbugs library" @@ -105,33 +102,29 @@ This corresponds to the Debbugs server to be accessed, either (defconst debbugs-max-hits-per-request 500 "The max number of bugs or results per soap invocation.") +(defvar debbugs-cache-data + (make-hash-table :test 'equal :size debbugs-max-hits-per-request) + "Hash table of retrieved bugs.") + +(defcustom debbugs-cache-expiry (* 60 60) + "How many seconds debbugs query results are cached. +`t' or 0 disables caching, `nil' disables expiring." + :group 'debbugs + :type '(choice (const :tag "Always" t) + (const :tag "Never" nil) + (integer :tag "Seconds"))) + (defvar debbugs-soap-invoke-async-object nil "The object manipulated by `debbugs-soap-invoke-async'.") (defun debbugs-soap-invoke-async (operation-name &rest parameters) - "Invoke the SOAP connection asynchronously. -If possible, it uses `soap-invoke-async' from soapclient 3.0. -Otherwise, `async-start' from the async package is used." - (if (fboundp 'soap-invoke-async) - ;; This is soap-client 3.0. - (apply - 'soap-invoke-async - (lambda (response &rest args) - (setq debbugs-soap-invoke-async-object - (append debbugs-soap-invoke-async-object (car response)))) - nil - debbugs-wsdl debbugs-port operation-name parameters) - ;; Fallback with async. - (async-start - `(lambda () - (load ,(locate-library "soap-client")) - (apply - 'soap-invoke - (soap-load-wsdl - ,(expand-file-name - "Debbugs.wsdl" - (file-name-directory (locate-library "debbugs")))) - ,debbugs-port ,operation-name ',parameters))))) + "Invoke the SOAP connection asynchronously." + (apply + #'soap-invoke-async + (lambda (response &rest _args) + (setq debbugs-soap-invoke-async-object + (append debbugs-soap-invoke-async-object (car response)))) + nil debbugs-wsdl debbugs-port operation-name parameters)) (defun debbugs-get-bugs (&rest query) "Return a list of bug numbers which match QUERY. @@ -188,8 +181,8 @@ Valid keywords are: the value of field `affects' in bug's status. The returned bugs do not necessary belong to this package. - :status -- Status of bug. Valid values are \"done\", - \"forwarded\" and \"open\". + :status -- Status of bug. Valid values are \"pending\", + \"forwarded\", \"fixed\" and \"done\". :archive -- A keyword to filter for bugs which are already archived, or not. Valid values are \"0\" (not archived), @@ -215,7 +208,7 @@ patch: (unless (and (keywordp kw) (stringp val)) (error "Wrong query: %s %s" kw val)) (setq key (substring (symbol-name kw) 1)) - (case kw + (cl-case kw ((:package :severity :tag :src :affects) ;; Value shall be one word. (if (string-match "\\`\\S-+\\'" val) @@ -232,8 +225,8 @@ patch: (setq vec (vconcat vec (list key val)))) (error "Wrong %s: %s" key val))) (:status - ;; Possible values: "done", "forwarded" and "open" - (if (string-match "\\`\\(done\\|forwarded\\|open\\)\\'" val) + ;; Possible values: "pending", "forwarded", "fixed" and "done". + (if (string-match "\\`\\(pending\\|forwarded\\|fixed\\|done\\)\\'" val) (setq vec (vconcat vec (list key val))) (error "Wrong %s: %s" key val))) (:archive @@ -251,6 +244,27 @@ patch: "Return the list of bug numbers, according to AMOUNT (a number) latest bugs." (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<)) +(defun debbugs-convert-soap-value-to-string (string-value) + "If STRING-VALUE is unibyte, decode its contents as a UTF-8 string. +If STRING-VALUE is a multibyte string, then `soap-client' +received an xsd:string for this value, and will have decoded it +already. + +If STRING-VALUE is a unibyte string, then `soap-client' received +an xsd:base64Binary, and ran `base64-decode-string' on it to +produce a unibyte string of bytes. + +For some reason, the Debbugs server code base64-encodes strings +that contain UTF-8 characters, and returns them as +xsd:base64Binary, instead of just returning them as xsd:string. +Therefore, when STRING-VALUE is a unibyte string, we assume its +bytes represent a UTF-8 string and decode them accordingly." + (if (stringp string-value) + (if (not (multibyte-string-p string-value)) + (decode-coding-string string-value 'utf-8) + string-value) + (error "Invalid string value"))) + (defun debbugs-get-status (&rest bug-numbers) "Return a list of status entries for the bugs identified by BUG-NUMBERS. @@ -261,13 +275,14 @@ Every returned entry is an association list with the following attributes: `package': A list of package names the bug belongs to. `severity': The severity of the bug report. This can be - \"important\", \"grave\", \"normal\", \"minor\" or \"wishlist\". + \"critical\", \"grave\", \"serious\", \"important\", + \"normal\", \"minor\" or \"wishlist\". `tags': The status of the bug report, a list of strings. This can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\", \"moreinfo\" or \"patch\". - `pending': The string \"pending\", \"forwarded\" or \"done\". + `pending': The string \"pending\", \"forwarded\", \"fixed\" or \"done\". `subject': Subject/Title of the bugreport. @@ -328,18 +343,35 @@ Example: \(last_modified . 1271200046.0) \(pending . \"pending\") \(package \"emacs\")))" - (when bug-numbers - (if (<= (length bug-numbers) debbugs-max-hits-per-request) - ;; Do it directly. - (setq debbugs-soap-invoke-async-object - (car (soap-invoke - debbugs-wsdl debbugs-port "get_status" - (apply 'vector bug-numbers)))) - + (let (cached-bugs) + ;; Check for cached bugs. + (setq bug-numbers (delete-dups bug-numbers) + bug-numbers + (delete + nil + (mapcar + (lambda (bug) + (let ((status (gethash bug debbugs-cache-data))) + (if (and + status + (or + (null debbugs-cache-expiry) + (and + (natnump debbugs-cache-expiry) + (> (cdr (assoc 'cache_time status)) + (- (float-time)) debbugs-cache-expiry)))) + (progn + (setq cached-bugs (append cached-bugs (list status))) + nil) + bug))) + bug-numbers))) + + ;; Retrieve the data. + (setq debbugs-soap-invoke-async-object nil) + (when bug-numbers ;; Retrieve bugs asynchronously. (let ((bug-ids bug-numbers) results) - (setq debbugs-soap-invoke-async-object nil) (while bug-ids (setq results (append @@ -348,7 +380,7 @@ Example: (debbugs-soap-invoke-async "get_status" (apply - 'vector + #'vector (butlast bug-ids (- (length bug-ids) debbugs-max-hits-per-request)))))) @@ -358,44 +390,56 @@ Example: debbugs-max-hits-per-request)))) (dolist (res results) - (if (bufferp res) - ;; This is soap-client 3.0. - (while (buffer-live-p res) - (accept-process-output (get-buffer-process res) 0.1)) - ;; Fallback with async. - (dolist (status (async-get res)) - (setq debbugs-soap-invoke-async-object - (append debbugs-soap-invoke-async-object status))))))) - - (mapcar - (lambda (x) - (let (y) - ;; "archived" is the number 1 or 0. - (setq y (assoc 'archived (cdr (assoc 'value x)))) - (setcdr y (= (cdr y) 1)) - ;; "found_versions" and "fixed_versions" are lists, - ;; containing strings or numbers. - (dolist (attribute '(found_versions fixed_versions)) - (setq y (assoc attribute (cdr (assoc 'value x)))) - (setcdr y (mapcar - (lambda (z) (if (numberp z) (number-to-string z) z)) - (cdr y)))) - ;; "mergedwith", "blocks" and "blockedby are strings, - ;; containing blank separated bug numbers. - (dolist (attribute '(mergedwith blocks blockedby)) - (setq y (assoc attribute (cdr (assoc 'value x)))) - (when (stringp (cdr y)) - (setcdr y (mapcar - 'string-to-number (split-string (cdr y) " " t))))) - ;; "package" is a string, containing comma separated - ;; package names. "keywords" and "tags" are strings, - ;; containing blank separated package names. - (dolist (attribute '(package keywords tags)) - (setq y (assoc attribute (cdr (assoc 'value x)))) - (when (stringp (cdr y)) - (setcdr y (split-string (cdr y) ",\\| " t)))) - (cdr (assoc 'value x)))) - debbugs-soap-invoke-async-object))) + (while (buffer-live-p res) + (accept-process-output (get-buffer-process res) 0.1))))) + + (append + cached-bugs + ;; Massage results. + (mapcar + (lambda (x) + (let (y) + ;; "archived" is the number 1 or 0. + (setq y (assoc 'archived (cdr (assoc 'value x)))) + (setcdr y (= (cdr y) 1)) + ;; "found_versions" and "fixed_versions" are lists, + ;; containing strings or numbers. + (dolist (attribute '(found_versions fixed_versions)) + (setq y (assoc attribute (cdr (assoc 'value x)))) + (setcdr y (mapcar + (lambda (z) (if (numberp z) (number-to-string z) z)) + (cdr y)))) + ;; "mergedwith", "blocks" and "blockedby are strings, + ;; containing blank separated bug numbers. + (dolist (attribute '(mergedwith blocks blockedby)) + (setq y (assoc attribute (cdr (assoc 'value x)))) + (when (stringp (cdr y)) + (setcdr y (mapcar + #'string-to-number (split-string (cdr y) " " t))))) + ;; "subject", "originator", "owner" and "summary" may be an + ;; xsd:base64Binary value containing a UTF-8-encoded string. + (dolist (attribute '(subject originator owner summary)) + (setq y (assoc attribute (cdr (assoc 'value x)))) + (when (stringp (cdr y)) + (setcdr y (debbugs-convert-soap-value-to-string (cdr y))))) + ;; "package" is a string, containing comma separated + ;; package names. "keywords" and "tags" are strings, + ;; containing blank separated package names. + (dolist (attribute '(package keywords tags)) + (setq y (assoc attribute (cdr (assoc 'value x)))) + (when (stringp (cdr y)) + (setcdr y (split-string (cdr y) ",\\| " t)))) + ;; Cache the result, and return. + (if (and debbugs-cache-expiry (natnump debbugs-cache-expiry)) + (puthash + (cdr (assoc 'key x)) + ;; Put also a time stamp. + (cons (cons 'cache_time (float-time)) + (cdr (assoc 'value x))) + debbugs-cache-data) + ;; Don't cache. + (cdr (assoc 'value x))))) + debbugs-soap-invoke-async-object)))) (defun debbugs-get-usertag (&rest query) "Return a list of bug numbers which match QUERY. @@ -437,7 +481,7 @@ Example: (unless (and (keywordp kw) (stringp val)) (error "Wrong query: %s %s" kw val)) (setq key (substring (symbol-name kw) 1)) - (case kw + (cl-case kw ((:user) ;; Value shall be one word. Extract email address, if existing. (if (string-match "\\`\\S-+\\'" val) @@ -446,12 +490,12 @@ Example: (setq val user-mail-address)) (when (string-match "<\\(.+\\)>" val) (setq val (match-string 1 val))) - (pushnew val user :test #'equal)) + (cl-pushnew val user :test #'equal)) (error "Wrong %s: %s" key val))) ((:tag) ;; Value shall be one word. (if (string-match "\\`\\S-+\\'" val) - (pushnew val tags :test #'equal) + (cl-pushnew val tags :test #'equal) (error "Wrong %s: %s" key val))) (t (error "Unknown key: %s" kw)))) @@ -512,7 +556,8 @@ The following conditions are possible: :skip and :max are optional. They specify, how many hits are skipped, and how many maximal hits are returned. This can be - used for paged results. Per default, :skip is 0 and :max is 10. + used for paged results. Per default, :skip is 0 and all + possible hits are returned. There must be exactly one such condition. @@ -520,17 +565,15 @@ The following conditions are possible: ATTRIBUTE is one of the following keywords: - :status -- Status of bug. Valid values are \"done\", - \"forwarded\" and \"open\". - :subject, :@title -- The subject of a message or the title of the bug, a string. :date, :@cdate -- The submission or modification dates of a message, a number. - :submitter, :@author -- The email address of the submitter of a - bug or the author of a message belonging to this bug, a string. + :@author -- The email address of the author of a message + belonging to this bug, a string. It may be different than + the email of the person submitting the bug. The special email address \"me\" is used as pattern, replaced with `user-mail-address'. @@ -614,134 +657,164 @@ Examples: ,\(floor \(float-time \(encode-time 0 0 0 31 8 2011))) :operator \"NUMBT\"))" - (let (args result) - ;; Compile search arguments. - (dolist (elt query) - (let (vec kw key val - phrase-cond attr-cond) - - ;; Phrase is mandatory, even if empty. - (when (and (or (member :skip elt) (member :max elt)) - (not (member :phrase elt))) - (setq vec (vector "phrase" ""))) - - ;; Parse condition. - (while (consp elt) - (setq kw (pop elt)) - (unless (keywordp kw) - (error "Wrong keyword: %s" kw)) - (setq key (substring (symbol-name kw) 1)) - (cl-case kw - ;; Phrase condition. - (:phrase - ;; It shouldn't happen in an attribute condition. - (if attr-cond - (error "Wrong keyword: %s" kw)) - (setq phrase-cond t val (pop elt)) - ;; Value is a string. - (if (stringp val) - (setq vec (vconcat vec (list key val))) - (error "Wrong %s: %s" key val))) - - ((:skip :max) - ;; It shouldn't happen in an attribute condition. - (if attr-cond - (error "Wrong keyword: %s" kw)) - (setq phrase-cond t val (pop elt)) - ;; Value is a number. - (if (numberp val) - (setq vec (vconcat vec (list key (number-to-string val)))) - (error "Wrong %s: %s" key val))) - - ;; Attribute condition. - ((:submitter :@author) - ;; It shouldn't happen in a phrase condition. - (if phrase-cond - (error "Wrong keyword: %s" kw)) - (if (not (stringp (car elt))) - (setq vec (vconcat vec (list key ""))) - ;; Value is an email address. - (while (and (stringp (car elt)) - (string-match "\\`\\S-+\\'" (car elt))) - (when (string-equal "me" (car elt)) - (setcar elt user-mail-address)) - (when (string-match "<\\(.+\\)>" (car elt)) - (setcar elt (match-string 1 (car elt)))) - (let ((x (pop elt))) - (unless (member x val) - (setq val (append val (list x)))))) - (setq vec - (vconcat vec (list key (mapconcat 'identity val " ")))))) - - (:status - ;; It shouldn't happen in a phrase condition. - (if phrase-cond - (error "Wrong keyword: %s" kw)) - (setq attr-cond t) - (if (not (stringp (car elt))) - (setq vec (vconcat vec (list key ""))) - ;; Possible values: "done", "forwarded" and "open" - (while (and (stringp (car elt)) - (string-match - "\\`\\(done\\|forwarded\\|open\\)\\'" (car elt))) - (let ((x (pop elt))) - (unless (member x val) - (setq val (append val (list x)))))) - (setq vec - (vconcat vec (list key (mapconcat 'identity val " ")))))) - - ((:subject :package :tags :severity :@title) - ;; It shouldn't happen in a phrase condition. - (if phrase-cond - (error "Wrong keyword: %s" kw)) - (setq attr-cond t) - (if (not (stringp (car elt))) - (setq vec (vconcat vec (list key ""))) - ;; Just a string. - (while (stringp (car elt)) - (let ((x (pop elt))) - (unless (member x val) - (setq val (append val (list x)))))) - (setq vec - (vconcat vec (list key (mapconcat 'identity val " ")))))) - - ((:date :@cdate) - ;; It shouldn't happen in a phrase condition. - (if phrase-cond - (error "Wrong keyword: %s" kw)) - (setq attr-cond t) - (if (not (numberp (car elt))) - (setq vec (vconcat vec (list key ""))) - ;; Just a number. - (while (numberp (car elt)) - (let ((x (pop elt))) - (unless (member x val) - (setq val (append val (list x)))))) - (setq vec - (vconcat - vec (list key (mapconcat 'number-to-string val " ")))))) - - ((:operator :order) - ;; It shouldn't happen in a phrase condition. - (if phrase-cond - (error "Wrong keyword: %s" kw)) - (setq attr-cond t val (pop elt)) - ;; Value is a number. - (if (stringp val) - (setq vec (vconcat vec (list key val))) - (error "Wrong %s: %s" key val))) - - (t (error "Unknown key: %s" kw)))) - - (setq args (vconcat args (list vec))))) - - (setq result - (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args))) - ;; The result contains lists (key value). We transform it into - ;; cons cells (key . value). - (dolist (elt1 result result) - (dolist (elt2 elt1) - (setcdr elt2 (cadr elt2)))))) + (let ((phrase (assoc :phrase query)) + args result) + (if (and phrase (not (member :skip phrase)) (not (member :max phrase))) + ;; We loop, until we have all results. + (let ((skip 0) + (query (delete phrase query)) + result1) + (while skip + (setq result1 + (apply + #'debbugs-search-est + (append + (list + (append + phrase `(:skip ,skip) + `(:max ,debbugs-max-hits-per-request))) + query)) + skip (and (= (length result1) debbugs-max-hits-per-request) + (+ skip debbugs-max-hits-per-request)) + result (append result result1))) + result) + + ;; Compile search arguments. + (dolist (elt query) + ;; FIXME: `vec' is used in an O(N²) way. It should be a list instead, + ;; on which we push elements, and we only convert it to a vector at + ;; the end. + (let (vec kw key val + phrase-cond attr-cond) + + ;; Phrase is mandatory, even if empty. + (when (and (or (member :skip elt) (member :max elt)) + (not (member :phrase elt))) + (setq vec (vector "phrase" ""))) + + ;; Parse condition. + (while (consp elt) + (setq kw (pop elt)) + (unless (keywordp kw) + (error "Wrong keyword: %s" kw)) + (setq key (substring (symbol-name kw) 1)) + (cl-case kw + ;; Phrase condition. + (:phrase + ;; It shouldn't happen in an attribute condition. + (if attr-cond + (error "Wrong keyword: %s" kw)) + (setq phrase-cond t val (pop elt)) + ;; Value is a string. + (if (stringp val) + (setq vec (vconcat vec (list key val))) + (error "Wrong %s: %s" key val))) + + ((:skip :max) + ;; It shouldn't happen in an attribute condition. + (if attr-cond + (error "Wrong keyword: %s" kw)) + (setq phrase-cond t val (pop elt)) + ;; Value is a number. + (if (numberp val) + (setq vec (vconcat vec (list key (number-to-string val)))) + (error "Wrong %s: %s" key val))) + + ;; Attribute condition. + ((:submitter :@author) + ;; It shouldn't happen. + (if (or (and (eq kw :submitter) phrase-cond) + (and (eq kw :@author) attr-cond)) + (error "Wrong keyword: %s" kw)) + (if (not (stringp (car elt))) + (setq vec (vconcat vec (list key ""))) + ;; Value is an email address. + (while (and (stringp (car elt)) + (string-match "\\`\\S-+\\'" (car elt))) + (when (string-equal "me" (car elt)) + (setcar elt user-mail-address)) + (when (string-match "<\\(.+\\)>" (car elt)) + (setcar elt (match-string 1 (car elt)))) + (let ((x (pop elt))) + (unless (member x val) + (setq val (append val (list x)))))) + (setq vec + (vconcat + vec (list key (mapconcat #'identity val " ")))))) + + (:status + ;; It shouldn't happen in a phrase condition. + (if phrase-cond + (error "Wrong keyword: %s" kw)) + (setq attr-cond t) + (if (not (stringp (car elt))) + (setq vec (vconcat vec (list key ""))) + ;; Possible values: "pending", "forwarded", "fixed" and "done". + (while (and (stringp (car elt)) + (string-match + "\\`\\(pending\\|forwarded\\|fixed\\|done\\)\\'" + (car elt))) + (let ((x (pop elt))) + (unless (member x val) + (setq val (append val (list x)))))) + (setq vec + (vconcat + vec (list key (mapconcat #'identity val " ")))))) + + ((:subject :package :tags :severity :@title) + ;; It shouldn't happen in a phrase condition. + (if phrase-cond + (error "Wrong keyword: %s" kw)) + (setq attr-cond t) + (if (not (stringp (car elt))) + (setq vec (vconcat vec (list key ""))) + ;; Just a string. + (while (stringp (car elt)) + (let ((x (pop elt))) + (unless (member x val) + (setq val (append val (list x)))))) + (setq vec + (vconcat + vec (list key (mapconcat #'identity val " ")))))) + + ((:date :@cdate) + ;; It shouldn't happen in a phrase condition. + (if phrase-cond + (error "Wrong keyword: %s" kw)) + (setq attr-cond t) + (if (not (numberp (car elt))) + (setq vec (vconcat vec (list key ""))) + ;; Just a number. + (while (numberp (car elt)) + (let ((x (pop elt))) + (unless (member x val) + (setq val (append val (list x)))))) + (setq vec + (vconcat + vec + (list key (mapconcat #'number-to-string val " ")))))) + + ((:operator :order) + ;; It shouldn't happen in a phrase condition. + (if phrase-cond + (error "Wrong keyword: %s" kw)) + (setq attr-cond t val (pop elt)) + ;; Value is a number. + (if (stringp val) + (setq vec (vconcat vec (list key val))) + (error "Wrong %s: %s" key val))) + + (t (error "Unknown key: %s" kw)))) + + (setq args (vconcat args (list vec))))) + + (setq result + (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args))) + ;; The result contains lists (key value). We transform it into + ;; cons cells (key . value). + (dolist (elt1 result result) + (dolist (elt2 elt1) + (setcdr elt2 (cadr elt2))))))) (defun debbugs-get-attribute (bug-or-message attribute) "Return the value of key ATTRIBUTE. @@ -752,7 +825,7 @@ BUG-OR-MESSAGE must be list element returned by either Example: Return the originator of last submitted bug. \(debbugs-get-attribute - \(car \(apply 'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)" + \(car \(apply #'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)" (cdr (assoc attribute bug-or-message))) (defun debbugs-get-message-numbers (messages) @@ -773,7 +846,7 @@ If there is no message with MESSAGE-NUMBER, the function returns `nil'. Example: Return the first message of last submitted bug. -\(let \(\(messages \(apply 'debbugs-get-bug-log \(debbugs-newest-bugs 1)))) +\(let \(\(messages \(apply #'debbugs-get-bug-log \(debbugs-newest-bugs 1)))) \(debbugs-get-message messages \(car \(debbugs-get-message-numbers messages))))" (while (and messages