├── .gitignore ├── README.rst ├── jira-rest.el └── sample.jira-auth-info.el /.gitignore: -------------------------------------------------------------------------------- 1 | dev/ 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | jira-rest.el 3 | ============ 4 | 5 | An Emacs major mode for interfacing with JIRA's REST API 6 | ======================================================== 7 | 8 | **Update**: I no longer use Jira (and haven't for about a year now) so this project should be considered unmaintained. 9 | 10 | This project is the result of seeing the state of the original `jira.el `_. Atlassian's JIRA API docs `state quite clearly `_ that the REST API unveiled for JIRA 5.0 is the only version that will be receiving development efforts going forward. Unfortunately, ``jira.el`` uses the XML RPC. So in the interest of scratching my own itch I started work on this. 11 | 12 | Requires JIRA 5.0+ & `json.el `_. 13 | 14 | There seem to be several versions of JIRA's API docs floating around, but in my opinion `these `_ are the best. 15 | 16 | To Install & Run 17 | ---------------- 18 | 19 | 1. Place ``jira-rest.el`` somewhere on your load-path 20 | 2. Add ``(require 'jira-rest)`` to your .emacs/init.el file. 21 | 3. Put ``.jira-auth-info.el`` in your home directory, and set the variables for your authentication information & API endpoint URL. 22 | 4. ``M-x jira-rest-mode`` 23 | 24 | 25 | To Do's 26 | ------- 27 | 28 | High-priority tasks: 29 | ~~~~~~~~~~~~~~~~~~~~ 30 | 31 | * Add automated tests with ERT 32 | * Implement issue search 33 | * Adding/removing/editing comments 34 | * Modifying issues (as supported by the API) 35 | 36 | Obviously implementing functions to consume every possible API endpoint is a to-do, but priority will likely go to those that get the most use. 37 | 38 | 39 | Caveats 40 | ------- 41 | 42 | The capabilities of this mode are limited by what is exposed by JIRA's API. Some notable deficiencies include **changing issue status** and **resolving/closing issues**. These deficiencies overlap with the XML RPC API, unfortunately. The hope is, however, that these holes will be plugged since this API will be getting future development. 43 | 44 | 45 | Contributing 46 | ------------ 47 | 48 | If you would like to contribute, please submit pull requests with your changes/additions. As APIs are prone to undocumented changes and breakage, any new code will require test coverage. (This includes myself. I'll be retrofitting the original code soon.) 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /jira-rest.el: -------------------------------------------------------------------------------- 1 | ;;; jira-rest.el -- Interact with JIRA REST API. 2 | 3 | ;; Copyright (C) 2012 Matt DeBoard 4 | 5 | ;; This work is incorporates many concepts & code from, and is heavily 6 | ;; influenced by jira.el, the work of Brian Zwahr and Dave Benjamin: 7 | ;; http://emacswiki.org/emacs/JiraMode 8 | 9 | ;; Documentation of JIRA REST API can be found at URL: 10 | ;; https://developer.atlassian.com/display/JIRADEV/JIRA+REST+APIs 11 | 12 | ;;; Code: 13 | (require 'cl) 14 | (require 'json) 15 | (require 'url) 16 | 17 | ;; ******************************** 18 | ;; JIRA REST Mode - By Matt DeBoard 19 | ;; ******************************** 20 | 21 | (defgroup jira-rest nil 22 | "JIRA customization group." 23 | :group 'applications) 24 | 25 | (defgroup jira-rest-faces nil 26 | "Faces for displaying JIRA information." 27 | :group 'jira) 28 | 29 | (defvar jira-rest-auth-info nil 30 | "The auth header used to authenticate each request. Please 31 | see URL https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+Basic+AuthenticationConsists for more information.") 32 | 33 | (defun load-auth-info () 34 | (let ((jira-pwd-file (expand-file-name "~/.jira-auth-info.el"))) 35 | (if (file-regular-p jira-pwd-file) 36 | (load jira-pwd-file)))) 37 | 38 | (defun jira-rest-login () 39 | (if (load-auth-info) 40 | (let ((enc (base64-encode-string 41 | (concat jira-username ":" jira-password)))) 42 | (setq jira-rest-auth-info (concat "Basic " enc))) 43 | (message "You must provide your login information."))) 44 | 45 | (defcustom jira-rest-endpoint "" 46 | "The URL of the REST API endpoint for user's JIRA 47 | installation." 48 | :group 'jira-rest 49 | :type 'string 50 | :initialize 'custom-initialize-set) 51 | 52 | (defface jira-rest-issue-info-face 53 | '((t (:foreground "black" :background "yellow4"))) 54 | "Base face for issue information." 55 | :group 'jira-rest-faces) 56 | 57 | (defface jira-rest-issue-info-header-face 58 | '((t (:bold t :inherit 'jira-rest-issue-info-face))) 59 | "Base face for issue headers." 60 | :group 'jira-rest-faces) 61 | 62 | (defface jira-rest-issue-summary-face 63 | '((t (:bold t))) 64 | "Base face for issue summary." 65 | :group 'jira-rest-faces) 66 | 67 | (defface jira-rest-comment-face 68 | '((t (:background "gray23"))) 69 | "Base face for comments." 70 | :group 'jira-rest-faces) 71 | 72 | (defface jira-rest-comment-header-face 73 | '((t (:bold t))) 74 | "Base face for comment headers." 75 | :group 'jira-rest-faces) 76 | 77 | (defface jira-rest-link-issue-face 78 | '((t (:underline t))) 79 | "Face for linked issues." 80 | :group 'jira-rest-faces) 81 | 82 | (defface jira-rest-link-project-face 83 | '((t (:underline t))) 84 | "Face for linked projects" 85 | :group 'jira-rest-faces) 86 | 87 | (defface jira-rest-link-filter-face 88 | '((t (:underline t))) 89 | "Face for linked filters" 90 | :group 'jira-rest-faces) 91 | 92 | (defvar jira-rest-mode-hook nil) 93 | 94 | (defvar jira-rest-mode-map nil) 95 | 96 | (if jira-rest-mode-map 97 | nil 98 | (progn 99 | (setq jira-rest-mode-map (make-sparse-keymap)) 100 | (define-key jira-rest-mode-map "c" 'jira-rest-create-issue) 101 | (define-key jira-rest-mode-map "di" 'jira-rest-delete-issue) 102 | (define-key jira-rest-mode-map "a" 'jira-rest-change-assignee) 103 | (define-key jira-rest-mode-map "gg" 'jira-rest-get-watchers) 104 | (define-key jira-rest-mode-map "ga" 'jira-rest-add-watcher) 105 | (define-key jira-rest-mode-map "gr" 'jira-rest-remove-watcher) 106 | (define-key jira-rest-mode-map "\S-q" 'jira-rest-mode-quit))) 107 | 108 | (defun jira-rest-mode () 109 | "A mode for working with JIRA's JSON REST API. The full 110 | specification for the API can be found at URL 111 | https://developer.atlassian.com/display/JIRADEV/JIRA+REST+APIs 112 | 113 | Requires JIRA 5.0 or greater. 114 | 115 | \\{jira-rest-mode-map}" 116 | (interactive) 117 | (jira-rest-login) 118 | (if (or (equal jira-rest-endpoint nil) 119 | (equal jira-rest-endpoint "")) 120 | (message "jira-rest-endpoint not set! Please set this 121 | value in .jira-auth-info.el.") 122 | (progn 123 | (switch-to-buffer "*JIRA-REST*") 124 | (kill-all-local-variables) 125 | (setq major-mode 'jira-rest-mode) 126 | (setq mode-name "JIRA-REST") 127 | (use-local-map jira-rest-mode-map) 128 | (run-hooks 'jira-rest-mode-hook) 129 | ;; (jira-rest-store-projects) 130 | ;; (jira-rest-store-priorities) 131 | ;; (jira-rest-store-statuses) 132 | ;; (jira-rest-store-types) 133 | (insert "Welcome to jira-rest-mode!") 134 | (message "jira rest mode loaded!")))) 135 | 136 | (defvar jira-rest-current-issue nil 137 | "This holds the currently selected issue.") 138 | 139 | (defvar jira-rest-projects-list nil 140 | "This holds a list of projects and their details.") 141 | 142 | (defvar jira-rest-types nil 143 | "This holds a list of issues types.") 144 | 145 | (defvar jira-rest-statuses nil 146 | "This holds a list of statuses.") 147 | 148 | (defvar jira-rest-priorities nil 149 | "This holds a list of priorities.") 150 | 151 | (defvar jira-rest-user-fullnames nil 152 | "This holds a list of user fullnames.") 153 | 154 | (defvar response nil) 155 | 156 | (defun jira-rest-api-interact (method data &optional path) 157 | "Interact with the API using method 'method' and data 'data'. 158 | Optional arg 'path' may be provided to specify another location further 159 | down the URL structure to send the request." 160 | (if (not jira-rest-auth-info) 161 | (message "You must login first, 'M-x jira-rest-login'.") 162 | (let ((url-request-method method) 163 | (url-request-extra-headers 164 | `(("Content-Type" . "application/json") 165 | ("Authorization" . ,jira-rest-auth-info))) 166 | (url-request-data data) 167 | (target (concat jira-rest-endpoint path))) 168 | (with-current-buffer (current-buffer) 169 | (url-retrieve target 'my-switch-to-url-buffer `(,method)))))) 170 | 171 | (defun my-switch-to-url-buffer (status method) 172 | "Callback function to capture the contents of the response." 173 | (with-current-buffer (current-buffer) 174 | ;; Don't try to read the buffer if the method was DELETE, 175 | ;; since we won't get a response back. 176 | (if (not (equal method "DELETE")) 177 | (let ((data (buffer-substring (search-forward-regexp "^$") 178 | (point-max)))) 179 | (setq response (json-read-from-string data)))) 180 | (kill-buffer (current-buffer)))) 181 | 182 | (defun jira-rest-mode-quit () 183 | (interactive) 184 | (kill-buffer "*JIRA-REST*")) 185 | 186 | (defun id-or (s) 187 | "Return ':id' if 's' is a numeric string. Otherwise, return 188 | nil. The idea here is that the JIRA REST API spec allows the 'project' 189 | and 'issuetype' keys to be either 'id' or some other value (in the 190 | case of 'project', the other is 'key'; for 'issuetype', 'name'). This fn 191 | enables us to allow either type of user input." 192 | (if (not (equal 0 (string-to-number s))) 193 | "id")) 194 | 195 | (defun jira-rest-create-issue (project summary description issuetype) 196 | "File a new issue with JIRA." 197 | (interactive (list (read-string "Project Key: ") 198 | (read-string "Summary: ") 199 | (read-string "Description: ") 200 | (read-string "Issue Type: "))) 201 | (if (or (equal project "") 202 | (equal summary "") 203 | (equal description "") 204 | (equal issuetype "")) 205 | (message "Must provide all information!") 206 | (let ((field-hash (make-hash-table :test 'equal)) 207 | (issue-hash (make-hash-table :test 'equal)) 208 | (project-hash (make-hash-table :test 'equal)) 209 | (issuetype-hash (make-hash-table :test 'equal))) 210 | ;; Create the JSON string that will be passed to create the ticket. 211 | (progn 212 | ;; Populate our hashes, from bottom to top. The format for these 213 | ;; nested hash tables follow the format outlined in the JIRA REST 214 | ;; API documentation. 215 | (puthash (or (id-or project) "key") project project-hash) 216 | (puthash (or (id-or issuetype) "name") issuetype issuetype-hash) 217 | (puthash "project" project-hash issue-hash) 218 | (puthash "issuetype" issuetype-hash issue-hash) 219 | (puthash "summary" summary issue-hash) 220 | (puthash "description" description issue-hash) 221 | (puthash "fields" issue-hash field-hash) 222 | ;; Return the JSON-encoded hash map. 223 | (jira-rest-api-interact "POST" (json-encode field-hash)) 224 | response)))) 225 | 226 | (defun jira-rest-delete-issue (k) 227 | "Delete an issue with unique identifier 'k'. 'k' is either an 228 | issueId or key." 229 | (interactive (list (read-string "Issue Key or ID: "))) 230 | (jira-rest-api-interact "DELETE" nil k)) 231 | 232 | (defun jira-rest-get-watchers (k) 233 | "Get all the watchers for an issue." 234 | (interactive (list (read-string "Issue Key or ID: "))) 235 | (jira-rest-api-interact "GET" nil (concat k "/watchers"))) 236 | 237 | (defun jira-rest-add-watcher (k name) 238 | "Add a watcher to an issue." 239 | (interactive (list (read-string "Issue Key or ID: ") 240 | (read-string "Username to Add as Watcher: "))) 241 | (jira-rest-api-interact "POST" (json-encode name) (concat k "/watchers"))) 242 | 243 | (defun jira-rest-remove-watcher (k name) 244 | "Remove a watcher from an issue." 245 | (interactive (list (read-string "Issue Key or ID: ") 246 | (read-string "Username to Remove as Watcher: "))) 247 | (jira-rest-api-interact "DELETE" nil (concat k "/watchers?" name))) 248 | 249 | (defun jira-rest-change-assignee (k &optional name) 250 | "Change the assignee for an issue." 251 | (interactive (list (read-string "Issue Key or ID: ") 252 | (read-string "New Assignee: "))) 253 | (let ((name-hash (make-hash-table :test 'equal))) 254 | (progn 255 | (puthash "name" name name-hash) 256 | (jira-rest-api-interact "PUT" (json-encode name-hash) 257 | (concat k "/assignee"))))) 258 | 259 | (provide 'jira-rest) 260 | ;;; jira-rest.el ends here 261 | -------------------------------------------------------------------------------- /sample.jira-auth-info.el: -------------------------------------------------------------------------------- 1 | ;; Fill in these values with your JIRA auth/API information. 2 | ;; These values will be loaded and used by the main module. 3 | (setq jira-username nil 4 | jira-password nil 5 | jira-rest-endpoint "") 6 | 7 | --------------------------------------------------------------------------------