Merge pull request #153 from KrzysiekJ/auth-source-search-port
[org-jira.git] / jiralib.el
blob3c1860fc5a6839c9e597a9c52a5794303a36d93f
1 ;;; jiralib.el -- Provide connectivity to JIRA SOAP/REST services.
3 ;; Copyright (C) 2016,2017 Matthew Carter <m@ahungry.com>
4 ;; Copyright (C) 2011 Bao Haojun
5 ;; original Copyright (C) 2009 Alex Harsanyi
7 ;; Also, used some code from jira.el, which use xml-rpc instead of soap.
8 ;; Thus Copyright (C) for jira.el related code:
9 ;; Brian Zwahr <echosa@gmail.com>
10 ;; Dave Benjamin <dave@ramenlabs.com>
12 ;; Authors:
13 ;; Matthew Carter <m@ahungry.com>
14 ;; Bao Haojun <baohaojun@gmail.com>
15 ;; Alex Harsanyi <AlexHarsanyi@gmail.com>
17 ;; Maintainer: Matthew Carter <m@ahungry.com>
18 ;; Version: 3.0.0
19 ;; Homepage: https://github.com/ahungry/org-jira
21 ;; This file is not part of GNU Emacs.
23 ;; This program is free software: you can redistribute it and/or modify
24 ;; it under the terms of the GNU General Public License as published by
25 ;; the Free Software Foundation, either version 3 of the License, or
26 ;; (at your option) any later version.
28 ;; This program is distributed in the hope that it will be useful,
29 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
30 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 ;; GNU General Public License for more details.
33 ;; You should have received a copy of the GNU General Public License
34 ;; along with this program. If not, see
35 ;; <http://www.gnu.org/licenses/> or write to the Free Software
36 ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
37 ;; 02110-1301, USA.
39 ;; Author: Alexandru Harsanyi (AlexHarsanyi@gmail.com)
40 ;; Created: December, 2009
41 ;; Keywords: soap, web-services, jira
42 ;; Homepage: http://code.google.com/p/emacs-soap-client
44 ;;; Commentary:
46 ;; This file provides a programatic interface to JIRA. It provides access to
47 ;; JIRA from other programs, but no user level functionality.
49 ;; Jira References:
51 ;; Primary reference (on current Jira, only REST is supported):
52 ;; https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis
54 ;; Full API list reference:
55 ;; https://docs.atlassian.com/jira/REST/cloud/
57 ;; Legacy reference (unsupported and deprecated/unavailable):
58 ;; http://confluence.atlassian.com/display/JIRA/Creating+a+SOAP+Client
60 ;; JavaDoc for the Jira SOAP service
61 ;; http://docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html
63 ;;; News:
65 ;;;; Changes since 2.6.3:
66 ;; - Add worklog import filter and control variable for external worklogs.
67 ;; - Add the worklog related endpoint/calls.
69 ;;;; Changes since 2.1.0:
70 ;; - Remove os_username / os_password manual http request as part of sign in process
71 ;; This produces sysadmin level warnings on Jira when these are used under the latest Jira.
72 ;; - Remove unused function jiralib-link-issue
73 ;; - Bring version up to match org-jira version so they can share tag
75 ;;;; Changes since 2.0.0:
76 ;; - Allow issue type query by project
78 ;;;; Changes since 0.0.0:
79 ;; - Converted many calls to async
80 ;; - Converted many calls to make use of caching
82 ;;; Code:
84 (eval-when-compile (require 'cl))
85 (require 'soap-client)
86 (require 'request)
87 (require 'json)
88 (require 'url-parse)
89 (require 'url-util)
91 (defconst jiralib-version "3.0.0"
92 "Current version of jiralib.el.")
94 (defgroup jiralib nil
95 "Jiralib customization group."
96 :group 'applications)
98 (defgroup jiralib-faces nil
99 "Faces for displaying Jiralib information."
100 :group 'jiralib)
102 (defcustom jiralib-use-restapi t
103 "Use restapi instead of soap."
104 :group 'jiralib
105 :type 'boolean
106 :initialize 'custom-initialize-set)
108 (defcustom jiralib-host ""
109 "User customizable host name of the Jiralib server.
111 This will be used with USERNAME to compute password from
112 .authinfo file. Will be calculated from jiralib-url if not set."
113 :group 'jiralib
114 :type 'string
115 :initialize 'custom-initialize-set)
117 (defface jiralib-issue-info-face
118 '((t (:foreground "black" :background "yellow4")))
119 "Base face for issue information."
120 :group 'jiralib-faces)
122 (defface jiralib-issue-info-header-face
123 '((t (:bold t :inherit 'jiralib-issue-info-face)))
124 "Base face for issue headers."
125 :group 'jiralib-faces)
127 (defface jiralib-issue-summary-face
128 '((t (:bold t)))
129 "Base face for issue summary."
130 :group 'jiralib-faces)
132 (defface jiralib-comment-face
133 '((t (:background "gray23")))
134 "Base face for comments."
135 :group 'jiralib-faces)
137 (defface jiralib-comment-header-face
138 '((t (:bold t)))
139 "Base face for comment headers."
140 :group 'jiralib-faces)
142 (defface jiralib-link-issue-face
143 '((t (:underline t)))
144 "Face for linked issues."
145 :group 'jiralib-faces)
147 (defface jiralib-link-project-face
148 '((t (:underline t)))
149 "Face for linked projects"
150 :group 'jiralib-faces)
152 (defface jiralib-link-filter-face
153 '((t (:underline t)))
154 "Face for linked filters"
155 :group 'jiralib-faces)
157 (defvar jiralib-mode-hook nil)
158 (defvar jiralib-mode-map nil)
159 (defvar jiralib-issue-regexp "\\<\\(?:[A-Za-z0-9]+\\)-[0-9]+\\>")
161 (defcustom jiralib-wsdl-descriptor-url
163 "The location for the WSDL descriptor for the JIRA service.
164 This is specific to your local JIRA installation. The URL is
165 typically:
167 http://YOUR_INSTALLATION/rpc/soap/jirasoapservice-v2?wsdl
169 The default value works if JIRA is located at a hostname named
170 'jira'."
171 :type 'string
172 :group 'jiralib)
174 (defcustom jiralib-url
175 "http://localhost:8081/"
176 "The address of the jira host."
177 :type 'string
178 :group 'jiralib)
180 (defcustom jiralib-agile-page-size
182 "Page size for agile API retrieve. Limited by server property jira.search.views.default.max"
183 :type 'integer
184 :group 'jiralib)
186 (defvar jiralib-token nil
187 "JIRA token used for authentication.")
189 (defvar jiralib-rest-auth-head nil
190 "JIRA restapi auth head.")
192 (defvar jiralib-user-login-name nil
193 "The name of the user logged into JIRA.
194 This is maintained by `jiralib-login'.")
196 (defvar jiralib-wsdl nil)
198 (defcustom jiralib-worklog-import--filters-alist
199 (list
200 '(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))))
201 '(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)))))
202 "A list of triplets: ('Global-Enable 'Descriptive-Label 'Function-Definition)
203 that apply worklog predicate filters during import.
205 Example: (list '('t \"descriptive-predicate-label\" (lambda (x) x)))"
206 :type '(repeat (list boolean string function))
207 :group 'org-jira)
211 (defun jiralib-load-wsdl ()
212 "Load the JIRA WSDL descriptor."
213 (setq jiralib-wsdl (soap-load-wsdl-from-url (if (string-equal jiralib-wsdl-descriptor-url "")
214 (concat jiralib-url "/rpc/soap/jirasoapservice-v2?wsdl")
215 jiralib-wsdl-descriptor-url))))
217 (defun jiralib-login (username password)
218 "Login into JIRA as user USERNAME with PASSWORD.
220 After a successful login, store the authentication token in
221 `jiralib-token'."
222 ;; NOTE that we cannot rely on `jiralib-call' because `jiralib-call' relies on
223 ;; us ;-)
224 (interactive
225 (if (> 24 emacs-major-version)
226 (let ((user (read-string "Username for Jira server login? "))
227 (password (read-passwd "Password for Jira server login? ")))
228 (list user password))
229 (let ((found (nth 0 (auth-source-search :max 1
230 :host (if (string= jiralib-host "")
231 (url-host (url-generic-parse-url jiralib-url))
232 jiralib-host)
233 ;; secrets.el wouldn’t accept a number.
234 :port (number-to-string (url-port (url-generic-parse-url jiralib-url)))
235 :require '(:user :secret)
236 :create t)))
237 user secret)
238 (when found
239 (setq user (plist-get found :user)
240 secret
241 (let ((sec (plist-get found :secret)))
242 (if (functionp sec)
243 (funcall sec)
244 sec)))
245 (list user secret)))))
246 (if jiralib-use-restapi
247 (setq jiralib-token `("Authorization" . , (format "Basic %s" (base64-encode-string (concat username ":" password)))))
248 (unless jiralib-wsdl
249 (jiralib-load-wsdl))
250 (setq jiralib-token
251 (car (soap-invoke jiralib-wsdl "jirasoapservice-v2" "login" username password))))
252 (setq jiralib-user-login-name username))
254 (defvar jiralib-complete-callback nil)
256 (defun jiralib-call (method callback &rest params)
257 "Invoke the Jira METHOD, then CALLBACK with supplied PARAMS.
259 This function should be used for all JIRA interface calls, as the
260 method ensures the user is logged in and invokes `soap-invoke'
261 with the correct service name and authentication token.
263 All JIRA interface methods take an authentication token as the
264 first argument. The authentication token is supplied by this
265 function, so PARAMS should omit this parameter. For example, the
266 \"getIssue\" method takes two parameters: auth and key, however,
267 when invoking it through `jiralib-call', the call should be:
269 (jiralib-call \"getIssue\" KEY)
271 CALLBACK should be the post processing function to run with the
272 completed data from the request result, which can be accessed with:
274 (getf data :data)
276 as such, the CALLBACK should follow this type of form:
278 (cl-function
279 (lambda (&rest data &allow-other-keys)
280 (print (getf data :data))))
282 If CALLBACK is set to nil then the request will occur with sync.
283 This produces a noticeable slowdown and is not recommended by
284 request.el, so if at all possible, it should be avoided."
285 ;; @todo :auth: Probably pass this all the way down, but I think
286 ;; it may be OK at the moment to just set the variable each time.
287 (setq jiralib-complete-callback
288 ;; Don't run with async if we don't have a login token yet.
289 (if jiralib-token callback nil))
291 ;; If we don't have a regex set, ensure it is set BEFORE any async
292 ;; calls are processing, or we're going to have a bad time.
293 ;; This should only end up running once per session.
294 (unless jiralib-issue-regexp
295 (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e))))
296 (append (jiralib--rest-call-it
297 "/rest/api/2/project"
298 :params '((expand . "description,lead,url,projectKeys")))
299 nil)
301 (when projects
302 (setq jiralib-issue-regexp
303 (concat "\\<" (regexp-opt projects) "-[0-9]+\\>")))))
305 (if (not jiralib-use-restapi)
306 (car (apply 'jiralib--call-it method params))
307 (unless jiralib-token
308 (call-interactively 'jiralib-login))
309 (case (intern method)
310 ('getStatuses (jiralib--rest-call-it "/rest/api/2/status"))
311 ('getIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype"))
312 ('getIssueTypesByProject
313 (let ((response (jiralib--rest-call-it (format "/rest/api/2/project/%s" (first params)))))
314 (cl-coerce (cdr (assoc 'issueTypes response)) 'list)))
315 ('getUser (jiralib--rest-call-it "/rest/api/2/user" :params `((username . ,(first params)))))
316 ('getVersions (jiralib--rest-call-it (format "/rest/api/2/project/%s/versions" (first params))))
318 ;; Worklog calls
319 ('getWorklogs
320 (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))))
322 ('addWorklog
323 (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))
324 :type "POST"
325 :data (json-encode (second params))))
327 ('updateWorklog
328 (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog/%s" (first params) (second params))
329 :type "PUT"
330 :data (json-encode (third params))))
332 ('addWorklogAndAutoAdjustRemainingEstimate
333 (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params))
334 :type "POST"
335 :data (json-encode (second params))))
337 ('addComment (jiralib--rest-call-it
338 (format "/rest/api/2/issue/%s/comment" (first params))
339 :type "POST"
340 :data (json-encode (second params))))
341 ('createIssue
342 ;; Creating the issue doesn't return it, a second call must be
343 ;; made to pull it in by using the self key in response.
344 (let ((response (jiralib--rest-call-it
345 "/rest/api/2/issue"
346 :type "POST"
347 :data (json-encode (first params)))))
348 (jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET")
350 ('createIssueWithParent (jiralib--rest-call-it
352 ('editComment (jiralib--rest-call-it
353 (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params))
354 :data (json-encode `((body . ,(third params))))
355 :type "PUT"))
356 ('getBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s" (first params))))
357 ('getBoards (apply 'jiralib--agile-call-it "/rest/agile/1.0/board" 'values params))
358 ('getComment (org-jira-find-value
359 (jiralib--rest-call-it
360 (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params)))
361 'comments))
362 ('getComments (org-jira-find-value
363 (jiralib--rest-call-it
364 (format "/rest/api/2/issue/%s/comment" (first params)))
365 'comments))
366 ('getAttachmentsFromIssue (org-jira-find-value
367 (jiralib--rest-call-it
368 (format "/rest/api/2/issue/%s?fields=attachment" (first params)))
369 'comments))
370 ('getComponents (jiralib--rest-call-it
371 (format "/rest/api/2/project/%s/components" (first params))))
372 ('getIssue (jiralib--rest-call-it
373 (format "/rest/api/2/issue/%s" (first params))))
374 ('getIssuesFromBoard (apply 'jiralib--agile-call-it
375 (format "rest/agile/1.0/board/%d/issue" (first params))
376 'issues
377 (cdr params)))
378 ('getIssuesFromJqlSearch (append (cdr ( assoc 'issues (jiralib--rest-call-it
379 "/rest/api/2/search"
380 :type "POST"
381 :data (json-encode `((jql . ,(first params))
382 (maxResults . ,(second params)))))))
383 nil))
384 ('getPriorities (jiralib--rest-call-it
385 "/rest/api/2/priority"))
386 ('getProjects (jiralib--rest-call-it "rest/api/2/project"))
387 ('getProjectsNoSchemes (append (jiralib--rest-call-it
388 "/rest/api/2/project"
389 :params '((expand . "description,lead,url,projectKeys"))) nil))
390 ('getResolutions (append (jiralib--rest-call-it
391 "/rest/api/2/resolution") nil))
392 ('getAvailableActions
393 (mapcar
394 (lambda (trans)
395 `(,(assoc 'name trans) ,(assoc 'id trans)))
396 (cdadr (jiralib--rest-call-it (format "/rest/api/2/issue/%s/transitions" (first params))))))
397 ('getFieldsForAction (org-jira-find-value (car (let ((issue (first params))
398 (action (second params)))
399 (seq-filter (lambda (trans)
400 (or (string-equal action (org-jira-find-value trans 'id))
401 (string-equal action (org-jira-find-value trans 'name))))
402 (cdadr (jiralib--rest-call-it
403 (format "/rest/api/2/issue/%s/transitions" (first params))
404 :params '((expand . "transitions.fields")))))))
405 'fields))
406 ('progressWorkflowAction (jiralib--rest-call-it
407 (format "/rest/api/2/issue/%s/transitions" (first params))
408 :type "POST"
409 :data (json-encode `(,(car (second params)) ,(car (third params))))))
410 ('getUsers
411 (jiralib--rest-call-it (format "/rest/api/2/user/assignable/search?project=%s&maxResults=10000" (first params))
412 :type "GET"))
413 ('updateIssue (jiralib--rest-call-it
414 (format "/rest/api/2/issue/%s" (first params))
415 :type "PUT"
416 :data (json-encode `((fields . ,(second params)))))))))
418 (defun jiralib--soap-call-it (&rest args)
419 "Deprecated SOAP call endpoint. Will be removed soon.
420 Pass ARGS to jiralib-call."
421 (let ((jiralib-token nil)
422 (jiralib-use-restapi nil))
423 (apply #'jiralib-call args)))
425 (defun jiralib--rest-call-it (api &rest args)
426 "Invoke the corresponding jira rest method API.
427 Invoking COMPLETE-CALLBACK when the
428 JIRALIB-COMPLETE-CALLBACK is non-nil, request finishes, and
429 passing ARGS to REQUEST."
430 (append (request-response-data
431 (apply #'request (if (string-match "^http[s]*://" api) api ;; If an absolute path, use it
432 (concat (replace-regexp-in-string "/*$" "/" jiralib-url)
433 (replace-regexp-in-string "^/*" "" api)))
434 :sync (not jiralib-complete-callback)
435 :headers `(,jiralib-token ("Content-Type" . "application/json"))
436 :parser 'json-read
437 :complete jiralib-complete-callback
438 args))
439 nil))
441 (defun jiralib--call-it (method &rest params)
442 "Invoke the JIRA METHOD with supplied PARAMS.
444 Internal use, returns a list of responses, of which only the
445 first is normally used."
446 (when (symbolp method)
447 (setq method (symbol-name method)))
448 (unless jiralib-token
449 (call-interactively 'jiralib-login))
450 (condition-case data
451 (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2"
452 method jiralib-token params)
453 (soap-error
454 ;; If we are here, we had a token, but it expired. Re-login and try
455 ;; again.
456 (setq jiralib-token nil)
457 (call-interactively 'jiralib-login)
458 (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2"
459 method jiralib-token params))))
462 ;;;; Some utility functions
464 (defun jiralib-make-list (data field)
465 "Map all assoc elements in DATA to the value of FIELD in that element."
466 (loop for element in data
467 collect (cdr (assoc field element))))
469 (defun jiralib-make-assoc-list (data key-field value-field)
470 "Create an association list from a SOAP structure array.
472 DATA is a list of association lists (a SOAP array-of type)
473 KEY-FIELD is the field to use as the key in the returned alist
474 VALUE-FIELD is the field to use as the value in the returned alist"
475 (loop for element in data
476 collect (cons (cdr (assoc key-field element))
477 (cdr (assoc value-field element)))))
479 (defun jiralib-make-remote-field-values (fields)
480 "Transform the (KEY . VALUE) list FIELDS into a RemoteFieldValue structure.
482 Each (KEY . VALUE) pair is transformed into
483 ((id . KEY) (values . (VALUE)))
485 This method exists because Several JIRA methods require a
486 RemoteFieldValue list, but it is easier to work with ALISTS in
487 emacs-lisp"
488 (let ((remote-field-values))
490 ;; we accept an ALIST of field-name field-values parameter, but we need to
491 ;; construct a structure that encodes as a RemoteFieldValue which is what
492 ;; updateIssue wants
493 (dolist (field fields)
494 (let ((name (car field))
495 (value (cdr field)))
496 (when (symbolp name)
497 (setq name (symbol-name name)))
498 ;; Value must be an "array" (for which soap-client accepts lists) even
499 ;; if it is just one value
500 (unless (vectorp value)
501 (setq value (vector value)))
502 (push `((id . ,name) (values . ,value))
503 remote-field-values)))
505 (apply 'vector (nreverse remote-field-values))))
507 ;;;; Wrappers around JIRA methods
509 (defun jiralib--rest-api-for-issue-key (key)
510 "Return jira rest api for issue KEY."
511 (concat "rest/api/2/issue/" key))
513 (defun jiralib-update-issue (key fields &optional callback)
514 "Update the issue with id KEY with the values in FIELDS, invoking CALLBACK."
515 (jiralib-call
516 "updateIssue"
517 callback
518 key (if jiralib-use-restapi
519 fields
520 (jiralib-make-remote-field-values fields))))
522 (defvar jiralib-status-codes-cache nil)
524 (defun jiralib-get-statuses ()
525 "Return an assoc list mapping a status code to its name.
526 NOTE: Status codes are stored as strings, not numbers.
528 This function will only ask JIRA for the list of codes once, then
529 will cache it."
530 (unless jiralib-status-codes-cache
531 (setq jiralib-status-codes-cache
532 (jiralib-make-assoc-list (jiralib-call "getStatuses" nil) 'id 'name)))
533 jiralib-status-codes-cache)
535 (defvar jiralib-issue-types-cache nil)
537 (defun jiralib-get-issue-types ()
538 "Return an assoc list mapping an issue type code to its name.
539 NOTE: Issue type codes are stored as strings, not numbers.
541 This function will only ask JIRA for the list of codes once, than
542 will cache it.
544 The issue types returned via getIssueTypes are all the ones
545 available to the user, but not necessarily available to the given
546 project.
548 This endpoint is essentially a master reference for when issue
549 types need a name lookup when given an id.
551 For applying issue types to a given project that is being created, see
552 the #'jiralib-get-issue-types-by-project call."
553 (unless jiralib-issue-types-cache
554 (setq jiralib-issue-types-cache
555 (jiralib-make-assoc-list (jiralib-call "getIssueTypes" nil) 'id 'name)))
556 jiralib-issue-types-cache)
558 (defvar jiralib-issue-types-by-project-cache nil "An alist of available issue types.")
560 (defun jiralib-get-issue-types-by-project (project)
561 "Return the available issue types for PROJECT.
563 PROJECT should be the key, such as `EX' or `DEMO'."
564 (unless (assoc project jiralib-issue-types-by-project-cache)
565 (push (cons project
566 (jiralib-make-assoc-list
567 (jiralib-call "getIssueTypesByProject" nil project)
568 'id 'name))
569 jiralib-issue-types-by-project-cache))
570 (cdr (assoc project jiralib-issue-types-by-project-cache)))
572 (defvar jiralib-priority-codes-cache nil)
574 (defun jiralib-get-priorities ()
575 "Return an assoc list mapping a priority code to its name.
576 NOTE: Priority codes are stored as strings, not numbers.
578 This function will only ask JIRA for the list of codes once, than
579 will cache it."
580 (unless jiralib-priority-codes-cache
581 (setq jiralib-priority-codes-cache
582 (jiralib-make-assoc-list (jiralib-call "getPriorities" nil) 'id 'name)))
583 jiralib-priority-codes-cache)
585 (defvar jiralib-resolution-code-cache nil)
587 (defun jiralib-get-resolutions ()
588 "Return an assoc list mapping a resolution code to its name.
589 NOTE: Resolution codes are stored as strings, not numbers.
591 This function will only ask JIRA for the list of codes once, than
592 will cache it."
593 (unless jiralib-resolution-code-cache
594 (setq jiralib-resolution-code-cache
595 (jiralib-make-assoc-list (jiralib-call "getResolutions" nil) 'id 'name)))
596 jiralib-resolution-code-cache)
598 ;; NOTE: it is not such a good idea to use this, as it needs a JIRA
599 ;; connection to construct the regexp (the user might be prompted for a JIRA
600 ;; username and password).
602 ;; The best use of this function is to generate the regexp once-off and
603 ;; persist it somewhere.
605 ;; FIXME: Probably just deprecate/remove this, we can assert we're on
606 ;; an issue with a general regexp that matches the common format, vs
607 ;; needing to know specific user project list.
608 (defun jiralib-get-issue-regexp ()
609 "Return a regexp that will match an issue id.
611 The regexp is constructed from the project keys in the JIRA
612 database. An issue is assumed to be in the format KEY-NUMBER,
613 where KEY is a project key and NUMBER is the issue number."
614 (unless jiralib-issue-regexp
615 (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e))))
616 (jiralib-call "getProjectsNoSchemes" nil))))
617 (when projects
618 (setq jiralib-issue-regexp
619 (concat "\\<" (regexp-opt projects) "-[0-9]+\\>")))))
620 jiralib-issue-regexp)
622 (defun jiralib-do-jql-search (jql &optional limit callback)
623 "Run a JQL query and return the list of issues that matched.
624 LIMIT is the maximum number of queries to return. Note that JIRA
625 has an internal limit of how many queries to return, as such, it
626 might not be possible to find *ALL* the issues that match a
627 query."
628 (unless (or limit (numberp limit))
629 (setq limit 100))
630 (jiralib-call "getIssuesFromJqlSearch" callback jql limit))
632 (defcustom jiralib-available-actions-cache-p t
633 "Set to t to enable caching for jiralib-get-available-actions.
635 If nil, will disable caching for this endpoint.
637 Possible side-effects:
639 - If the server has the project workflow updated, the cache
640 saved here will be incorrect.
642 - If the issue is not up to date with the remote, the wrong
643 cache key may be queried."
644 :type 'boolean
645 :group 'jiralib)
647 (defvar jiralib-available-actions-cache nil "An alist of available actions.")
649 (defun jiralib-get-available-actions (issue-key &optional status)
650 "Return the available workflow actions for ISSUE-KEY.
651 This uses STATUS as the cache key.
652 This runs the getAvailableActions SOAP method."
653 (if (and jiralib-available-actions-cache-p status)
654 (progn
655 (unless (assoc status jiralib-available-actions-cache)
656 (push (cons status
657 (jiralib-make-assoc-list
658 (mapcar (lambda (x)
659 (let ((namestring (cdr (car x)))
660 (id (cdr x)))
661 (cons
662 (cons 'name (org-jira-decode namestring))
663 id)))
664 (jiralib-call "getAvailableActions" nil issue-key))
665 'id 'name))
666 jiralib-available-actions-cache))
667 (cdr (assoc status jiralib-available-actions-cache)))
668 (progn
669 (jiralib-make-assoc-list
670 (mapcar (lambda (x)
671 (let ((namestring (cdr (car x)))
672 (id (cdr x)))
673 (cons
674 (cons 'name (org-jira-decode namestring))
675 id)))
676 (jiralib-call "getAvailableActions" nil issue-key))
677 'id 'name))))
679 (defcustom jiralib-fields-for-action-cache-p t
680 "Set to t to enable caching for jiralib-get-fields-for-action.
682 If nil, will disable caching for this endpoint.
684 Possible side-effects:
686 - If many tasks have different workflows, you may want to disable this."
687 :type 'boolean
688 :group 'jiralib)
690 (defvar jiralib-fields-for-action-cache nil "An alist of available fields.")
692 (defun jiralib-get-fields-for-action-with-cache (issue-key action-id)
693 "Return the required fields to change ISSUE-KEY to ACTION-ID."
694 (if (and jiralib-fields-for-action-cache-p action-id)
695 (progn
696 (unless (assoc action-id jiralib-fields-for-action-cache)
697 (push (cons action-id
698 (jiralib-call "getFieldsForAction" nil issue-key action-id))
699 jiralib-fields-for-action-cache))
700 (cdr (assoc action-id jiralib-fields-for-action-cache)))
701 (jiralib-call "getFieldsForAction" nil issue-key action-id)))
703 (defun jiralib-get-fields-for-action (issue-key action-id)
704 "Return the required fields to change ISSUE-KEY to ACTION-ID."
705 (if jiralib-use-restapi
706 (let ((fields (jiralib-get-fields-for-action-with-cache issue-key action-id)))
707 (mapcar (lambda (field)
708 (cons (symbol-name (car field))
709 (format "%s (required: %s)"
710 (org-jira-find-value field 'name)
711 (if (eq (org-jira-find-value field 'required) :json-false)
712 "nil"
713 "t"))))
714 fields))
715 (jiralib-make-assoc-list
716 (jiralib-get-fields-for-action-with-cache issue-key action-id)
717 'id 'name)))
719 (defun jiralib-progress-workflow-action (issue-key action-id params &optional callback)
720 "Progress issue with ISSUE-KEY to action ACTION-ID, and provide the needed PARAMS.
722 When CALLBACK is present, this will run async."
723 (if jiralib-use-restapi
724 (jiralib-call "progressWorkflowAction"
725 callback issue-key `((transition (id . ,action-id)))
726 `((fields . ,params)))
727 (jiralib-call "progressWorkflowAction"
728 callback issue-key action-id (jiralib-make-remote-field-values params))))
731 (defun jiralib-format-datetime (&optional datetime)
732 "Convert a mixed DATETIME format into the Jira required datetime format.
734 This will produce a datetime string such as:
736 2010-02-05T14:30:00.000+0000
738 for being consumed in the Jira API.
740 If DATETIME is not passed in, it will default to the current time."
741 (let* ((defaults (format-time-string "%Y-%m-%d %H:%M:%S" (current-time)))
742 (datetime (concat datetime (subseq defaults (length datetime))))
743 (parts (parse-time-string datetime)))
744 (format "%04d-%02d-%02dT%02d:%02d:%02d.000+0000"
745 (nth 5 parts)
746 (nth 4 parts)
747 (nth 3 parts)
748 (nth 2 parts)
749 (nth 1 parts)
750 (nth 0 parts))))
752 (defvar jiralib-worklog-coming-soon-message
753 "WORKLOG FEATURES ARE NOT IMPLEMENTED YET, COMING SOON!")
755 (defun jiralib-add-worklog-and-autoadjust-remaining-estimate (issue-key start-date time-spent comment)
756 "Log time spent on ISSUE-KEY to its worklog.
757 The time worked begins at START-DATE and has a TIME-SPENT
758 duration. JIRA will automatically update the remaining estimate
759 by subtracting TIME-SPENT from it.
761 START-DATE should be in the format 2010-02-05T14:30:00Z
763 TIME-SPENT can be in one of the following formats: 10m, 120m
764 hours; 10h, 120h days; 10d, 120d weeks.
766 COMMENT will be added to this worklog."
767 (let ((formatted-start-date (jiralib-format-datetime start-date)))
768 (jiralib-call "addWorklogAndAutoAdjustRemainingEstimate"
770 issue-key
771 ;; Expects data such as: '{"timeSpent":"1h", "started":"2017-02-21T00:00:00.000+0000", "comment":"woot!"}'
772 ;; and only that format will work (no loose formatting on the started date)
773 `((started . ,formatted-start-date)
774 (timeSpent . ,time-spent)
775 (comment . ,comment)))))
778 ;;;; Issue field accessors
780 (defun jiralib-issue-key (issue)
781 "Return the key of ISSUE."
782 (cdr (assoc 'key issue)))
784 (defun jiralib-issue-owner (issue)
785 "Return the owner of ISSUE."
786 (cdr (assq 'assignee issue)))
788 (defun jiralib-issue-status (issue)
789 "Return the status of ISSUE as a status string (not as a number!)."
790 (let ((status-code (cdr (assq 'status issue))))
791 (cdr (assoc status-code (jiralib-get-statuses)))))
793 (defun jiralib-custom-field-value (custom-field issue)
794 "Return the value of CUSTOM-FIELD for ISSUE.
795 Return nil if the field is not found"
796 (catch 'found
797 (dolist (field (cdr (assq 'customFieldValues issue)))
798 (when (equal (cdr (assq 'customfieldId field)) custom-field)
799 (throw 'found (cadr (assq 'values field)))))))
801 (defvar jiralib-current-issue nil
802 "This holds the currently selected issue.")
804 (defvar jiralib-projects-list nil
805 "This holds a list of projects and their details.")
807 (defvar jiralib-types nil
808 "This holds a list of issues types.")
810 (defvar jiralib-priorities nil
811 "This holds a list of priorities.")
813 (defvar jiralib-user-fullnames nil
814 "This holds a list of user fullnames.")
816 (defun jiralib-get-project-name (key)
817 "Return the name of the JIRA project with id KEY."
818 (let ((projects jiralib-projects-list)
819 (name nil))
820 (dolist (project projects)
821 (if (equal (cdr (assoc 'key project)) key)
822 (setf name (cdr (assoc 'name project)))))
823 name))
825 (defun jiralib-get-type-name (id)
826 "Return the name of the issue type with ID."
827 (let ((types jiralib-types)
828 (name nil))
829 (dolist (type types)
830 (if (equal (cdr (assoc 'id type)) id)
831 (setf name (cdr (assoc 'name type)))))
832 name))
834 (defun jiralib-get-user-fullname (username)
835 "Return the full name (display name) of the user with USERNAME."
836 (if (assoc username jiralib-user-fullnames)
837 (cdr (assoc username jiralib-user-fullnames))
838 (progn
839 (let ((user (jiralib-get-user username)))
840 (setf jiralib-user-fullnames (append jiralib-user-fullnames (list (cons username (cdr (assoc 'fullname user))))))
841 (cdr (assoc 'fullname user))))))
844 (defun jiralib-get-filter (filter-id)
845 "Return a filter given its FILTER-ID."
846 (cl-flet ((id-match (filter)
847 (equal filter-id (cdr (assoc 'id filter)))))
848 (cl-find-if 'id-match (jiralib-get-saved-filters))))
850 (defun jiralib-get-filter-alist ()
851 "Return an association list mapping filter names to IDs."
852 (mapcar (lambda (filter)
853 (cons (cdr (assoc 'name filter))
854 (cdr (assoc 'id filter))))
855 (jiralib-get-saved-filters)))
857 (defun jiralib-add-comment (issue-key comment &optional callback)
858 "Add to issue with ISSUE-KEY the given COMMENT, invoke CALLBACK."
859 (jiralib-call "addComment" callback issue-key `((body . ,comment))))
861 (defun jiralib-edit-comment (issue-id comment-id comment &optional callback)
862 "Edit ISSUE-ID's comment COMMENT-ID to reflect the new COMMENT, invoke CALLBACK."
863 (if (not jiralib-use-restapi)
864 (jiralib-call "editComment" callback `((id . ,comment-id)
865 (body . ,comment)))
866 (jiralib-call "editComment" callback issue-id comment-id comment)))
868 (defun jiralib-create-issue (issue)
869 "Create a new ISSUE in JIRALIB.
871 ISSUE is a Hashtable object."
872 (jiralib-call "createIssue" nil issue))
874 (defun jiralib-create-subtask (subtask parent-issue-id)
875 "Create SUBTASK for issue with PARENT-ISSUE-ID.
877 SUBTASK is a Hashtable object."
878 (jiralib-call "createIssueWithParent" nil subtask parent-issue-id))
881 (defvar jiralib-subtask-types-cache nil)
883 (defun jiralib-get-subtask-types ()
884 "Return an assoc list mapping an issue type code to its name.
885 NOTE: Issue type codes are stored as strings, not numbers.
887 This function will only ask JIRA for the list of codes once, than
888 will cache it."
889 (unless jiralib-subtask-types-cache
890 (setq jiralib-subtask-types-cache
891 (jiralib-make-assoc-list (jiralib-call "getSubTaskIssueTypes" nil) 'id 'name)))
892 jiralib-subtask-types-cache)
894 (defun jiralib-get-comment (issue-key comment-id &optional callback)
895 "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK."
896 (jiralib-call "getComment" callback issue-key comment-id))
898 (defun jiralib-get-comments (issue-key &optional callback)
899 "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK."
900 (jiralib-call "getComments" callback issue-key))
902 (defun jiralib-get-attachments (issue-key &optional callback)
903 "Return all attachments associated with issue ISSUE-KEY, invoking CALLBACK."
904 (jiralib-call "getAttachmentsFromIssue" callback issue-key))
906 (defun jiralib-get-worklogs (issue-key &optional callback)
907 "Return all worklogs associated with issue ISSUE-KEY, invoking CALLBACK."
908 (jiralib-call "getWorklogs" callback issue-key))
910 (defun jiralib-add-worklog (issue-id started time-spent-seconds comment &optional callback)
911 "Add the worklog linked to ISSUE-ID.
913 Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT.
914 CALLBACK will be invoked if passed in upon endpoint completion."
915 ;; Call will fail if 0 seconds are set as the time, so always do at least one min.
916 (setq time-spent-seconds (max 60 time-spent-seconds))
917 (let ((worklog `((started . ,started)
918 ;; @todo :worklog: timeSpentSeconds changes into incorrect values
919 ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days...
920 ;; We should convert to a Xd Xh Xm format from our seconds ourselves.
921 (timeSpentSeconds . ,time-spent-seconds)
922 (comment . ,comment))))
923 (jiralib-call "addWorklog" callback issue-id worklog)))
925 (defun jiralib-update-worklog (issue-id worklog-id started time-spent-seconds comment &optional callback)
926 "Update the worklog linked to ISSUE-ID and WORKLOG-ID.
928 Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT.
929 CALLBACK will be invoked if passed in upon endpoint completion."
930 ;; Call will fail if 0 seconds are set as the time, so always do at least one min.
931 (setq time-spent-seconds (max 60 time-spent-seconds))
932 (let ((worklog `((started . ,started)
933 ;; @todo :worklog: timeSpentSeconds changes into incorrect values
934 ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days...
935 ;; We should convert to a Xd Xh Xm format from our seconds ourselves.
936 (timeSpentSeconds . ,time-spent-seconds)
937 (comment . ,comment))))
938 (jiralib-call "updateWorklog" callback issue-id worklog-id worklog)))
940 (defvar jiralib-components-cache nil "An alist of project components.")
942 (defun jiralib-get-components (project-key)
943 "Return all components available in the project PROJECT-KEY."
944 (unless (assoc project-key jiralib-components-cache)
945 (push (cons project-key
946 (jiralib-make-assoc-list
947 (jiralib-call "getComponents" nil project-key) 'id 'name))
948 jiralib-components-cache))
949 (cdr (assoc project-key jiralib-components-cache)))
951 (defun jiralib-get-issue (issue-key &optional callback)
952 "Get the issue with key ISSUE-KEY, running CALLBACK after."
953 (jiralib-call "getIssue" callback issue-key))
955 (defun jiralib-get-issues-from-filter (filter-id)
956 "Get the issues from applying saved filter FILTER-ID."
957 (message "jiralib-get-issues-from-filter is NOT IMPLEMENTED!! Do not use!")
958 (jiralib-call "getIssuesFromFilter" nil filter-id))
960 (defun jiralib-get-issues-from-text-search (search-terms)
961 "Find issues using free text search SEARCH-TERMS."
962 (jiralib-call "getIssuesFromTextSearch" nil search-terms))
964 (defun jiralib-get-issues-from-text-search-with-project
965 (project-keys search-terms max-num-results)
966 "Find issues in projects PROJECT-KEYS, using free text search SEARCH-TERMS.
968 Return no more than MAX-NUM-RESULTS."
969 (jiralib-call "getIssuesFromTextSearchWithProject"
971 (apply 'vector project-keys) search-terms max-num-results))
973 ;; Modified by Brian Zwahr to use getProjectsNoSchemes instead of getProjects
974 (defun jiralib-get-projects ()
975 "Return a list of projects available to the user."
976 (if jiralib-projects-list
977 jiralib-projects-list
978 (setq jiralib-projects-list
979 (if jiralib-use-restapi
980 (jiralib-call "getProjects" nil)
981 (jiralib-call "getProjectsNoSchemes" nil)))))
983 (defun jiralib-get-saved-filters ()
984 "Get all saved filters available for the currently logged in user."
985 (jiralib-make-assoc-list (jiralib-call "getSavedFilters" nil) 'id 'name))
987 (defun jiralib-get-server-info ()
988 "Return the Server information such as baseUrl, version, edition, buildDate, buildNumber."
989 (jiralib-call "getServerInfo" nil))
991 (defun jiralib-get-sub-task-issue-types ()
992 "Return all visible subtask issue types in the system."
993 (jiralib-call "getSubTaskIssueTypes" nil))
995 (defun jiralib-get-user (username)
996 "Return a user's information given their USERNAME."
997 (cond ((eq 0 (length username)) nil) ;; Unassigned
998 (t (jiralib-call "getUser" nil username))))
1000 (defvar jiralib-users-cache nil "Cached list of users.")
1002 (defun jiralib-get-users (project-key)
1003 "Return assignable users information given the PROJECT-KEY."
1004 (unless jiralib-users-cache
1005 (setq jiralib-users-cache
1006 (jiralib-call "getUsers" nil project-key)))
1007 jiralib-users-cache)
1009 (defun jiralib-get-versions (project-key)
1010 "Return all versions available in project PROJECT-KEY."
1011 (jiralib-call "getVersions" nil project-key))
1013 (defun jiralib-strip-cr (string)
1014 "Remove carriage returns from STRING."
1015 (when string (replace-regexp-in-string "\r" "" string)))
1017 (defun jiralib-worklog-import--filter-apply
1018 (worklog-obj &optional predicate-fn-lst unwrap-worklog-records-fn rewrap-worklog-records-fn)
1019 "Remove non-matching org-jira issue worklogs.
1021 Variables:
1022 WORKLOG-OBJ is the passed in object
1023 PREDICATE-FN-LST is the list of lambdas used as match predicates.
1024 UNWRAP-WORKLOG-RECORDS-FN is the function used to produce the list of worklog records from within the worklog-obj
1025 REWRAP-WORKLOG-RECORDS-FN is the function used to reshape the worklog records back into the form they were received in.
1027 Auxiliary Notes:
1028 Only the WORKLOG-OBJ variable is required.
1029 The value of PPREDICATE-FN-LST is filled from the jiralib-worklog-import--filters-alist variable by default.
1030 If PREDICATE-FN-LST is empty the unmodified value of WORKLOG-OBJ is returned.
1031 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.
1032 The variable 'jiralib-user-login-name is used by many lambda filters."
1034 (let
1035 ((unwrap-worklog-records-fn)
1036 (rewrap-worklog-records-fn)
1037 (predicate-fn-lst)
1038 (worklogs worklog-obj)
1039 (predicate-fn))
1040 ;; let-body
1041 (progn
1042 (setq unwrap-worklog-records-fn
1043 (if (and
1044 (boundp 'unwrap-worklog-records-fn)
1045 (functionp unwrap-worklog-records-fn))
1046 unwrap-worklog-records-fn
1047 (lambda (x) (coerce x 'list))))
1048 (setq rewrap-worklog-records-fn
1049 (if (and
1050 (boundp 'rewrap-worklog-records-fn)
1051 (functionp rewrap-worklog-records-fn))
1052 rewrap-worklog-records-fn
1053 (lambda (x) (remove 'nil (coerce x 'vector)))))
1054 (setq predicate-fn-lst
1055 (if (and (boundp 'predicate-fn-lst)
1056 (not (null predicate-fn-lst))
1057 (listp predicate-fn-lst))
1058 predicate-fn-lst
1059 (mapcar 'caddr
1060 (remove 'nil
1061 (mapcar (lambda (x) (unless (null (car x)) x))
1062 jiralib-worklog-import--filters-alist)))))
1063 ;; final condition/sanity checks before processing
1064 (cond
1065 ;; pass cases, don't apply filters, return unaltered worklog-obj
1066 ((or (not (boundp 'predicate-fn-lst)) (not (listp predicate-fn-lst)) (null predicate-fn-lst))
1067 worklog-obj)
1068 ;; default-case, apply worklog filters and return only matching worklogs
1070 (setq worklogs (funcall unwrap-worklog-records-fn worklogs))
1071 (while (setq predicate-fn (pop predicate-fn-lst))
1072 (setq worklogs (mapcar predicate-fn worklogs)))
1073 (funcall rewrap-worklog-records-fn worklogs))))))
1076 (defun jiralib-get-board (id &optional callback)
1077 "Return details on given board"
1078 (jiralib-call "getBoard" nil id))
1080 (defun jiralib-get-boards ()
1081 "Return list of jira boards"
1082 (jiralib-call "getBoards" nil))
1084 (defun jiralib-get-board-issues (board-id &rest params)
1085 "Return list of jira issues in the specified jira board"
1086 (apply 'jiralib-call "getIssuesFromBoard"
1087 (cl-getf params :callback) board-id params))
1089 (defun jiralib--agile-not-last-entry (num-entries total start-at limit)
1090 "Return true if need to retrieve next page from agile api"
1091 (and (> num-entries 0)
1092 (or (not limit) ; not required to be set
1093 (< limit 1) ; ignore invalid limit
1094 (> limit start-at))
1095 (or (not total) ; not always returned
1096 (> total start-at))))
1098 (defun jiralib--agile-limit-page-size (page-size start-at limit)
1099 (if (and limit
1100 (> (+ start-at page-size) limit))
1101 (- limit start-at)
1102 page-size))
1105 (defun jiralib--agile-rest-call-it (api max-results start-at limit query-params)
1106 (let ((callurl
1107 (format "%s?%s" api
1108 (url-build-query-string
1109 (append `((maxResults ,(jiralib--agile-limit-page-size max-results start-at limit))
1110 (startAt ,start-at))
1111 query-params)))))
1112 (jiralib--rest-call-it callurl)))
1114 (defun jiralib--agile-call-it (api values-key &rest params)
1115 "Invoke Jira agile method api and retrieve the results using
1116 paging.
1118 If JIRALIB-COMPLETE-CALLBACK is non-nil, then the call will be
1119 performed asynchronously and JIRALIB-COMPLETE-CALLBACK will be
1120 called when all data are retrieved.
1122 If JIRALIB-COMPLETE-CALLBACK is nil, then the call will be
1123 performed syncronously and this function will return the
1124 retrieved data.
1126 API - path to called API that must start with /rest/agile/1.0.
1128 VALUES-KEY - key of the actual reply data in the reply assoc list.
1130 PARAMS - optional additional parameters.
1131 :limit - limit total number of retrieved entries.
1132 :query-params - extra query parameters in the format of url-build-query-string.
1134 (if jiralib-complete-callback
1135 (apply 'jiralib--agile-call-async api values-key params)
1136 (apply 'jiralib--agile-call-sync api values-key params)))
1138 (defun jiralib--agile-call-sync (api values-key &rest params)
1139 "Syncroniously invoke Jira agile method api retrieve all the
1140 results using paging and return results.
1142 VALUES-KEY - key of the actual reply data in the reply assoc list.
1144 PARAMS - extra parameters (as keyword arguments), the supported parameters are:
1146 :limit - limit total number of retrieved entries.
1147 :query-params - extra query parameters in the format of url-build-query-string.
1149 (setq jiralib-complete-callback nil)
1150 (let ((not-last t)
1151 (start-at 0)
1152 (limit (getf params :limit))
1153 (query-params (getf params :query-params))
1154 ;; maximum page size, 50 is server side maximum
1155 (max-results jiralib-agile-page-size)
1156 (values ()))
1157 (while not-last
1158 (let* ((reply-alist
1159 (jiralib--agile-rest-call-it api max-results start-at limit query-params))
1160 (values-array (cdr (assoc values-key reply-alist)))
1161 (num-entries (length values-array))
1162 (total (cdr (assq 'total reply-alist))))
1163 (setf values (append values (append values-array nil)))
1164 (setf start-at (+ start-at num-entries))
1165 (setf not-last (jiralib--agile-not-last-entry num-entries total start-at limit))))
1166 values))
1168 (defun jiralib--agile-call-async (api values-key &rest params)
1169 "Asyncroniously invoke Jira agile method api,
1170 retrieve all the results using paging and call
1171 JIRALIB-COMPLETE_CALLBACK when all the data are retrieved.
1173 VALUES-KEY - key of the actual reply data in the reply assoc list.
1175 PARAMS - extra parameters (as keyword arguments), the supported parameters are:
1177 limit - limit total number of retrieved entries."
1178 (lexical-let
1179 ((start-at 0)
1180 (limit (getf params :limit))
1181 (query-params (getf params :query-params))
1182 ;; maximum page size, 50 is server side maximum
1183 (max-results jiralib-agile-page-size)
1184 (values-list ())
1185 (vk values-key)
1186 (url api)
1187 ;; save the call back to be called later after the last page
1188 (complete-callback jiralib-complete-callback))
1189 ;; setup new callback to be called after each page
1190 (setf jiralib-complete-callback
1191 (cl-function
1192 (lambda (&rest data &allow-other-keys)
1193 (condition-case err
1194 (let* ((reply-alist (cl-getf data :data))
1195 (values-array (cdr (assoc vk reply-alist)))
1196 (num-entries (length values-array))
1197 (total (cdr (assq 'total reply-alist))))
1198 (setf values-list (append values-list (append values-array nil)))
1199 (setf start-at (+ start-at num-entries))
1200 (message "jiralib agile retrieve: got %d values%s%s"
1201 start-at
1202 (if total " of " "")
1203 (if total (int-to-string total) ""))
1204 (if (jiralib--agile-not-last-entry num-entries total start-at limit)
1205 (jiralib--agile-rest-call-it url max-results start-at limit query-params)
1206 ;; last page: call originall callback
1207 (message "jiralib agile retrieve: calling callback")
1208 (setf jiralib-complete-callback complete-callback)
1209 (funcall jiralib-complete-callback
1210 :data (list (cons vk values-list)))
1211 (message "jiralib agile retrieve: all done")))
1212 ('error (message (format "jiralib agile retrieve: caught error: %s" err)))))))
1213 (jiralib--agile-rest-call-it api max-results start-at limit query-params)))
1215 (provide 'jiralib)
1216 ;;; jiralib.el ends here