]> code.delx.au - gnu-emacs-elpa/blob - packages/debbugs/debbugs-org.el
Merge commit '2c5d608ddfeb2dc1acc15d645d94cac087f001d4'
[gnu-emacs-elpa] / packages / debbugs / debbugs-org.el
1 ;;; debbugs-org.el --- Org-mode interface for the GNU bug tracker
2
3 ;; Copyright (C) 2013-2016 Free Software Foundation, Inc.
4
5 ;; Author: Michael Albinus <michael.albinus@gmx.org>
6 ;; Keywords: comm, hypermedia, maint, outlines
7 ;; Package: debbugs
8
9 ;; This file is not part of GNU Emacs.
10
11 ;; This program is free software: you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
15
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23
24 ;;; Commentary:
25
26 ;; This package provides an interface to bug reports which are located
27 ;; on the GNU bug tracker debbugs.gnu.org. Its main purpose is to
28 ;; show and manipulate bug reports as org-mode TODO items.
29
30 ;; If you have `debbugs-org.el' in your load-path, you could enable
31 ;; the bug tracker commands by the following lines in your ~/.emacs
32 ;;
33 ;; (autoload 'debbugs-org "debbugs-org" "" 'interactive)
34 ;; (autoload 'debbugs-org-search "debbugs-org" "" 'interactive)
35 ;; (autoload 'debbugs-org-bugs "debbugs-org" "" 'interactive)
36
37 ;; The bug tracker is called interactively by
38 ;;
39 ;; M-x debbugs-org
40
41 ;; It asks for the severities, for which bugs shall be shown. This can
42 ;; be either just one severity, or a list of severities, separated by
43 ;; comma. Valid severities are "serious", "important", "normal",
44 ;; "minor" or "wishlist". Severities "critical" and "grave" are not
45 ;; used, although configured on the GNU bug tracker. If no severity
46 ;; is given, all bugs are selected.
47
48 ;; There is also the pseudo severity "tagged". When it is used, the
49 ;; function will ask for user tags (a comma separated list), and shows
50 ;; just the bugs which are tagged with them. In general, user tags
51 ;; shall be strings denoting to subprojects of the package, like
52 ;; "cedet" or "tramp" of the package "emacs". If no user tag is
53 ;; given, locally tagged bugs are shown.
54
55 ;; If a prefix is given to the command, more search parameters are
56 ;; asked for, like packages (also a comma separated list, "emacs" is
57 ;; the default), whether archived bugs shall be shown, and whether
58 ;; closed bugs shall be shown.
59
60 ;; Another command is
61 ;;
62 ;; M-x debbugs-org-search
63
64 ;; It behaves like `debbugs-org', but asks at the beginning for a
65 ;; search phrase to be used for full text search. Additionally, it
66 ;; asks for key-value pairs to filter bugs. Keys are as described in
67 ;; `debbugs-get-status', the corresponding value must be a regular
68 ;; expression to match for. The other parameters are as described in
69 ;; `debbugs-org'.
70
71 ;; The bug reports are downloaded from the bug tracker. In order to
72 ;; not generate too much load of the server, up to 500 bugs will be
73 ;; downloaded at once. If there are more hits, you will be asked to
74 ;; change this limit, but please don't increase this number too much.
75
76 ;; These default values could be changed also by customer options
77 ;; `debbugs-gnu-default-severities', `debbugs-gnu-default-packages'
78 ;; and `debbugs-gnu-default-hits-per-page'.
79
80 ;; The commands create a TODO list. Besides the usual handling of
81 ;; TODO items, you could apply the following actions by the following
82 ;; keystrokes:
83
84 ;; "C-c # C": Send a debbugs control message
85 ;; "C-c # t": Mark the bug locally as tagged
86 ;; "C-c # d": Show bug attributes
87
88 ;; The last entry in a TODO record is the link [[Messages]]. If you
89 ;; follow this link, a Gnus ephemeral group is opened presenting all
90 ;; related messages for this bug. Here you could also send debbugs
91 ;; control messages by keystroke "C".
92
93 ;; Finally, if you simply want to list some bugs with known bug
94 ;; numbers, call the command
95 ;;
96 ;; M-x debbugs-org-bugs
97
98 ;; The bug numbers to be shown shall be entered as comma separated list.
99
100 ;;; Code:
101
102 (require 'debbugs-gnu)
103 (require 'org)
104 (eval-when-compile (require 'cl))
105
106 (defconst debbugs-org-severity-priority
107 (let ((priority ?A))
108 (mapcar
109 (lambda (x) (prog1 (cons x (char-to-string priority)) (incf priority)))
110 debbugs-gnu-all-severities))
111 "Mapping of debbugs severities to TODO priorities.")
112
113 (defun debbugs-org-get-severity-priority (state)
114 "Returns the TODO priority of STATE."
115 (or (cdr (assoc (cdr (assq 'severity state))
116 debbugs-org-severity-priority))
117 (cdr (assoc "minor" debbugs-org-severity-priority))))
118
119 (defconst debbugs-org-priority-faces
120 '(("A" . org-warning)
121 ("B" . org-warning))
122 "Highlighting of prioritized TODO items.")
123
124 ;; We do not add the bug numbers list to the elisp:link, because this
125 ;; would be much too long. Instead, this variable shall keep the bug
126 ;; numbers.
127 (defvar-local debbugs-org-ids nil
128 "The list of bug ids to be shown following the elisp link.")
129
130 (defvar debbugs-org-show-buffer-name "*Org Bugs*"
131 "The buffer name we present the bug reports.
132 This could be a temporary buffer, or a buffer linked with a file.")
133
134 (defvar debbugs-org-mode) ;; Silence compiler.
135 (defun debbugs-org-show-buffer-name ()
136 "The buffer name we present the bug reports.
137 This could be a temporary buffer, or a buffer linked with a file."
138 (if debbugs-org-mode (buffer-name) debbugs-org-show-buffer-name))
139
140 ;;;###autoload
141 (defun debbugs-org-search ()
142 "Search for bugs interactively.
143 Search arguments are requested interactively. The \"search
144 phrase\" is used for full text search in the bugs database.
145 Further key-value pairs are requested until an empty key is
146 returned."
147 (interactive)
148
149 (unwind-protect
150 ;; Check for the phrase.
151 (let ((phrase (read-string debbugs-gnu-phrase-prompt))
152 key val1 severities packages)
153
154 (add-to-list 'debbugs-gnu-current-query (cons 'phrase phrase))
155
156 ;; The other queries.
157 (catch :finished
158 (while t
159 (setq key (completing-read
160 "Enter attribute: "
161 '("severity" "package" "tags" "submitter" "author"
162 "subject" "status")
163 nil t))
164 (cond
165 ;; Server-side queries.
166 ((equal key "severity")
167 (setq
168 severities
169 (completing-read-multiple
170 "Enter severities: " debbugs-gnu-all-severities nil t
171 (mapconcat 'identity debbugs-gnu-default-severities ","))))
172
173 ((equal key "package")
174 (setq
175 packages
176 (completing-read-multiple
177 "Enter packages: " debbugs-gnu-all-packages nil t
178 (mapconcat 'identity debbugs-gnu-default-packages ","))))
179
180 ((member key '("tags" "subject"))
181 (setq val1 (read-string (format "Enter %s: " key)))
182 (when (not (zerop (length val1)))
183 (add-to-list
184 'debbugs-gnu-current-query (cons (intern key) val1))))
185
186 ((member key '("submitter" "author"))
187 (when (equal key "author") (setq key "@author"))
188 (setq val1 (read-string "Enter email address: "))
189 (when (not (zerop (length val1)))
190 (add-to-list
191 'debbugs-gnu-current-query (cons (intern key) val1))))
192
193 ((equal key "status")
194 (setq
195 val1
196 (completing-read "Enter status: " '("done" "forwarded" "open")))
197 (when (not (zerop (length val1)))
198 (add-to-list
199 'debbugs-gnu-current-query (cons (intern key) val1))))
200
201 ;; The End.
202 (t (throw :finished nil)))))
203
204 ;; Do the search.
205 (debbugs-org severities packages))
206
207 ;; Reset query and filter.
208 (setq debbugs-gnu-current-query nil)))
209
210 ;;;###autoload
211 (defun debbugs-org (severities &optional packages archivedp suppress tags)
212 "List all outstanding bugs."
213 (interactive
214 (let (severities archivedp)
215 (list
216 (setq severities
217 (completing-read-multiple
218 "Severities: " debbugs-gnu-all-severities nil t
219 (mapconcat 'identity debbugs-gnu-default-severities ",")))
220 ;; The next parameters are asked only when there is a prefix.
221 (if current-prefix-arg
222 (completing-read-multiple
223 "Packages: " debbugs-gnu-all-packages nil t
224 (mapconcat 'identity debbugs-gnu-default-packages ","))
225 debbugs-gnu-default-packages)
226 (when current-prefix-arg
227 (setq archivedp (y-or-n-p "Show archived bugs?")))
228 (when (and current-prefix-arg (not archivedp))
229 (y-or-n-p "Suppress unwanted bugs?"))
230 ;; This one must be asked for severity "tagged".
231 (when (member "tagged" severities)
232 (split-string (read-string "User tag(s): ") "," t)))))
233
234 ;; Initialize variables.
235 (when (and (file-exists-p debbugs-gnu-persistency-file)
236 (not debbugs-gnu-local-tags))
237 (with-temp-buffer
238 (insert-file-contents debbugs-gnu-persistency-file)
239 (eval (read (current-buffer)))))
240
241 ;; Add queries.
242 (dolist (severity (if (consp severities) severities (list severities)))
243 (when (not (zerop (length severity)))
244 (add-to-list 'debbugs-gnu-current-query (cons 'severity severity))))
245 (dolist (package (if (consp packages) packages (list packages)))
246 (when (not (zerop (length package)))
247 (add-to-list 'debbugs-gnu-current-query (cons 'package package))))
248 (when archivedp
249 (add-to-list 'debbugs-gnu-current-query '(archive . "1")))
250 (when suppress
251 (add-to-list 'debbugs-gnu-current-query '(status . "open"))
252 (add-to-list 'debbugs-gnu-current-query '(status . "forwarded")))
253 (dolist (tag (if (consp tags) tags (list tags)))
254 (when (not (zerop (length tag)))
255 (add-to-list 'debbugs-gnu-current-query (cons 'tag tag))))
256
257 (unwind-protect
258 (with-current-buffer (get-buffer-create (debbugs-org-show-buffer-name))
259 (erase-buffer)
260
261 (let ((hits debbugs-gnu-default-hits-per-page))
262 (setq debbugs-org-ids
263 (debbugs-gnu-get-bugs debbugs-gnu-current-query))
264
265 (when (> (length debbugs-org-ids) hits)
266 (let ((cursor-in-echo-area nil))
267 (setq hits
268 (string-to-number
269 (read-string
270 (format
271 "How many reports (available %d, default %d): "
272 (length debbugs-org-ids) hits)
273 nil
274 nil
275 (number-to-string hits))))))
276
277 (debbugs-org-show-next-reports hits)))
278
279 ;; Reset query.
280 (setq debbugs-gnu-current-query nil)))
281
282 (defun debbugs-org-show-reports (bug-numbers)
283 "Show bug reports as given in BUG-NUMBERS."
284 (pop-to-buffer (get-buffer-create (debbugs-org-show-buffer-name)))
285 ;; Local variable `debbugs-org-ids' must survive.
286 (let ((doi debbugs-org-ids))
287 (org-mode)
288 (debbugs-org-mode 1)
289 (setq debbugs-org-ids doi))
290
291 (let ((inhibit-read-only t)
292 (debbugs-port "gnu.org"))
293 (dolist (status
294 (sort
295 (apply 'debbugs-get-status bug-numbers)
296 (lambda (x y) (< (cdr (assq 'id x)) (cdr (assq 'id y))))))
297 (let* ((beg (point))
298 (id (cdr (assq 'id status)))
299 (done (string-equal (cdr (assq 'pending status)) "done"))
300 (priority (debbugs-org-get-severity-priority status))
301 (archived (cdr (assq 'archived status)))
302 (tags (append (cdr (assq 'found_versions status))
303 (cdr (assq 'tags status))))
304 (subject (when (cdr (assq 'subject status))
305 (decode-coding-string
306 (cdr (assq 'subject status)) 'utf-8)))
307 (date (cdr (assq 'date status)))
308 (last-modified (cdr (assq 'last_modified status)))
309 (originator (when (cdr (assq 'originator status))
310 (decode-coding-string
311 (cdr (assq 'originator status)) 'utf-8)))
312 (owner (when (cdr (assq 'owner status))
313 (decode-coding-string (cdr (assq 'owner status)) 'utf-8)))
314 (closed-by (when (cdr (assq 'done status))
315 (decode-coding-string
316 (cdr (assq 'done status)) 'utf-8)))
317 (merged (cdr (assq 'mergedwith status))))
318
319 ;; Handle tags.
320 (when (string-match "^\\([0-9.]+\\); \\(.+\\)$" subject)
321 (let ((x (match-string 1 subject))) (pushnew x tags :test #'equal))
322 (setq subject (match-string 2 subject)))
323 (when archived
324 (pushnew "ARCHIVE" tags :test #'equal))
325 (setq tags
326 (mapcar
327 ;; Replace all invalid TAG characters by "_".
328 (lambda (x) (replace-regexp-in-string "[^A-Za-z0-9_@]" "_" x))
329 tags))
330
331 ;; Headline.
332 (insert
333 (format
334 "* %s [#%s] %s %s\n"
335 (if done "DONE" "TODO")
336 priority subject
337 (if tags (mapconcat 'identity (append '("") tags '("")) ":") "")))
338
339 ;; Submitted.
340 (when date
341 (insert
342 (format-time-string
343 " [%Y-%m-%d %a] Submitted\n" (seconds-to-time date))))
344
345 ;; Properties.
346 (insert " :PROPERTIES:\n")
347 (insert (format " :DEBBUGS_ID: %s\n" id))
348 (when merged
349 (insert
350 (format
351 " :MERGED_WITH: %s\n"
352 (if (numberp merged)
353 merged (mapconcat 'number-to-string merged " ")))))
354 (insert (format " :CREATOR: %s\n" originator))
355 (when owner (insert (format " :OWNER: %s\n" owner)))
356 (when closed-by (insert (format " :CLOSED_BY: %s\n" closed-by)))
357 (insert " :END:\n")
358
359 ;; Messages.
360 (insert
361 " [[elisp:(debbugs-gnu-select-report)][Messages]]\n")
362
363 ;; Last modified.
364 (when last-modified
365 (insert
366 (format-time-string
367 " [%Y-%m-%d %a] Last modified\n"
368 (seconds-to-time last-modified))))
369
370 ;; Add text properties.
371 (add-text-properties beg (point) `(tabulated-list-id ,status))))))
372
373 (defun debbugs-org-regenerate-status ()
374 "Regenerate the `tabulated-list-id' text property.
375 This property is used when following the [Messages] link, so you
376 need to regenerate it when opening an .org file after you killed
377 the corresponding buffer (e.g. by closing Emacs)."
378 (save-excursion
379 (goto-char (point-min))
380 (while (re-search-forward ":DEBBUGS_ID:[ \t]*\\([0-9]+\\)" nil t)
381 (let* ((bugnum (string-to-number (match-string 1)))
382 (mw (org-entry-get (point) "MERGEDWIDTH"))
383 (tli (list (cons 'id bugnum)
384 (cons 'bug_num bugnum)
385 (cons 'mergedwidth (if mw (string-to-number mw)))))
386 (beg (org-back-to-heading t))
387 (end (org-end-of-subtree t)))
388 (add-text-properties beg end `(tabulated-list-id ,tli))))))
389
390 (defun debbugs-org-show-next-reports (hits)
391 "Show next HITS of bug reports."
392 (with-current-buffer (get-buffer-create (debbugs-org-show-buffer-name))
393 (save-excursion
394 (goto-char (point-max))
395 (when (re-search-backward
396 "^* COMMENT \\[\\[elisp:(debbugs-org-show-next-reports" nil t)
397 (forward-line -1)
398 (delete-region (point) (point-max)))
399 (debbugs-org-show-reports
400 (butlast debbugs-org-ids (- (length debbugs-org-ids) hits)))
401 (setq debbugs-org-ids
402 (last debbugs-org-ids (- (length debbugs-org-ids) hits)))
403 (goto-char (point-max))
404 (when debbugs-org-ids
405 (insert
406 (format
407 "* COMMENT [[elisp:(debbugs-org-show-next-reports %s)][Next bugs]]\n\n"
408 hits)))
409 (insert "* COMMENT Local " "Variables\n")
410 (when debbugs-org-ids
411 (insert "#+NAME: init\n"
412 "#+BEGIN_SRC elisp\n"
413 (format "(setq debbugs-org-ids '%s)\n" debbugs-org-ids)
414 "#+END_SRC\n\n"))
415 (insert "# Local " "Variables:\n"
416 "# mode: org\n"
417 "# eval: (debbugs-org-mode 1)\n")
418 (when debbugs-org-ids
419 (insert (format "# eval: (%s \"init\")\n"
420 (if (macrop 'org-sbe) "org-sbe" "sbe"))))
421 (insert "# End:\n")
422 (goto-char (point-min))
423 (org-overview)
424 (set-buffer-modified-p nil))))
425
426 (defconst debbugs-org-mode-map
427 (let ((map (make-sparse-keymap)))
428 (define-key map (kbd "C-c # t") 'debbugs-gnu-toggle-tag)
429 (define-key map (kbd "C-c # C") 'debbugs-gnu-send-control-message)
430 (define-key map (kbd "C-c # d") 'debbugs-gnu-display-status)
431 map)
432 "Keymap for the `debbugs-org-mode' minor mode.")
433
434 ;; Make byte-compiler quiet.
435 (defvar gnus-posting-styles)
436
437 ;;;###autoload
438 (define-minor-mode debbugs-org-mode
439 "Minor mode for providing a debbugs interface in org-mode buffers.
440
441 \\{debbugs-org-mode-map}"
442 :lighter " Debbugs" :keymap debbugs-org-mode-map
443 ;; FIXME: Does not show any effect.
444 (set (make-local-variable 'org-priority-faces) debbugs-org-priority-faces)
445 (set (make-local-variable 'gnus-posting-styles)
446 `((".*"
447 (eval
448 (when (buffer-live-p gnus-article-copy)
449 (with-current-buffer gnus-article-copy
450 (set (make-local-variable 'message-prune-recipient-rules)
451 '((".*@debbugs.*" "emacs-pretest-bug")
452 (".*@debbugs.*" "bug-gnu-emacs")
453 ("[0-9]+@debbugs.*" "submit@debbugs.gnu.org")
454 ("[0-9]+@debbugs.*" "quiet@debbugs.gnu.org")))
455 ;; `gnus-posting-styles' is eval'ed after
456 ;; `message-simplify-subject'. So we cannot use m-s-s.
457 (setq subject ,debbugs-gnu-subject)))))))
458 (debbugs-org-regenerate-status))
459
460 ;;;###autoload
461 (defun debbugs-org-bugs (&rest bugs)
462 "List all BUGS, a list of bug numbers."
463 (interactive
464 (mapcar 'string-to-number
465 (completing-read-multiple "Bug numbers: " nil 'natnump)))
466 (dolist (elt bugs)
467 (unless (natnump elt) (signal 'wrong-type-argument (list 'natnump elt))))
468 (add-to-list 'debbugs-gnu-current-query (cons 'bugs bugs))
469 (debbugs-org nil))
470
471 ;; TODO
472
473 ;; - Refactor it in order to avoid code duplication with debbugs-gnu.el.
474 ;; - Make headline customizable.
475 ;; - Sort according to different TODO properties.
476
477 (provide 'debbugs-org)