-;;; debbugs-gnu.el --- interface for the GNU bug tracker
+;;; debbugs-gnu.el --- interface for the GNU bug tracker -*- lexical-binding:t -*-
;; Copyright (C) 2011-2016 Free Software Foundation, Inc.
(require 'debbugs)
(require 'tabulated-list)
(require 'add-log)
-(require 'subr-x)
-(eval-when-compile (require 'cl))
+(eval-when-compile (require 'subr-x))
+(eval-when-compile (require 'cl-lib))
(autoload 'article-decode-charset "gnus-art")
(autoload 'diff-goto-source "diff-mode")
:group 'debbugs
:version "24.1")
-(defvar debbugs-gnu-blocking-report 19759
- "The ID of the current release report used to track blocking bug reports.")
-
(defcustom debbugs-gnu-default-severities '("serious" "important" "normal")
"*The list severities bugs are searched for.
\"tagged\" is not a severity but marks locally tagged bugs."
(const "tagged"))
:version "24.1")
+(defcustom debbugs-gnu-suppress-closed t
+ "If non-nil, don't show closed bugs."
+ :group 'debbugs-gnu
+ :type 'boolean
+ :version "25.1")
+
(defconst debbugs-gnu-all-severities
(mapcar 'cadr (cdr (get 'debbugs-gnu-default-severities 'custom-type)))
"*List of all possible severities.")
(const "guile")
(const "guix")
(const "gzip")
+ (const "hyperbole")
(const "idutils")
(const "libtool")
(const "mh-e")
(defcustom debbugs-gnu-mail-backend 'gnus
"*The email backend to use for reading bug report email exchange.
-If this is 'gnus, the default, use Gnus.
-If this is 'rmail, use Rmail instead."
+If this is `gnus', the default, use Gnus.
+If this is `rmail', use Rmail instead."
:group 'debbugs-gnu
:type '(choice (const :tag "Use Gnus" 'gnus)
(const :tag "Use Rmail" 'rmail))
The specification which bugs shall be suppressed is taken from
`debbugs-gnu-default-suppress-bugs'.")
+(defcustom debbugs-gnu-emacs-current-release "25.1"
+ "The current Emacs relase developped for."
+ :group 'debbugs-gnu
+ :type '(set (const "24.5")
+ (const "25.1")
+ (const "25.2"))
+ :version "25.1")
+
+(defconst debbugs-gnu-blocking-reports
+ '(("24.5" . 19758)
+ ("25.1" . 19759)
+ ("25.2" . 21966))
+ "The IDs of the Emacs report used to track blocking bug reports.
+It is a list of cons cells, each one containing the Emacs
+version (a string) and the bug report number (a number).")
+
(defun debbugs-gnu-calendar-read (prompt acceptable &optional initial-contents)
"Return a string read from the minibuffer.
Derived from `calendar-read'."
(setq phrase nil)
(add-to-list 'debbugs-gnu-current-query (cons 'phrase phrase)))
;; We suppress closed bugs if there is no phrase.
- (setq debbugs-gnu-current-suppress (null phrase))
+ (setq debbugs-gnu-current-suppress
+ (if (not debbugs-gnu-suppress-closed)
+ nil
+ (null phrase)))
;; The other queries.
(catch :finished
(setq key (completing-read
"Enter attribute: "
(if phrase
- '("severity" "package" "tags" "submitter" "date"
- "subject" "status")
- '("severity" "package" "archive" "src" "tag"
- "owner" "submitter" "maint" "correspondent"
- "date" "log_modified" "last_modified"
- "found_date" "fixed_date" "unarchived"
- "subject" "done" "forwarded" "msgid" "summary"))
+ (append
+ '("severity" "package" "tags"
+ "author" "date" "subject")
+ ;; Client-side filters.
+ (mapcar
+ (lambda (key)
+ (propertize
+ key 'face 'debbugs-gnu-done
+ 'help-echo "Client-side filter"))
+ '("status")))
+ (append
+ '("severity" "package" "archive" "src" "status" "tag"
+ "owner" "submitter" "maint" "correspondent")
+ ;; Client-side filters.
+ (mapcar
+ (lambda (key)
+ (propertize
+ key 'face 'debbugs-gnu-done
+ 'help-echo "Client-side filter"))
+ '("date" "log_modified" "last_modified"
+ "found_date" "fixed_date" "unarchived"
+ "subject" "done" "forwarded" "msgid" "summary"))))
nil t))
(cond
;; Server-side queries.
(add-to-list
'debbugs-gnu-current-query (cons (intern key) val1))))
- ((member key '("owner" "submitter" "maint" "correspondent"))
+ ((member
+ key '("author" "owner" "submitter" "maint" "correspondent"))
(setq val1 (read-string "Enter email address: "))
(when (not (zerop (length val1)))
(add-to-list
- 'debbugs-gnu-current-query (cons (intern key) val1))))
+ 'debbugs-gnu-current-query
+ (cons (intern (if (equal key "author") "@author" key)) val1))))
+ ;; Client-side filters.
((equal key "status")
(setq
val1
- (completing-read "Enter status: " '("done" "forwarded" "open")))
+ (completing-read
+ (format "Enter status%s: "
+ (if (null phrase) "" " (client-side filter)"))
+ '("open" "forwarded" "done") nil t))
(when (not (zerop (length val1)))
- (add-to-list
- 'debbugs-gnu-current-query (cons (intern key) val1))))
+ (if (null phrase)
+ (add-to-list
+ 'debbugs-gnu-current-query (cons (intern key) val1))
+ (add-to-list
+ 'debbugs-gnu-current-filter (cons 'pending val1)))))
- ;; Client-side filters.
((member key '("date" "log_modified" "last_modified"
"found_date" "fixed_date" "unarchived"))
(setq val1
'debbugs-gnu-current-query 'debbugs-gnu-current-filter)
(cons (intern key) (cons val1 val2)))))
+ ;; "subject", "done", "forwarded", "msgid", "summary".
((not (zerop (length key)))
(setq val1
(funcall
(if phrase 'read-string 'read-regexp)
- (format "Enter %s%s"
- key (if phrase ": " " (client-side filter)"))))
+ (format "Enter %s%s: "
+ key (if phrase "" " (client-side filter)"))))
(when (not (zerop (length val1)))
(add-to-list
(if phrase
;; Do the search.
(debbugs-gnu severities packages archivedp))))
+;;;###autoload
+(defun debbugs-gnu-patches ()
+ "List the bug reports that have been marked as containing a patch."
+ (interactive)
+ (debbugs-gnu nil debbugs-gnu-default-packages nil nil "patch"))
+
;;;###autoload
(defun debbugs-gnu (severities &optional packages archivedp suppress tags)
"List all outstanding bugs."
(when (member "tagged" severities)
(split-string (read-string "User tag(s): ") "," t)))))
- ;; Initialize variables.
- (when (and (file-exists-p debbugs-gnu-persistency-file)
- (not debbugs-gnu-local-tags))
- (with-temp-buffer
- (insert-file-contents debbugs-gnu-persistency-file)
- (eval (read (current-buffer)))))
- ;; Per default, we suppress retrieved unwanted bugs.
- (when (called-interactively-p 'any)
- (setq debbugs-gnu-current-suppress t))
-
- ;; Add queries.
- (dolist (severity (if (consp severities) severities (list severities)))
- (when (not (zerop (length severity)))
- (when (string-equal severity "tagged")
- (setq debbugs-gnu-current-suppress nil))
- (add-to-list 'debbugs-gnu-current-query (cons 'severity severity))))
- (dolist (package (if (consp packages) packages (list packages)))
- (when (not (zerop (length package)))
- (add-to-list 'debbugs-gnu-current-query (cons 'package package))))
- (when archivedp
- (setq debbugs-gnu-current-suppress nil)
- (add-to-list 'debbugs-gnu-current-query '(archive . "1")))
- (when suppress
- (setq debbugs-gnu-current-suppress t)
- (add-to-list 'debbugs-gnu-current-query '(status . "open"))
- (add-to-list 'debbugs-gnu-current-query '(status . "forwarded")))
- (dolist (tag (if (consp tags) tags (list tags)))
- (when (not (zerop (length tag)))
- (add-to-list 'debbugs-gnu-current-query (cons 'tag tag))))
-
- ;; Show result.
- (debbugs-gnu-show-reports)
-
- ;; Reset query, filter and suppress.
- (setq debbugs-gnu-current-query nil
- debbugs-gnu-current-filter nil
- debbugs-gnu-current-suppress nil))
+ (unwind-protect
+ (progn
+ ;; Initialize variables.
+ (when (and (file-exists-p debbugs-gnu-persistency-file)
+ (not debbugs-gnu-local-tags))
+ (with-temp-buffer
+ (insert-file-contents debbugs-gnu-persistency-file)
+ (eval (read (current-buffer)))))
+ ;; Per default, we suppress retrieved unwanted bugs.
+ (when (and (called-interactively-p 'any)
+ debbugs-gnu-suppress-closed)
+ (setq debbugs-gnu-current-suppress t))
+
+ ;; Add queries.
+ (dolist (severity (if (consp severities) severities (list severities)))
+ (when (not (zerop (length severity)))
+ (when (string-equal severity "tagged")
+ (setq debbugs-gnu-current-suppress nil))
+ (add-to-list 'debbugs-gnu-current-query (cons 'severity severity))))
+ (dolist (package (if (consp packages) packages (list packages)))
+ (when (not (zerop (length package)))
+ (add-to-list 'debbugs-gnu-current-query (cons 'package package))))
+ (when archivedp
+ (setq debbugs-gnu-current-suppress nil)
+ (add-to-list 'debbugs-gnu-current-query '(archive . "1")))
+ (when suppress
+ (setq debbugs-gnu-current-suppress t)
+ (add-to-list 'debbugs-gnu-current-query '(status . "open"))
+ (add-to-list 'debbugs-gnu-current-query '(status . "forwarded")))
+ (dolist (tag (if (consp tags) tags (list tags)))
+ (when (not (zerop (length tag)))
+ (add-to-list 'debbugs-gnu-current-query (cons 'tag tag))))
+
+ ;; Show result.
+ (debbugs-gnu-show-reports))
+
+ ;; Reset query, filter and suppress.
+ (setq debbugs-gnu-current-query nil
+ debbugs-gnu-current-filter nil
+ debbugs-gnu-current-suppress nil)))
(defun debbugs-gnu-get-bugs (query)
"Retrieve bug numbers from debbugs.gnu.org according search criteria."
(let* ((debbugs-port "gnu.org")
(bugs (assoc 'bugs query))
- (tags (assoc 'tag query))
+ (tags (and (member '(severity . "tagged") query) (assoc 'tag query)))
(local-tags (and (member '(severity . "tagged") query) (not tags)))
(phrase (assoc 'phrase query))
args)
"Show bug reports.
If OFFLINE is non-nil, the query is not sent to the server. Bugs
are taken from the cache instead."
- (let ((inhibit-read-only t)
- (buffer-name "*Emacs Bugs*"))
+ (let* ((inhibit-read-only t)
+ string
+ (buffer-name
+ (cond
+ ((setq string (cdr (assq 'phrase debbugs-gnu-current-query)))
+ (format "*%S Bugs*" string))
+ ((setq string (cdr (assq 'package debbugs-gnu-current-query)))
+ (format "*%s Bugs*" (capitalize string)))
+ (t "*Bugs*"))))
;; The tabulated mode sets several local variables. We must get
;; rid of them.
(when (get-buffer buffer-name)
merged)
(unless (equal (cdr (assq 'pending status)) "pending")
(setq words (concat words "," (cdr (assq 'pending status)))))
- (let ((packages (delete "emacs" (cdr (assq 'package status)))))
+ (let ((packages (cdr (assq 'package status))))
+ (dolist (elt packages)
+ (when (member elt debbugs-gnu-default-packages)
+ (setq packages (delete elt packages))))
(when packages
(setq words (concat words "," (mapconcat 'identity packages ",")))))
(when (setq merged (cdr (assq 'mergedwith status)))
'debbugs-gnu-done)
((member "pending" (cdr (assq 'keywords status)))
'debbugs-gnu-pending)
- ((= (cdr (assq 'date status))
- (cdr (assq 'log_modified status)))
+ ;; For some new bugs `date' and `log_modified' may
+ ;; differ in 1 second.
+ ((< (abs (- (cdr (assq 'date status))
+ (cdr (assq 'log_modified status))))
+ 3)
'debbugs-gnu-new)
((< (- (float-time)
(cdr (assq 'log_modified status)))
(submitter (aref cols 2))
(submitter-length (nth 1 (aref tabulated-list-format 2)))
(title (aref cols 3))
- (title-length (nth 1 (aref tabulated-list-format 3))))
+ ;; (title-length (nth 1 (aref tabulated-list-format 3)))
+ )
(when (and
;; We may have a narrowing in effect.
(or (not debbugs-gnu-limit)
;; Package "emacs" has been selected.
(member '(package . "emacs") debbugs-gnu-local-query)))
+(defun debbugs-gnu-manual ()
+ "Display the Debbugs manual in Info mode."
+ (interactive)
+ (info "debbugs-ug"))
+
(defconst debbugs-gnu-bug-triage-file
(expand-file-name "../admin/notes/bug-triage" data-directory)
"The \"bug-triage\" file.")
(define-key-after menu-map [debbugs-gnu-separator2]
'(menu-item "--") 'debbugs-gnu-bugs)
+ (define-key-after menu-map [debbugs-gnu-manual]
+ '(menu-item "Debbugs Manual" debbugs-gnu-manual
+ :help "Show Debbugs Manual")
+ 'debbugs-gnu-separator2)
(define-key-after menu-map [debbugs-gnu-view-bug-triage]
'(menu-item "Describe Bug Triage Procedure"
debbugs-gnu-view-bug-triage
:enable (debbugs-gnu-menu-map-bug-triage-enabled)
:help "Show procedure of triaging bugs")
- 'debbugs-gnu-separator2)
+ 'debbugs-gnu-manual)
map))
(defun debbugs-gnu-rescan ()
t)
(t nil))))
-(defun debbugs-gnu-sort-title (s1 s2)
+(defun debbugs-gnu-sort-title (s1 _s2)
(let ((owner (if (cdr (assq 'owner (car s1)))
(car (mail-header-parse-address
(decode-coding-string (cdr (assq 'owner (car s1)))
(defun debbugs-gnu-show-all-blocking-reports ()
"Narrow the display to just the reports that are blocking a release."
(interactive)
- (let ((blockers (cdr (assq 'blockedby
- (car (debbugs-get-status
- debbugs-gnu-blocking-report)))))
+ (let ((blockers
+ (cdr
+ (assq
+ 'blockedby
+ (car
+ (debbugs-get-status
+ (cdr
+ (assoc
+ debbugs-gnu-emacs-current-release
+ debbugs-gnu-blocking-reports)))))))
(id (debbugs-gnu-current-id t))
(inhibit-read-only t)
status)
(defvar debbugs-gnu-send-mail-function nil
"A function to send control messages from debbugs.")
+(defvar debbugs-gnu-completion-table
+ (completion-table-dynamic
+ (lambda (string)
+ (if (string-equal string "")
+ (mapcar
+ (lambda (x)
+ (list (format "%d" x) x))
+ '(1 2 3 4 5 6 7 8 9))
+ (let ((newest-bug (car (debbugs-newest-bugs 1))))
+ (and (string-match "^[1-9][0-9]*$" string)
+ (<= (string-to-number string) newest-bug)
+ (append
+ `(,string)
+ (mapcar
+ (lambda (x)
+ (let ((y (format "%s%d" string x)))
+ (and (<= (string-to-number y) newest-bug)
+ (list y x))))
+ '(0 1 2 3 4 5 6 7 8 9))))))))
+ "Dynamic completion table for reading bug numbers.")
+
(defun debbugs-gnu-send-control-message (message &optional reverse)
"Send a control message for the current bug report.
You can set the severity or add a tag, or close the report. If
"usertag")
nil t)
current-prefix-arg))
- (let* ((id (or debbugs-gnu-bug-number ; Set on group entry.
- (debbugs-gnu-guess-current-id)
- (debbugs-gnu-current-id)))
+ (let* ((id (or (debbugs-gnu-current-id t)
+ debbugs-gnu-bug-number ; Set on group entry.
+ (debbugs-gnu-guess-current-id)))
+ (status (debbugs-gnu-current-status))
(version
- (when (member message '("close" "done"))
+ (when (and
+ (member message '("close" "done"))
+ (member "emacs" (cdr (assq 'package status))))
(read-string
"Version: "
(cond
(format "%s.%s"
(match-string 1 emacs-version)
(match-string 2 emacs-version)))
- (t emacs-version)))))
- (status (debbugs-gnu-current-status)))
+ (t emacs-version))))))
(with-temp-buffer
(insert "To: control@debbugs.gnu.org\n"
"From: " (message-make-from) "\n"
((member message '("unarchive" "unmerge" "reopen" "noowner"))
(format "%s %d\n" message id))
((member message '("merge" "forcemerge"))
- (format "%s %d %s\n" message id
- (read-string "Merge with bug #: ")))
+ (format
+ "%s %d %s\n" message id
+ (mapconcat
+ 'identity
+ (completing-read-multiple
+ (format "%s with bug(s) #: " (capitalize message))
+ debbugs-gnu-completion-table)
+ " ")))
((member message '("block" "unblock"))
(format
"%s %d by %s\n" message id
(format "%s with bug(s) #: " (capitalize message))
(if (equal message "unblock")
(mapcar 'number-to-string
- (cdr (assq 'blockedby status))))
+ (cdr (assq 'blockedby status)))
+ debbugs-gnu-completion-table)
nil (and (equal message "unblock") status))
" ")))
((equal message "owner")
((equal message "reassign")
(format "reassign %d %s\n" id (read-string "Package(s): ")))
((equal message "close")
- (format "close %d %s\n" id version))
+ (format "close %d %s\n" id (or version "")))
((equal message "done")
- (format "tags %d fixed\nclose %d %s\n" id id version))
+ (format "tags %d fixed\nclose %d %s\n" id id (or version "")))
((member message '("donenotabug" "donewontfix"
"doneunreproducible"))
(format "tags %d %s\nclose %d\n" id (substring message 4) id))
(defun debbugs-gnu-bugs (&rest bugs)
"List all BUGS, a list of bug numbers."
(interactive
- (mapcar 'string-to-number
- (completing-read-multiple "Bug numbers: " nil 'natnump)))
+ (mapcar
+ 'string-to-number
+ (completing-read-multiple "Bug numbers: " debbugs-gnu-completion-table)))
(dolist (elt bugs)
(unless (natnump elt) (signal 'wrong-type-argument (list 'natnump elt))))
(add-to-list 'debbugs-gnu-current-query (cons 'bugs bugs))
(message "%s is a contributor %d times" string found)
found))
+(defvar debbugs-gnu-patch-subject nil)
+
(defun debbugs-gnu-insert-changelog ()
"Add a ChangeLog from a recently applied patch from a third party."
(interactive)
- (let (from subject)
+ (let (from subject patch-subject changelog)
(gnus-with-article-buffer
(widen)
(goto-char (point-min))
(setq from (mail-extract-address-components (gnus-fetch-field "from"))
- subject (gnus-fetch-field "subject")))
+ subject (gnus-fetch-field "subject"))
+ ;; If it's a patch formatted the right way, extract that data.
+ (dolist (handle (mapcar 'cdr (gnus-article-mime-handles)))
+ (when (string-match "diff\\|patch\\|plain"
+ (mm-handle-media-type handle))
+ (with-temp-buffer
+ (insert-buffer-substring (mm-handle-buffer handle))
+ (cond ((eq (mm-handle-encoding handle) 'base64)
+ (base64-decode-region (point-min) (point-max)))
+ ((eq (mm-handle-encoding handle) 'quoted-printable)
+ (quoted-printable-decode-region (point-min) (point-max))))
+ (setq patch-subject
+ (or (gnus-fetch-field "subject") patch-subject))
+ (goto-char (point-min))
+ (when (re-search-forward "^[*] " nil t)
+ (let ((start (match-beginning 0)))
+ (while (and (not (eobp))
+ (not (looking-at "---")))
+ (forward-line 1))
+ (setq changelog (buffer-substring
+ start (line-end-position 0)))))))))
(let ((add-log-full-name (car from))
(add-log-mailing-address (cadr from)))
(add-change-log-entry-other-window)
+ (when patch-subject
+ (setq-local debbugs-gnu-patch-subject patch-subject))
+ (when changelog
+ (delete-region (line-beginning-position) (point-max))
+ (save-restriction
+ (narrow-to-region (point) (point))
+ (insert changelog)
+ (indent-region (point-min) (point-max))))
(let ((point (point)))
(when (string-match "\\(bug#[0-9]+\\)" subject)
(insert " (" (match-string 1 subject) ")."))
(cadr from))))))
(goto-char (point-max))
(end-of-line)
+ (when changelog
+ (insert "\n\n"))
(insert " Copyright-paperwork-exempt: yes"))
(goto-char point)))))
"Prepare checking in the current changes."
(interactive)
(save-some-buffers t)
- (when (get-buffer "*vc-dir*")
- (kill-buffer (get-buffer "*vc-dir*")))
- (let ((trunk (expand-file-name debbugs-gnu-trunk-directory)))
- (if (equal (cl-subseq default-directory 0 (length trunk))
- trunk)
- (vc-dir debbugs-gnu-trunk-directory)
- (vc-dir debbugs-gnu-branch-directory)))
- (goto-char (point-min))
- (while (not (search-forward "edited" nil t))
- (sit-for 0.01))
- (beginning-of-line)
- (while (search-forward "edited" nil t)
- (vc-dir-mark)
- (beginning-of-line))
- (vc-diff nil)
- (vc-next-action nil)
- (log-edit-insert-changelog t)
- (delete-other-windows)
- (split-window)
- (other-window 1)
- (switch-to-buffer "*vc-diff*")
- (other-window 1))
+ (when (get-buffer "*vc-dir*")
+ (kill-buffer (get-buffer "*vc-dir*")))
+ (let ((patch-subject debbugs-gnu-patch-subject))
+ (let ((trunk (expand-file-name debbugs-gnu-trunk-directory)))
+ (if (equal (cl-subseq default-directory 0 (length trunk))
+ trunk)
+ (vc-dir debbugs-gnu-trunk-directory)
+ (vc-dir debbugs-gnu-branch-directory)))
+ (goto-char (point-min))
+ (while (not (search-forward "edited" nil t))
+ (sit-for 0.01))
+ (beginning-of-line)
+ (while (search-forward "edited" nil t)
+ (vc-dir-mark)
+ (beginning-of-line))
+ (vc-diff nil)
+ (vc-next-action nil)
+ (delete-region (point-min) (point-max))
+ (log-edit-insert-changelog t)
+ (delete-other-windows)
+ (split-window)
+ (other-window 1)
+ (switch-to-buffer "*vc-diff*")
+ (other-window 1)
+ (when patch-subject
+ (insert "Summary: "
+ (replace-regexp-in-string "^ *\\[PATCH\\] *" "" patch-subject)
+ "\n"))))
(defun debbugs-gnu-save-cache ()
"Save the bugs cache to a file."