]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs.el
7ed1a453965086cfe92aad1485654cfa3ad55d73
[gnu-emacs-elpa] / packages / debbugs / debbugs.el
1 ;;; debbugs.el --- SOAP library to access debbugs servers
2
3 ;; Copyright (C) 2011 Free Software Foundation, Inc.
4
5 ;; Author: Michael Albinus <michael.albinus@gmx.de>
6 ;; Keywords: comm, hypermedia
7 ;; Package: debbugs
8 ;; Version: 0.1
9
10 ;; This file is part of GNU Emacs.
11
12 ;; GNU Emacs is free software: you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
16
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
21
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
24
25 ;;; Commentary:
26
27 ;; This package provides some basic functions to access a debbugs SOAP
28 ;; server (see <http://wiki.debian.org/DebbugsSoapInterface>).
29
30 ;; The SOAP functions "get_usertag" and "get_versions" are not
31 ;; implemented (yet).
32
33 ;;; Code:
34
35 ;(setq soap-debug t message-log-max t)
36 (require 'soap-client)
37 (eval-when-compile (require 'cl))
38
39 (defgroup debbugs nil
40 "Debbugs library"
41 :group 'hypermedia)
42
43 (defcustom debbugs-servers
44 '(("gnu.org"
45 :wsdl "http://debbugs.gnu.org/cgi/soap.cgi?WSDL"
46 :bugreport-url "http://debbugs.gnu.org/cgi/bugreport.cgi")
47 ("debian.org"
48 :wsdl "http://bugs.debian.org/cgi-bin/soap.cgi?WSDL"
49 :bugreport-url "http://bugs.debian.org/cgi-bin/bugreport.cgi"))
50 "*List of Debbugs server specifiers.
51 Each entry is a list that contains a string identifying the port
52 name and the server parameters in keyword-value form. Allowed
53 keywords are:
54
55 `:wsdl' -- Location of WSDL. The value is a string with URL that
56 should return the WSDL specification of Debbugs/SOAP service.
57
58 `:bugreport-url' -- URL of the server script that returns mboxes
59 with bug logs.
60
61 The list initially contains two predefined and configured Debbugs
62 servers: \"gnu.org\" and \"debian.org\"."
63 :group 'debbugs
64 :link '(custom-manual "(debbugs)Debbugs server specifiers")
65 :type '(choice
66 (const nil)
67 (repeat
68 (cons :tag "Server"
69 (string :tag "Port name")
70 (checklist :tag "Options" :greedy t
71 (group :inline t
72 (const :format "" :value :wsdl)
73 (string :tag "WSDL"))
74 (group :inline t
75 (const :format "" :value :bugreport-url)
76 (string :tag "Bugreport URL")))))))
77
78 (defcustom debbugs-port "gnu.org"
79 "The port instance to be applied from `debbugs-wsdl'.
80 This corresponds to the Debbugs server to be accessed, either
81 \"gnu.org\", or \"debian.org\", or user defined port name."
82 ;; Maybe we should create an own group?
83 :group 'debbugs
84 :type '(choice :tag "Debbugs server" (const "gnu.org") (const "debian.org")
85 (string :tag "user defined port name")))
86
87 ;; It would be nice if we could retrieve it from the debbugs server.
88 ;; Not supported yet.
89 (defconst debbugs-wsdl
90 (soap-load-wsdl
91 (expand-file-name
92 "Debbugs.wsdl"
93 (if load-in-progress
94 (file-name-directory load-file-name)
95 default-directory)))
96 "The WSDL object to be used describing the SOAP interface.")
97
98 (defun debbugs-get-bugs (&rest query)
99 "Return a list of bug numbers which match QUERY.
100
101 QUERY is a keyword value sequence, whereby the values are strings.
102 All queries are concatenated via AND.
103
104 Valid keywords are:
105
106 :package -- The value is the name of the package a bug belongs
107 to, like \"emacs\", \"coreutils\", \"gnus\", or \"tramp\".
108
109 :severity -- This is the severity of the bug. Currently,
110 there exists the severities \"important\", \"grave\",
111 \"normal\", \"minor\" and \"wishlist\".
112
113 :tag -- An arbitrary string the bug is annotated with.
114 Usually, this is used to mark the status of the bug, like
115 \"fixed\", \"moreinfo\", \"notabug\", \"patch\",
116 \"unreproducible\" or \"wontfix\".
117
118 :owner -- This is used to identify bugs by the owner's email
119 address. The special email address \"me\" is used as pattern,
120 replaced with `user-mail-address'.
121
122 :submitter -- With this keyword it is possible to filter bugs
123 by the submitter's email address. The special email address
124 \"me\" is used as pattern, replaced with `user-mail-address'.
125
126 :archive -- A keyword to filter for bugs which are already
127 archived, or not. Valid values are \"0\" (not archived),
128 \"1\" (archived) or \"both\". If this keyword is not given in
129 the query, `:archive \"0\"' is assumed by default.
130
131 Example:
132
133 \(debbugs-get-bugs :submitter \"me\" :archive \"both\")
134 => \(5516 5551 5645 7259)"
135
136 (let (vec key val)
137 ;; Check query.
138 (while (and (consp query) (<= 2 (length query)))
139 (setq key (pop query)
140 val (pop query)
141 vec (vconcat vec (list (substring (symbol-name key) 1))))
142 (unless (and (keywordp key) (stringp val))
143 (error "Wrong query: %s %s" key val))
144 (case key
145 ((:package :severity :tag)
146 ;; Value shall be one word.
147 (if (string-match "\\`[A-Za-z]+\\'" val)
148 (setq vec (vconcat vec (list val)))
149 (error "Wrong %s: %s" (car (last vec)) val)))
150 ;; Value is an email address.
151 ((:owner :submitter)
152 (if (string-match "\\`\\S-+\\'" val)
153 (progn
154 (when (string-equal "me" val)
155 (setq val user-mail-address))
156 (when (string-match "<\\(.+\\)>" val)
157 (setq val (match-string 1 val)))
158 (setq vec (vconcat vec (list val))))
159 (error "Wrong %s: %s" (car (last vec)) val)))
160 (:archive
161 ;; Value is `0' or `1' or `both'.
162 (if (string-match "\\`\\(0\\|1\\|both\\)\\'" val)
163 (setq vec (vconcat vec (list val)))
164 (error "Wrong %s: %s" (car (last vec)) val)))
165 (t (error "Unknown key: %s" (car (last vec))))))
166
167 (unless (null query)
168 (error "Unknown key: %s" (car query)))
169
170 (sort (car (soap-invoke debbugs-wsdl debbugs-port "get_bugs" vec)) '<)))
171
172 (defun debbugs-newest-bugs (amount)
173 "Return the list of bug numbers, according to AMOUNT (a number) latest bugs."
174 (sort (car (soap-invoke debbugs-wsdl debbugs-port "newest_bugs" amount)) '<))
175
176 (defun debbugs-get-status (&rest bug-numbers)
177 "Return a list of status entries for the bugs identified by BUG-NUMBERS.
178
179 Every returned entry is an association list with the following attributes:
180
181 `bug_num': The bug number.
182
183 `package': A list of package names the bug belongs to.
184
185 `severity': The severity of the bug report. This can be
186 \"important\", \"grave\", \"normal\", \"minor\" or \"wishlist\".
187
188 `tags': The status of the bug report, a list of strings. This
189 can be \"fixed\", \"notabug\", \"wontfix\", \"unreproducible\",
190 \"moreinfo\" or \"patch\".
191
192 `pending': The string \"pending\", \"forwarded\" or \"done\".
193
194 `subject': Subject/Title of the bugreport.
195
196 `originator': Submitter of the bugreport.
197
198 `mergedwith': A list of bug numbers this bug was merged with.
199
200 `source': Source package name of the bug report.
201
202 `date': Date of bug creation.
203
204 `log_modified', `last_modified': Date of last update.
205
206 `found_date', `fixed_date': Date of bug report / bug fix
207 \(empty for now).
208
209 `done': The email address of the worker who has closed the bug (if done).
210
211 `archived': `t' if the bug is archived, `nil' otherwise.
212
213 `unarchived': The date the bug has been unarchived, if ever.
214
215 `found_versions', `fixed_versions': List of version strings.
216
217 `forwarded': A URL or an email address.
218
219 `blocks': A list of bug numbers this bug blocks.
220
221 `blockedby': A list of bug numbers this bug is blocked by.
222
223 `msgid': The message id of the initial bug report.
224
225 `owner': Who is responsible for fixing.
226
227 `location': Always the string \"db-h\" or \"archive\".
228
229 `affects': A list of package names.
230
231 `summary': Arbitrary text.
232
233 Example:
234
235 \(debbugs-get-status 10)
236
237 => ;; Attributes with empty values are not show
238 \(\(\(bug_num . 10)
239 \(source . \"unknown\")
240 \(date . 1203606305.0)
241 \(msgid . \"<87zltuz7eh.fsf@freemail.hu>\")
242 \(severity . \"wishlist\")
243 \(owner . \"Magnus Henoch <mange@freemail.hu>\")
244 \(log_modified . 1261079402.0)
245 \(location . \"db-h\")
246 \(subject . \"url-gw should support HTTP CONNECT proxies\")
247 \(originator . \"Magnus Henoch <mange@freemail.hu>\")
248 \(last_modified . 1271200046.0)
249 \(pending . \"pending\")
250 \(package \"emacs\")))"
251 (let ((object
252 (car
253 (soap-invoke
254 debbugs-wsdl debbugs-port "get_status"
255 (apply 'vector bug-numbers)))))
256 (mapcar
257 (lambda (x)
258 (let (y)
259 ;; "archived" is the number 1 or 0.
260 (setq y (assoc 'archived (cdr (assoc 'value x))))
261 (setcdr y (= (cdr y) 1))
262 ;; "found_versions" and "fixed_versions" are lists,
263 ;; containing strings or numbers.
264 (dolist (attribute '(found_versions fixed_versions))
265 (setq y (assoc attribute (cdr (assoc 'value x))))
266 (setcdr y (mapcar
267 (lambda (z) (if (numberp z) (number-to-string z) z))
268 (cdr y))))
269 ;; "mergedwith" is a string, containing blank separated bug numbers.
270 (setq y (assoc 'mergedwith (cdr (assoc 'value x))))
271 (when (stringp (cdr y))
272 (setcdr y (mapcar 'string-to-number (split-string (cdr y) " " t))))
273 ;; "package" is a string, containing comma separated package names.
274 ;; "keywords" and "tags" are strings, containing blank
275 ;; separated package names.
276 (dolist (attribute '(package keywords tags))
277 (setq y (assoc attribute (cdr (assoc 'value x))))
278 (when (stringp (cdr y))
279 (setcdr y (split-string (cdr y) ",\\| " t))))
280 (cdr (assoc 'value x))))
281 object)))
282
283 (defun debbugs-get-bug-log (bug-number)
284 "Return a list of messages related to BUG-NUMBER.
285
286 Every message is an association list with the following attributes:
287
288 `msg_num': The number of the message inside the bug log. The
289 numbers are ascending, newer messages have a higher number.
290
291 `header': The message header lines, as arrived at the bug tracker.
292
293 `body': The message body.
294
295 `attachments' A list of possible attachments, or `nil'. Not
296 implemented yet server side."
297 (car (soap-invoke debbugs-wsdl debbugs-port "get_bug_log" bug-number)))
298
299 (defun debbugs-get-attribute (bug-or-message attribute)
300 "Return the value of key ATTRIBUTE.
301
302 BUG-OR-MESSAGE must be list element returned by either
303 `debbugs-get-status' or `debbugs-get-bug-log'.
304
305 Example: Return the originator of last submitted bug.
306
307 \(debbugs-get-attribute
308 \(car \(apply 'debbugs-get-status \(debbugs-newest-bugs 1))) 'originator)"
309 (cdr (assoc attribute bug-or-message)))
310
311 (defun debbugs-get-message-numbers (messages)
312 "Return the message numbers of MESSAGES.
313 MESSAGES must be the result of a `debbugs-get-bug-log' call."
314 (mapcar (lambda (x) (debbugs-get-attribute x 'msg_num)) messages))
315
316 (defun debbugs-get-message (messages message-number)
317 "Return the message MESSAGE-NUMBER of MESSAGES.
318 MESSAGES must be the result of a `debbugs-get-bug-log' call.
319
320 The returned message is a list of strings. The first element are
321 the header lines of the message, the second element is the body
322 of the message. Further elements of the list, if any, are
323 attachments of the message.
324
325 If there is no message with MESSAGE-NUMBER, the function returns `nil'.
326
327 Example: Return the first message of last submitted bug.
328
329 \(let \(\(messages \(apply 'debbugs-get-bug-log \(debbugs-newest-bugs 1))))
330 \(debbugs-get-message messages
331 \(car \(debbugs-get-message-numbers messages))))"
332 (while (and messages
333 (/= (debbugs-get-attribute (car messages) 'msg_num)
334 message-number))
335 (setq messages (cdr messages)))
336 (when messages
337 (append (list (debbugs-get-attribute (car messages) 'header)
338 (debbugs-get-attribute (car messages) 'body))
339 (debbugs-get-attribute (car messages) 'attachments))))
340
341 (defun debbugs-get-mbox (bug-number mbox-type &optional filename)
342 "Download mbox with messages of bug BUG-NUMBER from Debbugs server.
343 BUG-NUMBER is a number of bug. It must be of integer type.
344
345 MBOX-TYPE specifies a type of mbox and can be one of the
346 following symbols:
347
348 `mboxfolder': Download mbox folder.
349
350 `mboxmaint': Download maintainer's mbox.
351
352 `mboxstat', `mboxstatus': Download status mbox. The use of
353 either symbol depends on actual Debbugs server
354 configuration. For gnu.org, use the former; for debian.org -
355 the latter.
356
357 FILENAME, if non-nil, is the name of file to store mbox. If
358 FILENAME is nil, the downloaded mbox is inserted into the current
359 buffer."
360 (let (url (mt "") bn)
361 (unless (setq url (plist-get
362 (cdr (assoc debbugs-port debbugs-servers))
363 :bugreport-url))
364 (error "URL of bugreport script for port %s is not specified"
365 debbugs-port))
366 (setq bn (format "bug=%s;" (number-to-string bug-number)))
367 (unless (eq mbox-type 'mboxfolder)
368 (if (memq mbox-type '(mboxmaint mboxstat mboxstatus))
369 (setq mt (concat (symbol-name mbox-type) "=yes;"))
370 (error "Unknown mbox type: %s" mbox-type)))
371 (setq url (concat url (format "?%s%smbox=yes" bn mt)))
372 (if filename
373 (url-copy-file url filename t)
374 (url-insert-file-contents url))))
375
376 ;; Interface for the Emacs bug tracker.
377
378 (autoload 'gnus-read-ephemeral-emacs-bug-group "gnus-group")
379 (autoload 'mail-header-subject "nnheader")
380 (autoload 'gnus-summary-article-header "gnus-sum")
381 (autoload 'message-make-from "message")
382
383 (defface debbugs-new '((t (:foreground "red")))
384 "Face for new reports that nobody has answered.")
385
386 (defface debbugs-handled '((t (:foreground "ForestGreen")))
387 "Face for new reports that nobody has answered.")
388
389 (defface debbugs-stale '((t (:foreground "orange")))
390 "Face for new reports that nobody has answered.")
391
392 (defun debbugs-emacs (severities &optional package list-done)
393 "List all outstanding Emacs bugs."
394 (interactive
395 (list
396 (completing-read "Severity: "
397 '("important" "normal" "minor" "wishlist")
398 nil t "normal")))
399 (unless (consp severities)
400 (setq severities (list severities)))
401 (pop-to-buffer (get-buffer-create "*Emacs Bugs*"))
402 (debbugs-mode)
403 (let ((buffer-read-only nil)
404 (ids nil))
405 (dolist (severity severities)
406 (setq ids (nconc ids
407 (debbugs-get-bugs :package (or package "emacs")
408 :severity severity))))
409 (erase-buffer)
410 (dolist (status (sort (apply 'debbugs-get-status ids)
411 (lambda (s1 s2)
412 (< (cdr (assq 'id s1))
413 (cdr (assq 'id s2))))))
414 (when (or list-done
415 (not (equal (cdr (assq 'pending status)) "done")))
416 (let ((address (mail-header-parse-address
417 (decode-coding-string (cdr (assq 'originator status))
418 'utf-8))))
419 (setq address
420 ;; Prefer the name over the address.
421 (or (cdr address)
422 (car address)))
423 (insert
424 (format "%5d %-20s [%-23s] %s\n"
425 (cdr (assq 'id status))
426 (let ((words
427 (mapconcat
428 'identity
429 (cons (cdr (assq 'severity status))
430 (cdr (assq 'keywords status)))
431 ",")))
432 (unless (equal (cdr (assq 'pending status)) "pending")
433 (setq words (concat words "," (cdr (assq 'pending status)))))
434 (if (> (length words) 20)
435 (substring words 0 20)
436 words))
437 (if (> (length address) 23)
438 (substring address 0 23)
439 address)
440 (decode-coding-string (cdr (assq 'subject status))
441 'utf-8)))
442 (forward-line -1)
443 (put-text-property
444 (+ (point) 5) (+ (point) 26)
445 'face
446 (cond
447 ((= (cdr (assq 'date status))
448 (cdr (assq 'log_modified status)))
449 'debbugs-new)
450 ((< (- (float-time)
451 (cdr (assq 'log_modified status)))
452 (* 60 60 24 4))
453 'debbugs-handled)
454 (t
455 'debbugs-stale)))
456 (forward-line 1)))))
457 (goto-char (point-min)))
458
459 (defvar debbugs-mode-map nil)
460 (unless debbugs-mode-map
461 (setq debbugs-mode-map (make-sparse-keymap))
462 (define-key debbugs-mode-map "\r" 'debbugs-select-report))
463
464 (defun debbugs-mode ()
465 "Major mode for listing bug reports.
466
467 All normal editing commands are switched off.
468 \\<debbugs-mode-map>
469
470 The following commands are available:
471
472 \\{debbugs-mode-map}"
473 (interactive)
474 (kill-all-local-variables)
475 (setq major-mode 'debbugs-mode)
476 (setq mode-name "Debbugs")
477 (use-local-map debbugs-mode-map)
478 (buffer-disable-undo)
479 (setq truncate-lines t)
480 (setq buffer-read-only t))
481
482 (defun debbugs-select-report ()
483 "Select the report on the current line."
484 (interactive)
485 (let (id)
486 (save-excursion
487 (beginning-of-line)
488 (if (not (looking-at " *\\([0-9]+\\)"))
489 (error "No bug report on the current line")
490 (setq id (string-to-number (match-string 1)))))
491 (gnus-read-ephemeral-emacs-bug-group
492 id (cons (current-buffer)
493 (current-window-configuration)))
494 (with-current-buffer (window-buffer (selected-window))
495 (debbugs-summary-mode 1))))
496
497 (defvar debbugs-summary-mode-map
498 (let ((map (make-sparse-keymap)))
499 (define-key map "C" 'debbugs-send-control-message)
500 map))
501
502 (define-minor-mode debbugs-summary-mode
503 "Minor mode for providing a debbugs interface in Gnus summary buffers.
504
505 \\{debbugs-summary-mode-map}"
506 :lighter " Debbugs" :keymap debbugs-summary-mode-map
507 nil)
508
509 (defun debbugs-send-control-message (message)
510 "Send a control message for the current bug report.
511 You can set the severity or add a tag, or close the report. If
512 you use the special \"done\" MESSAGE, the report will be marked as
513 fixed, and then closed."
514 (interactive
515 (list (completing-read
516 "Control message: "
517 '("important" "normal" "minor" "wishlist"
518 "close" "done"
519 "patch" "wontfix" "moreinfo" "unreproducible" "fixed" "notabug")
520 nil t)))
521 (let* ((subject (mail-header-subject (gnus-summary-article-header)))
522 (id
523 (if (string-match "bug#\\([0-9]+\\)" subject)
524 (string-to-number (match-string 1 subject))
525 (error "No bug number present"))))
526 (with-temp-buffer
527 (insert "To: control@debbugs.gnu.org\n"
528 "From: " (message-make-from) "\n"
529 (format "Subject: control message for bug #%d\n" id)
530 "\n"
531 (cond
532 ((equal message "close")
533 (format "close %d\n" id))
534 ((equal message "done")
535 (format "tags %d fixed\bclose %d\n" id id))
536 ((member message '("important" "normal" "minor" "wishlist"))
537 (format "severity %d %s\n" id message))
538 (t
539 (format "tags %d %s\n" id message))))
540 (funcall send-mail-function))))
541
542 (provide 'debbugs)
543
544 ;;; TODO:
545
546 ;; * SOAP interface extensions (wishlist).
547 ;; - Server-side sorting.
548 ;; - Regexp and/or wildcards search.
549 ;; - Fulltext search.
550 ;; - Returning message attachments.
551 ;; * Widget-oriented bug overview like webDDTs.
552 ;; * Actions on bugs.
553 ;; * Integration into gnus (nnir).
554
555 ;;; debbugs.el ends here