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