]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs.el
* debbugs: Use cl-lib. Use lexical-binding when available
[gnu-emacs-elpa] / packages / debbugs / debbugs.el
1 ;;; debbugs.el --- SOAP library to access debbugs servers -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
4
5 ;; Author: Michael Albinus <michael.albinus@gmx.de>
6 ;; Keywords: comm, hypermedia
7 ;; Package: debbugs
8 ;; Version: 0.9.3
9 ;; Package-Requires: ((soap-client "3.1.1") (cl-lib "0.5"))
10
11 ;; This file is not part of GNU Emacs.
12
13 ;; This program is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25
26 ;;; Commentary:
27
28 ;; This package provides basic functions to access a Debbugs SOAP
29 ;; server (see <http://wiki.debian.org/DebbugsSoapInterface>).
30
31 ;; The function "get_versions" is not implemented (yet). "search_est"
32 ;; is an extension on <http://debbugs.gnu.org>.
33
34 ;;; Code:
35
36 ;(setq soap-debug t message-log-max t)
37 (require 'soap-client)
38 (eval-when-compile (require 'cl-lib))
39
40 (defgroup debbugs nil
41 "Debbugs library"
42 :group 'hypermedia)
43
44 (defcustom debbugs-servers
45 '(("gnu.org"
46 :wsdl "http://debbugs.gnu.org/cgi/soap.cgi?WSDL"
47 :bugreport-url "http://debbugs.gnu.org/cgi/bugreport.cgi")
48 ("debian.org"
49 :wsdl "http://bugs.debian.org/cgi-bin/soap.cgi?WSDL"
50 :bugreport-url "http://bugs.debian.org/cgi-bin/bugreport.cgi"))
51 "*List of Debbugs server specifiers.
52 Each entry is a list that contains a string identifying the port
53 name and the server parameters in keyword-value form. Allowed
54 keywords are:
55
56 `:wsdl' -- Location of WSDL. The value is a string with URL that
57 should return the WSDL specification of Debbugs/SOAP service.
58
59 `:bugreport-url' -- URL of the server script that returns mboxes
60 with bug logs.
61
62 The list initially contains two predefined and configured Debbugs
63 servers: \"gnu.org\" and \"debian.org\"."
64 :group 'debbugs
65 :link '(custom-manual "(debbugs)Debbugs server specifiers")
66 :type '(choice
67 (const nil)
68 (repeat
69 (cons :tag "Server"
70 (string :tag "Port name")
71 (checklist :tag "Options" :greedy t
72 (group :inline t
73 (const :format "" :value :wsdl)
74 (string :tag "WSDL"))
75 (group :inline t
76 (const :format "" :value :bugreport-url)
77 (string :tag "Bugreport URL")))))))
78
79 (defcustom debbugs-port "gnu.org"
80 "The port instance to be applied from `debbugs-wsdl'.
81 This corresponds to the Debbugs server to be accessed, either
82 \"gnu.org\", or \"debian.org\", or user defined port name."
83 ;; Maybe we should create an own group?
84 :group 'debbugs
85 :type '(choice :tag "Debbugs server" (const "gnu.org") (const "debian.org")
86 (string :tag "user defined port name")))
87
88 ;; It would be nice if we could retrieve it from the debbugs server.
89 ;; Not supported yet.
90 (defconst debbugs-wsdl
91 (soap-load-wsdl
92 (expand-file-name
93 "Debbugs.wsdl"
94 (if load-in-progress
95 (file-name-directory load-file-name)
96 default-directory)))
97 "The WSDL object to be used describing the SOAP interface.")
98
99 ;; Please do not increase this value, otherwise we would run into
100 ;; performance problems on the server. Maybe we need to change this a
101 ;; server specific value.
102 (defconst debbugs-max-hits-per-request 500
103 "The max number of bugs or results per soap invocation.")
104
105 (defvar debbugs-cache-data
106 (make-hash-table :test 'equal :size debbugs-max-hits-per-request)
107 "Hash table of retrieved bugs.")
108
109 (defcustom debbugs-cache-expiry (* 60 60)
110 "How many seconds debbugs query results are cached.
111 `t' or 0 disables caching, `nil' disables expiring."
112 :group 'debbugs
113 :type '(choice (const :tag "Always" t)
114 (const :tag "Never" nil)
115 (integer :tag "Seconds")))
116
117 (defvar debbugs-soap-invoke-async-object nil
118 "The object manipulated by `debbugs-soap-invoke-async'.")
119
120 (defun debbugs-soap-invoke-async (operation-name &rest parameters)
121 "Invoke the SOAP connection asynchronously."
122 (apply
123 #'soap-invoke-async
124 (lambda (response &rest _args)
125 (setq debbugs-soap-invoke-async-object
126 (append debbugs-soap-invoke-async-object (car response))))
127 nil debbugs-wsdl debbugs-port operation-name parameters))
128
129 (defun debbugs-get-bugs (&rest query)
130 "Return a list of bug numbers which match QUERY.
131
132 QUERY is a sequence of keyword-value pairs where the values are
133 strings, i.e. :KEYWORD \"VALUE\" [:KEYWORD \"VALUE\"]*
134
135 The keyword-value pair is a subquery. The keywords are allowed to
136 have multiple occurrence within the query at any place. The
137 subqueries with the same keyword form the logical subquery, which
138 returns the union of bugs of every subquery it contains.
139
140 The result of the QUERY is an intersection of results of all
141 subqueries.
142
143 Valid keywords are:
144
145 :package -- The value is the name of the package a bug belongs
146 to, like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\".
147
148 :src -- This is used to retrieve bugs that belong to source
149 with given name.
150
151 :severity -- This is the severity of the bug. The exact set of
152 allowed values depends on the Debbugs port. Examples are
153 \"normal\", \"minor\", \"wishlist\" etc.
154
155 :tag -- An arbitrary string the bug is annotated with.
156 Usually, this is used to mark the status of the bug, like
157 \"fixed\", \"moreinfo\", \"notabug\", \"patch\",
158 \"unreproducible\" or \"wontfix\". The exact set of tags
159 depends on the Debbugs port.
160
161 :owner -- This is used to identify bugs by the owner's email
162 address. The special email address \"me\" is used as pattern,
163 replaced with `user-mail-address'.
164
165 :submitter -- With this keyword it is possible to filter bugs
166 by the submitter's email address. The special email address
167 \"me\" is used as pattern, replaced with `user-mail-address'.
168
169 :maint -- This is used to find bugs of the packages which are
170 maintained by the person with the given email address. The
171 special email address \"me\" is used as pattern, replaced with
172 `user-mail-address'.
173
174 :correspondent -- This allows to find bug reports where the
175 person with the given email address has participated. The
176 special email address \"me\" is used as pattern, replaced with
177 `user-mail-address'.
178
179 :affects -- With this keyword it is possible to find bugs which
180 affect the package with the given name. The bugs are chosen by
181 the value of field `affects' in bug's status. The returned bugs
182 do not necessary belong to this package.
183
184 :status -- Status of bug. Valid values are \"done\",
185 \"forwarded\" and \"open\".
186
187 :archive -- A keyword to filter for bugs which are already
188 archived, or not. Valid values are \"0\" (not archived),
189 \"1\" (archived) or \"both\". If this keyword is not given in
190 the query, `:archive \"0\"' is assumed by default.
191
192 Example. Get all opened and forwarded release critical bugs for
193 the packages which are maintained by \"me\" and which have a
194 patch:
195
196 \(debbugs-get-bugs :maint \"me\" :tag \"patch\"
197 :severity \"critical\"
198 :status \"open\"
199 :severity \"grave\"
200 :status \"forwarded\"
201 :severity \"serious\")"
202
203 (let (vec kw key val)
204 ;; Check query.
205 (while (and (consp query) (<= 2 (length query)))
206 (setq kw (pop query)
207 val (pop query))
208 (unless (and (keywordp kw) (stringp val))
209 (error "Wrong query: %s %s" kw val))
210 (setq key (substring (symbol-name kw) 1))
211 (cl-case kw
212 ((:package :severity :tag :src :affects)
213 ;; Value shall be one word.
214 (if (string-match "\\`\\S-+\\'" val)
215 (setq vec (vconcat vec (list key val)))
216 (error "Wrong %s: %s" key val)))
217 ((:owner :submitter :maint :correspondent)
218 ;; Value is an email address.
219 (if (string-match "\\`\\S-+\\'" val)
220 (progn
221 (when (string-equal "me" val)
222 (setq val user-mail-address))
223 (when (string-match "<\\(.+\\)>" val)
224 (setq val (match-string 1 val)))
225 (setq vec (vconcat vec (list key val))))
226 (error "Wrong %s: %s" key val)))
227 (:status
228 ;; Possible values: "done", "forwarded" and "open"
229 (if (string-match "\\`\\(done\\|forwarded\\|open\\)\\'" val)
230 (setq vec (vconcat vec (list key val)))
231 (error "Wrong %s: %s" key val)))
232 (:archive
233 ;; Value is `0' or `1' or `both'.
234 (if (string-match "\\`\\(0\\|1\\|both\\)\\'" val)
235 (setq vec (vconcat vec (list key val)))
236 (error "Wrong %s: %s" key val)))
237 (t (error "Unknown key: %s" kw))))
238
239 (unless (null query)
240 (error "Unknown key: %s" (car query)))
241 (sort (car (soap-invoke debbugs-wsdl debbugs-port "get_bugs" vec)) '<)))
242
243 (defun debbugs-newest-bugs (amount)
244 "Return the list of bug numbers, according to AMOUNT (a number) latest bugs."
245 (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<))
246
247 (defun debbugs-convert-soap-value-to-string (string-value)
248 "If STRING-VALUE is unibyte, decode its contents as a UTF-8 string.
249 If STRING-VALUE is a multibyte string, then `soap-client'
250 received an xsd:string for this value, and will have decoded it
251 already.
252
253 If STRING-VALUE is a unibyte string, then `soap-client' received
254 an xsd:base64Binary, and ran `base64-decode-string' on it to
255 produce a unibyte string of bytes.
256
257 For some reason, the Debbugs server code base64-encodes strings
258 that contain UTF-8 characters, and returns them as
259 xsd:base64Binary, instead of just returning them as xsd:string.
260 Therefore, when STRING-VALUE is a unibyte string, we assume its
261 bytes represent a UTF-8 string and decode them accordingly."
262 (if (stringp string-value)
263 (if (not (multibyte-string-p string-value))
264 (decode-coding-string string-value 'utf-8)
265 string-value)
266 (error "Invalid string value")))
267
268 (defun debbugs-get-status (&rest bug-numbers)
269 "Return a list of status entries for the bugs identified by BUG-NUMBERS.
270
271 Every returned entry is an association list with the following attributes:
272
273 `bug_num': The bug number.
274
275 `package': A list of package names the bug belongs to.
276
277 `severity': The severity of the bug report. This can be
278 \"critical\", \"grave\", \"serious\", \"important\",
279 \"normal\", \"minor\" or \"wishlist\".
280
281 `tags': The status of the bug report, a list of strings. This
282 can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\",
283 \"moreinfo\" or \"patch\".
284
285 `pending': The string \"pending\", \"forwarded\" or \"done\".
286
287 `subject': Subject/Title of the bugreport.
288
289 `originator': Submitter of the bugreport.
290
291 `mergedwith': A list of bug numbers this bug was merged with.
292 If it is a single bug, then this attribute contains just a
293 number.
294
295 `source': Source package name of the bug report.
296
297 `date': Date of bug creation.
298
299 `log_modified', `last_modified': Date of last update.
300
301 `found_date', `fixed_date': Date of bug report / bug fix
302 \(empty for now).
303
304 `done': The email address of the worker who has closed the bug (if done).
305
306 `archived': `t' if the bug is archived, `nil' otherwise.
307
308 `unarchived': The date the bug has been unarchived, if ever.
309
310 `found_versions', `fixed_versions': List of version strings.
311
312 `forwarded': A URL or an email address.
313
314 `blocks': A list of bug numbers this bug blocks.
315
316 `blockedby': A list of bug numbers this bug is blocked by.
317
318 `msgid': The message id of the initial bug report.
319
320 `owner': Who is responsible for fixing.
321
322 `location': Always the string \"db-h\" or \"archive\".
323
324 `affects': A list of package names.
325
326 `summary': Arbitrary text.
327
328 Example:
329
330 \(debbugs-get-status 10)
331
332 => ;; Attributes with empty values are not shown
333 \(\(\(bug_num . 10)
334 \(source . \"unknown\")
335 \(date . 1203606305.0)
336 \(msgid . \"<87zltuz7eh.fsf@freemail.hu>\")
337 \(severity . \"wishlist\")
338 \(owner . \"Magnus Henoch <mange@freemail.hu>\")
339 \(log_modified . 1261079402.0)
340 \(location . \"db-h\")
341 \(subject . \"url-gw should support HTTP CONNECT proxies\")
342 \(originator . \"Magnus Henoch <mange@freemail.hu>\")
343 \(last_modified . 1271200046.0)
344 \(pending . \"pending\")
345 \(package \"emacs\")))"
346 (let (cached-bugs)
347 ;; Check for cached bugs.
348 (setq bug-numbers (delete-dups bug-numbers)
349 bug-numbers
350 (delete
351 nil
352 (mapcar
353 (lambda (bug)
354 (let ((status (gethash bug debbugs-cache-data)))
355 (if (and
356 status
357 (or
358 (null debbugs-cache-expiry)
359 (and
360 (natnump debbugs-cache-expiry)
361 (> (cdr (assoc 'cache_time status))
362 (- (float-time)) debbugs-cache-expiry))))
363 (progn
364 (setq cached-bugs (append cached-bugs (list status)))
365 nil)
366 bug)))
367 bug-numbers)))
368
369 ;; Retrieve the data.
370 (setq debbugs-soap-invoke-async-object nil)
371 (when bug-numbers
372 ;; Retrieve bugs asynchronously.
373 (let ((bug-ids bug-numbers)
374 results)
375 (while bug-ids
376 (setq results
377 (append
378 results
379 (list
380 (debbugs-soap-invoke-async
381 "get_status"
382 (apply
383 #'vector
384 (butlast
385 bug-ids (- (length bug-ids)
386 debbugs-max-hits-per-request))))))
387
388 bug-ids
389 (last bug-ids (- (length bug-ids)
390 debbugs-max-hits-per-request))))
391
392 (dolist (res results)
393 (while (buffer-live-p res)
394 (accept-process-output (get-buffer-process res) 0.1)))))
395
396 (append
397 cached-bugs
398 ;; Massage results.
399 (mapcar
400 (lambda (x)
401 (let (y)
402 ;; "archived" is the number 1 or 0.
403 (setq y (assoc 'archived (cdr (assoc 'value x))))
404 (setcdr y (= (cdr y) 1))
405 ;; "found_versions" and "fixed_versions" are lists,
406 ;; containing strings or numbers.
407 (dolist (attribute '(found_versions fixed_versions))
408 (setq y (assoc attribute (cdr (assoc 'value x))))
409 (setcdr y (mapcar
410 (lambda (z) (if (numberp z) (number-to-string z) z))
411 (cdr y))))
412 ;; "mergedwith", "blocks" and "blockedby are strings,
413 ;; containing blank separated bug numbers.
414 (dolist (attribute '(mergedwith blocks blockedby))
415 (setq y (assoc attribute (cdr (assoc 'value x))))
416 (when (stringp (cdr y))
417 (setcdr y (mapcar
418 #'string-to-number (split-string (cdr y) " " t)))))
419 ;; "subject", "originator", "owner" and "summary" may be an
420 ;; xsd:base64Binary value containing a UTF-8-encoded string.
421 (dolist (attribute '(subject originator owner summary))
422 (setq y (assoc attribute (cdr (assoc 'value x))))
423 (when (stringp (cdr y))
424 (setcdr y (debbugs-convert-soap-value-to-string (cdr y)))))
425 ;; "package" is a string, containing comma separated
426 ;; package names. "keywords" and "tags" are strings,
427 ;; containing blank separated package names.
428 (dolist (attribute '(package keywords tags))
429 (setq y (assoc attribute (cdr (assoc 'value x))))
430 (when (stringp (cdr y))
431 (setcdr y (split-string (cdr y) ",\\| " t))))
432 ;; Cache the result, and return.
433 (if (and debbugs-cache-expiry (natnump debbugs-cache-expiry))
434 (puthash
435 (cdr (assoc 'key x))
436 ;; Put also a time stamp.
437 (cons (cons 'cache_time (float-time))
438 (cdr (assoc 'value x)))
439 debbugs-cache-data)
440 ;; Don't cache.
441 (cdr (assoc 'value x)))))
442 debbugs-soap-invoke-async-object))))
443
444 (defun debbugs-get-usertag (&rest query)
445 "Return a list of bug numbers which match QUERY.
446
447 QUERY is a sequence of keyword-value pairs where the values are
448 strings, i.e. :KEYWORD \"VALUE\" [:KEYWORD \"VALUE\"]*
449
450 Valid keywords are:
451
452 :user -- The value is the name of the package a bug belongs to,
453 like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\". It can
454 also be an email address of a user who has applied a user tag.
455 The special email address \"me\" is used as pattern, replaced
456 with `user-mail-address'. There must be at least one such
457 entry; it is recommended to have exactly one.
458
459 :tag -- A string applied as user tag. Often, it is a
460 subproduct identification, like \"cedet\" or \"tramp\" for the
461 package \"emacs\".
462
463 If there is no :tag entry, no bug numbers will be returned but a list of
464 existing user tags for :user.
465
466 Example:
467
468 \(debbugs-get-usertag :user \"emacs\")
469
470 => (\"www\" \"solaris\" \"ls-lisp\" \"cygwin\")
471
472 \(debbugs-get-usertag :user \"emacs\" :tag \"www\" :tag \"cygwin\")
473
474 => (807 1223 5637)"
475
476 (let (user tags kw key val object result)
477 ;; Check query.
478 (while (and (consp query) (<= 2 (length query)))
479 (setq kw (pop query)
480 val (pop query))
481 (unless (and (keywordp kw) (stringp val))
482 (error "Wrong query: %s %s" kw val))
483 (setq key (substring (symbol-name kw) 1))
484 (cl-case kw
485 ((:user)
486 ;; Value shall be one word. Extract email address, if existing.
487 (if (string-match "\\`\\S-+\\'" val)
488 (progn
489 (when (string-equal "me" val)
490 (setq val user-mail-address))
491 (when (string-match "<\\(.+\\)>" val)
492 (setq val (match-string 1 val)))
493 (cl-pushnew val user :test #'equal))
494 (error "Wrong %s: %s" key val)))
495 ((:tag)
496 ;; Value shall be one word.
497 (if (string-match "\\`\\S-+\\'" val)
498 (cl-pushnew val tags :test #'equal)
499 (error "Wrong %s: %s" key val)))
500 (t (error "Unknown key: %s" kw))))
501
502 (unless (null query)
503 (error "Unknown key: %s" (car query)))
504 (unless (= (length user) 1)
505 (error "There must be exactly one :user entry"))
506
507 (setq
508 object
509 (car (soap-invoke debbugs-wsdl debbugs-port "get_usertag" (car user))))
510
511 (if (null tags)
512 ;; Return the list of existing tags.
513 (mapcar (lambda (x) (symbol-name (car x))) object)
514
515 ;; Return bug numbers.
516 (dolist (elt object result)
517 (when (member (symbol-name (car elt)) tags)
518 (setq result (append (cdr elt) result)))))))
519
520 (defun debbugs-get-bug-log (bug-number)
521 "Return a list of messages related to BUG-NUMBER.
522
523 Every message is an association list with the following attributes:
524
525 `msg_num': The number of the message inside the bug log. The
526 numbers are ascending, newer messages have a higher number.
527
528 `header': The message header lines, as arrived at the bug tracker.
529
530 `body': The message body.
531
532 `attachments' A list of possible attachments, or `nil'. Not
533 implemented yet server side."
534 (car (soap-invoke debbugs-wsdl debbugs-port "get_bug_log" bug-number)))
535
536 (defun debbugs-search-est (&rest query)
537 "Return the result of a full text search according to QUERY.
538
539 QUERY is a sequence of lists of keyword-value pairs where the
540 values are strings or numbers, i.e. :KEYWORD \"VALUE\" [:KEYWORD
541 VALUE]*
542
543 Every sublist of the QUERY forms a hyperestraier condition. A
544 detailed description of hyperestraier conditions can be found at
545 URL `http://fallabs.com/hyperestraier/uguide-en.html#searchcond'.
546
547 The following conditions are possible:
548
549 \[:phrase SEARCH-PHRASE :skip NUMBER :max NUMBER\]
550
551 The string SEARCH-PHRASE forms the search on the database. It
552 contains words to be searched for, combined by operators like
553 AND, ANDNOT and OR. If there is no operator between the words,
554 AND is used by default. The phrase keyword and value can also
555 be omitted, this is useful in combination with other conditions.
556
557 :skip and :max are optional. They specify, how many hits are
558 skipped, and how many maximal hits are returned. This can be
559 used for paged results. Per default, :skip is 0 and all
560 possible hits are returned.
561
562 There must be exactly one such condition.
563
564 \[ATTRIBUTE VALUE+ :operation OPERATION :order ORDER\]
565
566 ATTRIBUTE is one of the following keywords:
567
568 :status -- Status of bug. Valid values are \"done\",
569 \"forwarded\" and \"open\".
570
571 :subject, :@title -- The subject of a message or the title of
572 the bug, a string.
573
574 :date, :@cdate -- The submission or modification dates of a
575 message, a number.
576
577 :submitter, :@author -- The email address of the submitter of a
578 bug or the author of a message belonging to this bug, a string.
579 The special email address \"me\" is used as pattern, replaced
580 with `user-mail-address'.
581
582 :package -- The value is the name of the package a bug belongs
583 to, like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\".
584
585 :tags -- An arbitrary string the bug is annotated with.
586
587 :severity -- This is the severity of the bug. The exact set of
588 allowed values depends on the Debbugs port. Examples are
589 \"normal\", \"minor\", \"wishlist\" etc.
590
591 :operator defines the comparison operator to be applied to
592 ATTRIBUTE. For string attributes this could be \"STREQ\" \(is
593 equal to the string), \"STRNE\" \(is not equal to the string),
594 \"STRINC\" \(includes the string), \"STRBW\" \(begins with the
595 string), \"STREW\" \(ends with the string), \"STRAND\"
596 \(includes all tokens in the string), \"STROR\" \(includes at
597 least one token in the string), \"STROREQ\" \(is equal to at
598 least one token in the string) or \"STRRX\" \(matches regular
599 expressions of the string). For operators with tokens, several
600 values for ATTRIBUTE shall be used.
601
602 Numbers can be compared by the operators \"NUMEQ\" \(is equal
603 to the number), \"NUMNE\" \(is not equal to the number),
604 \"NUMGT\" \(is greater than the number), \"NUMGE\" \(is greater
605 than or equal to the number), \"NUMLT\" \(is less than the
606 number), \"NUMLE\" \(is less than or equal to the number) or
607 \"NUMBT\" \(is between the two numbers). In the last case,
608 there must be two values for ATTRIBUTE.
609
610 If an operator is leaded by \"!\", the meaning is inverted. If
611 a string operator is leaded by \"I\", the case of the value is
612 ignored.
613
614 The optional :order can be specified only in one condition. It
615 means, that ATTRIBUTE is used for sorting the results. The
616 following order operators exist: \"STRA\" \(ascending by
617 string), \"STRD\" \(descending by string), \"NUMA\" \(ascending
618 by number) or \"NUMD\" \(descending by number).
619
620 A special case is an :order, where there is no corresponding
621 attribute value and no operator. In this case, ATTRIBUTE is
622 not used for the search.
623
624 The result of the QUERY is a list of association lists with the
625 same attributes as in the conditions. Additional attributes are
626
627 `id': The bug number.
628
629 `msg_num': The number of the message inside the bug log.
630
631 `snippet': The surrounding text found by the search. For the
632 syntax of the snippet, consult the hyperestraier user guide.
633
634 Examples:
635
636 \(debbugs-search-est
637 '\(:phrase \"armstrong AND debbugs\" :skip 10 :max 2)
638 '\(:severity \"normal\" :operator \"STRINC\")
639 '\(:date :order \"NUMA\"))
640
641 => \(\(\(msg_num . 21)
642 \(date . 1229208302)
643 \(@author . \"Glenn Morris <rgm@gnu.org>\")
644 \(@title . \"Re: bug#1567: Mailing an archived bug\")
645 \(id . 1567)
646 \(severity . \"normal\")
647 \(@cdate . \"Wed, 17 Dec 2008 14:34:50 -0500\")
648 \(snippet . \"...\")
649 \(subject . \"Mailing an archived bug\")
650 \(package . \"debbugs.gnu.org\"))
651 ...)
652
653 ;; Show all messages from me between 2011-08-01 and 2011-08-31.
654 \(debbugs-search-est
655 '\(:max 20)
656 '\(:@author \"me\" :operator \"ISTRINC\")
657 `\(:date
658 ,\(floor \(float-time \(encode-time 0 0 0 1 8 2011)))
659 ,\(floor \(float-time \(encode-time 0 0 0 31 8 2011)))
660 :operator \"NUMBT\"))"
661
662 (let ((phrase (assoc :phrase query))
663 args result)
664 (if (and phrase (not (member :skip phrase)) (not (member :max phrase)))
665 ;; We loop, until we have all results.
666 (let ((skip 0)
667 (query (delete phrase query))
668 result1)
669 (while skip
670 (setq result1
671 (apply
672 #'debbugs-search-est
673 (append
674 (list
675 (append
676 phrase `(:skip ,skip)
677 `(:max ,debbugs-max-hits-per-request)))
678 query))
679 skip (and (= (length result1) debbugs-max-hits-per-request)
680 (+ skip debbugs-max-hits-per-request))
681 result (append result result1)))
682 result)
683
684 ;; Compile search arguments.
685 (dolist (elt query)
686 ;; FIXME: `vec' is used in an O(N²) way. It should be a list instead,
687 ;; on which we push elements, and we only convert it to a vector at
688 ;; the end.
689 (let (vec kw key val
690 phrase-cond attr-cond)
691
692 ;; Phrase is mandatory, even if empty.
693 (when (and (or (member :skip elt) (member :max elt))
694 (not (member :phrase elt)))
695 (setq vec (vector "phrase" "")))
696
697 ;; Parse condition.
698 (while (consp elt)
699 (setq kw (pop elt))
700 (unless (keywordp kw)
701 (error "Wrong keyword: %s" kw))
702 (setq key (substring (symbol-name kw) 1))
703 (cl-case kw
704 ;; Phrase condition.
705 (:phrase
706 ;; It shouldn't happen in an attribute condition.
707 (if attr-cond
708 (error "Wrong keyword: %s" kw))
709 (setq phrase-cond t val (pop elt))
710 ;; Value is a string.
711 (if (stringp val)
712 (setq vec (vconcat vec (list key val)))
713 (error "Wrong %s: %s" key val)))
714
715 ((:skip :max)
716 ;; It shouldn't happen in an attribute condition.
717 (if attr-cond
718 (error "Wrong keyword: %s" kw))
719 (setq phrase-cond t val (pop elt))
720 ;; Value is a number.
721 (if (numberp val)
722 (setq vec (vconcat vec (list key (number-to-string val))))
723 (error "Wrong %s: %s" key val)))
724
725 ;; Attribute condition.
726 ((:submitter :@author)
727 ;; It shouldn't happen in a phrase condition.
728 (if phrase-cond
729 (error "Wrong keyword: %s" kw))
730 (if (not (stringp (car elt)))
731 (setq vec (vconcat vec (list key "")))
732 ;; Value is an email address.
733 (while (and (stringp (car elt))
734 (string-match "\\`\\S-+\\'" (car elt)))
735 (when (string-equal "me" (car elt))
736 (setcar elt user-mail-address))
737 (when (string-match "<\\(.+\\)>" (car elt))
738 (setcar elt (match-string 1 (car elt))))
739 (let ((x (pop elt)))
740 (unless (member x val)
741 (setq val (append val (list x))))))
742 (setq vec
743 (vconcat vec (list key (mapconcat #'identity val " "))))))
744
745 (:status
746 ;; It shouldn't happen in a phrase condition.
747 (if phrase-cond
748 (error "Wrong keyword: %s" kw))
749 (setq attr-cond t)
750 (if (not (stringp (car elt)))
751 (setq vec (vconcat vec (list key "")))
752 ;; Possible values: "done", "forwarded" and "open"
753 (while (and (stringp (car elt))
754 (string-match
755 "\\`\\(done\\|forwarded\\|open\\)\\'" (car elt)))
756 (let ((x (pop elt)))
757 (unless (member x val)
758 (setq val (append val (list x))))))
759 (setq vec
760 (vconcat vec (list key (mapconcat #'identity val " "))))))
761
762 ((:subject :package :tags :severity :@title)
763 ;; It shouldn't happen in a phrase condition.
764 (if phrase-cond
765 (error "Wrong keyword: %s" kw))
766 (setq attr-cond t)
767 (if (not (stringp (car elt)))
768 (setq vec (vconcat vec (list key "")))
769 ;; Just a string.
770 (while (stringp (car elt))
771 (let ((x (pop elt)))
772 (unless (member x val)
773 (setq val (append val (list x))))))
774 (setq vec
775 (vconcat vec (list key (mapconcat #'identity val " "))))))
776
777 ((:date :@cdate)
778 ;; It shouldn't happen in a phrase condition.
779 (if phrase-cond
780 (error "Wrong keyword: %s" kw))
781 (setq attr-cond t)
782 (if (not (numberp (car elt)))
783 (setq vec (vconcat vec (list key "")))
784 ;; Just a number.
785 (while (numberp (car elt))
786 (let ((x (pop elt)))
787 (unless (member x val)
788 (setq val (append val (list x))))))
789 (setq vec
790 (vconcat
791 vec (list key (mapconcat #'number-to-string val " "))))))
792
793 ((:operator :order)
794 ;; It shouldn't happen in a phrase condition.
795 (if phrase-cond
796 (error "Wrong keyword: %s" kw))
797 (setq attr-cond t val (pop elt))
798 ;; Value is a number.
799 (if (stringp val)
800 (setq vec (vconcat vec (list key val)))
801 (error "Wrong %s: %s" key val)))
802
803 (t (error "Unknown key: %s" kw))))
804
805 (setq args (vconcat args (list vec)))))
806
807 (setq result
808 (car (soap-invoke debbugs-wsdl debbugs-port "search_est" args)))
809 ;; The result contains lists (key value). We transform it into
810 ;; cons cells (key . value).
811 (dolist (elt1 result result)
812 (dolist (elt2 elt1)
813 (setcdr elt2 (cadr elt2)))))))
814
815 (defun debbugs-get-attribute (bug-or-message attribute)
816 "Return the value of key ATTRIBUTE.
817
818 BUG-OR-MESSAGE must be list element returned by either
819 `debbugs-get-status' or `debbugs-get-bug-log'.
820
821 Example: Return the originator of last submitted bug.
822
823 \(debbugs-get-attribute
824 \(car \(apply #'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)"
825 (cdr (assoc attribute bug-or-message)))
826
827 (defun debbugs-get-message-numbers (messages)
828 "Return the message numbers of MESSAGES.
829 MESSAGES must be the result of a `debbugs-get-bug-log' call."
830 (mapcar (lambda (x) (debbugs-get-attribute x 'msg_num)) messages))
831
832 (defun debbugs-get-message (messages message-number)
833 "Return the message MESSAGE-NUMBER of MESSAGES.
834 MESSAGES must be the result of a `debbugs-get-bug-log' call.
835
836 The returned message is a list of strings. The first element are
837 the header lines of the message, the second element is the body
838 of the message. Further elements of the list, if any, are
839 attachments of the message.
840
841 If there is no message with MESSAGE-NUMBER, the function returns `nil'.
842
843 Example: Return the first message of last submitted bug.
844
845 \(let \(\(messages \(apply #'debbugs-get-bug-log \(debbugs-newest-bugs 1))))
846 \(debbugs-get-message messages
847 \(car \(debbugs-get-message-numbers messages))))"
848 (while (and messages
849 (/= (debbugs-get-attribute (car messages) 'msg_num)
850 message-number))
851 (setq messages (cdr messages)))
852 (when messages
853 (append (list (debbugs-get-attribute (car messages) 'header)
854 (debbugs-get-attribute (car messages) 'body))
855 (debbugs-get-attribute (car messages) 'attachments))))
856
857 (defun debbugs-get-mbox (bug-number mbox-type &optional filename)
858 "Download mbox with messages of bug BUG-NUMBER from Debbugs server.
859 BUG-NUMBER is a number of bug. It must be of integer type.
860
861 MBOX-TYPE specifies a type of mbox and can be one of the
862 following symbols:
863
864 `mboxfolder': Download mbox folder.
865
866 `mboxmaint': Download maintainer's mbox.
867
868 `mboxstat', `mboxstatus': Download status mbox. The use of
869 either symbol depends on actual Debbugs server configuration.
870 For gnu.org, use the former; for debian.org - the latter.
871
872 FILENAME, if non-`nil', is the name of file to store mbox. If
873 FILENAME is `nil', the downloaded mbox is inserted into the
874 current buffer."
875 (let (url (mt "") bn)
876 (unless (setq url (plist-get
877 (cdr (assoc debbugs-port debbugs-servers))
878 :bugreport-url))
879 (error "URL of bugreport script for port %s is not specified"
880 debbugs-port))
881 (setq bn (format "bug=%s;" (number-to-string bug-number)))
882 (unless (eq mbox-type 'mboxfolder)
883 (if (memq mbox-type '(mboxmaint mboxstat mboxstatus))
884 (setq mt (concat (symbol-name mbox-type) "=yes;"))
885 (error "Unknown mbox type: %s" mbox-type)))
886 (setq url (concat url (format "?%s%smbox=yes" bn mt)))
887 (if filename
888 (url-copy-file url filename t)
889 (url-insert-file-contents url))))
890
891 (provide 'debbugs)
892
893 ;;; TODO:
894
895 ;; * SOAP interface extensions (wishlist).
896 ;; - Server-side sorting.
897 ;; - Regexp and/or wildcards search.
898 ;; - Returning message attachments.
899
900 ;;; debbugs.el ends here