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