1234 lines
49 KiB
EmacsLisp
1234 lines
49 KiB
EmacsLisp
;;; jiralib.el -- Provide connectivity to JIRA SOAP/REST services.
|
||
|
||
;; Copyright (C) 2016,2017 Matthew Carter <m@ahungry.com>
|
||
;; Copyright (C) 2011 Bao Haojun
|
||
;; original Copyright (C) 2009 Alex Harsanyi
|
||
|
||
;; Also, used some code from jira.el, which use xml-rpc instead of soap.
|
||
;; Thus Copyright (C) for jira.el related code:
|
||
;; Brian Zwahr <echosa@gmail.com>
|
||
;; Dave Benjamin <dave@ramenlabs.com>
|
||
|
||
;; Authors:
|
||
;; Matthew Carter <m@ahungry.com>
|
||
;; Bao Haojun <baohaojun@gmail.com>
|
||
;; Alex Harsanyi <AlexHarsanyi@gmail.com>
|
||
|
||
;; Maintainer: Matthew Carter <m@ahungry.com>
|
||
;; Version: 3.0.0
|
||
;; Homepage: https://github.com/ahungry/org-jira
|
||
|
||
;; This file is not part of GNU Emacs.
|
||
|
||
;; 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 of the License, 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 this program. If not, see
|
||
;; <http://www.gnu.org/licenses/> or write to the Free Software
|
||
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||
;; 02110-1301, USA.
|
||
|
||
;; Author: Alexandru Harsanyi (AlexHarsanyi@gmail.com)
|
||
;; Created: December, 2009
|
||
;; Keywords: soap, web-services, jira
|
||
;; Homepage: http://code.google.com/p/emacs-soap-client
|
||
|
||
;;; Commentary:
|
||
|
||
;; This file provides a programatic interface to JIRA. It provides access to
|
||
;; JIRA from other programs, but no user level functionality.
|
||
|
||
;; Jira References:
|
||
|
||
;; Primary reference (on current Jira, only REST is supported):
|
||
;; https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis
|
||
|
||
;; Full API list reference:
|
||
;; https://docs.atlassian.com/jira/REST/cloud/
|
||
|
||
;; Legacy reference (unsupported and deprecated/unavailable):
|
||
;; http://confluence.atlassian.com/display/JIRA/Creating+a+SOAP+Client
|
||
|
||
;; JavaDoc for the Jira SOAP service
|
||
;; http://docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html
|
||
|
||
;;; News:
|
||
|
||
;;;; Changes since 2.6.3:
|
||
;; - Add worklog import filter and control variable for external worklogs.
|
||
;; - Add the worklog related endpoint/calls.
|
||
|
||
;;;; Changes since 2.1.0:
|
||
;; - Remove os_username / os_password manual http request as part of sign in process
|
||
;; This produces sysadmin level warnings on Jira when these are used under the latest Jira.
|
||
;; - Remove unused function jiralib-link-issue
|
||
;; - Bring version up to match org-jira version so they can share tag
|
||
|
||
;;;; Changes since 2.0.0:
|
||
;; - Allow issue type query by project
|
||
|
||
;;;; Changes since 0.0.0:
|
||
;; - Converted many calls to async
|
||
;; - Converted many calls to make use of caching
|
||
|
||
;;; Code:
|
||
|
||
(eval-when-compile (require 'cl))
|
||
(require 'soap-client)
|
||
(require 'request)
|
||
(require 'json)
|
||
(require 'url-parse)
|
||
(require 'url-util)
|
||
|
||
(defconst jiralib-version "3.0.0"
|
||
"Current version of jiralib.el.")
|
||
|
||
(defgroup jiralib nil
|
||
"Jiralib customization group."
|
||
:group 'applications)
|
||
|
||
(defgroup jiralib-faces nil
|
||
"Faces for displaying Jiralib information."
|
||
:group 'jiralib)
|
||
|
||
(defcustom jiralib-use-restapi t
|
||
"Use restapi instead of soap."
|
||
:group 'jiralib
|
||
:type 'boolean
|
||
:initialize 'custom-initialize-set)
|
||
|
||
(defcustom jiralib-coding-system 'utf-8
|
||
"Use custom coding system for Jiralib."
|
||
:group 'jiralib)
|
||
|
||
(defcustom jiralib-host ""
|
||
"User customizable host name of the Jiralib server.
|
||
|
||
This will be used with USERNAME to compute password from
|
||
.authinfo file. Will be calculated from jiralib-url if not set."
|
||
:group 'jiralib
|
||
:type 'string
|
||
:initialize 'custom-initialize-set)
|
||
|
||
(defface jiralib-issue-info-face
|
||
'((t (:foreground "black" :background "yellow4")))
|
||
"Base face for issue information."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-issue-info-header-face
|
||
'((t (:bold t :inherit 'jiralib-issue-info-face)))
|
||
"Base face for issue headers."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-issue-summary-face
|
||
'((t (:bold t)))
|
||
"Base face for issue summary."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-comment-face
|
||
'((t (:background "gray23")))
|
||
"Base face for comments."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-comment-header-face
|
||
'((t (:bold t)))
|
||
"Base face for comment headers."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-link-issue-face
|
||
'((t (:underline t)))
|
||
"Face for linked issues."
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-link-project-face
|
||
'((t (:underline t)))
|
||
"Face for linked projects"
|
||
:group 'jiralib-faces)
|
||
|
||
(defface jiralib-link-filter-face
|
||
'((t (:underline t)))
|
||
"Face for linked filters"
|
||
:group 'jiralib-faces)
|
||
|
||
(defvar jiralib-mode-hook nil)
|
||
(defvar jiralib-mode-map nil)
|
||
(defvar jiralib-issue-regexp "\\<\\(?:[A-Za-z0-9]+\\)-[0-9]+\\>")
|
||
|
||
(defcustom jiralib-wsdl-descriptor-url
|
||
""
|
||
"The location for the WSDL descriptor for the JIRA service.
|
||
This is specific to your local JIRA installation. The URL is
|
||
typically:
|
||
|
||
http://YOUR_INSTALLATION/rpc/soap/jirasoapservice-v2?wsdl
|
||
|
||
The default value works if JIRA is located at a hostname named
|
||
'jira'."
|
||
:type 'string
|
||
:group 'jiralib)
|
||
|
||
(defcustom jiralib-url
|
||
"http://localhost:8081/"
|
||
"The address of the jira host."
|
||
:type 'string
|
||
:group 'jiralib)
|
||
|
||
(defcustom jiralib-agile-page-size
|
||
50
|
||
"Page size for agile API retrieve. Limited by server property jira.search.views.default.max"
|
||
:type 'integer
|
||
:group 'jiralib)
|
||
|
||
(defvar jiralib-token nil
|
||
"JIRA token used for authentication.")
|
||
|
||
(defvar jiralib-user-login-name nil
|
||
"The name of the user logged into JIRA.
|
||
This is maintained by `jiralib-login'.")
|
||
|
||
(defvar jiralib-wsdl nil)
|
||
|
||
(defcustom jiralib-worklog-import--filters-alist
|
||
(list
|
||
'(nil "WorklogUpdatedByCurrentUser" (lambda (wl) (let-alist wl (when (and wl (string-equal (downcase (or jiralib-user-login-name user-login-name)) (downcase .updateAuthor.name))) wl))))
|
||
'(nil "WorklogAuthoredByCurrentUser" (lambda (wl) (let-alist wl (when (and wl (string-equal (downcase (or jiralib-user-login-name user-login-name)) (downcase .author.name))) wl)))))
|
||
"A list of triplets: ('Global-Enable 'Descriptive-Label 'Function-Definition)
|
||
that apply worklog predicate filters during import.
|
||
|
||
Example: (list '('t \"descriptive-predicate-label\" (lambda (x) x)))"
|
||
:type '(repeat (list boolean string function))
|
||
:group 'org-jira)
|
||
|
||
|
||
|
||
(defun jiralib-load-wsdl ()
|
||
"Load the JIRA WSDL descriptor."
|
||
(setq jiralib-wsdl (soap-load-wsdl-from-url (if (string-equal jiralib-wsdl-descriptor-url "")
|
||
(concat jiralib-url "/rpc/soap/jirasoapservice-v2?wsdl")
|
||
jiralib-wsdl-descriptor-url))))
|
||
|
||
(defun jiralib-login (username password)
|
||
"Login into JIRA as user USERNAME with PASSWORD.
|
||
|
||
After a successful login, store the authentication token in
|
||
`jiralib-token'."
|
||
;; NOTE that we cannot rely on `jiralib-call' because `jiralib-call' relies on
|
||
;; us ;-)
|
||
(interactive
|
||
(if (> 24 emacs-major-version)
|
||
(let ((user (read-string "Username for Jira server login? "))
|
||
(password (read-passwd "Password for Jira server login? ")))
|
||
(list user password))
|
||
(let ((found (nth 0 (auth-source-search :max 1
|
||
:host (if (string= jiralib-host "")
|
||
(url-host (url-generic-parse-url jiralib-url))
|
||
jiralib-host)
|
||
;; secrets.el wouldn’t accept a number.
|
||
:port (number-to-string (url-port (url-generic-parse-url jiralib-url)))
|
||
:require '(:user :secret)
|
||
:create t)))
|
||
user secret)
|
||
(when found
|
||
(setq user (plist-get found :user)
|
||
secret
|
||
(let ((sec (plist-get found :secret)))
|
||
(if (functionp sec)
|
||
(funcall sec)
|
||
sec)))
|
||
(list user secret)))))
|
||
(if jiralib-use-restapi
|
||
(setq jiralib-token `("Authorization" . , (format "Basic %s" (base64-encode-string (concat username ":" password) t))))
|
||
(unless jiralib-wsdl
|
||
(jiralib-load-wsdl))
|
||
(setq jiralib-token
|
||
(car (soap-invoke jiralib-wsdl "jirasoapservice-v2" "login" username password))))
|
||
(setq jiralib-user-login-name username))
|
||
|
||
(defvar jiralib-complete-callback nil)
|
||
|
||
(defun jiralib-call (method callback &rest params)
|
||
"Invoke the Jira METHOD, then CALLBACK with supplied PARAMS.
|
||
|
||
This function should be used for all JIRA interface calls, as the
|
||
method ensures the user is logged in and invokes `soap-invoke'
|
||
with the correct service name and authentication token.
|
||
|
||
All JIRA interface methods take an authentication token as the
|
||
first argument. The authentication token is supplied by this
|
||
function, so PARAMS should omit this parameter. For example, the
|
||
\"getIssue\" method takes two parameters: auth and key, however,
|
||
when invoking it through `jiralib-call', the call should be:
|
||
|
||
(jiralib-call \"getIssue\" KEY)
|
||
|
||
CALLBACK should be the post processing function to run with the
|
||
completed data from the request result, which can be accessed with:
|
||
|
||
(getf data :data)
|
||
|
||
as such, the CALLBACK should follow this type of form:
|
||
|
||
(cl-function
|
||
(lambda (&rest data &allow-other-keys)
|
||
(print (getf data :data))))
|
||
|
||
If CALLBACK is set to nil then the request will occur with sync.
|
||
This produces a noticeable slowdown and is not recommended by
|
||
request.el, so if at all possible, it should be avoided."
|
||
;; @TODO :auth: Probably pass this all the way down, but I think
|
||
;; it may be OK at the moment to just set the variable each time.
|
||
(setq jiralib-complete-callback
|
||
;; Don't run with async if we don't have a login token yet.
|
||
(if jiralib-token callback nil))
|
||
|
||
;; If we don't have a regex set, ensure it is set BEFORE any async
|
||
;; calls are processing, or we're going to have a bad time.
|
||
;; This should only end up running once per session.
|
||
(unless jiralib-issue-regexp
|
||
(let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e))))
|
||
(append (jiralib--rest-call-it
|
||
"/rest/api/2/project"
|
||
:params '((expand . "description,lead,url,projectKeys")))
|
||
nil)
|
||
)))
|
||
(when projects
|
||
(setq jiralib-issue-regexp
|
||
(concat "\\<" (regexp-opt projects) "-[0-9]+\\>")))))
|
||
|
||
(if (not jiralib-use-restapi)
|
||
(car (apply 'jiralib--call-it method params))
|
||
(unless jiralib-token
|
||
(call-interactively 'jiralib-login))
|
||
(case (intern method)
|
||
('getStatuses (jiralib--rest-call-it "/rest/api/2/status"))
|
||
('getIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype"))
|
||
('getSubTaskIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype"))
|
||
('getIssueTypesByProject
|
||
(let ((response (jiralib--rest-call-it (format "/rest/api/2/project/%s" (first params)))))
|
||
(cl-coerce (cdr (assoc 'issueTypes response)) 'list)))
|
||
('getUser (jiralib--rest-call-it "/rest/api/2/user" :params `((username . ,(first params)))))
|
||
('getVersions (jiralib--rest-call-it (format "/rest/api/2/project/%s/versions" (first params))))
|
||
|
||
;; Worklog calls
|
||
('getWorklogs
|
||
(jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))))
|
||
|
||
('addWorklog
|
||
(jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))
|
||
:type "POST"
|
||
:data (json-encode (second params))))
|
||
|
||
('updateWorklog
|
||
(jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog/%s" (first params) (second params))
|
||
:type "PUT"
|
||
:data (json-encode (third params))))
|
||
|
||
('addWorklogAndAutoAdjustRemainingEstimate
|
||
(jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))
|
||
:type "POST"
|
||
:data (json-encode (second params))))
|
||
|
||
('addComment (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/comment" (first params))
|
||
:type "POST"
|
||
:data (json-encode (second params))))
|
||
('createIssue
|
||
;; Creating the issue doesn't return it, a second call must be
|
||
;; made to pull it in by using the self key in response.
|
||
(let ((response (jiralib--rest-call-it
|
||
"/rest/api/2/issue"
|
||
:type "POST"
|
||
:data (json-encode (first params)))))
|
||
(jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET")
|
||
))
|
||
('createIssueWithParent
|
||
(let ((response (jiralib--rest-call-it
|
||
"/rest/api/2/issue"
|
||
:type "POST"
|
||
:data (json-encode (first params)))))
|
||
(jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET")
|
||
))
|
||
('editComment (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/comment/%s" (first params) (second params))
|
||
:data (json-encode `((body . ,(third params))))
|
||
:type "PUT"))
|
||
('getBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s" (first params))))
|
||
('getBoards (apply 'jiralib--agile-call-it "/rest/agile/1.0/board" 'values params))
|
||
('getComment (org-jira-find-value
|
||
(jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/comment/%s" (first params) (second params)))
|
||
'comments))
|
||
('getComments (org-jira-find-value
|
||
(jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/comment" (first params)))
|
||
'comments))
|
||
('getAttachmentsFromIssue (org-jira-find-value
|
||
(jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s?fields=attachment" (first params)))
|
||
'comments))
|
||
('getComponents (jiralib--rest-call-it
|
||
(format "/rest/api/2/project/%s/components" (first params))))
|
||
('getIssue (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s" (first params))))
|
||
('getIssuesFromBoard (apply 'jiralib--agile-call-it
|
||
(format "rest/agile/1.0/board/%d/issue" (first params))
|
||
'issues
|
||
(cdr params)))
|
||
('getIssuesFromJqlSearch (append (cdr ( assoc 'issues (jiralib--rest-call-it
|
||
"/rest/api/2/search"
|
||
:type "POST"
|
||
:data (json-encode `((jql . ,(first params))
|
||
(maxResults . ,(second params)))))))
|
||
nil))
|
||
('getPriorities (jiralib--rest-call-it
|
||
"/rest/api/2/priority"))
|
||
('getProjects (jiralib--rest-call-it "rest/api/2/project"))
|
||
('getProjectsNoSchemes (append (jiralib--rest-call-it
|
||
"/rest/api/2/project"
|
||
:params '((expand . "description,lead,url,projectKeys"))) nil))
|
||
('getResolutions (append (jiralib--rest-call-it
|
||
"/rest/api/2/resolution") nil))
|
||
('getAvailableActions
|
||
(mapcar
|
||
(lambda (trans)
|
||
`(,(assoc 'name trans) ,(assoc 'id trans)))
|
||
(cdadr (jiralib--rest-call-it (format "/rest/api/2/issue/%s/transitions" (first params))))))
|
||
('getFieldsForAction (org-jira-find-value (car (let ((issue (first params))
|
||
(action (second params)))
|
||
(seq-filter (lambda (trans)
|
||
(or (string-equal action (org-jira-find-value trans 'id))
|
||
(string-equal action (org-jira-find-value trans 'name))))
|
||
(cdadr (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/transitions" (first params))
|
||
:params '((expand . "transitions.fields")))))))
|
||
'fields))
|
||
('progressWorkflowAction (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s/transitions" (first params))
|
||
:type "POST"
|
||
:data (json-encode `(,(car (second params)) ,(car (third params))))))
|
||
('getUsers
|
||
(jiralib--rest-call-it (format "/rest/api/2/user/assignable/search?project=%s&maxResults=10000" (first params))
|
||
:type "GET"))
|
||
('updateIssue (jiralib--rest-call-it
|
||
(format "/rest/api/2/issue/%s" (first params))
|
||
:type "PUT"
|
||
:data (json-encode `((fields . ,(second params)))))))))
|
||
|
||
(defun jiralib--soap-call-it (&rest args)
|
||
"Deprecated SOAP call endpoint. Will be removed soon.
|
||
Pass ARGS to jiralib-call."
|
||
(let ((jiralib-token nil)
|
||
(jiralib-use-restapi nil))
|
||
(apply #'jiralib-call args)))
|
||
|
||
(defun jiralib--json-read ()
|
||
"Read with json, force utf-8"
|
||
(decode-coding-region (point) (point-max) jiralib-coding-system)
|
||
(json-read))
|
||
|
||
(defun jiralib--rest-call-it (api &rest args)
|
||
"Invoke the corresponding jira rest method API.
|
||
Invoking COMPLETE-CALLBACK when the
|
||
JIRALIB-COMPLETE-CALLBACK is non-nil, request finishes, and
|
||
passing ARGS to REQUEST."
|
||
(setq args
|
||
(mapcar
|
||
(lambda (arg)
|
||
(if (stringp arg)
|
||
(encode-coding-string arg jiralib-coding-system)
|
||
arg))
|
||
args))
|
||
(append (request-response-data
|
||
(apply #'request (if (string-match "^http[s]*://" api) api ;; If an absolute path, use it
|
||
(concat (replace-regexp-in-string "/*$" "/" jiralib-url)
|
||
(replace-regexp-in-string "^/*" "" api)))
|
||
:sync (not jiralib-complete-callback)
|
||
:headers `(,jiralib-token ("Content-Type" . "application/json"))
|
||
:parser 'jiralib--json-read
|
||
:complete jiralib-complete-callback
|
||
args))
|
||
nil))
|
||
|
||
(defun jiralib--call-it (method &rest params)
|
||
"Invoke the JIRA METHOD with supplied PARAMS.
|
||
|
||
Internal use, returns a list of responses, of which only the
|
||
first is normally used."
|
||
(when (symbolp method)
|
||
(setq method (symbol-name method)))
|
||
(unless jiralib-token
|
||
(call-interactively 'jiralib-login))
|
||
(condition-case data
|
||
(apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2"
|
||
method jiralib-token params)
|
||
(soap-error
|
||
;; If we are here, we had a token, but it expired. Re-login and try
|
||
;; again.
|
||
(setq jiralib-token nil)
|
||
(call-interactively 'jiralib-login)
|
||
(apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2"
|
||
method jiralib-token params))))
|
||
|
||
|
||
;;;; Some utility functions
|
||
|
||
(defun jiralib-make-list (data field)
|
||
"Map all assoc elements in DATA to the value of FIELD in that element."
|
||
(loop for element in data
|
||
collect (cdr (assoc field element))))
|
||
|
||
(defun jiralib-make-assoc-list (data key-field value-field)
|
||
"Create an association list from a SOAP structure array.
|
||
|
||
DATA is a list of association lists (a SOAP array-of type)
|
||
KEY-FIELD is the field to use as the key in the returned alist
|
||
VALUE-FIELD is the field to use as the value in the returned alist"
|
||
(loop for element in data
|
||
collect (cons (cdr (assoc key-field element))
|
||
(cdr (assoc value-field element)))))
|
||
|
||
(defun jiralib-make-remote-field-values (fields)
|
||
"Transform the (KEY . VALUE) list FIELDS into a RemoteFieldValue structure.
|
||
|
||
Each (KEY . VALUE) pair is transformed into
|
||
((id . KEY) (values . (VALUE)))
|
||
|
||
This method exists because Several JIRA methods require a
|
||
RemoteFieldValue list, but it is easier to work with ALISTS in
|
||
emacs-lisp"
|
||
(let ((remote-field-values))
|
||
|
||
;; we accept an ALIST of field-name field-values parameter, but we need to
|
||
;; construct a structure that encodes as a RemoteFieldValue which is what
|
||
;; updateIssue wants
|
||
(dolist (field fields)
|
||
(let ((name (car field))
|
||
(value (cdr field)))
|
||
(when (symbolp name)
|
||
(setq name (symbol-name name)))
|
||
;; Value must be an "array" (for which soap-client accepts lists) even
|
||
;; if it is just one value
|
||
(unless (vectorp value)
|
||
(setq value (vector value)))
|
||
(push `((id . ,name) (values . ,value))
|
||
remote-field-values)))
|
||
|
||
(apply 'vector (nreverse remote-field-values))))
|
||
|
||
;;;; Wrappers around JIRA methods
|
||
|
||
(defun jiralib--rest-api-for-issue-key (key)
|
||
"Return jira rest api for issue KEY."
|
||
(concat "rest/api/2/issue/" key))
|
||
|
||
(defun jiralib-update-issue (key fields &optional callback)
|
||
"Update the issue with id KEY with the values in FIELDS, invoking CALLBACK."
|
||
(jiralib-call
|
||
"updateIssue"
|
||
callback
|
||
key (if jiralib-use-restapi
|
||
fields
|
||
(jiralib-make-remote-field-values fields))))
|
||
|
||
(defvar jiralib-status-codes-cache nil)
|
||
|
||
(defun jiralib-get-statuses ()
|
||
"Return an assoc list mapping a status code to its name.
|
||
NOTE: Status codes are stored as strings, not numbers.
|
||
|
||
This function will only ask JIRA for the list of codes once, then
|
||
will cache it."
|
||
(unless jiralib-status-codes-cache
|
||
(setq jiralib-status-codes-cache
|
||
(jiralib-make-assoc-list (jiralib-call "getStatuses" nil) 'id 'name)))
|
||
jiralib-status-codes-cache)
|
||
|
||
(defvar jiralib-issue-types-cache nil)
|
||
|
||
(defun jiralib-get-issue-types ()
|
||
"Return an assoc list mapping an issue type code to its name.
|
||
NOTE: Issue type codes are stored as strings, not numbers.
|
||
|
||
This function will only ask JIRA for the list of codes once, than
|
||
will cache it.
|
||
|
||
The issue types returned via getIssueTypes are all the ones
|
||
available to the user, but not necessarily available to the given
|
||
project.
|
||
|
||
This endpoint is essentially a master reference for when issue
|
||
types need a name lookup when given an id.
|
||
|
||
For applying issue types to a given project that is being created, see
|
||
the #'jiralib-get-issue-types-by-project call."
|
||
(unless jiralib-issue-types-cache
|
||
(setq jiralib-issue-types-cache
|
||
(jiralib-make-assoc-list (jiralib-call "getIssueTypes" nil) 'id 'name)))
|
||
jiralib-issue-types-cache)
|
||
|
||
(defvar jiralib-issue-types-by-project-cache nil "An alist of available issue types.")
|
||
|
||
(defun jiralib-get-issue-types-by-project (project)
|
||
"Return the available issue types for PROJECT.
|
||
|
||
PROJECT should be the key, such as `EX' or `DEMO'."
|
||
(unless (assoc project jiralib-issue-types-by-project-cache)
|
||
(push (cons project
|
||
(jiralib-make-assoc-list
|
||
(jiralib-call "getIssueTypesByProject" nil project)
|
||
'id 'name))
|
||
jiralib-issue-types-by-project-cache))
|
||
(cdr (assoc project jiralib-issue-types-by-project-cache)))
|
||
|
||
(defvar jiralib-priority-codes-cache nil)
|
||
|
||
(defun jiralib-get-priorities ()
|
||
"Return an assoc list mapping a priority code to its name.
|
||
NOTE: Priority codes are stored as strings, not numbers.
|
||
|
||
This function will only ask JIRA for the list of codes once, than
|
||
will cache it."
|
||
(unless jiralib-priority-codes-cache
|
||
(setq jiralib-priority-codes-cache
|
||
(jiralib-make-assoc-list (jiralib-call "getPriorities" nil) 'id 'name)))
|
||
jiralib-priority-codes-cache)
|
||
|
||
(defvar jiralib-resolution-code-cache nil)
|
||
|
||
(defun jiralib-get-resolutions ()
|
||
"Return an assoc list mapping a resolution code to its name.
|
||
NOTE: Resolution codes are stored as strings, not numbers.
|
||
|
||
This function will only ask JIRA for the list of codes once, than
|
||
will cache it."
|
||
(unless jiralib-resolution-code-cache
|
||
(setq jiralib-resolution-code-cache
|
||
(jiralib-make-assoc-list (jiralib-call "getResolutions" nil) 'id 'name)))
|
||
jiralib-resolution-code-cache)
|
||
|
||
;; NOTE: it is not such a good idea to use this, as it needs a JIRA
|
||
;; connection to construct the regexp (the user might be prompted for a JIRA
|
||
;; username and password).
|
||
;;
|
||
;; The best use of this function is to generate the regexp once-off and
|
||
;; persist it somewhere.
|
||
;;
|
||
;; FIXME: Probably just deprecate/remove this, we can assert we're on
|
||
;; an issue with a general regexp that matches the common format, vs
|
||
;; needing to know specific user project list.
|
||
(defun jiralib-get-issue-regexp ()
|
||
"Return a regexp that will match an issue id.
|
||
|
||
The regexp is constructed from the project keys in the JIRA
|
||
database. An issue is assumed to be in the format KEY-NUMBER,
|
||
where KEY is a project key and NUMBER is the issue number."
|
||
(unless jiralib-issue-regexp
|
||
(let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e))))
|
||
(jiralib-call "getProjectsNoSchemes" nil))))
|
||
(when projects
|
||
(setq jiralib-issue-regexp
|
||
(concat "\\<" (regexp-opt projects) "-[0-9]+\\>")))))
|
||
jiralib-issue-regexp)
|
||
|
||
(defun jiralib-do-jql-search (jql &optional limit callback)
|
||
"Run a JQL query and return the list of issues that matched.
|
||
LIMIT is the maximum number of queries to return. Note that JIRA
|
||
has an internal limit of how many queries to return, as such, it
|
||
might not be possible to find *ALL* the issues that match a
|
||
query."
|
||
(unless (or limit (numberp limit))
|
||
(setq limit 100))
|
||
(jiralib-call "getIssuesFromJqlSearch" callback jql limit))
|
||
|
||
(defcustom jiralib-available-actions-cache-p t
|
||
"Set to t to enable caching for jiralib-get-available-actions.
|
||
|
||
If nil, will disable caching for this endpoint.
|
||
|
||
Possible side-effects:
|
||
|
||
- If the server has the project workflow updated, the cache
|
||
saved here will be incorrect.
|
||
|
||
- If the issue is not up to date with the remote, the wrong
|
||
cache key may be queried."
|
||
:type 'boolean
|
||
:group 'jiralib)
|
||
|
||
(defvar jiralib-available-actions-cache nil "An alist of available actions.")
|
||
|
||
(defun jiralib-get-available-actions (issue-key &optional status)
|
||
"Return the available workflow actions for ISSUE-KEY.
|
||
This uses STATUS as the cache key.
|
||
This runs the getAvailableActions SOAP method."
|
||
(if (and jiralib-available-actions-cache-p status)
|
||
(progn
|
||
(unless (assoc status jiralib-available-actions-cache)
|
||
(push (cons status
|
||
(jiralib-make-assoc-list
|
||
(mapcar (lambda (x)
|
||
(let ((namestring (cdr (car x)))
|
||
(id (cdr x)))
|
||
(cons
|
||
(cons 'name (org-jira-decode namestring))
|
||
id)))
|
||
(jiralib-call "getAvailableActions" nil issue-key))
|
||
'id 'name))
|
||
jiralib-available-actions-cache))
|
||
(cdr (assoc status jiralib-available-actions-cache)))
|
||
(progn
|
||
(jiralib-make-assoc-list
|
||
(mapcar (lambda (x)
|
||
(let ((namestring (cdr (car x)))
|
||
(id (cdr x)))
|
||
(cons
|
||
(cons 'name (org-jira-decode namestring))
|
||
id)))
|
||
(jiralib-call "getAvailableActions" nil issue-key))
|
||
'id 'name))))
|
||
|
||
(defcustom jiralib-fields-for-action-cache-p t
|
||
"Set to t to enable caching for jiralib-get-fields-for-action.
|
||
|
||
If nil, will disable caching for this endpoint.
|
||
|
||
Possible side-effects:
|
||
|
||
- If many tasks have different workflows, you may want to disable this."
|
||
:type 'boolean
|
||
:group 'jiralib)
|
||
|
||
(defvar jiralib-fields-for-action-cache nil "An alist of available fields.")
|
||
|
||
(defun jiralib-get-fields-for-action-with-cache (issue-key action-id)
|
||
"Return the required fields to change ISSUE-KEY to ACTION-ID."
|
||
(if (and jiralib-fields-for-action-cache-p action-id)
|
||
(progn
|
||
(unless (assoc action-id jiralib-fields-for-action-cache)
|
||
(push (cons action-id
|
||
(jiralib-call "getFieldsForAction" nil issue-key action-id))
|
||
jiralib-fields-for-action-cache))
|
||
(cdr (assoc action-id jiralib-fields-for-action-cache)))
|
||
(jiralib-call "getFieldsForAction" nil issue-key action-id)))
|
||
|
||
(defun jiralib-get-fields-for-action (issue-key action-id)
|
||
"Return the required fields to change ISSUE-KEY to ACTION-ID."
|
||
(if jiralib-use-restapi
|
||
(let ((fields (jiralib-get-fields-for-action-with-cache issue-key action-id)))
|
||
(mapcar (lambda (field)
|
||
(cons (symbol-name (car field))
|
||
(format "%s (required: %s)"
|
||
(org-jira-find-value field 'name)
|
||
(if (eq (org-jira-find-value field 'required) :json-false)
|
||
"nil"
|
||
"t"))))
|
||
fields))
|
||
(jiralib-make-assoc-list
|
||
(jiralib-get-fields-for-action-with-cache issue-key action-id)
|
||
'id 'name)))
|
||
|
||
(defun jiralib-progress-workflow-action (issue-key action-id params &optional callback)
|
||
"Progress issue with ISSUE-KEY to action ACTION-ID, and provide the needed PARAMS.
|
||
|
||
When CALLBACK is present, this will run async."
|
||
(if jiralib-use-restapi
|
||
(jiralib-call "progressWorkflowAction"
|
||
callback issue-key `((transition (id . ,action-id)))
|
||
`((fields . ,params)))
|
||
(jiralib-call "progressWorkflowAction"
|
||
callback issue-key action-id (jiralib-make-remote-field-values params))))
|
||
|
||
|
||
(defun jiralib-format-datetime (&optional datetime)
|
||
"Convert a mixed DATETIME format into the Jira required datetime format.
|
||
|
||
This will produce a datetime string such as:
|
||
|
||
2010-02-05T14:30:00.000+0000
|
||
|
||
for being consumed in the Jira API.
|
||
|
||
If DATETIME is not passed in, it will default to the current time."
|
||
(let* ((defaults (format-time-string "%Y-%m-%d %H:%M:%S" (current-time)))
|
||
(datetime (concat datetime (subseq defaults (length datetime))))
|
||
(parts (parse-time-string datetime)))
|
||
(format "%04d-%02d-%02dT%02d:%02d:%02d.000+0000"
|
||
(nth 5 parts)
|
||
(nth 4 parts)
|
||
(nth 3 parts)
|
||
(nth 2 parts)
|
||
(nth 1 parts)
|
||
(nth 0 parts))))
|
||
|
||
(defvar jiralib-worklog-coming-soon-message
|
||
"WORKLOG FEATURES ARE NOT IMPLEMENTED YET, COMING SOON!")
|
||
|
||
(defun jiralib-add-worklog-and-autoadjust-remaining-estimate (issue-key start-date time-spent comment)
|
||
"Log time spent on ISSUE-KEY to its worklog.
|
||
The time worked begins at START-DATE and has a TIME-SPENT
|
||
duration. JIRA will automatically update the remaining estimate
|
||
by subtracting TIME-SPENT from it.
|
||
|
||
START-DATE should be in the format 2010-02-05T14:30:00Z
|
||
|
||
TIME-SPENT can be in one of the following formats: 10m, 120m
|
||
hours; 10h, 120h days; 10d, 120d weeks.
|
||
|
||
COMMENT will be added to this worklog."
|
||
(let ((formatted-start-date (jiralib-format-datetime start-date)))
|
||
(jiralib-call "addWorklogAndAutoAdjustRemainingEstimate"
|
||
nil
|
||
issue-key
|
||
;; Expects data such as: '{"timeSpent":"1h", "started":"2017-02-21T00:00:00.000+0000", "comment":"woot!"}'
|
||
;; and only that format will work (no loose formatting on the started date)
|
||
`((started . ,formatted-start-date)
|
||
(timeSpent . ,time-spent)
|
||
(comment . ,comment)))))
|
||
|
||
|
||
;;;; Issue field accessors
|
||
|
||
(defun jiralib-issue-key (issue)
|
||
"Return the key of ISSUE."
|
||
(cdr (assoc 'key issue)))
|
||
|
||
(defun jiralib-issue-owner (issue)
|
||
"Return the owner of ISSUE."
|
||
(cdr (assq 'assignee issue)))
|
||
|
||
(defun jiralib-issue-status (issue)
|
||
"Return the status of ISSUE as a status string (not as a number!)."
|
||
(let ((status-code (cdr (assq 'status issue))))
|
||
(cdr (assoc status-code (jiralib-get-statuses)))))
|
||
|
||
(defun jiralib-custom-field-value (custom-field issue)
|
||
"Return the value of CUSTOM-FIELD for ISSUE.
|
||
Return nil if the field is not found"
|
||
(catch 'found
|
||
(dolist (field (cdr (assq 'customFieldValues issue)))
|
||
(when (equal (cdr (assq 'customfieldId field)) custom-field)
|
||
(throw 'found (cadr (assq 'values field)))))))
|
||
|
||
(defvar jiralib-current-issue nil
|
||
"This holds the currently selected issue.")
|
||
|
||
(defvar jiralib-projects-list nil
|
||
"This holds a list of projects and their details.")
|
||
|
||
(defvar jiralib-types nil
|
||
"This holds a list of issues types.")
|
||
|
||
(defvar jiralib-priorities nil
|
||
"This holds a list of priorities.")
|
||
|
||
(defvar jiralib-user-fullnames nil
|
||
"This holds a list of user fullnames.")
|
||
|
||
(defun jiralib-get-project-name (key)
|
||
"Return the name of the JIRA project with id KEY."
|
||
(let ((projects jiralib-projects-list)
|
||
(name nil))
|
||
(dolist (project projects)
|
||
(if (equal (cdr (assoc 'key project)) key)
|
||
(setf name (cdr (assoc 'name project)))))
|
||
name))
|
||
|
||
(defun jiralib-get-type-name (id)
|
||
"Return the name of the issue type with ID."
|
||
(let ((types jiralib-types)
|
||
(name nil))
|
||
(dolist (type types)
|
||
(if (equal (cdr (assoc 'id type)) id)
|
||
(setf name (cdr (assoc 'name type)))))
|
||
name))
|
||
|
||
(defun jiralib-get-user-fullname (username)
|
||
"Return the full name (display name) of the user with USERNAME."
|
||
(if (assoc username jiralib-user-fullnames)
|
||
(cdr (assoc username jiralib-user-fullnames))
|
||
(progn
|
||
(let ((user (jiralib-get-user username)))
|
||
(setf jiralib-user-fullnames (append jiralib-user-fullnames (list (cons username (cdr (assoc 'fullname user))))))
|
||
(cdr (assoc 'fullname user))))))
|
||
|
||
|
||
(defun jiralib-get-filter (filter-id)
|
||
"Return a filter given its FILTER-ID."
|
||
(cl-flet ((id-match (filter)
|
||
(equal filter-id (cdr (assoc 'id filter)))))
|
||
(cl-find-if 'id-match (jiralib-get-saved-filters))))
|
||
|
||
(defun jiralib-get-filter-alist ()
|
||
"Return an association list mapping filter names to IDs."
|
||
(mapcar (lambda (filter)
|
||
(cons (cdr (assoc 'name filter))
|
||
(cdr (assoc 'id filter))))
|
||
(jiralib-get-saved-filters)))
|
||
|
||
(defun jiralib-add-comment (issue-key comment &optional callback)
|
||
"Add to issue with ISSUE-KEY the given COMMENT, invoke CALLBACK."
|
||
(jiralib-call "addComment" callback issue-key `((body . ,comment))))
|
||
|
||
(defun jiralib-edit-comment (issue-id comment-id comment &optional callback)
|
||
"Edit ISSUE-ID's comment COMMENT-ID to reflect the new COMMENT, invoke CALLBACK."
|
||
(if (not jiralib-use-restapi)
|
||
(jiralib-call "editComment" callback `((id . ,comment-id)
|
||
(body . ,comment)))
|
||
(jiralib-call "editComment" callback issue-id comment-id comment)))
|
||
|
||
(defun jiralib-create-issue (issue)
|
||
"Create a new ISSUE in JIRALIB.
|
||
|
||
ISSUE is a Hashtable object."
|
||
(jiralib-call "createIssue" nil issue))
|
||
|
||
(defun jiralib-create-subtask (subtask)
|
||
"Create SUBTASK for issue with PARENT-ISSUE-ID.
|
||
|
||
SUBTASK is a Hashtable object."
|
||
(jiralib-call "createIssueWithParent" nil subtask))
|
||
|
||
(defvar jiralib-subtask-types-cache nil)
|
||
|
||
(defun jiralib-get-subtask-types ()
|
||
"Return an assoc list mapping an issue type code to its name.
|
||
NOTE: Issue type codes are stored as strings, not numbers.
|
||
|
||
This function will only ask JIRA for the list of codes once, than
|
||
will cache it."
|
||
(unless jiralib-subtask-types-cache
|
||
(setq jiralib-subtask-types-cache
|
||
(jiralib-make-assoc-list (jiralib-call "getSubTaskIssueTypes" nil) 'id 'name)))
|
||
jiralib-subtask-types-cache)
|
||
|
||
(defun jiralib-get-comment (issue-key comment-id &optional callback)
|
||
"Return all comments associated with issue ISSUE-KEY, invoking CALLBACK."
|
||
(jiralib-call "getComment" callback issue-key comment-id))
|
||
|
||
(defun jiralib-get-comments (issue-key &optional callback)
|
||
"Return all comments associated with issue ISSUE-KEY, invoking CALLBACK."
|
||
(jiralib-call "getComments" callback issue-key))
|
||
|
||
(defun jiralib-get-attachments (issue-key &optional callback)
|
||
"Return all attachments associated with issue ISSUE-KEY, invoking CALLBACK."
|
||
(jiralib-call "getAttachmentsFromIssue" callback issue-key))
|
||
|
||
(defun jiralib-get-worklogs (issue-key &optional callback)
|
||
"Return all worklogs associated with issue ISSUE-KEY, invoking CALLBACK."
|
||
(jiralib-call "getWorklogs" callback issue-key))
|
||
|
||
(defun jiralib-add-worklog (issue-id started time-spent-seconds comment &optional callback)
|
||
"Add the worklog linked to ISSUE-ID.
|
||
|
||
Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT.
|
||
CALLBACK will be invoked if passed in upon endpoint completion."
|
||
;; Call will fail if 0 seconds are set as the time, so always do at least one min.
|
||
(setq time-spent-seconds (max 60 time-spent-seconds))
|
||
(let ((worklog `((started . ,started)
|
||
;; @TODO :worklog: timeSpentSeconds changes into incorrect values
|
||
;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days...
|
||
;; We should convert to a Xd Xh Xm format from our seconds ourselves.
|
||
(timeSpentSeconds . ,time-spent-seconds)
|
||
(comment . ,comment))))
|
||
(jiralib-call "addWorklog" callback issue-id worklog)))
|
||
|
||
(defun jiralib-update-worklog (issue-id worklog-id started time-spent-seconds comment &optional callback)
|
||
"Update the worklog linked to ISSUE-ID and WORKLOG-ID.
|
||
|
||
Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT.
|
||
CALLBACK will be invoked if passed in upon endpoint completion."
|
||
;; Call will fail if 0 seconds are set as the time, so always do at least one min.
|
||
(setq time-spent-seconds (max 60 time-spent-seconds))
|
||
(let ((worklog `((started . ,started)
|
||
;; @TODO :worklog: timeSpentSeconds changes into incorrect values
|
||
;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days...
|
||
;; We should convert to a Xd Xh Xm format from our seconds ourselves.
|
||
(timeSpentSeconds . ,time-spent-seconds)
|
||
(comment . ,comment))))
|
||
(jiralib-call "updateWorklog" callback issue-id worklog-id worklog)))
|
||
|
||
(defvar jiralib-components-cache nil "An alist of project components.")
|
||
|
||
(defun jiralib-get-components (project-key)
|
||
"Return all components available in the project PROJECT-KEY."
|
||
(unless (assoc project-key jiralib-components-cache)
|
||
(push (cons project-key
|
||
(jiralib-make-assoc-list
|
||
(jiralib-call "getComponents" nil project-key) 'id 'name))
|
||
jiralib-components-cache))
|
||
(cdr (assoc project-key jiralib-components-cache)))
|
||
|
||
(defun jiralib-get-issue (issue-key &optional callback)
|
||
"Get the issue with key ISSUE-KEY, running CALLBACK after."
|
||
(jiralib-call "getIssue" callback issue-key))
|
||
|
||
(defun jiralib-get-issues-from-filter (filter-id)
|
||
"Get the issues from applying saved filter FILTER-ID."
|
||
(message "jiralib-get-issues-from-filter is NOT IMPLEMENTED!! Do not use!")
|
||
(jiralib-call "getIssuesFromFilter" nil filter-id))
|
||
|
||
(defun jiralib-get-issues-from-text-search (search-terms)
|
||
"Find issues using free text search SEARCH-TERMS."
|
||
(jiralib-call "getIssuesFromTextSearch" nil search-terms))
|
||
|
||
(defun jiralib-get-issues-from-text-search-with-project
|
||
(project-keys search-terms max-num-results)
|
||
"Find issues in projects PROJECT-KEYS, using free text search SEARCH-TERMS.
|
||
|
||
Return no more than MAX-NUM-RESULTS."
|
||
(jiralib-call "getIssuesFromTextSearchWithProject"
|
||
nil
|
||
(apply 'vector project-keys) search-terms max-num-results))
|
||
|
||
;; Modified by Brian Zwahr to use getProjectsNoSchemes instead of getProjects
|
||
(defun jiralib-get-projects ()
|
||
"Return a list of projects available to the user."
|
||
(if jiralib-projects-list
|
||
jiralib-projects-list
|
||
(setq jiralib-projects-list
|
||
(if jiralib-use-restapi
|
||
(jiralib-call "getProjects" nil)
|
||
(jiralib-call "getProjectsNoSchemes" nil)))))
|
||
|
||
(defun jiralib-get-saved-filters ()
|
||
"Get all saved filters available for the currently logged in user."
|
||
(jiralib-make-assoc-list (jiralib-call "getSavedFilters" nil) 'id 'name))
|
||
|
||
(defun jiralib-get-server-info ()
|
||
"Return the Server information such as baseUrl, version, edition, buildDate, buildNumber."
|
||
(jiralib-call "getServerInfo" nil))
|
||
|
||
(defun jiralib-get-sub-task-issue-types ()
|
||
"Return all visible subtask issue types in the system."
|
||
(jiralib-call "getSubTaskIssueTypes" nil))
|
||
|
||
(defun jiralib-get-user (username)
|
||
"Return a user's information given their USERNAME."
|
||
(cond ((eq 0 (length username)) nil) ;; Unassigned
|
||
(t (jiralib-call "getUser" nil username))))
|
||
|
||
(defvar jiralib-users-cache nil "Cached list of users.")
|
||
|
||
(defun jiralib-get-users (project-key)
|
||
"Return assignable users information given the PROJECT-KEY."
|
||
(unless jiralib-users-cache
|
||
(setq jiralib-users-cache
|
||
(jiralib-call "getUsers" nil project-key)))
|
||
jiralib-users-cache)
|
||
|
||
(defun jiralib-get-versions (project-key)
|
||
"Return all versions available in project PROJECT-KEY."
|
||
(jiralib-call "getVersions" nil project-key))
|
||
|
||
(defun jiralib-strip-cr (string)
|
||
"Remove carriage returns from STRING."
|
||
(when string (replace-regexp-in-string "\r" "" string)))
|
||
|
||
(defun jiralib-worklog-import--filter-apply
|
||
(worklog-obj &optional predicate-fn-lst unwrap-worklog-records-fn rewrap-worklog-records-fn)
|
||
"Remove non-matching org-jira issue worklogs.
|
||
|
||
Variables:
|
||
WORKLOG-OBJ is the passed in object
|
||
PREDICATE-FN-LST is the list of lambdas used as match predicates.
|
||
UNWRAP-WORKLOG-RECORDS-FN is the function used to produce the list of worklog records from within the worklog-obj
|
||
REWRAP-WORKLOG-RECORDS-FN is the function used to reshape the worklog records back into the form they were received in.
|
||
|
||
Auxiliary Notes:
|
||
Only the WORKLOG-OBJ variable is required.
|
||
The value of PPREDICATE-FN-LST is filled from the jiralib-worklog-import--filters-alist variable by default.
|
||
If PREDICATE-FN-LST is empty the unmodified value of WORKLOG-OBJ is returned.
|
||
If PREDICATE-FN-LST contains multiple predicate functions, each predicate filters operates as a clause in an AND match. In effect, a worklog must match all predicates to be returned.
|
||
The variable 'jiralib-user-login-name is used by many lambda filters."
|
||
|
||
(let
|
||
((unwrap-worklog-records-fn)
|
||
(rewrap-worklog-records-fn)
|
||
(predicate-fn-lst)
|
||
(worklogs worklog-obj)
|
||
(predicate-fn))
|
||
;; let-body
|
||
(progn
|
||
(setq unwrap-worklog-records-fn
|
||
(if (and
|
||
(boundp 'unwrap-worklog-records-fn)
|
||
(functionp unwrap-worklog-records-fn))
|
||
unwrap-worklog-records-fn
|
||
(lambda (x) (coerce x 'list))))
|
||
(setq rewrap-worklog-records-fn
|
||
(if (and
|
||
(boundp 'rewrap-worklog-records-fn)
|
||
(functionp rewrap-worklog-records-fn))
|
||
rewrap-worklog-records-fn
|
||
(lambda (x) (remove 'nil (coerce x 'vector)))))
|
||
(setq predicate-fn-lst
|
||
(if (and (boundp 'predicate-fn-lst)
|
||
(not (null predicate-fn-lst))
|
||
(listp predicate-fn-lst))
|
||
predicate-fn-lst
|
||
(mapcar 'caddr
|
||
(remove 'nil
|
||
(mapcar (lambda (x) (unless (null (car x)) x))
|
||
jiralib-worklog-import--filters-alist)))))
|
||
;; final condition/sanity checks before processing
|
||
(cond
|
||
;; pass cases, don't apply filters, return unaltered worklog-obj
|
||
((or (not (boundp 'predicate-fn-lst)) (not (listp predicate-fn-lst)) (null predicate-fn-lst))
|
||
worklog-obj)
|
||
;; default-case, apply worklog filters and return only matching worklogs
|
||
(t
|
||
(setq worklogs (funcall unwrap-worklog-records-fn worklogs))
|
||
(while (setq predicate-fn (pop predicate-fn-lst))
|
||
(setq worklogs (mapcar predicate-fn worklogs)))
|
||
(funcall rewrap-worklog-records-fn worklogs))))))
|
||
|
||
|
||
(defun jiralib-get-board (id &optional callback)
|
||
"Return details on given board"
|
||
(jiralib-call "getBoard" nil id))
|
||
|
||
(defun jiralib-get-boards ()
|
||
"Return list of jira boards"
|
||
(jiralib-call "getBoards" nil))
|
||
|
||
(defun jiralib-get-board-issues (board-id &rest params)
|
||
"Return list of jira issues in the specified jira board"
|
||
(apply 'jiralib-call "getIssuesFromBoard"
|
||
(cl-getf params :callback) board-id params))
|
||
|
||
(defun jiralib--agile-not-last-entry (num-entries total start-at limit)
|
||
"Return true if need to retrieve next page from agile api"
|
||
(and (> num-entries 0)
|
||
(or (not limit) ; not required to be set
|
||
(< limit 1) ; ignore invalid limit
|
||
(> limit start-at))
|
||
(or (not total) ; not always returned
|
||
(> total start-at))))
|
||
|
||
(defun jiralib--agile-limit-page-size (page-size start-at limit)
|
||
(if (and limit
|
||
(> (+ start-at page-size) limit))
|
||
(- limit start-at)
|
||
page-size))
|
||
|
||
|
||
(defun jiralib--agile-rest-call-it (api max-results start-at limit query-params)
|
||
(let ((callurl
|
||
(format "%s?%s" api
|
||
(url-build-query-string
|
||
(append `((maxResults ,(jiralib--agile-limit-page-size max-results start-at limit))
|
||
(startAt ,start-at))
|
||
query-params)))))
|
||
(jiralib--rest-call-it callurl)))
|
||
|
||
(defun jiralib--agile-call-it (api values-key &rest params)
|
||
"Invoke Jira agile method api and retrieve the results using
|
||
paging.
|
||
|
||
If JIRALIB-COMPLETE-CALLBACK is non-nil, then the call will be
|
||
performed asynchronously and JIRALIB-COMPLETE-CALLBACK will be
|
||
called when all data are retrieved.
|
||
|
||
If JIRALIB-COMPLETE-CALLBACK is nil, then the call will be
|
||
performed syncronously and this function will return the
|
||
retrieved data.
|
||
|
||
API - path to called API that must start with /rest/agile/1.0.
|
||
|
||
VALUES-KEY - key of the actual reply data in the reply assoc list.
|
||
|
||
PARAMS - optional additional parameters.
|
||
:limit - limit total number of retrieved entries.
|
||
:query-params - extra query parameters in the format of url-build-query-string.
|
||
"
|
||
(if jiralib-complete-callback
|
||
(apply 'jiralib--agile-call-async api values-key params)
|
||
(apply 'jiralib--agile-call-sync api values-key params)))
|
||
|
||
(defun jiralib--agile-call-sync (api values-key &rest params)
|
||
"Syncroniously invoke Jira agile method api retrieve all the
|
||
results using paging and return results.
|
||
|
||
VALUES-KEY - key of the actual reply data in the reply assoc list.
|
||
|
||
PARAMS - extra parameters (as keyword arguments), the supported parameters are:
|
||
|
||
:limit - limit total number of retrieved entries.
|
||
:query-params - extra query parameters in the format of url-build-query-string.
|
||
"
|
||
(setq jiralib-complete-callback nil)
|
||
(let ((not-last t)
|
||
(start-at 0)
|
||
(limit (getf params :limit))
|
||
(query-params (getf params :query-params))
|
||
;; maximum page size, 50 is server side maximum
|
||
(max-results jiralib-agile-page-size)
|
||
(values ()))
|
||
(while not-last
|
||
(let* ((reply-alist
|
||
(jiralib--agile-rest-call-it api max-results start-at limit query-params))
|
||
(values-array (cdr (assoc values-key reply-alist)))
|
||
(num-entries (length values-array))
|
||
(total (cdr (assq 'total reply-alist))))
|
||
(setf values (append values (append values-array nil)))
|
||
(setf start-at (+ start-at num-entries))
|
||
(setf not-last (jiralib--agile-not-last-entry num-entries total start-at limit))))
|
||
values))
|
||
|
||
(defun jiralib--agile-call-async (api values-key &rest params)
|
||
"Asyncroniously invoke Jira agile method api,
|
||
retrieve all the results using paging and call
|
||
JIRALIB-COMPLETE_CALLBACK when all the data are retrieved.
|
||
|
||
VALUES-KEY - key of the actual reply data in the reply assoc list.
|
||
|
||
PARAMS - extra parameters (as keyword arguments), the supported parameters are:
|
||
|
||
limit - limit total number of retrieved entries."
|
||
(lexical-let
|
||
((start-at 0)
|
||
(limit (getf params :limit))
|
||
(query-params (getf params :query-params))
|
||
;; maximum page size, 50 is server side maximum
|
||
(max-results jiralib-agile-page-size)
|
||
(values-list ())
|
||
(vk values-key)
|
||
(url api)
|
||
;; save the call back to be called later after the last page
|
||
(complete-callback jiralib-complete-callback))
|
||
;; setup new callback to be called after each page
|
||
(setf jiralib-complete-callback
|
||
(cl-function
|
||
(lambda (&rest data &allow-other-keys)
|
||
(condition-case err
|
||
(let* ((reply-alist (cl-getf data :data))
|
||
(values-array (cdr (assoc vk reply-alist)))
|
||
(num-entries (length values-array))
|
||
(total (cdr (assq 'total reply-alist))))
|
||
(setf values-list (append values-list (append values-array nil)))
|
||
(setf start-at (+ start-at num-entries))
|
||
(message "jiralib agile retrieve: got %d values%s%s"
|
||
start-at
|
||
(if total " of " "")
|
||
(if total (int-to-string total) ""))
|
||
(if (jiralib--agile-not-last-entry num-entries total start-at limit)
|
||
(jiralib--agile-rest-call-it url max-results start-at limit query-params)
|
||
;; last page: call originall callback
|
||
(message "jiralib agile retrieve: calling callback")
|
||
(setf jiralib-complete-callback complete-callback)
|
||
(funcall jiralib-complete-callback
|
||
:data (list (cons vk values-list)))
|
||
(message "jiralib agile retrieve: all done")))
|
||
('error (message (format "jiralib agile retrieve: caught error: %s" err)))))))
|
||
(jiralib--agile-rest-call-it api max-results start-at limit query-params)))
|
||
|
||
(provide 'jiralib)
|
||
;;; jiralib.el ends here
|