]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs-gnu.el
Merge commit 'c44ef65515f50bd38304a6f50adebc984fb8e431'
[gnu-emacs-elpa] / packages / debbugs / debbugs-gnu.el
1 ;;; debbugs-gnu.el --- interface for the GNU bug tracker
2
3 ;; Copyright (C) 2011-2015 Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Michael Albinus <michael.albinus@gmx.org>
7 ;; Keywords: comm, hypermedia, maint
8 ;; Package: debbugs
9 ;; Version: 0.6
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 an interface to bug reports which are located
29 ;; on the GNU bug tracker debbugs.gnu.org. Its main purpose is to
30 ;; show and manipulate bug reports from Emacs, but it could be used
31 ;; also for other GNU projects which use the same bug tracker.
32
33 ;; If you have `debbugs-gnu.el' in your load-path, you could enable
34 ;; the bug tracker commands by the following lines in your ~/.emacs
35 ;;
36 ;; (autoload 'debbugs-gnu "debbugs-gnu" "" 'interactive)
37 ;; (autoload 'debbugs-gnu-search "debbugs-gnu" "" 'interactive)
38 ;; (autoload 'debbugs-gnu-usertags "debbugs-gnu" "" 'interactive)
39 ;; (autoload 'debbugs-gnu-bugs "debbugs-gnu" "" 'interactive)
40
41 ;; The bug tracker is called interactively by
42 ;;
43 ;; M-x debbugs-gnu
44
45 ;; It asks for the severities, for which bugs shall be shown. This can
46 ;; be either just one severity, or a list of severities, separated by
47 ;; comma. Valid severities are "serious", "important", "normal",
48 ;; "minor" or "wishlist". Severities "critical" and "grave" are not
49 ;; used, although configured on the GNU bug tracker. If no severity
50 ;; is given, all bugs are selected.
51
52 ;; There is also the pseudo severity "tagged". When it is used, the
53 ;; function will ask for user tags (a comma separated list), and shows
54 ;; just the bugs which are tagged with them. In general, user tags
55 ;; shall be strings denoting to subprojects of the package, like
56 ;; "cedet" or "tramp" of the package "emacs". If no user tag is
57 ;; given, locally tagged bugs are shown.
58
59 ;; If a prefix is given to the command, more search parameters are
60 ;; asked for, like packages (also a comma separated list, "emacs" is
61 ;; the default), whether archived bugs shall be shown, and whether
62 ;; closed bugs shall be shown.
63
64 ;; Another command is
65 ;;
66 ;; M-x debbugs-gnu-search
67
68 ;; It behaves like `debbugs-gnu', but asks at the beginning for a
69 ;; search phrase to be used for full text search. Additionally, it
70 ;; asks for key-value pairs to filter bugs. Keys are as described in
71 ;; `debbugs-get-status', the corresponding value must be a regular
72 ;; expression to match for. The other parameters are as described in
73 ;; `debbugs-gnu'. Usually, there is just one value except for the
74 ;; attribute "date", which needs two arguments specifying a period in
75 ;; which the bug has been submitted or modified.
76
77 ;; The bug reports are downloaded from the bug tracker. In order to
78 ;; not generate too much load of the server, up to 500 bugs will be
79 ;; downloaded at once. If there are more hits, you will be asked to
80 ;; change this limit, but please don't increase this number too much.
81
82 ;; These default values could be changed also by customer options
83 ;; `debbugs-gnu-default-severities', `debbugs-gnu-default-packages',
84 ;; `debbugs-gnu-default-hits-per-page' and `debbugs-gnu-default-suppress-bugs'.
85
86 ;; The commands create one or more pages of bug lists. Every bug is
87 ;; shown in one line, including the bug number, the status (combining
88 ;; merged bug numbers, keywords and severities), the name of the
89 ;; submitter, and the title of the bug. On every bug line you could
90 ;; apply the following actions by the following keystrokes:
91
92 ;; RET: Show corresponding messages in Gnus
93 ;; "C": Send a control message
94 ;; "t": Mark the bug locally as tagged
95 ;; "b": Show bugs this bug is blocked by
96 ;; "B": Show bugs this bug is blocking
97 ;; "d": Show bug attributes
98
99 ;; Furthermore, you could apply the global actions
100
101 ;; "g": Rescan bugs
102 ;; "q": Quit the buffer
103 ;; "s": Toggle bug sorting for age or for state
104 ;; "x": Toggle suppressing of bugs
105 ;; "/": Display only bugs matching a string
106 ;; "w": Display all the currently selected bug reports
107
108 ;; When you visit the related bug messages in Gnus, you could also
109 ;; send control messages by keystroke "C".
110
111 ;; In the header line of every bug list page, you can toggle sorting
112 ;; per column by selecting a column with the mouse. The sorting
113 ;; happens as expected for the respective column; sorting in the Title
114 ;; column is depending on whether you are the owner of a bug.
115
116 ;; Another approach for listing bugs is calling the command
117 ;;
118 ;; M-x debbugs-gnu-usertags
119
120 ;; This command shows you all existing user tags for the packages
121 ;; defined in `debbugs-gnu-default-packages'. A prefix for the
122 ;; command allows you to use other packe names, or an arbitrary string
123 ;; for a user who has tagged bugs. The command returns the list of
124 ;; existing user tags for the given user(s) or package name(s),
125 ;; respectively. Applying RET on a user tag, all bugs tagged with
126 ;; this user tag are shown.
127
128 ;; Unfortunately, it is not possible with the SOAP interface to show
129 ;; all users who have tagged bugs. This list can be retrieved via
130 ;; <http://debbugs.gnu.org/cgi/pkgindex.cgi?indexon=users>.
131
132 ;; Finally, if you simply want to list some bugs with known bug
133 ;; numbers, call the command
134 ;;
135 ;; M-x debbugs-gnu-bugs
136
137 ;; The bug numbers to be shown shall be entered as comma separated list.
138
139 ;;; Code:
140
141 (require 'debbugs)
142 (require 'widget)
143 (require 'wid-edit)
144 (require 'tabulated-list)
145 (require 'add-log)
146 (eval-when-compile (require 'cl))
147
148 (autoload 'article-decode-charset "gnus-art")
149 (autoload 'diff-goto-source "diff-mode")
150 (autoload 'gnus-article-mime-handles "gnus-art")
151 (autoload 'gnus-read-ephemeral-emacs-bug-group "gnus-group")
152 (autoload 'gnus-summary-article-header "gnus-sum")
153 (autoload 'gnus-summary-select-article "gnus-sum")
154 (autoload 'gnus-summary-show-article "gnus-sum")
155 (autoload 'gnus-with-article-buffer "gnus-art")
156 (autoload 'log-edit-insert-changelog "log-edit")
157 (autoload 'mail-header-subject "nnheader")
158 (autoload 'message-make-from "message")
159 (autoload 'vc-dir-hide-up-to-date "vc-dir")
160 (autoload 'vc-dir-mark "vc-dir")
161 (defvar compilation-in-progress)
162
163 (defgroup debbugs-gnu ()
164 "UI for the debbugs.gnu.org bug tracker."
165 :group 'debbugs
166 :version "24.1")
167
168 (defcustom debbugs-gnu-default-severities '("serious" "important" "normal")
169 "*The list severities bugs are searched for.
170 \"tagged\" is not a severity but marks locally tagged bugs."
171 ;; <http://debbugs.gnu.org/Developer.html#severities>
172 :group 'debbugs-gnu
173 :type '(set (const "serious")
174 (const "important")
175 (const "normal")
176 (const "minor")
177 (const "wishlist")
178 (const "tagged"))
179 :version "24.1")
180
181 (defconst debbugs-gnu-all-severities
182 (mapcar 'cadr (cdr (get 'debbugs-gnu-default-severities 'custom-type)))
183 "*List of all possible severities.")
184
185 (defcustom debbugs-gnu-default-packages '("emacs")
186 "*The list of packages to be searched for."
187 ;; <http://debbugs.gnu.org/Packages.html>
188 ;; <http://debbugs.gnu.org/cgi/pkgindex.cgi>
189 :group 'debbugs-gnu
190 :type '(set (const "auctex")
191 (const "automake")
192 (const "cc-mode")
193 (const "coreutils")
194 (const "cppi")
195 (const "debbugs.gnu.org")
196 (const "diffutils")
197 (const "emacs")
198 (const "emacs-xwidgets")
199 (const "fm")
200 (const "gnus")
201 (const "grep")
202 (const "guile")
203 (const "guix")
204 (const "gzip")
205 (const "idutils")
206 (const "libtool")
207 (const "mh-e")
208 (const "org-mode")
209 (const "parted")
210 (const "vc-dwim")
211 (const "woodchuck"))
212 :version "24.4")
213
214 (defconst debbugs-gnu-all-packages
215 (mapcar 'cadr (cdr (get 'debbugs-gnu-default-packages 'custom-type)))
216 "*List of all possible package names.")
217
218 (defcustom debbugs-gnu-default-hits-per-page 500
219 "*The number of bugs shown per page."
220 :group 'debbugs-gnu
221 :type 'integer
222 :version "24.1")
223
224 (defcustom debbugs-gnu-default-suppress-bugs
225 '((pending . "done"))
226 "*A list of specs for bugs to be suppressed.
227 An element of this list is a cons cell \(KEY . REGEXP\), with key
228 being returned by `debbugs-get-status', and VAL a regular
229 expression matching the corresponding value, a string. Showing
230 suppressed bugs is toggled by `debbugs-gnu-toggle-suppress'."
231 :group 'debbugs-gnu
232 :type '(alist :key-type symbol :value-type regexp)
233 :version "24.1")
234
235 (defface debbugs-gnu-new '((t (:foreground "red")))
236 "Face for new reports that nobody has answered.")
237
238 (defface debbugs-gnu-handled '((t (:foreground "ForestGreen")))
239 "Face for reports that have been modified recently.")
240
241 (defface debbugs-gnu-pending '((t (:foreground "MidnightBlue")))
242 "Face for reports that are pending.")
243
244 (defface debbugs-gnu-stale '((t (:foreground "orange")))
245 "Face for reports that have not been touched for a week.")
246
247 (defface debbugs-gnu-done '((t (:foreground "DarkGrey")))
248 "Face for closed bug reports.")
249
250 (defface debbugs-gnu-tagged '((t (:foreground "red")))
251 "Face for reports that have been tagged locally.")
252
253 (defvar debbugs-gnu-widgets nil)
254
255 (defvar debbugs-gnu-widget-map
256 (let ((map (make-sparse-keymap)))
257 (define-key map "\r" 'widget-button-press)
258 (define-key map [mouse-2] 'widget-button-press)
259 map))
260
261 (defvar debbugs-gnu-local-tags nil
262 "List of bug numbers tagged locally, and kept persistent.")
263
264 (defvar debbugs-gnu-persistency-file
265 (expand-file-name (locate-user-emacs-file "debbugs"))
266 "File name of a persistency store for debbugs variables")
267
268 (defun debbugs-gnu-dump-persistency-file ()
269 "Function to store debbugs variables persistently."
270 (with-temp-file debbugs-gnu-persistency-file
271 (insert
272 ";; -*- emacs-lisp -*-\n"
273 ";; Debbugs tags connection history. Don't change this file.\n\n"
274 (format "(setq debbugs-gnu-local-tags '%S)"
275 (sort (copy-sequence debbugs-gnu-local-tags) '<)))))
276
277 (defvar debbugs-gnu-current-query nil
278 "The query object of the current search.
279 It will be applied server-side, when calling `debbugs-get-bugs'.
280 It has the same format as `debbugs-gnu-default-suppress-bugs'.")
281
282 (defvar debbugs-gnu-current-filter nil
283 "The filter object for the current search.
284 It will be applied client-side, when parsing the results of
285 `debbugs-get-status'. It has a similar format as
286 `debbugs-gnu-default-suppress-bugs'. In case of keys representing
287 a date, value is the cons cell \(BEFORE . AFTER\).")
288
289 (defun debbugs-gnu-calendar-read (prompt acceptable &optional initial-contents)
290 "Return a string read from the minibuffer.
291 Derived from `calendar-read'."
292 (let ((value (read-string prompt initial-contents)))
293 (while (not (funcall acceptable value))
294 (setq value (read-string prompt initial-contents)))
295 value))
296
297 (defconst debbugs-gnu-phrase-prompt
298 (propertize
299 "Enter search phrase: "
300 'help-echo "\
301 The search phrase contains words to be searched for, combined by
302 operators like AND, ANDNOT and OR. If there is no operator
303 between the words, AND is used by default. The phrase can also
304 be empty, in this case only the following attributes are used for
305 search."))
306
307 ;;;###autoload
308 (defun debbugs-gnu-search ()
309 "Search for Emacs bugs interactively.
310 Search arguments are requested interactively. The \"search
311 phrase\" is used for full text search in the bugs database.
312 Further key-value pairs are requested until an empty key is
313 returned. If a key cannot be queried by a SOAP request, it is
314 marked as \"client-side filter\"."
315 (interactive)
316
317 (unwind-protect
318 (let ((date-format "\\([[:digit:]]\\{4\\}\\)-\\([[:digit:]]\\{1,2\\}\\)-\\([[:digit:]]\\{1,2\\}\\)")
319 key val1 val2 phrase severities packages archivedp)
320
321 ;; Check for the phrase.
322 (setq phrase (read-string debbugs-gnu-phrase-prompt))
323 (if (zerop (length phrase))
324 (setq phrase nil)
325 (add-to-list 'debbugs-gnu-current-query (cons 'phrase phrase)))
326
327 ;; The other queries.
328 (catch :finished
329 (while t
330 (setq key (completing-read
331 "Enter attribute: "
332 (if phrase
333 '("severity" "package" "tags" "submitter" "date"
334 "subject" "status")
335 '("severity" "package" "archive" "src" "tag"
336 "owner" "submitter" "maint" "correspondent"
337 "date" "log_modified" "last_modified"
338 "found_date" "fixed_date" "unarchived"
339 "subject" "done" "forwarded" "msgid" "summary"))
340 nil t))
341 (cond
342 ;; Server-side queries.
343 ((equal key "severity")
344 (setq
345 severities
346 (completing-read-multiple
347 "Enter severities: " debbugs-gnu-all-severities nil t
348 (mapconcat 'identity debbugs-gnu-default-severities ","))))
349
350 ((equal key "package")
351 (setq
352 packages
353 (completing-read-multiple
354 "Enter packages: " debbugs-gnu-all-packages nil t
355 (mapconcat 'identity debbugs-gnu-default-packages ","))))
356
357 ((equal key "archive")
358 ;; We simplify, by assuming just archived bugs are requested.
359 (setq archivedp t))
360
361 ((member key '("src" "tag" "tags"))
362 (setq val1 (read-string (format "Enter %s: " key)))
363 (when (not (zerop (length val1)))
364 (add-to-list
365 'debbugs-gnu-current-query (cons (intern key) val1))))
366
367 ((member key '("owner" "submitter" "maint" "correspondent"))
368 (setq val1 (read-string "Enter email address: "))
369 (when (not (zerop (length val1)))
370 (add-to-list
371 'debbugs-gnu-current-query (cons (intern key) val1))))
372
373 ((equal key "status")
374 (setq
375 val1
376 (completing-read "Enter status: " '("done" "forwarded" "open")))
377 (when (not (zerop (length val1)))
378 (add-to-list
379 'debbugs-gnu-current-query (cons (intern key) val1))))
380
381 ;; Client-side filters.
382 ((member key '("date" "log_modified" "last_modified"
383 "found_date" "fixed_date" "unarchived"))
384 (setq val1
385 (debbugs-gnu-calendar-read
386 (format "Enter %s before YYYY-MM-DD%s: "
387 key (if phrase "" " (client-side filter)"))
388 (lambda (x)
389 (string-match (concat "^\\(" date-format "\\|\\)$") x))))
390 (if (string-match date-format val1)
391 (setq val1 (floor
392 (float-time
393 (encode-time
394 0 0 0
395 (string-to-number (match-string 3 val1))
396 (string-to-number (match-string 2 val1))
397 (string-to-number (match-string 1 val1))))))
398 (setq val1 nil))
399 (setq val2
400 (debbugs-gnu-calendar-read
401 (format "Enter %s after YYYY-MM-DD%s: "
402 key (if phrase "" " (client-side filter)"))
403 (lambda (x)
404 (string-match (concat "^\\(" date-format "\\|\\)$") x))))
405 (if (string-match date-format val2)
406 (setq val2 (floor
407 (float-time
408 (encode-time
409 0 0 0
410 (string-to-number (match-string 3 val2))
411 (string-to-number (match-string 2 val2))
412 (string-to-number (match-string 1 val2))))))
413 (setq val2 nil))
414 (when (or val1 val2)
415 (add-to-list
416 (if phrase
417 'debbugs-gnu-current-query 'debbugs-gnu-current-filter)
418 (cons (intern key) (cons val1 val2)))))
419
420 ((not (zerop (length key)))
421 (setq val1
422 (funcall
423 (if phrase 'read-string 'read-regexp)
424 (format "Enter %s%s"
425 key (if phrase ": " " (client-side filter)"))))
426 (when (not (zerop (length val1)))
427 (add-to-list
428 (if phrase
429 'debbugs-gnu-current-query 'debbugs-gnu-current-filter)
430 (cons (intern key) val1))))
431
432 ;; The End.
433 (t (throw :finished nil)))))
434
435 ;; Do the search.
436 (debbugs-gnu severities packages archivedp))
437
438 ;; Reset query and filter.
439 (setq debbugs-gnu-current-query nil
440 debbugs-gnu-current-filter nil)))
441
442 ;;;###autoload
443 (defun debbugs-gnu (severities &optional packages archivedp suppress tags)
444 "List all outstanding bugs."
445 (interactive
446 (let (severities archivedp)
447 (list
448 (setq severities
449 (completing-read-multiple
450 "Severities: " debbugs-gnu-all-severities nil t
451 (mapconcat 'identity debbugs-gnu-default-severities ",")))
452 ;; The next parameters are asked only when there is a prefix.
453 (if current-prefix-arg
454 (completing-read-multiple
455 "Packages: " debbugs-gnu-all-packages nil t
456 (mapconcat 'identity debbugs-gnu-default-packages ","))
457 debbugs-gnu-default-packages)
458 (when current-prefix-arg
459 (setq archivedp (y-or-n-p "Show archived bugs?")))
460 (when (and current-prefix-arg (not archivedp))
461 (y-or-n-p "Suppress unwanted bugs?"))
462 ;; This one must be asked for severity "tagged".
463 (when (member "tagged" severities)
464 (split-string (read-string "User tag(s): ") "," t)))))
465
466 ;; Initialize variables.
467 (when (and (file-exists-p debbugs-gnu-persistency-file)
468 (not debbugs-gnu-local-tags))
469 (with-temp-buffer
470 (insert-file-contents debbugs-gnu-persistency-file)
471 (eval (read (current-buffer)))))
472 (setq debbugs-gnu-widgets nil)
473
474 ;; Add queries.
475 (dolist (severity (if (consp severities) severities (list severities)))
476 (when (not (zerop (length severity)))
477 (add-to-list 'debbugs-gnu-current-query (cons 'severity severity))))
478 (dolist (package (if (consp packages) packages (list packages)))
479 (when (not (zerop (length package)))
480 (add-to-list 'debbugs-gnu-current-query (cons 'package package))))
481 (when archivedp
482 (add-to-list 'debbugs-gnu-current-query '(archive . "1")))
483 (when suppress
484 (add-to-list 'debbugs-gnu-current-query '(status . "open"))
485 (add-to-list 'debbugs-gnu-current-query '(status . "forwarded")))
486 (dolist (tag (if (consp tags) tags (list tags)))
487 (when (not (zerop (length tag)))
488 (add-to-list 'debbugs-gnu-current-query (cons 'tag tag))))
489
490 (unwind-protect
491 (let ((hits debbugs-gnu-default-hits-per-page)
492 (ids (debbugs-gnu-get-bugs debbugs-gnu-current-query)))
493
494 (if (> (length ids) hits)
495 (let ((cursor-in-echo-area nil))
496 (setq hits
497 (string-to-number
498 (read-string
499 (format
500 "How many reports (available %d, default %d): "
501 (length ids) hits)
502 nil
503 nil
504 (number-to-string hits))))))
505
506 (if (> (length ids) hits)
507 (let ((i 0)
508 curr-ids)
509 (while ids
510 (setq i (1+ i)
511 curr-ids (butlast ids (- (length ids) hits)))
512 (add-to-list
513 'debbugs-gnu-widgets
514 (widget-convert
515 'push-button
516 :follow-link 'mouse-face
517 :notify (lambda (widget &rest ignore)
518 (debbugs-gnu-show-reports widget))
519 :keymap debbugs-gnu-widget-map
520 :suppress suppress
521 :buffer-name (format "*Emacs Bugs*<%d>" i)
522 :bug-ids curr-ids
523 :query debbugs-gnu-current-query
524 :filter debbugs-gnu-current-filter
525 :help-echo (format "%d-%d" (car ids) (car (last curr-ids)))
526 :format " %[%v%]"
527 (number-to-string i))
528 'append)
529 (setq ids (last ids (- (length ids) hits))))
530 (debbugs-gnu-show-reports (car debbugs-gnu-widgets)))
531
532 (debbugs-gnu-show-reports
533 (widget-convert
534 'const
535 :suppress suppress
536 :buffer-name "*Emacs Bugs*"
537 :bug-ids ids
538 :query debbugs-gnu-current-query
539 :filter debbugs-gnu-current-filter))))
540
541 ;; Reset query and filter.
542 (setq debbugs-gnu-current-query nil
543 debbugs-gnu-current-filter nil)))
544
545 (defun debbugs-gnu-get-bugs (query)
546 "Retrieve bugs numbers from debbugs.gnu.org according search criteria."
547 (let* ((debbugs-port "gnu.org")
548 (bugs (assoc 'bugs query))
549 (tags (assoc 'tag query))
550 (local-tags (and (member '(severity . "tagged") query) (not tags)))
551 (phrase (assoc 'phrase query))
552 args)
553 ;; Compile query arguments.
554 (unless (or query tags)
555 (dolist (elt debbugs-gnu-default-packages)
556 (setq args (append args (list :package elt)))))
557 (dolist (elt query)
558 (unless (equal elt '(severity . "tagged"))
559 (setq args
560 (append
561 args
562 (if phrase
563 (cond
564 ((eq (car elt) 'phrase)
565 (list (list :phrase (cdr elt) :max 500)))
566 ((eq (car elt) 'date)
567 (list (list :date (cddr elt) (cadr elt)
568 :operator "NUMBT")))
569 (t
570 (list (list (intern (concat ":" (symbol-name (car elt))))
571 (cdr elt) :operator "ISTRINC"))))
572 (list (intern (concat ":" (symbol-name (car elt))))
573 (cdr elt)))))))
574
575 (sort
576 (cond
577 ;; If the query is just a list of bug numbers, we return them.
578 (bugs (cdr bugs))
579 ;; If the query contains the pseudo-severity "tagged", we return
580 ;; just the local tagged bugs.
581 (local-tags (copy-sequence debbugs-gnu-local-tags))
582 ;; A full text query.
583 (phrase
584 (mapcar
585 (lambda (x) (cdr (assoc "id" x)))
586 (apply 'debbugs-search-est args)))
587 ;; User tags.
588 (tags
589 (setq args (mapcar (lambda (x) (if (eq x :package) :user x)) args))
590 (apply 'debbugs-get-usertag args))
591 ;; Otherwise, we retrieve the bugs from the server.
592 (t (apply 'debbugs-get-bugs args)))
593 ;; Sort function.
594 '<)))
595
596 (defvar debbugs-gnu-current-widget nil)
597 (defvar debbugs-gnu-current-limit nil)
598
599 (defun debbugs-gnu-show-reports (widget)
600 "Show bug reports as given in WIDGET property :bug-ids."
601 ;; The tabulated mode sets several local variables. We must get rid
602 ;; of them.
603 (when (get-buffer (widget-get widget :buffer-name))
604 (kill-buffer (widget-get widget :buffer-name)))
605 (pop-to-buffer (get-buffer-create (widget-get widget :buffer-name)))
606 (debbugs-gnu-mode)
607 (let ((inhibit-read-only t)
608 (debbugs-port "gnu.org"))
609 (erase-buffer)
610 (set (make-local-variable 'debbugs-gnu-current-widget) widget)
611
612 (dolist (status (apply 'debbugs-get-status (widget-get widget :bug-ids)))
613 (let* ((id (cdr (assq 'id status)))
614 (words
615 (mapconcat
616 'identity
617 (cons (cdr (assq 'severity status))
618 (cdr (assq 'keywords status)))
619 ","))
620 (address (mail-header-parse-address
621 (decode-coding-string (cdr (assq 'originator status))
622 'utf-8)))
623 (owner (if (cdr (assq 'owner status))
624 (car (mail-header-parse-address
625 (decode-coding-string (cdr (assq 'owner status))
626 'utf-8)))))
627 (subject (decode-coding-string (cdr (assq 'subject status))
628 'utf-8))
629 merged)
630 (unless (equal (cdr (assq 'pending status)) "pending")
631 (setq words
632 (concat words "," (cdr (assq 'pending status)))))
633 (let ((packages (delete "emacs" (cdr (assq 'package status)))))
634 (when packages
635 (setq words (concat words "," (mapconcat 'identity packages ",")))))
636 (when (setq merged (cdr (assq 'mergedwith status)))
637 (setq words (format "%s,%s"
638 (if (numberp merged)
639 merged
640 (mapconcat 'number-to-string merged ","))
641 words)))
642 (when (or (not merged)
643 (not (let ((found nil))
644 (dolist (id (if (listp merged)
645 merged
646 (list merged)))
647 (dolist (entry tabulated-list-entries)
648 (when (equal id (cdr (assq 'id (car entry))))
649 (setq found t))))
650 found)))
651 (add-to-list
652 'tabulated-list-entries
653 (list
654 status
655 (vector
656 (propertize
657 (format "%5d" id)
658 'face
659 ;; Mark tagged bugs.
660 (if (memq id debbugs-gnu-local-tags)
661 'debbugs-gnu-tagged
662 'default))
663 (propertize
664 ;; Mark status and age.
665 words
666 'face
667 (cond
668 ((equal (cdr (assq 'pending status)) "done")
669 'debbugs-gnu-done)
670 ((member "pending" (cdr (assq 'keywords status)))
671 'debbugs-gnu-pending)
672 ((= (cdr (assq 'date status))
673 (cdr (assq 'log_modified status)))
674 'debbugs-gnu-new)
675 ((< (- (float-time)
676 (cdr (assq 'log_modified status)))
677 (* 60 60 24 7 2))
678 'debbugs-gnu-handled)
679 (t
680 'debbugs-gnu-stale)))
681 (propertize
682 ;; Prefer the name over the address.
683 (or (cdr address)
684 (car address))
685 'face
686 ;; Mark own submitted bugs.
687 (if (and (stringp (car address))
688 (string-equal (car address) user-mail-address))
689 'debbugs-gnu-tagged
690 'default))
691 (propertize
692 subject
693 'face
694 ;; Mark owned bugs.
695 (if (and (stringp owner)
696 (string-equal owner user-mail-address))
697 'debbugs-gnu-tagged
698 'default))))
699 'append))))
700 (tabulated-list-init-header)
701 (tabulated-list-print)
702
703 (set-buffer-modified-p nil)
704 (goto-char (point-min))))
705
706 (defun debbugs-gnu-print-entry (list-id cols)
707 "Insert a debbugs entry at point.
708 Used instead of `tabulated-list-print-entry'."
709 ;; This shall be in `debbugs-gnu-show-reports'. But
710 ;; `tabulated-list-print' erases the buffer, therefore we do it
711 ;; here. (bug#9047)
712 (when (and debbugs-gnu-widgets (= (point) (point-min)))
713 (widget-insert "Page:")
714 (mapc
715 (lambda (obj)
716 (if (eq obj debbugs-gnu-current-widget)
717 (widget-put obj :button-face 'widget-button-pressed)
718 (widget-put obj :button-face 'widget-button-face))
719 (widget-apply obj :create))
720 debbugs-gnu-widgets)
721 (widget-insert "\n\n")
722 (save-excursion
723 (widget-insert "\nPage:")
724 (mapc (lambda (obj) (widget-apply obj :create)) debbugs-gnu-widgets)
725 (widget-setup)))
726
727 (let ((beg (point))
728 (pos 0)
729 (case-fold-search t)
730 (id (aref cols 0))
731 (id-length (nth 1 (aref tabulated-list-format 0)))
732 (state (aref cols 1))
733 (state-length (nth 1 (aref tabulated-list-format 1)))
734 (submitter (aref cols 2))
735 (submitter-length (nth 1 (aref tabulated-list-format 2)))
736 (title (aref cols 3))
737 (title-length (nth 1 (aref tabulated-list-format 3))))
738 (when (and
739 ;; We may have a narrowing in effect.
740 (or (not debbugs-gnu-current-limit)
741 (memq (cdr (assq 'id list-id)) debbugs-gnu-current-limit))
742 ;; Filter suppressed bugs.
743 (or (not (widget-get debbugs-gnu-current-widget :suppress))
744 (and (not (memq (cdr (assq 'id list-id)) debbugs-gnu-local-tags))
745 (not (catch :suppress
746 (dolist (check debbugs-gnu-default-suppress-bugs)
747 (when
748 (string-match
749 (cdr check)
750 (or (cdr (assq (car check) list-id)) ""))
751 (throw :suppress t)))))))
752 ;; Filter search list.
753 (not (catch :suppress
754 (dolist (check
755 (widget-get debbugs-gnu-current-widget :filter))
756 (let ((val (cdr (assq (car check) list-id))))
757 (if (stringp (cdr check))
758 ;; Regular expression.
759 (when (not (string-match (cdr check) (or val "")))
760 (throw :suppress t))
761 ;; Time value.
762 (when (or (and (numberp (cadr check))
763 (< (cadr check) val))
764 (and (numberp (cddr check))
765 (> (cddr check) val)))
766 (throw :suppress t))))))))
767
768 ;; Insert id.
769 (indent-to (- id-length (length id)))
770 (insert id)
771 ;; Insert state.
772 (indent-to (setq pos (+ pos id-length 1)) 1)
773 (insert (if (> (length state) state-length)
774 (propertize (substring state 0 state-length)
775 'help-echo state)
776 state))
777 ;; Insert submitter.
778 (indent-to (setq pos (+ pos state-length 1)) 1)
779 (insert "[" (if (> (length submitter) (- submitter-length 2))
780 (propertize (substring submitter 0 (- submitter-length 2))
781 'help-echo submitter)
782 submitter))
783 (indent-to (+ pos (1- submitter-length)))
784 (insert "]")
785 ;; Insert title.
786 (indent-to (setq pos (+ pos submitter-length 1)) 1)
787 (insert (propertize title 'help-echo title))
788 ;; Add properties.
789 (add-text-properties
790 beg (point) `(tabulated-list-id ,list-id mouse-face ,widget-mouse-face))
791 (insert ?\n))))
792
793 (defvar debbugs-gnu-mode-map
794 (let ((map (make-sparse-keymap)))
795 (set-keymap-parent map tabulated-list-mode-map)
796 (define-key map "\r" 'debbugs-gnu-select-report)
797 (define-key map [mouse-1] 'debbugs-gnu-select-report)
798 (define-key map [mouse-2] 'debbugs-gnu-select-report)
799 (define-key map "s" 'debbugs-gnu-toggle-sort)
800 (define-key map "t" 'debbugs-gnu-toggle-tag)
801 (define-key map "d" 'debbugs-gnu-display-status)
802 (define-key map "g" 'debbugs-gnu-rescan)
803 (define-key map "x" 'debbugs-gnu-toggle-suppress)
804 (define-key map "/" 'debbugs-gnu-narrow-to-status)
805 (define-key map "w" 'debbugs-gnu-widen)
806 (define-key map "b" 'debbugs-gnu-show-blocked-by-reports)
807 (define-key map "B" 'debbugs-gnu-show-blocking-reports)
808 (define-key map "C" 'debbugs-gnu-send-control-message)
809 map))
810
811 (defun debbugs-gnu-rescan ()
812 "Rescan the current set of bug reports."
813 (interactive)
814
815 ;; The last page will be provided with new bug ids.
816 ;; TODO: Do it also for the other pages.
817 (when (and debbugs-gnu-widgets
818 (eq debbugs-gnu-current-widget (car (last debbugs-gnu-widgets))))
819 (let ((first-id (car (widget-get debbugs-gnu-current-widget :bug-ids)))
820 (last-id (car
821 (last (widget-get debbugs-gnu-current-widget :bug-ids))))
822 (ids (debbugs-gnu-get-bugs
823 (widget-get debbugs-gnu-current-widget :query))))
824
825 (while (and (<= first-id last-id) (not (memq first-id ids)))
826 (setq first-id (1+ first-id)))
827
828 (when (<= first-id last-id)
829 (widget-put debbugs-gnu-current-widget :bug-ids (memq first-id ids)))))
830
831 ;; Refresh the buffer. `save-excursion' does not work, so we
832 ;; remember the position.
833 (let ((pos (point)))
834 (debbugs-gnu-show-reports debbugs-gnu-current-widget)
835 (goto-char pos)))
836
837 (defvar debbugs-gnu-sort-state 'number)
838
839 (define-derived-mode debbugs-gnu-mode tabulated-list-mode "Debbugs"
840 "Major mode for listing bug reports.
841
842 All normal editing commands are switched off.
843 \\<debbugs-gnu-mode-map>
844
845 The following commands are available:
846
847 \\{debbugs-gnu-mode-map}"
848 (set (make-local-variable 'debbugs-gnu-sort-state) 'number)
849 (set (make-local-variable 'debbugs-gnu-current-limit) nil)
850 (setq tabulated-list-format [("Id" 5 debbugs-gnu-sort-id)
851 ("State" 20 debbugs-gnu-sort-state)
852 ("Submitter" 25 t)
853 ("Title" 10 debbugs-gnu-sort-title)])
854 (setq tabulated-list-sort-key (cons "Id" nil))
855 (setq tabulated-list-printer 'debbugs-gnu-print-entry)
856 (buffer-disable-undo)
857 (setq truncate-lines t)
858 (setq buffer-read-only t))
859
860 (defun debbugs-gnu-sort-id (s1 s2)
861 (< (cdr (assq 'id (car s1)))
862 (cdr (assq 'id (car s2)))))
863
864 (defconst debbugs-gnu-state-preference
865 '((debbugs-gnu-new . 1)
866 (debbugs-gnu-stale . 2)
867 (debbugs-gnu-handled . 3)
868 (debbugs-gnu-done . 4)
869 (debbugs-gnu-pending . 5)))
870
871 (defun debbugs-gnu-get-state-preference (face-string)
872 (or (cdr (assq (get-text-property 0 'face face-string)
873 debbugs-gnu-state-preference))
874 10))
875
876 (defconst debbugs-gnu-severity-preference
877 '(("serious" . 1)
878 ("important" . 2)
879 ("normal" . 3)
880 ("minor" . 4)
881 ("wishlist" . 5)))
882
883 (defun debbugs-gnu-get-severity-preference (state)
884 (or (cdr (assoc (cdr (assq 'severity state))
885 debbugs-gnu-severity-preference))
886 10))
887
888 (defun debbugs-gnu-sort-state (s1 s2)
889 (let ((id1 (cdr (assq 'id (car s1))))
890 (age1 (debbugs-gnu-get-state-preference (aref (nth 1 s1) 1)))
891 (id2 (cdr (assq 'id (car s2))))
892 (age2 (debbugs-gnu-get-state-preference (aref (nth 1 s2) 1))))
893 (cond
894 ;; Tagged bugs go to the end.
895 ((and (not (memq id1 debbugs-gnu-local-tags))
896 (memq id2 debbugs-gnu-local-tags))
897 t)
898 ((and (memq id1 debbugs-gnu-local-tags)
899 (not (memq id2 debbugs-gnu-local-tags)))
900 nil)
901 ;; Then, we check the age of the bugs.
902 ((< age1 age2)
903 t)
904 ((> age1 age2)
905 nil)
906 ;; If they have the same age, we check for severity.
907 ((< (debbugs-gnu-get-severity-preference (car s1))
908 (debbugs-gnu-get-severity-preference (car s2)))
909 t)
910 (t nil))))
911
912 (defun debbugs-gnu-sort-title (s1 s2)
913 (let ((owner (if (cdr (assq 'owner (car s1)))
914 (car (mail-header-parse-address
915 (decode-coding-string (cdr (assq 'owner (car s1)))
916 'utf-8))))))
917 (and (stringp owner)
918 (string-equal owner user-mail-address))))
919
920 (defun debbugs-gnu-toggle-sort ()
921 "Toggle sorting by age and by state."
922 (interactive)
923 (if (eq debbugs-gnu-sort-state 'number)
924 (progn
925 (setq debbugs-gnu-sort-state 'state)
926 (setq tabulated-list-sort-key (cons "Id" nil)))
927 (setq debbugs-gnu-sort-state 'number)
928 (setq tabulated-list-sort-key (cons "State" nil)))
929 (tabulated-list-init-header)
930 (tabulated-list-print))
931
932 (defun debbugs-gnu-widen ()
933 "Display all the currently selected bug reports."
934 (interactive)
935 (let ((id (debbugs-gnu-current-id t))
936 (inhibit-read-only t))
937 (setq debbugs-gnu-current-limit nil)
938 (tabulated-list-init-header)
939 (tabulated-list-print)
940 (when id
941 (debbugs-gnu-goto id))))
942
943 (defun debbugs-gnu-show-blocked-by-reports ()
944 "Display all bug reports this report is blocked by."
945 (interactive)
946 (let ((id (debbugs-gnu-current-id))
947 (status (debbugs-gnu-current-status)))
948 (if (null (cdr (assq 'blockedby status)))
949 (message "Bug %d is not blocked by any other bug" id)
950 (apply 'debbugs-gnu-bugs (cdr (assq 'blockedby status))))))
951
952 (defun debbugs-gnu-show-blocking-reports ()
953 "Display all bug reports this report is blocking."
954 (interactive)
955 (let ((id (debbugs-gnu-current-id))
956 (status (debbugs-gnu-current-status)))
957 (if (null (cdr (assq 'blocks status)))
958 (message "Bug %d is not blocking any other bug" id)
959 (apply 'debbugs-gnu-bugs (cdr (assq 'blocks status))))))
960
961 (defun debbugs-gnu-narrow-to-status (string &optional status-only)
962 "Only display the bugs matching STRING.
963 If STATUS-ONLY (the prefix), ignore matches in the From and
964 Subject fields."
965 (interactive "sNarrow to: \nP")
966 (let ((id (debbugs-gnu-current-id t))
967 (inhibit-read-only t)
968 status)
969 (setq debbugs-gnu-current-limit nil)
970 (if (equal string "")
971 (debbugs-gnu-toggle-suppress)
972 (goto-char (point-min))
973 (while (not (eobp))
974 (setq status (debbugs-gnu-current-status))
975 (if (and (not (member string (assq 'keywords status)))
976 (not (member string (assq 'severity status)))
977 (or status-only
978 (not (string-match string (cdr (assq 'originator status)))))
979 (or status-only
980 (not (string-match string (cdr (assq 'subject status))))))
981 (delete-region (point) (progn (forward-line 1) (point)))
982 (push (cdr (assq 'id status)) debbugs-gnu-current-limit)
983 (forward-line 1)))
984 (when id
985 (debbugs-gnu-goto id)))))
986
987 (defun debbugs-gnu-goto (id)
988 "Go to the line displaying bug ID."
989 (goto-char (point-min))
990 (while (and (not (eobp))
991 (not (equal (debbugs-gnu-current-id t) id)))
992 (forward-line 1)))
993
994 (defun debbugs-gnu-toggle-tag ()
995 "Toggle the local tag of the report in the current line.
996 If a report is tagged locally, it is presumed to be of little
997 interest to you."
998 (interactive)
999 (save-excursion
1000 (beginning-of-line)
1001 (let ((inhibit-read-only t)
1002 (id (debbugs-gnu-current-id)))
1003 (if (memq id debbugs-gnu-local-tags)
1004 (progn
1005 (setq debbugs-gnu-local-tags (delq id debbugs-gnu-local-tags))
1006 (put-text-property (point) (+ (point) 5) 'face 'default))
1007 (add-to-list 'debbugs-gnu-local-tags id)
1008 (put-text-property
1009 (+ (point) (- 5 (length (number-to-string id)))) (+ (point) 5)
1010 'face 'debbugs-gnu-tagged))
1011 (debbugs-gnu--update-tag-face id)))
1012 (debbugs-gnu-dump-persistency-file))
1013
1014 (defun debbugs-gnu--update-tag-face (id)
1015 (dolist (entry tabulated-list-entries)
1016 (when (equal (cdr (assq 'id (car entry))) id)
1017 (aset (cadr entry) 0
1018 (propertize
1019 (format "%5d" id)
1020 'face
1021 ;; Mark tagged bugs.
1022 (if (memq id debbugs-gnu-local-tags)
1023 'debbugs-gnu-tagged
1024 'default))))))
1025
1026 (defun debbugs-gnu-toggle-suppress ()
1027 "Suppress bugs marked in `debbugs-gnu-suppress-bugs'."
1028 (interactive)
1029 (widget-put debbugs-gnu-current-widget :suppress
1030 (not (widget-get debbugs-gnu-current-widget :suppress)))
1031 (tabulated-list-init-header)
1032 (tabulated-list-print))
1033
1034 (defvar debbugs-gnu-bug-number nil)
1035 (defvar debbugs-gnu-subject nil)
1036
1037 (defun debbugs-gnu-current-id (&optional noerror)
1038 (or (cdr (assq 'id (debbugs-gnu-current-status)))
1039 (and (not noerror)
1040 (error "No bug on the current line"))))
1041
1042 (defun debbugs-gnu-current-status ()
1043 (get-text-property (line-beginning-position) 'tabulated-list-id))
1044
1045 (defun debbugs-gnu-current-query ()
1046 (widget-get debbugs-gnu-current-widget :query))
1047
1048 (defun debbugs-gnu-display-status (query status)
1049 "Display the query and status of the report on the current line."
1050 (interactive (list (debbugs-gnu-current-query)
1051 (debbugs-gnu-current-status)))
1052 (pop-to-buffer "*Bug Status*")
1053 (let ((inhibit-read-only t))
1054 (erase-buffer)
1055 (when query (pp query (current-buffer)))
1056 (when status (pp status (current-buffer)))
1057 (goto-char (point-min)))
1058 (set-buffer-modified-p nil)
1059 (special-mode))
1060
1061 (defun debbugs-gnu-select-report ()
1062 "Select the report on the current line."
1063 (interactive)
1064 ;; We open the report messages.
1065 (let* ((status (debbugs-gnu-current-status))
1066 (id (cdr (assq 'id status)))
1067 (merged (cdr (assq 'mergedwith status))))
1068 (gnus-read-ephemeral-emacs-bug-group
1069 (cons id (if (listp merged)
1070 merged
1071 (list merged)))
1072 (cons (current-buffer)
1073 (current-window-configuration)))
1074 (with-current-buffer (window-buffer (selected-window))
1075 (set (make-local-variable 'debbugs-gnu-bug-number) id)
1076 (set (make-local-variable 'debbugs-gnu-subject)
1077 (format "Re: bug#%d: %s" id (cdr (assq 'subject status))))
1078 (debbugs-gnu-summary-mode 1))))
1079
1080 (defvar debbugs-gnu-summary-mode-map
1081 (let ((map (make-sparse-keymap)))
1082 (define-key map "C" 'debbugs-gnu-send-control-message)
1083 (define-key map [(meta m)] 'debbugs-gnu-apply-patch)
1084 map))
1085
1086 (defvar gnus-posting-styles)
1087
1088 (define-minor-mode debbugs-gnu-summary-mode
1089 "Minor mode for providing a debbugs interface in Gnus summary buffers.
1090
1091 \\{debbugs-gnu-summary-mode-map}"
1092 :lighter " Debbugs" :keymap debbugs-gnu-summary-mode-map
1093 (set (make-local-variable 'gnus-posting-styles)
1094 `((".*"
1095 (eval
1096 (when (buffer-live-p gnus-article-copy)
1097 (with-current-buffer gnus-article-copy
1098 (set (make-local-variable 'message-prune-recipient-rules)
1099 '((".*@debbugs.*" "emacs-pretest-bug")
1100 (".*@debbugs.*" "bug-gnu-emacs")
1101 ("[0-9]+@debbugs.*" "submit@debbugs.gnu.org")
1102 ("[0-9]+@debbugs.*" "quiet@debbugs.gnu.org")))
1103 (set (make-local-variable 'message-alter-recipients-function)
1104 (lambda (address)
1105 (if (string-match "\\([0-9]+\\)@donarmstrong"
1106 (car address))
1107 (let ((new (format "%s@debbugs.gnu.org"
1108 (match-string 1 (car address)))))
1109 (cons new new))
1110 address)))
1111 ;; `gnus-posting-styles' is eval'ed after
1112 ;; `message-simplify-subject'. So we cannot use m-s-s.
1113 (setq subject ,debbugs-gnu-subject))))))))
1114
1115 (defun debbugs-gnu-guess-current-id ()
1116 "Guess the ID based on \"#23\"."
1117 (save-excursion
1118 (beginning-of-line)
1119 (and
1120 (or (re-search-forward "#\\([0-9]+\\)" (line-end-position) t)
1121 (progn
1122 (goto-char (point-min))
1123 (re-search-forward "#\\([0-9]+\\)" nil t)))
1124 (string-to-number (match-string 1)))))
1125
1126 (defun debbugs-gnu-send-control-message (message &optional reverse)
1127 "Send a control message for the current bug report.
1128 You can set the severity or add a tag, or close the report. If
1129 you use the special \"done\" MESSAGE, the report will be marked as
1130 fixed, and then closed.
1131
1132 If given a prefix, and given a tag to set, the tag will be
1133 removed instead."
1134 (interactive
1135 (list (completing-read
1136 "Control message: "
1137 '("serious" "important" "normal" "minor" "wishlist"
1138 "done" "donenotabug" "donewontfix" "doneunreproducible"
1139 "unarchive" "unmerge" "reopen" "close"
1140 "merge" "forcemerge"
1141 "block" "unblock"
1142 "owner" "noowner"
1143 "invalid"
1144 "reassign"
1145 "patch" "wontfix" "moreinfo" "unreproducible" "fixed" "notabug"
1146 "pending" "help" "security" "confirmed"
1147 "usertag")
1148 nil t)
1149 current-prefix-arg))
1150 (let* ((id (or debbugs-gnu-bug-number ; Set on group entry.
1151 (debbugs-gnu-guess-current-id)
1152 (debbugs-gnu-current-id)))
1153 (version
1154 (when (member message '("close" "done"))
1155 (read-string
1156 "Version: "
1157 (cond
1158 ;; Emacs development versions.
1159 ((string-match
1160 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\." emacs-version)
1161 (format "%s.%d"
1162 (match-string 1 emacs-version)
1163 (1+ (string-to-number (match-string 2 emacs-version)))))
1164 ;; Emacs release versions.
1165 ((string-match
1166 "^\\([0-9]+\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)$" emacs-version)
1167 (format "%s.%s"
1168 (match-string 1 emacs-version)
1169 (match-string 2 emacs-version)))
1170 (t emacs-version)))))
1171 (status (debbugs-gnu-current-status)))
1172 (with-temp-buffer
1173 (insert "To: control@debbugs.gnu.org\n"
1174 "From: " (message-make-from) "\n"
1175 (format "Subject: control message for bug #%d\n" id)
1176 "\n"
1177 (cond
1178 ((member message '("unarchive" "unmerge" "reopen" "noowner"))
1179 (format "%s %d\n" message id))
1180 ((member message '("merge" "forcemerge"))
1181 (format "%s %d %s\n" message id
1182 (read-string "Merge with bug #: ")))
1183 ((member message '("block" "unblock"))
1184 (format
1185 "%s %d by %s\n" message id
1186 (mapconcat
1187 'identity
1188 (completing-read-multiple
1189 (format "%s with bug(s) #: " (capitalize message))
1190 (if (equal message "unblock")
1191 (mapcar 'number-to-string
1192 (cdr (assq 'blockedby status))))
1193 nil (and (equal message "unblock") status))
1194 " ")))
1195 ((equal message "owner")
1196 (format "owner %d !\n" id))
1197 ((equal message "reassign")
1198 (format "reassign %d %s\n" id (read-string "Package(s): ")))
1199 ((equal message "close")
1200 (format "close %d %s\n" id version))
1201 ((equal message "done")
1202 (format "tags %d fixed\nclose %d %s\n" id id version))
1203 ((member message '("donenotabug" "donewontfix"
1204 "doneunreproducible"))
1205 (format "tags %d %s\nclose %d\n" id (substring message 4) id))
1206 ((member message '("serious" "important" "normal"
1207 "minor" "wishlist"))
1208 (format "severity %d %s\n" id message))
1209 ((equal message "invalid")
1210 (format "tags %d notabug\ntags %d wontfix\nclose %d\n"
1211 id id id))
1212 ((equal message "usertag")
1213 (format "user %s\nusertag %d %s\n"
1214 (completing-read
1215 "Package name or email address: "
1216 (append
1217 debbugs-gnu-all-packages (list user-mail-address))
1218 nil nil (car debbugs-gnu-default-packages))
1219 id (read-string "User tag: ")))
1220 (t
1221 (format "tags %d%s %s\n"
1222 id (if reverse " -" "")
1223 message))))
1224 (funcall send-mail-function))))
1225
1226 (defvar debbugs-gnu-usertags-mode-map
1227 (let ((map (make-sparse-keymap)))
1228 (set-keymap-parent map tabulated-list-mode-map)
1229 (define-key map "\r" 'debbugs-gnu-select-usertag)
1230 (define-key map [mouse-1] 'debbugs-gnu-select-usertag)
1231 (define-key map [mouse-2] 'debbugs-gnu-select-usertag)
1232 map))
1233
1234 (define-derived-mode debbugs-gnu-usertags-mode tabulated-list-mode "Usertags"
1235 "Major mode for listing user tags.
1236
1237 All normal editing commands are switched off.
1238 \\<debbugs-gnu-usertags-mode-map>
1239
1240 The following commands are available:
1241
1242 \\{debbugs-gnu-usertags-mode-map}"
1243 (buffer-disable-undo)
1244 (setq truncate-lines t)
1245 (setq buffer-read-only t))
1246
1247 ;;;###autoload
1248 (defun debbugs-gnu-usertags (&rest users)
1249 "List all user tags for USERS, which is \(\"emacs\"\) by default."
1250 (interactive
1251 (if current-prefix-arg
1252 (completing-read-multiple
1253 "Package name(s) or email address: "
1254 (append debbugs-gnu-all-packages (list user-mail-address)) nil nil
1255 (mapconcat 'identity debbugs-gnu-default-packages ","))
1256 debbugs-gnu-default-packages))
1257
1258 (unwind-protect
1259 (let ((inhibit-read-only t)
1260 (debbugs-port "gnu.org")
1261 (buffer-name "*Emacs User Tags*")
1262 (user-tab-length
1263 (1+ (apply 'max (length "User") (mapcar 'length users)))))
1264
1265 ;; Initialize variables.
1266 (when (and (file-exists-p debbugs-gnu-persistency-file)
1267 (not debbugs-gnu-local-tags))
1268 (with-temp-buffer
1269 (insert-file-contents debbugs-gnu-persistency-file)
1270 (eval (read (current-buffer)))))
1271
1272 ;; Create buffer.
1273 (when (get-buffer buffer-name)
1274 (kill-buffer buffer-name))
1275 (pop-to-buffer (get-buffer-create buffer-name))
1276 (debbugs-gnu-usertags-mode)
1277 (setq tabulated-list-format `[("User" ,user-tab-length t)
1278 ("Tag" 10 t)])
1279 (setq tabulated-list-sort-key (cons "User" nil))
1280 ;(setq tabulated-list-printer 'debbugs-gnu-print-entry)
1281 (erase-buffer)
1282
1283 ;; Retrieve user tags.
1284 (dolist (user users)
1285 (dolist (tag (sort (debbugs-get-usertag :user user) 'string<))
1286 (add-to-list
1287 'tabulated-list-entries
1288 ;; `tabulated-list-id' is the parameter list for `debbugs-gnu'.
1289 `((("tagged") (,user) nil nil (,tag))
1290 ,(vector (propertize user 'mouse-face widget-mouse-face)
1291 (propertize tag 'mouse-face widget-mouse-face)))
1292 'append)))
1293
1294 ;; Add local tags.
1295 (when debbugs-gnu-local-tags
1296 (add-to-list
1297 'tabulated-list-entries
1298 `((("tagged"))
1299 ,(vector "" (propertize "(local tags)"
1300 'mouse-face widget-mouse-face)))))
1301
1302 ;; Show them.
1303 (tabulated-list-init-header)
1304 (tabulated-list-print)
1305
1306 (set-buffer-modified-p nil)
1307 (goto-char (point-min)))))
1308
1309 (defun debbugs-gnu-select-usertag ()
1310 "Select the user tag on the current line."
1311 (interactive)
1312 ;; We open the bug reports.
1313 (let ((args (get-text-property (line-beginning-position) 'tabulated-list-id)))
1314 (when args (apply 'debbugs-gnu args))))
1315
1316 ;;;###autoload
1317 (defun debbugs-gnu-bugs (&rest bugs)
1318 "List all BUGS, a list of bug numbers."
1319 (interactive
1320 (mapcar 'string-to-number
1321 (completing-read-multiple "Bug numbers: " nil 'natnump)))
1322 (dolist (elt bugs)
1323 (unless (natnump elt) (signal 'wrong-type-argument (list 'natnump elt))))
1324 (add-to-list 'debbugs-gnu-current-query (cons 'bugs bugs))
1325 (debbugs-gnu nil))
1326
1327 (defvar debbugs-gnu-trunk-directory "~/src/emacs/trunk/"
1328 "The directory where the main source tree lives.")
1329
1330 (defvar debbugs-gnu-branch-directory "~/src/emacs/emacs-24/"
1331 "The directory where the previous source tree lives.")
1332
1333 (defun debbugs-gnu-apply-patch (&optional branch)
1334 "Apply the patch from the current message.
1335 If given a prefix, patch in the branch directory instead."
1336 (interactive "P")
1337 (add-hook 'emacs-lisp-mode-hook 'debbugs-gnu-lisp-mode)
1338 (add-hook 'diff-mode-hook 'debbugs-gnu-diff-mode)
1339 (add-hook 'change-log-mode-hook 'debbugs-gnu-change-mode)
1340 (let ((rej "/tmp/debbugs-gnu.rej")
1341 (output-buffer (get-buffer-create "*debbugs patch*"))
1342 (dir (if branch
1343 debbugs-gnu-branch-directory
1344 debbugs-gnu-trunk-directory))
1345 (patch-buffers nil))
1346 (when (file-exists-p rej)
1347 (delete-file rej))
1348 (with-current-buffer output-buffer
1349 (erase-buffer))
1350 (gnus-summary-select-article nil t)
1351 ;; The patches are either in MIME attachements or the main article
1352 ;; buffer. Determine which.
1353 (gnus-with-article-buffer
1354 (dolist (handle (mapcar 'cdr (gnus-article-mime-handles)))
1355 (when (string-match "diff\\|patch" (mm-handle-media-type handle))
1356 (push (mm-handle-buffer handle) patch-buffers))))
1357 (unless patch-buffers
1358 (gnus-summary-show-article 'raw)
1359 (article-decode-charset)
1360 (push (current-buffer) patch-buffers))
1361 (dolist (buffer patch-buffers)
1362 (with-current-buffer buffer
1363 (call-process-region (point-min) (point-max)
1364 "patch" nil output-buffer nil
1365 "-r" rej "--no-backup-if-mismatch"
1366 "-l" "-f"
1367 "-d" (expand-file-name dir)
1368 "-p1")))
1369 (set-buffer output-buffer)
1370 (when (file-exists-p rej)
1371 (goto-char (point-max))
1372 (insert-file-contents-literally rej))
1373 (goto-char (point-max))
1374 (save-some-buffers t)
1375 (require 'compile)
1376 (mapc 'kill-process compilation-in-progress)
1377 (compile (format "cd %s; make -k" (expand-file-name "lisp" dir)))
1378 (vc-dir dir)
1379 (vc-dir-hide-up-to-date)
1380 (goto-char (point-min))
1381 (sit-for 1)
1382 (vc-diff)
1383 ;; All these commands are asynchronous, so just wait a bit. This
1384 ;; should be done properly a different way.
1385 (sit-for 2)
1386 ;; We've now done everything, so arrange the windows we need to see.
1387 (delete-other-windows)
1388 (switch-to-buffer output-buffer)
1389 (split-window)
1390 (split-window)
1391 (other-window 1)
1392 (switch-to-buffer "*compilation*")
1393 (goto-char (point-max))
1394 (other-window 1)
1395 (switch-to-buffer "*vc-diff*")
1396 (goto-char (point-min))))
1397
1398 (defun debbugs-gnu-find-contributor (string)
1399 "Search through ChangeLogs to find contributors."
1400 (interactive "sContributor match: ")
1401 (let ((found 0)
1402 (match (concat "^[0-9].*" string)))
1403 (dolist (file (directory-files-recursively
1404 debbugs-gnu-trunk-directory "ChangeLog\\(.[0-9]+\\)?$"))
1405 (with-temp-buffer
1406 (when (file-exists-p file)
1407 (insert-file-contents file))
1408 (goto-char (point-min))
1409 (while (and (re-search-forward match nil t)
1410 (not (looking-at ".*tiny change")))
1411 (cl-incf found))))
1412 (message "%s is a contributor %d times" string found)
1413 found))
1414
1415 (defun debbugs-gnu-insert-changelog ()
1416 "Add a ChangeLog from a recently applied patch from a third party."
1417 (interactive)
1418 (let (from subject)
1419 (gnus-with-article-buffer
1420 (widen)
1421 (goto-char (point-min))
1422 (setq from (mail-extract-address-components (gnus-fetch-field "from"))
1423 subject (gnus-fetch-field "subject")))
1424 (let ((add-log-full-name (car from))
1425 (add-log-mailing-address (cadr from)))
1426 (add-change-log-entry-other-window)
1427 (let ((point (point)))
1428 (when (string-match "\\(bug#[0-9]+\\)" subject)
1429 (insert " (" (match-string 1 subject) ")."))
1430 (when (zerop (debbugs-gnu-find-contributor
1431 (let ((bits (split-string (car from))))
1432 (cond
1433 ((>= (length bits) 2)
1434 (format "%s.*%s" (car bits) (car (last bits))))
1435 ((= (length bits) 1)
1436 (car bits))
1437 ;; Fall back on the email address.
1438 (t
1439 (cadr from))))))
1440 (goto-char (point-min))
1441 (end-of-line)
1442 (insert " (tiny change"))
1443 (goto-char point)))))
1444
1445 (defvar debbugs-gnu-lisp-mode-map
1446 (let ((map (make-sparse-keymap)))
1447 (define-key map [(meta m)] 'debbugs-gnu-insert-changelog)
1448 map))
1449
1450 (define-minor-mode debbugs-gnu-lisp-mode
1451 "Minor mode for providing a debbugs interface in Lisp buffers.
1452 \\{debbugs-gnu-lisp-mode-map}"
1453 :lighter " Debbugs" :keymap debbugs-gnu-lisp-mode-map)
1454
1455 (defvar debbugs-gnu-diff-mode-map
1456 (let ((map (make-sparse-keymap)))
1457 (define-key map [(meta m)] 'debbugs-gnu-diff-select)
1458 map))
1459
1460 (define-minor-mode debbugs-gnu-diff-mode
1461 "Minor mode for providing a debbugs interface in diff buffers.
1462 \\{debbugs-gnu-diff-mode-map}"
1463 :lighter " Debbugs" :keymap debbugs-gnu-diff-mode-map)
1464
1465 (defun debbugs-gnu-diff-select ()
1466 "Select the diff under point."
1467 (interactive)
1468 (delete-other-windows)
1469 (diff-goto-source))
1470
1471 (defvar debbugs-gnu-change-mode-map
1472 (let ((map (make-sparse-keymap)))
1473 (define-key map [(meta m)] 'debbugs-gnu-change-checkin)
1474 map))
1475
1476 (define-minor-mode debbugs-gnu-change-mode
1477 "Minor mode for providing a debbugs interface in ChangeLog buffers.
1478 \\{debbugs-gnu-change-mode-map}"
1479 :lighter " Debbugs" :keymap debbugs-gnu-change-mode-map)
1480
1481 (defun debbugs-gnu-change-checkin ()
1482 "Prepare checking in the current changes."
1483 (interactive)
1484 (save-some-buffers t)
1485 (when (get-buffer "*vc-dir*")
1486 (kill-buffer (get-buffer "*vc-dir*")))
1487 (vc-dir debbugs-gnu-trunk-directory)
1488 (goto-char (point-min))
1489 (while (not (search-forward "edited" nil t))
1490 (sit-for 0.01))
1491 (beginning-of-line)
1492 (while (search-forward "edited" nil t)
1493 (vc-dir-mark)
1494 (beginning-of-line))
1495 (vc-diff nil)
1496 (vc-next-action nil)
1497 (log-edit-insert-changelog t)
1498 (delete-other-windows)
1499 (split-window)
1500 (other-window 1)
1501 (switch-to-buffer "*vc-diff*")
1502 (other-window 1))
1503
1504 (provide 'debbugs-gnu)
1505
1506 ;;; TODO:
1507
1508 ;; * Reorganize pages after client-side filtering.
1509
1510 ;;; debbugs-gnu.el ends here