]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/debbugs/debbugs.el
Release debbugs 0.9.6.
[gnu-emacs-elpa] / packages / debbugs / debbugs.el
index aacd7a0067758a3e039b2659da96df54498f889b..ad5147444fa4f2e9e7ecdd6549e086d3b1f54727 100644 (file)
@@ -1,12 +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 <michael.albinus@gmx.de>
 ;; Keywords: comm, hypermedia
 ;; Package: debbugs
-;; Version: 0.8
-;; Package-Requires: ((async "1.6"))
+;; Version: 0.9.6
+;; 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))
-
-(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"
@@ -106,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.
@@ -189,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),
@@ -216,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)
@@ -233,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
@@ -252,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.
 
@@ -269,7 +282,7 @@ Every returned entry is an association list with the following attributes:
   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.
 
@@ -330,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
@@ -350,7 +380,7 @@ Example:
                  (debbugs-soap-invoke-async
                   "get_status"
                   (apply
-                   'vector
+                   #'vector
                    (butlast
                     bug-ids (- (length bug-ids)
                                debbugs-max-hits-per-request))))))
@@ -360,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.
@@ -439,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)
@@ -448,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))))
 
@@ -514,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.
 
@@ -522,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'.
 
@@ -616,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.
@@ -754,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)
@@ -775,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