1 ;;; debbugs-gnu.el --- interface for the GNU bug tracker
3 ;; Copyright (C) 2011 Free Software Foundation, Inc.
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Keywords: comm, hypermedia, maint
10 ;; This file is part of GNU Emacs.
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.
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.
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/>.
31 (eval-when-compile (require 'cl))
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")
38 (defgroup debbugs-gnu ()
39 "UI for the debbugs.gnu.org bug tracker."
42 (defface debbugs-new '((t (:foreground "red")))
43 "Face for new reports that nobody has answered.")
45 (defface debbugs-handled '((t (:foreground "ForestGreen")))
46 "Face for new reports that have been modified recently.")
48 (defface debbugs-stale '((t (:foreground "orange")))
49 "Face for new reports that nobody has answered.")
51 (defface debbugs-done '((t (:foreground "DarkGrey")))
52 "Face for closed bug reports.")
54 (defface debbugs-tagged '((t (:foreground "red")))
55 "Face for reports that have been tagged locally.")
57 (defvar debbugs-widgets nil)
59 (defvar debbugs-widget-map
60 (let ((map (make-sparse-keymap)))
61 (define-key map "\r" 'widget-button-press)
62 (define-key map [mouse-1] 'widget-button-press)
63 (define-key map [mouse-2] 'widget-button-press)
66 (defvar debbugs-local-tags nil
67 "List of bug numbers tagged locally, and kept persistent.")
69 (defvar debbugs-persistency-file
70 (expand-file-name (locate-user-emacs-file "debbugs"))
71 "File name of a persistency store for debbugs variables")
73 (defun debbugs-dump-persistency-file ()
74 "Function to store debbugs variables persistently."
78 ";; -*- emacs-lisp -*-\n"
79 ";; Debbugs tags connection history. Don't change this file.\n\n"
80 (format "(setq debbugs-local-tags '%S)"
81 (sort (copy-sequence debbugs-local-tags) '<)))
83 (point-min) (point-max) debbugs-persistency-file))))
86 (unless noninteractive
87 (add-hook 'kill-emacs-hook 'debbugs-dump-persistency-file))
89 (defvar debbugs-package nil
90 "The package name to be searched for.")
92 (defvar debbugs-severities nil
93 "The severities strings to be searched for.")
95 (defvar debbugs-archive nil
96 "The archive flag to be searched for.")
98 (defun debbugs-emacs (severities &optional package suppress-done archivedp)
99 "List all outstanding Emacs bugs."
102 (completing-read "Severity: "
103 '("important" "normal" "minor" "wishlist")
105 ;; Initialize variables.
106 (when (and (file-exists-p debbugs-persistency-file)
107 (not debbugs-local-tags))
109 (insert-file-contents debbugs-persistency-file)
110 (eval (read (current-buffer)))))
111 (unless (consp severities)
112 (setq severities (list severities)))
114 (setq debbugs-package (or package "emacs")
115 debbugs-severities severities
116 debbugs-archive (if archivedp "1" "0")
119 (let ((debbugs-port "gnu.org")
122 (dolist (severity debbugs-severities)
124 (debbugs-get-bugs :package debbugs-package
126 :archive debbugs-archive))))
127 (setq ids (sort ids '<))
129 (if (> (length ids) default)
130 (let ((cursor-in-echo-area nil))
135 "How many reports (available %d, default %d): "
136 (length ids) default)
139 (number-to-string default))))))
141 (if (> (length ids) default)
146 curr-ids (butlast ids (- (length ids) default)))
151 :follow-link 'mouse-face
152 :notify (lambda (widget &rest ignore)
153 (debbugs-show-reports widget))
154 :keymap debbugs-widget-map
155 :suppress-done suppress-done
156 :buffer-name (format "*Emacs Bugs*<%d>" i)
158 :help-echo (format "%d-%d" (car ids) (car (last curr-ids)))
160 (number-to-string i))
162 (setq ids (last ids (- (length ids) default))))
163 (debbugs-show-reports (car debbugs-widgets)))
165 (debbugs-show-reports
168 :suppress-done suppress-done
169 :buffer-name "*Emacs Bugs*"
172 (defvar debbugs-current-widget nil)
174 (defvar widget-mouse-face)
176 (defun debbugs-show-reports (widget)
177 "Show bug reports as given in WIDGET property :bug-ids."
178 (pop-to-buffer (get-buffer-create (widget-get widget :buffer-name)))
180 (let ((inhibit-read-only t)
181 (suppress-done (widget-get widget :suppress-done)))
184 (when debbugs-widgets
185 (widget-insert "Page:")
189 (widget-put obj :button-face 'widget-button-pressed)
190 (widget-put obj :button-face 'widget-button-face))
191 (widget-apply obj :create))
193 (widget-insert "\n\n"))
195 (dolist (status (sort (apply 'debbugs-get-status
196 (widget-get widget :bug-ids))
198 (< (cdr (assq 'id s1))
199 (cdr (assq 'id s2))))))
200 (when (or (not suppress-done)
201 (not (equal (cdr (assq 'pending status)) "done")))
202 (let* ((id (cdr (assq 'id status)))
206 (cons (cdr (assq 'severity status))
207 (cdr (assq 'keywords status)))
210 ((equal (cdr (assq 'pending status)) "done")
212 ((= (cdr (assq 'date status))
213 (cdr (assq 'log_modified status)))
216 (cdr (assq 'log_modified status)))
221 (address (mail-header-parse-address
222 (decode-coding-string (cdr (assq 'originator status))
224 (owner (if (cdr (assq 'owner status))
225 (car (mail-header-parse-address
226 (decode-coding-string (cdr (assq 'owner status))
228 (subject (decode-coding-string (cdr (assq 'subject status))
231 (unless (equal (cdr (assq 'pending status)) "pending")
233 (concat words "," (cdr (assq 'pending status)))))
234 (when (setq merged (cdr (assq 'mergedwith status)))
235 (setq words (format "%s,%s"
238 (mapconcat 'number-to-string merged ","))
240 (setq words (propertize words 'face face))
243 ;; Prefer the name over the address.
247 ;; Mark own submitted bugs.
248 (if (and (stringp (car address))
249 (string-equal (car address) user-mail-address))
253 (format "%5d %-20s [%-23s] %s\n"
255 (if (> (length words) 20)
256 (propertize (substring words 0 20) 'help-echo words)
258 (if (> (length address) 23)
259 (propertize (substring address 0 23) 'help-echo address)
262 (if (and (stringp owner)
263 (string-equal owner user-mail-address))
265 'face 'debbugs-tagged 'help-echo subject)
266 (propertize subject 'help-echo subject))))
268 (put-text-property (point) (1+ (point)) 'debbugs-status status)
270 (point-at-bol) (point-at-eol) 'mouse-face widget-mouse-face)
271 (when (memq id debbugs-local-tags)
273 (+ (point) (- 5 (length (number-to-string id)))) (+ (point) 5)
274 'face 'debbugs-tagged))
277 (when debbugs-widgets
278 (widget-insert "\nPage:")
279 (mapc (lambda (obj) (widget-apply obj :create)) debbugs-widgets)
282 (set-buffer-modified-p nil)
283 (set (make-local-variable 'debbugs-current-widget)
285 (goto-char (point-min))))
287 (defvar debbugs-mode-map
288 (let ((map (make-sparse-keymap)))
289 (define-key map "\r" 'debbugs-select-report)
290 (define-key map [mouse-1] 'debbugs-select-report)
291 (define-key map [mouse-2] 'debbugs-select-report)
292 (define-key map "q" 'kill-buffer)
293 (define-key map "s" 'debbugs-toggle-sort)
294 (define-key map "t" 'debbugs-toggle-tag)
295 (define-key map "d" 'debbugs-display-status)
296 (define-key map "g" 'debbugs-rescan)
297 (define-key map "x" 'debbugs-suppress-done)
298 (define-key map "C" 'debbugs-send-control-message)
301 (defun debbugs-rescan ()
302 "Rescan the current set of bug reports."
305 ;; The last page will be provided with new bug ids.
306 ;; TODO: Do it also for the other pages.
307 (when (and debbugs-widgets
308 (eq debbugs-current-widget (car (last debbugs-widgets))))
309 (let ((debbugs-port "gnu.org")
310 (first-id (car (widget-get debbugs-current-widget :bug-ids)))
311 (last-id (car (last (widget-get debbugs-current-widget :bug-ids))))
313 (dolist (severity debbugs-severities)
315 (debbugs-get-bugs :package debbugs-package
317 :archive debbugs-archive))))
318 (setq ids (sort ids '<))
320 (while (and (<= first-id last-id) (not (memq first-id ids)))
321 (setq first-id (1+ first-id)))
323 (when (<= first-id last-id)
324 (widget-put debbugs-current-widget :bug-ids (memq first-id ids)))))
326 ;; Refresh the buffer. `save-excursion' does not work, so we
327 ;; remember the position.
329 (debbugs-show-reports debbugs-current-widget)
332 (defvar debbugs-sort-state 'number)
334 (defun debbugs-mode ()
335 "Major mode for listing bug reports.
337 All normal editing commands are switched off.
340 The following commands are available:
342 \\{debbugs-mode-map}"
344 (kill-all-local-variables)
345 (setq major-mode 'debbugs-mode)
346 (setq mode-name "Debbugs")
347 (use-local-map debbugs-mode-map)
348 (set (make-local-variable 'debbugs-sort-state)
350 (buffer-disable-undo)
351 (setq truncate-lines t)
352 (setq buffer-read-only t))
354 (defvar debbugs-state-preference
357 (debbugs-handled . 3)
360 (defun debbugs-toggle-sort ()
361 "Toggle sorting by age and by state."
364 (let ((buffer-read-only nil)
365 (before-change-functions nil)
366 (current-bug (debbugs-current-id t))
367 (start-point (point)))
368 (setq debbugs-sort-state
369 (if (eq debbugs-sort-state 'number)
372 (goto-char (point-min))
373 (while (and (not (eobp))
374 (not (get-text-property (point) 'debbugs-status)))
380 (goto-char (point-max))
382 (while (and (not (bobp))
383 (not (get-text-property (point) 'debbugs-status)))
387 (goto-char (point-min))
389 nil (lambda () (forward-line 1)) 'end-of-line
391 (let ((id (debbugs-current-id)))
392 (if (eq debbugs-sort-state 'number)
394 ;; Sort the tagged ones at the end.
395 (or (and (memq id debbugs-local-tags)
397 (cdr (assq (get-text-property (+ (point) 7) 'face)
398 debbugs-state-preference))
400 (if (not current-bug)
401 (goto-char start-point)
402 (goto-char (point-min))
403 (re-search-forward (format "^%d" current-bug) nil t))))
405 (defun debbugs-toggle-tag ()
406 "Toggle tag of the report in the current line."
410 (let ((inhibit-read-only t)
411 (id (debbugs-current-id)))
412 (if (memq id debbugs-local-tags)
414 (setq debbugs-local-tags (delq id debbugs-local-tags))
415 (put-text-property (point) (+ (point) 5) 'face 'default))
416 (add-to-list 'debbugs-local-tags id)
418 (+ (point) (- 5 (length (number-to-string id)))) (+ (point) 5)
419 'face 'debbugs-tagged)))))
421 (defun debbugs-suppress-done ()
422 "Suppress bugs marked as done."
425 (unless (widget-get debbugs-current-widget :suppress-done)
426 (let ((inhibit-read-only t))
427 (widget-put debbugs-current-widget :suppress-done t)
428 (goto-char (point-min))
429 (while (and (not (eobp))
430 (not (get-text-property (point) 'debbugs-status)))
432 (while (and (not (eobp))
433 (get-text-property (point) 'debbugs-status))
434 (if (equal (cdr (assq 'pending (debbugs-current-status))) "done")
435 (kill-region (point) (progn (forward-line 1) (point)))
436 (forward-line 1)))))))
438 (defvar debbugs-bug-number nil)
440 (defun debbugs-current-id (&optional noerror)
441 (or (cdr (assq 'id (debbugs-current-status)))
443 (error "No bug on the current line"))))
445 (defun debbugs-current-status ()
446 (get-text-property (line-beginning-position)
449 (defun debbugs-display-status (status)
450 "Display the status of the report on the current line."
451 (interactive (list (debbugs-current-status)))
452 (pop-to-buffer "*Bug Status*")
454 (pp status (current-buffer))
455 (goto-char (point-min)))
457 (defun debbugs-select-report ()
458 "Select the report on the current line."
460 ;; We open the report messages.
461 (let* ((status (debbugs-current-status))
462 (id (cdr (assq 'id status)))
463 (merged (cdr (assq 'mergedwith status))))
464 (gnus-read-ephemeral-emacs-bug-group
465 (cons id (if (listp merged)
468 (cons (current-buffer)
469 (current-window-configuration)))
470 (with-current-buffer (window-buffer (selected-window))
471 (debbugs-summary-mode 1)
472 (set (make-local-variable 'debbugs-bug-number) id))))
474 (defvar debbugs-summary-mode-map
475 (let ((map (make-sparse-keymap)))
476 (define-key map "C" 'debbugs-send-control-message)
479 (defvar gnus-posting-styles)
481 (define-minor-mode debbugs-summary-mode
482 "Minor mode for providing a debbugs interface in Gnus summary buffers.
484 \\{debbugs-summary-mode-map}"
485 :lighter " Debbugs" :keymap debbugs-summary-mode-map
486 (set (make-local-variable 'gnus-posting-styles)
489 (with-current-buffer gnus-article-copy
490 (set (make-local-variable 'message-prune-recipient-rules)
491 '((".*@debbugs.*" "emacs-pretest-bug")
492 (".*@debbugs.*" "bug-gnu-emacs")
493 ("[0-9]+@debbugs.*" "submit@debbugs.gnu.org")))
494 (set (make-local-variable 'message-alter-recipients-function)
496 (if (string-match "\\([0-9]+\\)@donarmstrong" (car address))
497 (let ((new (format "%s@debbugs.gnu.org"
498 (match-string 1 (car address)))))
502 (defun debbugs-send-control-message (message)
503 "Send a control message for the current bug report.
504 You can set the severity or add a tag, or close the report. If
505 you use the special \"done\" MESSAGE, the report will be marked as
506 fixed, and then closed."
508 (list (completing-read
510 '("important" "normal" "minor" "wishlist"
512 "unarchive" "reopen" "close"
515 "patch" "wontfix" "moreinfo" "unreproducible" "fixed" "notabug")
517 (let* ((id (or debbugs-bug-number ; Set on group entry.
518 (debbugs-current-id)))
520 (when (member message '("close" "done"))
524 ;; Emacs development versions.
526 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\." emacs-version)
528 (match-string 1 emacs-version)
529 (1+ (string-to-number (match-string 2 emacs-version)))))
530 ;; Emacs release versions.
532 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)$" emacs-version)
534 (match-string 1 emacs-version)
535 (match-string 2 emacs-version)))
536 (t emacs-version))))))
538 (insert "To: control@debbugs.gnu.org\n"
539 "From: " (message-make-from) "\n"
540 (format "Subject: control message for bug #%d\n" id)
543 ((member message '("unarchive" "reopen" "noowner"))
544 (format "%s %d\n" message id))
545 ((member message '("merge" "forcemerge"))
546 (format "%s %d %s\n" message id
547 (read-string "Merge with bug #: ")))
548 ((equal message "owner")
549 (format "owner %d !\n" id))
550 ((equal message "close")
551 (format "close %d %s\n" id version))
552 ((equal message "done")
553 (format "tags %d fixed\nclose %d %s\n" id id version))
554 ((member message '("important" "normal" "minor" "wishlist"))
555 (format "severity %d %s\n" id message))
557 (format "tags %d %s\n" id message))))
558 (funcall send-mail-function))))
560 (provide 'debbugs-gnu)
564 ;; * Widget-oriented bug overview like webDDTs.
565 ;; * Actions on bugs.
566 ;; * Integration into gnus (nnir).
568 ;;; debbugs-gnu.el ends here