dotfiles / emacs.d / jira.el

;;; jira.el --- Connect to JIRA issue tracking software

;; Copyright (C) 2007  Dave Benjamin

;; Author: Dave Benjamin <>
;; Version: 0.2.1

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; Modify the variable `jira-url' to indicate the XML-RPC URL for your
;; JIRA installation.

;; Use `M-x jira-login RET' to log in and `M-x jira-logout RET' to log out.
;; Or, just run a command and you'll be prompted for your username and
;; password the first time.

;; The following commands can be used interactively:

;; M-x jira-list-projects
;; M-x jira-list-filters
;; M-x jira-list-issues
;; M-x jira-search-issues
;; M-x jira-search-project-issues
;; M-x jira-show-issue
;; M-x jira-send-region-as-comment

;; All of the XML-RPC API is wrapped, though not all of the API is exposed
;; via interactive functions. For API details, see:


;;; Code:

(require 'cl)
(require 'xml-rpc)

(defvar jira-url ""
  "URL to JIRA XML-RPC server")

(defvar jira-token nil
  "JIRA token used for authentication")

(defun jira-login (username password)
  "Logs the user into JIRA."
  (interactive (list (read-string "Username: ")
                     (read-passwd "Password: ")))
  (setq jira-token (jira-call-noauth 'jira1.login username password)))

(defun jira-logout ()
  "Logs the user out of JIRA"
  (jira-call 'jira1.logout)
  (setq jira-token nil))

(defun jira-list-projects ()
  "Displays a list of all available JIRA projects"
  (let ((projects (jira-get-projects)))
     (insert (number-to-string (length projects)) " JIRA projects found:\n\n")
     (dolist (project projects)
       (insert (format "%-12s %s\n"
                       (cdr (assoc "key" project))
                       (cdr (assoc "name" project))))))))

(defun jira-list-filters ()
  "Displays a list of all saved JIRA filters"
  (let ((filters (jira-get-saved-filters)))
     (insert (number-to-string (length filters)) " JIRA filters found:\n\n")
     (dolist (filter filters)
       (insert (format "%-8s %s\n"
                       (cdr (assoc "id" filter))
                       (cdr (assoc "name" filter))))))))

(defun jira-list-issues ()
  "Displays a list of issues matching a filter"
  (let ((filter-id))
    (setq filter-id
          (let ((filter-alist (jira-get-filter-alist)))
            (cdr (assoc (completing-read "Filter: " filter-alist nil t)
    (when filter-id
      (let ((filter (jira-get-filter filter-id))
            (issues (jira-get-issues-from-filter filter-id)))
         (insert "Filter:\n" (cdr (assoc "name" filter))
                 " (" (cdr (assoc "id" filter)) ")\n\n")
         (when (cdr (assoc "description" filter))
           (insert "Description:\n")
           (let ((start (point)))
             (insert (cdr (assoc "description" filter)) "\n\n")
             (fill-region start (point))))
         (jira-display-issues issues))))))

(defun jira-search-issues (text)
  "Displays a list of issues maching a fulltext search"
  (interactive "sSearch: ")
  (let ((issues (jira-get-issues-from-text-search text)))
     (insert "Search: " text "\n\n")
     (jira-display-issues issues))))

(defun jira-search-project-issues (project text max-results)
  "Displays a list of issues within a project matching a fulltext search"
   (let ((project-keys
          (mapcar (lambda (project)
                    (cdr (assoc "key" project)))
      (completing-read "Project Key: " project-keys nil t)
      (read-string "Search: ")
      (read-number "Max Results: " 20))))
  (let ((issues (jira-get-issues-from-text-search-with-project
                 (list project) (if (equal text "") " " text) max-results)))
     (insert "Project Key: " project "\n"
             "Search: " text "\n"
             "Max Results: " (number-to-string max-results) "\n\n")
     (jira-display-issues issues))))

(defun jira-show-issue (issue-key)
  "Displays details about a particular issue."
  (interactive "sIssue Key: ")
  (let ((issue (jira-get-issue issue-key))
        (comments (jira-get-comments issue-key)))
     (setq truncate-lines nil)
     (insert "JIRA issue details for " issue-key ":\n\n")
     (dolist (pair issue)
       (unless (equal (car pair) "customFieldValues")
         (insert (format "%16s %s\n"
                         (car pair)
                         (jira-strip-cr (format "%s" (cdr pair)))))))
     (when comments
       (insert "\nComments:\n")
       (dolist (comment comments)
         (insert "\n"
                 (cdr (assoc "author" comment)) " "
                 (cdr (assoc "created" comment)) "\n")
         (insert (jira-strip-cr (cdr (assoc "body" comment))) "\n"))))))

(defun jira-send-region-as-comment (start end issue-key)
  "Send the currently selected region as an issue comment"
  (interactive "r\nsIssue Key: ")
  (jira-add-comment issue-key (buffer-substring start end)))

(defun jira-get-filter (filter-id)
  "Returns a filter given its filter ID."
  (flet ((id-match (filter)
                   (equal filter-id (cdr (assoc "id" filter)))))
    (find-if 'id-match (jira-get-saved-filters))))

(defun jira-get-filter-alist ()
  "Returns an association list mapping filter names to IDs"
  (mapcar (lambda (filter)
            (cons (cdr (assoc "name" filter))
                  (cdr (assoc "id" filter))))

(defun jira-get-status-abbrevs ()
  "Returns an association list of status IDs to abreviated names"
  (flet ((pair (status)
               (cons (cdr (assoc "id" status))
                     (substring (replace-regexp-in-string
                                 " *" "" (cdr (assoc "name" status)))
                                0 3))))
    (mapcar 'pair (jira-get-statuses))))

(defun jira-display-issues (issues)
  "Inserts a list of issues into the current buffer"
  (let ((status-abbrevs (jira-get-status-abbrevs))
    (insert (number-to-string (length issues))
            " JIRA issues found:\n")
    (dolist (issue issues)
      (let ((status (cdr (assoc "status" issue)))
            (priority (cdr (assoc "priority" issue))))
        (when (not (equal last-status status))
          (setq last-status status)
          (insert "\n"))
        (insert (format "%-16s %-10s %s %5s %s\n"
                        (cdr (assoc "key" issue))
                        (cdr (assoc "assignee" issue))
                        (cdr (assoc status status-abbrevs))
                        (if priority
                            (make-string (- 6 (string-to-number priority))
                        (cdr (assoc "summary" issue))))))))

(defun jira-add-comment (issue-key comment)
  "Adds a comment to an issue"
  (jira-call 'jira1.addComment issue-key comment))

(defun jira-create-issue (r-issue-struct)
  "Creates an issue in JIRA from a Hashtable object."
  (jira-call 'jira1.createIssue r-issue-struct))

(defun jira-get-comments (issue-key)
  "Returns all comments associated with the issue"
  (jira-call 'jira1.getComments issue-key))

(defun jira-get-components (project-key)
  "Returns all components available in the specified project"
  (jira-call 'jira1.getComponents project-key))

(defun jira-get-issue (issue-key)
  "Gets an issue from a given issue key."
  (jira-call 'jira1.getIssue issue-key))

(defun jira-get-issues-from-filter (filter-id)
  "Executes a saved filter"
  (jira-call 'jira1.getIssuesFromFilter filter-id))

(defun jira-get-issues-from-text-search (search-terms)
  "Find issues using a free text search"
  (jira-call 'jira1.getIssuesFromTextSearch search-terms))

(defun jira-get-issues-from-text-search-with-project
  (project-keys search-terms max-num-results)
  "Find issues using a free text search, limited to certain projects"
  (jira-call 'jira1.getIssuesFromTextSearchWithProject
             project-keys search-terms max-num-results))

(defun jira-get-issue-types ()
  "Returns all visible issue types in the system"
  (jira-call 'jira1.getIssueTypes))

(defun jira-get-priorities ()
  "Returns all priorities in the system"
  (jira-call 'jira1.getPriorities))

(defun jira-get-projects ()
  "Returns a list of projects available to the user"
  (jira-call 'jira1.getProjects))

(defun jira-get-resolutions ()
  "Returns all resolutions in the system"
  (jira-call 'jira1.getResolutions))

(defun jira-get-saved-filters ()
  "Gets all saved filters available for the currently logged in user"
  (jira-call 'jira1.getSavedFilters))

(defun jira-get-server-info ()
  "Returns the Server information such as baseUrl, version, edition, buildDate, buildNumber."
  (jira-call 'jira1.getServerInfo))

(defun jira-get-statuses ()
  "Returns all statuses in the system"
  (jira-call 'jira1.getStatuses))

(defun jira-get-sub-task-issue-types ()
  "Returns all visible subtask issue types in the system"
  (jira-call 'jira1.getSubTaskIssueTypes))

(defun jira-get-user (username)
  "Returns a user's information given a username"
  (jira-call 'jira1.getUser username))

(defun jira-get-versions (project-key)
  "Returns all versions available in the specified project"
  (jira-call 'jira1.getVersions project-key))

(defun jira-update-issue (issue-key field-values)
  "Updates an issue in JIRA from a Hashtable object."
  (jira-call 'jira1.updateIssue issue-key field-values))

(defun jira-ensure-token ()
  "Makes sure that a JIRA token has been set, logging in if necessary."
  (unless jira-token
    (jira-login (read-string "Username: ")
                (read-passwd "Password: "))))

(defun jira-call (method &rest params)
  "Calls an XML-RPC method on the JIRA server (low-level)"
  (apply 'jira-call-noauth method jira-token params))

(defun jira-call-noauth (method &rest params)
  "Calls an XML-RPC method on the JIRA server without authentication (low-level)"
  (let ((url-version "Exp")             ; hack due to status bug in xml-rpc.el
        (server-url jira-url))
    (apply 'xml-rpc-method-call server-url method params)))

(defun jira-strip-cr (string)
  "Removes carriage returns from a string"
  (when string (replace-regexp-in-string "\r" "" string)))

(defmacro jira-with-jira-buffer (&rest body)
  "Sends all output and buffer modifications to a temporary buffer."
  `(with-output-to-temp-buffer "*jira*"
     (with-current-buffer standard-output
       (setq truncate-lines t)

(provide 'jira)

;;; jira.el ends here