commit 3353a7c082ac57098ce434fd4961d022dfc9be9b Author: Correl Roush Date: Mon Apr 4 14:15:47 2016 -0400 Initial commit diff --git a/Cask b/Cask new file mode 100644 index 0000000..0051bc0 --- /dev/null +++ b/Cask @@ -0,0 +1,10 @@ +;; -*- mode: emacs-lisp -*- +(source melpa) + +(package "jira-api" "0.0.1" "JIRA REST API") + +(depends-on "dash") + +(development + (depends-on "f") + (depends-on "ert-runner")) diff --git a/jira-api.el b/jira-api.el new file mode 100644 index 0000000..d66c986 --- /dev/null +++ b/jira-api.el @@ -0,0 +1,199 @@ +;;; JIRA-API -- JIRA REST API + +;; Copyright (c) 2015 Correl Roush + +;; Author: Correl Roush +;; Version: 0.1 +;; Created: 2015-06-18 + +;; This file is NOT part of GNU Emacs. + +;;; License: + +;; This program 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 3, or (at your option) +;; any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; 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: + +;;; Code: + +(require 'dash) +(require 'url) +(require 'json) + +(defcustom jira-api-host "jira.company.com" + "JIRA Hostname" + :group 'jira-api + :type 'string) + +(defcustom jira-api-user "" + "JIRA username" + :group 'jira-api + :type 'string) + +(defcustom jira-api-use-ssl t + "Use HTTPS" + :group 'jira-api + :type 'boolean) + +(defcustom jira-api-org-story-points-field "StoryPoints" + "Org story points property" + :group 'jira-api + :type 'string) + +(defcustom jira-api-org-time-estimate-field "Effort" + "Org time estimate field" + :group 'jira-api + :type 'string) + +(defvar jira-api-agile-sprint-field 'customfield_10300 + "JIRA Agile sprint field") + +(defvar jira-api-agile-story-points-field 'customfield_10003 + "JIRA Agile story points field") + +(defconst jira-api-timetracking-units + '(("m" . 60) ; 60 minutes in an hour + ("h" . 8) ; 8 hours in a day + ("d" . 5) ; 5 days in a week + ("w" . nil))) + +(defun jira-api-seconds-to-duration (seconds) + (let ((minutes (ceiling seconds 60))) + (mapconcat (lambda (pair) (format "%d%s" (car pair) (cdr pair))) + (--filter (not (zerop (car it))) + (-zip + (mapcar #'cdr (--reduce-from + (let ((remainder (or (caar acc) minutes))) + (cons (cons (floor remainder it) + (if it + (% remainder it) + remainder)) + acc)) + (list) + (mapcar #'cdr jira-api-timetracking-units))) + (reverse (mapcar #'car jira-api-timetracking-units)))) + " "))) + +(defun jira-api--get-credentials () + (let ((info (nth 0 (auth-source-search :host jira-api-host + :port (if jira-api-use-ssl 443 80) + :require '(:user :secret) + :create t)))) + (if info + (let ((user (plist-get info :user)) + (secret (plist-get info :secret))) + (cons user + (if (functionp secret) + (funcall secret) + secret)))))) + +(defun jira-api-post (endpoint &optional data) + (let ((url-request-method "POST")) + (jira-api-get endpoint data))) + +(defun jira-api-get (endpoint &optional data) + (let* ((url-request-method (or url-request-method "GET")) + (url-request-data + (json-encode data)) + (credentials (jira-api--get-credentials)) + (jira-api-user (car credentials)) + (jira-api-password (cdr credentials)) + (url-request-extra-headers + `(("Authorization" . ,(concat "Basic " + (base64-encode-string (concat jira-api-user ":" jira-api-password)))) + ("Content-Type" . "application/json"))) + (protocol (if jira-api-use-ssl "https" "http")) + (result-buffer (url-retrieve-synchronously (concat protocol "://" jira-api-host endpoint)))) + (when result-buffer + (with-current-buffer result-buffer + (goto-char (point-min)) + (while (not (looking-at "^$")) + (forward-line)) + (let ((json-object-type 'alist) + (json-array-type 'list) + (json-key-type 'symbol)) + (json-read)))))) + +(defun jira-api-get-issue (issue-id) + (jira-api-get (concat "/rest/api/latest/issue/" + issue-id + "?expand=names"))) + +(defun jira-api-get-attribute (issue &rest names) + (-reduce-from (lambda (acc value) + (cdr (assoc value acc))) + issue + names)) + +(defun jira-api-parse-sprint-attribute (attribute-array) + (let* ((attribute-string (elt attribute-array 0)) + (sprint-matches (if (s-starts-with? "[" attribute-string) + (cdr (s-match-strings-all "\\[\\(.*?\\)\\]" attribute-string)) + (s-match-strings-all "\\[\\(.*?\\)\\]" attribute-string))) + (sprint-strings (mapcar #'cadr sprint-matches)) + (sprint-attributes (-tree-map (lambda (s) (let* ((pair (split-string s "="))) + (cons (intern (car pair)) (cadr pair)))) + (-map (lambda (s) (split-string s ",")) sprint-strings)))) + sprint-attributes)) + +(defun jira-api-get-issue-sprints (issue) + (jira-api-parse-sprint-attribute + (jira-api-get-attribute issue + 'fields jira-api-agile-sprint-field))) + + +(defun jira-api-agile-get-sprints (board-id) + (jira-api-get (format "/rest/greenhopper/experimental-api/latest/board/%d/sprint" + board-id))) + +(defun jira-api-get-issue-story-points (issue) + (round + (jira-api-get-attribute issue + 'fields jira-api-agile-story-points-field))) + +(defun jira-api-get-issue-original-estimate (issue) + (let ((seconds (jira-api-get-attribute issue + 'fields 'timetracking 'originalEstimateSeconds))) + (ceiling (/ seconds 60)))) + +(defun jira-api-get-worklog (issue-id) + (jira-api-get (concat "/rest/api/latest/issue/" + issue-id + "/worklog"))) + +(defun jira-api-org-update-entry () + (let* ((jira-id (org-entry-get (point) "JIRA_ID")) + (jira-issue (jira-api-get-issue jira-id))) + (org-set-property + "StoryPoints" + (number-to-string (jira-api-get-issue-story-points jira-issue))) + (org-set-property + "Effort" + (let ((jira-estimate + (jira-api-get-issue-original-estimate jira-issue))) + (format "%d:%02d" + (floor (/ jira-estimate 60)) + (% jira-estimate 60)))))) + +(defun jira-api-log-work (issue-id seconds) + (jira-api-post + (concat "/rest/api/latest/issue/" + issue-id + "/worklog") + `((timeSpentSeconds . ,seconds)))) + +(provide 'org-jira-rest) +;;; jira-api.el ends here