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