]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs-gnu.el
bb4fd38405e33efcd70c6980f9363dac443337a8
[gnu-emacs-elpa] / packages / debbugs / debbugs-gnu.el
1 ;;; debbugs-gnu.el --- interface for the GNU bug tracker
2
3 ;; Copyright (C) 2011 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Keywords: comm, hypermedia, maint
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 ;;; Code:
28
29 (require 'debbugs)
30 (require 'widget)
31 (eval-when-compile (require 'cl))
32
33 (autoload 'gnus-read-ephemeral-emacs-bug-group "gnus-group")
34 (autoload 'mail-header-subject "nnheader")
35 (autoload 'gnus-summary-article-header "gnus-sum")
36 (autoload 'message-make-from "message")
37
38 (defface debbugs-new '((t (:foreground "red")))
39 "Face for new reports that nobody has answered.")
40
41 (defface debbugs-handled '((t (:foreground "ForestGreen")))
42 "Face for new reports that have been modified recently.")
43
44 (defface debbugs-stale '((t (:foreground "orange")))
45 "Face for new reports that nobody has answered.")
46
47 (defface debbugs-done '((t (:foreground "DarkGrey")))
48 "Face for closed bug reports.")
49
50 (defface debbugs-tagged '((t (:foreground "red")))
51 "Face for reports that have been tagged locally.")
52
53 (defvar debbugs-widgets nil)
54
55 (defvar debbugs-widget-map
56 (let ((map (make-sparse-keymap)))
57 (define-key map "\r" 'widget-button-press)
58 (define-key map [mouse-1] 'widget-button-press)
59 (define-key map [mouse-2] 'widget-button-press)
60 map))
61
62 (defvar debbugs-local-tags nil
63 "List of bug numbers tagged locally, and kept persistent.")
64
65 (defvar debbugs-persistency-file
66 (expand-file-name (locate-user-emacs-file "debbugs"))
67 "File name of a persistency store for debbugs variables")
68
69 (defun debbugs-dump-persistency-file ()
70 "Function to store debbugs variables persistently."
71 (ignore-errors
72 (with-temp-buffer
73 (insert
74 ";; -*- emacs-lisp -*-\n"
75 ";; Debbugs tags connection history. Don't change this file.\n\n"
76 (format "(setq debbugs-local-tags '%S)"
77 (sort (copy-sequence debbugs-local-tags) '<)))
78 (write-region
79 (point-min) (point-max) debbugs-persistency-file))))
80
81 ;; Save variables.
82 (unless noninteractive
83 (add-hook 'kill-emacs-hook 'debbugs-dump-persistency-file))
84
85 (defvar debbugs-package nil
86 "The package name to be searched for.")
87
88 (defvar debbugs-severities nil
89 "The severities strings to be searched for.")
90
91 (defvar debbugs-archive nil
92 "The archive flag to be searched for.")
93
94 (defun debbugs-emacs (severities &optional package suppress-done archivedp)
95 "List all outstanding Emacs bugs."
96 (interactive
97 (list
98 (completing-read "Severity: "
99 '("important" "normal" "minor" "wishlist")
100 nil t "normal")))
101 ;; Initialize variables.
102 (when (and (file-exists-p debbugs-persistency-file)
103 (not debbugs-local-tags))
104 (with-temp-buffer
105 (insert-file-contents debbugs-persistency-file)
106 (eval (read (current-buffer)))))
107 (unless (consp severities)
108 (setq severities (list severities)))
109
110 (setq debbugs-package (or package "emacs")
111 debbugs-severities severities
112 debbugs-archive (if archivedp "1" "0")
113 debbugs-widgets nil)
114
115 (let ((debbugs-port "gnu.org")
116 (default 500)
117 ids)
118 (dolist (severity debbugs-severities)
119 (setq ids (nconc ids
120 (debbugs-get-bugs :package debbugs-package
121 :severity severity
122 :archive debbugs-archive))))
123 (setq ids (sort ids '<))
124
125 (if (> (length ids) default)
126 (let ((cursor-in-echo-area nil))
127 (setq default
128 (string-to-number
129 (read-string
130 (format
131 "How many reports (available %d, default %d): "
132 (length ids) default)
133 nil
134 nil
135 (number-to-string default))))))
136
137 (if (> (length ids) default)
138 (let ((i 0)
139 curr-ids)
140 (while ids
141 (setq i (1+ i)
142 curr-ids (butlast ids (- (length ids) default)))
143 (add-to-list
144 'debbugs-widgets
145 (widget-convert
146 'push-button
147 :follow-link 'mouse-face
148 :notify (lambda (widget &rest ignore)
149 (debbugs-show-reports widget))
150 :keymap debbugs-widget-map
151 :suppress-done suppress-done
152 :buffer-name (format "*Emacs Bugs*<%d>" i)
153 :bug-ids curr-ids
154 :help-echo (format "%d-%d" (car ids) (car (last curr-ids)))
155 :format " %[%v%]"
156 (number-to-string i))
157 'append)
158 (setq ids (last ids (- (length ids) default))))
159 (debbugs-show-reports (car debbugs-widgets)))
160
161 (debbugs-show-reports
162 (widget-convert
163 'const
164 :suppress-done suppress-done
165 :buffer-name "*Emacs Bugs*"
166 :bug-ids ids)))))
167
168 (defvar debbugs-current-widget nil)
169
170 (defun debbugs-show-reports (widget)
171 "Show bug reports as given in WIDGET property :bug-ids."
172 (pop-to-buffer (get-buffer-create (widget-get widget :buffer-name)))
173 (debbugs-mode)
174 (let ((inhibit-read-only t)
175 (suppress-done (widget-get widget :suppress-done)))
176 (erase-buffer)
177
178 (when debbugs-widgets
179 (widget-insert "Page:")
180 (mapc
181 (lambda (obj)
182 (if (eq obj widget)
183 (widget-put obj :button-face 'widget-button-pressed)
184 (widget-put obj :button-face 'widget-button-face))
185 (widget-apply obj :create))
186 debbugs-widgets)
187 (widget-insert "\n\n"))
188
189 (dolist (status (sort (apply 'debbugs-get-status
190 (widget-get widget :bug-ids))
191 (lambda (s1 s2)
192 (< (cdr (assq 'id s1))
193 (cdr (assq 'id s2))))))
194 (when (or (not suppress-done)
195 (not (equal (cdr (assq 'pending status)) "done")))
196 (let* ((id (cdr (assq 'id status)))
197 (words
198 (mapconcat
199 'identity
200 (cons (cdr (assq 'severity status))
201 (cdr (assq 'keywords status)))
202 ","))
203 (face (cond
204 ((equal (cdr (assq 'pending status)) "done")
205 'debbugs-done)
206 ((= (cdr (assq 'date status))
207 (cdr (assq 'log_modified status)))
208 'debbugs-new)
209 ((< (- (float-time)
210 (cdr (assq 'log_modified status)))
211 (* 60 60 24 4))
212 'debbugs-handled)
213 (t
214 'debbugs-stale)))
215 (address (mail-header-parse-address
216 (decode-coding-string (cdr (assq 'originator status))
217 'utf-8)))
218 (owner (if (cdr (assq 'owner status))
219 (car (mail-header-parse-address
220 (decode-coding-string (cdr (assq 'owner status))
221 'utf-8)))))
222 (subject (decode-coding-string (cdr (assq 'subject status))
223 'utf-8))
224 merged)
225 (unless (equal (cdr (assq 'pending status)) "pending")
226 (setq words
227 (concat words "," (cdr (assq 'pending status)))))
228 (when (setq merged (cdr (assq 'mergedwith status)))
229 (setq words (format "%s,%s"
230 (if (numberp merged)
231 merged
232 (mapconcat 'number-to-string merged ","))
233 words)))
234 (setq words (propertize words 'face face))
235 (setq address
236 (propertize
237 ;; Prefer the name over the address.
238 (or (cdr address)
239 (car address))
240 'face
241 ;; Mark own submitted bugs.
242 (if (and (stringp (car address))
243 (string-equal (car address) user-mail-address))
244 'debbugs-tagged
245 'default)))
246 (insert
247 (format "%5d %-20s [%-23s] %s\n"
248 id
249 (if (> (length words) 20)
250 (propertize (substring words 0 20) 'help-echo words)
251 words)
252 (if (> (length address) 23)
253 (propertize (substring address 0 23) 'help-echo address)
254 address)
255 ;; Mark owned bugs.
256 (if (and (stringp owner)
257 (string-equal owner user-mail-address))
258 (propertize subject
259 'face 'debbugs-tagged 'help-echo subject)
260 (propertize subject 'help-echo subject))))
261 (forward-line -1)
262 (put-text-property (point) (1+ (point)) 'debbugs-status status)
263 (put-text-property
264 (point-at-bol) (point-at-eol) 'mouse-face widget-mouse-face)
265 (when (memq id debbugs-local-tags)
266 (put-text-property
267 (+ (point) (- 5 (length (number-to-string id)))) (+ (point) 5)
268 'face 'debbugs-tagged))
269 (forward-line 1))))
270
271 (when debbugs-widgets
272 (widget-insert "\nPage:")
273 (mapc (lambda (obj) (widget-apply obj :create)) debbugs-widgets)
274 (widget-setup))
275
276 (set-buffer-modified-p nil)
277 (set (make-local-variable 'debbugs-current-widget)
278 widget)
279 (goto-char (point-min))))
280
281 (defvar debbugs-mode-map
282 (let ((map (make-sparse-keymap)))
283 (define-key map "\r" 'debbugs-select-report)
284 (define-key map [mouse-1] 'debbugs-select-report)
285 (define-key map [mouse-2] 'debbugs-select-report)
286 (define-key map "q" 'kill-buffer)
287 (define-key map "s" 'debbugs-toggle-sort)
288 (define-key map "t" 'debbugs-toggle-tag)
289 (define-key map "d" 'debbugs-display-status)
290 (define-key map "g" 'debbugs-rescan)
291 (define-key map "x" 'debbugs-suppress-done)
292 (define-key map "C" 'debbugs-send-control-message)
293 map))
294
295 (defun debbugs-rescan ()
296 "Rescan the current set of bug reports."
297 (interactive)
298
299 ;; The last page will be provided with new bug ids.
300 ;; TODO: Do it also for the other pages.
301 (when (and debbugs-widgets
302 (eq debbugs-current-widget (car (last debbugs-widgets))))
303 (let ((debbugs-port "gnu.org")
304 (first-id (car (widget-get debbugs-current-widget :bug-ids)))
305 (last-id (car (last (widget-get debbugs-current-widget :bug-ids))))
306 ids)
307 (dolist (severity debbugs-severities)
308 (setq ids (nconc ids
309 (debbugs-get-bugs :package debbugs-package
310 :severity severity
311 :archive debbugs-archive))))
312 (setq ids (sort ids '<))
313
314 (while (and (<= first-id last-id) (not (memq first-id ids)))
315 (setq first-id (1+ first-id)))
316
317 (when (<= first-id last-id)
318 (widget-put debbugs-current-widget :bug-ids (memq first-id ids)))))
319
320 ;; Refresh the buffer. `save-excursion' does not work, so we
321 ;; remember the position.
322 (let ((pos (point)))
323 (debbugs-show-reports debbugs-current-widget)
324 (goto-char pos)))
325
326 (defvar debbugs-sort-state 'number)
327
328 (defun debbugs-mode ()
329 "Major mode for listing bug reports.
330
331 All normal editing commands are switched off.
332 \\<debbugs-mode-map>
333
334 The following commands are available:
335
336 \\{debbugs-mode-map}"
337 (interactive)
338 (kill-all-local-variables)
339 (setq major-mode 'debbugs-mode)
340 (setq mode-name "Debbugs")
341 (use-local-map debbugs-mode-map)
342 (set (make-local-variable 'debbugs-sort-state)
343 'number)
344 (buffer-disable-undo)
345 (setq truncate-lines t)
346 (setq buffer-read-only t))
347
348 (defvar debbugs-state-preference
349 '((debbugs-new . 1)
350 (debbugs-stale . 2)
351 (debbugs-handled . 3)
352 (debbugs-done . 4)))
353
354 (defun debbugs-toggle-sort ()
355 "Toggle sorting by age and by state."
356 (interactive)
357 (beginning-of-line)
358 (let ((buffer-read-only nil)
359 (before-change-functions nil)
360 (current-bug (debbugs-current-id t))
361 (start-point (point)))
362 (setq debbugs-sort-state
363 (if (eq debbugs-sort-state 'number)
364 'state
365 'number))
366 (goto-char (point-min))
367 (while (and (not (eobp))
368 (not (get-text-property (point) 'debbugs-status)))
369 (forward-line 1))
370 (save-restriction
371 (narrow-to-region
372 (point)
373 (progn
374 (goto-char (point-max))
375 (beginning-of-line)
376 (while (and (not (bobp))
377 (not (get-text-property (point) 'debbugs-status)))
378 (forward-line -1))
379 (forward-line 1)
380 (point)))
381 (goto-char (point-min))
382 (sort-subr
383 nil (lambda () (forward-line 1)) 'end-of-line
384 (lambda ()
385 (let ((id (debbugs-current-id)))
386 (if (eq debbugs-sort-state 'number)
387 id
388 ;; Sort the tagged ones at the end.
389 (or (and (memq id debbugs-local-tags)
390 20)
391 (cdr (assq (get-text-property (+ (point) 7) 'face)
392 debbugs-state-preference))
393 10))))))
394 (if (not current-bug)
395 (goto-char start-point)
396 (goto-char (point-min))
397 (re-search-forward (format "^%d" current-bug) nil t))))
398
399 (defun debbugs-toggle-tag ()
400 "Toggle tag of the report in the current line."
401 (interactive)
402 (save-excursion
403 (beginning-of-line)
404 (let ((inhibit-read-only t)
405 (id (debbugs-current-id)))
406 (if (memq id debbugs-local-tags)
407 (progn
408 (setq debbugs-local-tags (delq id debbugs-local-tags))
409 (put-text-property (point) (+ (point) 5) 'face 'default))
410 (add-to-list 'debbugs-local-tags id)
411 (put-text-property
412 (+ (point) (- 5 (length (number-to-string id)))) (+ (point) 5)
413 'face 'debbugs-tagged)))))
414
415 (defun debbugs-suppress-done ()
416 "Suppress bugs marked as done."
417 (interactive)
418 (save-excursion
419 (unless (widget-get debbugs-current-widget :suppress-done)
420 (let ((inhibit-read-only t))
421 (widget-put debbugs-current-widget :suppress-done t)
422 (beginning-of-buffer)
423 (while (and (not (eobp))
424 (not (get-text-property (point) 'debbugs-status)))
425 (forward-line 1))
426 (while (and (not (eobp))
427 (get-text-property (point) 'debbugs-status))
428 (if (equal (cdr (assq 'pending (debbugs-current-status))) "done")
429 (kill-region (point) (progn (forward-line 1) (point)))
430 (forward-line 1)))))))
431
432 (defvar debbugs-bug-number nil)
433
434 (defun debbugs-current-id (&optional noerror)
435 (or (cdr (assq 'id (debbugs-current-status)))
436 (and (not noerror)
437 (error "No bug on the current line"))))
438
439 (defun debbugs-current-status ()
440 (get-text-property (line-beginning-position)
441 'debbugs-status))
442
443 (defun debbugs-display-status (status)
444 "Display the status of the report on the current line."
445 (interactive (list (debbugs-current-status)))
446 (pop-to-buffer "*Bug Status*")
447 (erase-buffer)
448 (pp status (current-buffer))
449 (goto-char (point-min)))
450
451 (defun debbugs-select-report ()
452 "Select the report on the current line."
453 (interactive)
454 ;; We open the report messages.
455 (let* ((status (debbugs-current-status))
456 (id (cdr (assq 'id status)))
457 (merged (cdr (assq 'mergedwith status))))
458 (gnus-read-ephemeral-emacs-bug-group
459 (cons id (if (listp merged)
460 merged
461 (list merged)))
462 (cons (current-buffer)
463 (current-window-configuration)))
464 (with-current-buffer (window-buffer (selected-window))
465 (debbugs-summary-mode 1)
466 (set (make-local-variable 'debbugs-bug-number) id))))
467
468 (defvar debbugs-summary-mode-map
469 (let ((map (make-sparse-keymap)))
470 (define-key map "C" 'debbugs-send-control-message)
471 map))
472
473 (define-minor-mode debbugs-summary-mode
474 "Minor mode for providing a debbugs interface in Gnus summary buffers.
475
476 \\{debbugs-summary-mode-map}"
477 :lighter " Debbugs" :keymap debbugs-summary-mode-map
478 (set (make-local-variable 'gnus-posting-styles)
479 '((".*"
480 (eval
481 (with-current-buffer gnus-article-copy
482 (set (make-local-variable 'message-prune-recipient-rules)
483 '((".*@debbugs.*" "emacs-pretest-bug")
484 (".*@debbugs.*" "bug-gnu-emacs")
485 ("[0-9]+@debbugs.*" "submit@debbugs.gnu.org")))
486 (set (make-local-variable 'message-alter-recipients-function)
487 (lambda (address)
488 (if (string-match "\\([0-9]+\\)@donarmstrong" (car address))
489 (let ((new (format "%s@debbugs.gnu.org"
490 (match-string 1 (car address)))))
491 (cons new new))
492 address)))))))))
493
494 (defun debbugs-send-control-message (message)
495 "Send a control message for the current bug report.
496 You can set the severity or add a tag, or close the report. If
497 you use the special \"done\" MESSAGE, the report will be marked as
498 fixed, and then closed."
499 (interactive
500 (list (completing-read
501 "Control message: "
502 '("important" "normal" "minor" "wishlist"
503 "done"
504 "unarchive" "reopen" "close"
505 "merge" "forcemerge"
506 "owner" "noowner"
507 "patch" "wontfix" "moreinfo" "unreproducible" "fixed" "notabug")
508 nil t)))
509 (let* ((id (or debbugs-bug-number ; Set on group entry.
510 (debbugs-current-id)))
511 (version
512 (when (member message '("close" "done"))
513 (read-string
514 "Version: "
515 (cond
516 ;; Emacs development versions.
517 ((string-match
518 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\." emacs-version)
519 (format "%s.%d"
520 (match-string 1 emacs-version)
521 (1+ (string-to-number (match-string 2 emacs-version)))))
522 ;; Emacs release versions.
523 ((string-match
524 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)$" emacs-version)
525 (format "%s.%s"
526 (match-string 1 emacs-version)
527 (match-string 2 emacs-version)))
528 (t emacs-version))))))
529 (with-temp-buffer
530 (insert "To: control@debbugs.gnu.org\n"
531 "From: " (message-make-from) "\n"
532 (format "Subject: control message for bug #%d\n" id)
533 "\n"
534 (cond
535 ((member message '("unarchive" "reopen" "noowner"))
536 (format "%s %d\n" message id))
537 ((member message '("merge" "forcemerge"))
538 (format "%s %d %s\n" message id
539 (read-string "Merge with bug #: ")))
540 ((equal message "owner")
541 (format "owner %d !\n" id))
542 ((equal message "close")
543 (format "close %d %s\n" id version))
544 ((equal message "done")
545 (format "tags %d fixed\nclose %d %s\n" id id version))
546 ((member message '("important" "normal" "minor" "wishlist"))
547 (format "severity %d %s\n" id message))
548 (t
549 (format "tags %d %s\n" id message))))
550 (funcall send-mail-function))))
551
552 (provide 'debbugs-gnu)
553
554 ;;; TODO:
555
556 ;; * Widget-oriented bug overview like webDDTs.
557 ;; * Actions on bugs.
558 ;; * Integration into gnus (nnir).
559
560 ;;; debbugs-gnu.el ends here