-;;; debbugs.el --- SOAP library to access debbugs servers
+;;; debbugs.el --- SOAP library to access debbugs servers -*- lexical-binding:t -*-
-;; Copyright (C) 2011-2015 Free Software Foundation, Inc.
+;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
;; Author: Michael Albinus <michael.albinus@gmx.de>
;; Keywords: comm, hypermedia
;; Package: debbugs
-;; Version: 0.7
+;; Version: 0.9.5
+;; Package-Requires: ((soap-client "3.1.1") (cl-lib "0.5"))
;; This file is not part of GNU Emacs.
;(setq soap-debug t message-log-max t)
(require 'soap-client)
-(eval-when-compile (require 'cl))
+(eval-when-compile (require 'cl-lib))
(defgroup debbugs nil
"Debbugs library"
default-directory)))
"The WSDL object to be used describing the SOAP interface.")
+;; Please do not increase this value, otherwise we would run into
+;; performance problems on the server. Maybe we need to change this a
+;; server specific value.
+(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."
+ (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.
(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)
"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.
`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\",
\(last_modified . 1271200046.0)
\(pending . \"pending\")
\(package \"emacs\")))"
- (when bug-numbers
- (let ((object
- (car
- (soap-invoke
- debbugs-wsdl debbugs-port "get_status"
- (apply 'vector bug-numbers)))))
- (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))))
- object))))
+ (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)
+ (while bug-ids
+ (setq results
+ (append
+ results
+ (list
+ (debbugs-soap-invoke-async
+ "get_status"
+ (apply
+ #'vector
+ (butlast
+ bug-ids (- (length bug-ids)
+ debbugs-max-hits-per-request))))))
+
+ bug-ids
+ (last bug-ids (- (length bug-ids)
+ debbugs-max-hits-per-request))))
+
+ (dolist (res results)
+ (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.
(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)
(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))))
: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.
,\(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 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)))))))
(defun debbugs-get-attribute (bug-or-message attribute)
"Return the value of key ATTRIBUTE.
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)
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