Merge pull request #153 from KrzysiekJ/auth-source-search-port
[org-jira.git] / org-jira.el
blob4119961f9155fd09d02899d7f4e97c40a799d245
1 ;;; org-jira.el --- Syncing between Jira and Org-mode. -*- lexical-binding: t -*-
3 ;; Copyright (C) 2016-2018 Matthew Carter <m@ahungry.com>
4 ;; Copyright (C) 2011 Bao Haojun
5 ;;
6 ;; Authors:
7 ;; Matthew Carter <m@ahungry.com>
8 ;; Bao Haojun <baohaojun@gmail.com>
9 ;;
10 ;; Maintainer: Matthew Carter <m@ahungry.com>
11 ;; URL: https://github.com/ahungry/org-jira
12 ;; Version: 4.0.0
13 ;; Keywords: ahungry jira org bug tracker
14 ;; Package-Requires: ((emacs "24.5") (cl-lib "0.5") (request "0.2.0") (s "0.0.0") (dash "2.14.1"))
16 ;; This file is not part of GNU Emacs.
18 ;; This program is free software: you can redistribute it and/or modify
19 ;; it under the terms of the GNU General Public License as published by
20 ;; the Free Software Foundation, either version 3 of the License, or
21 ;; (at your option) any later version.
23 ;; This program is distributed in the hope that it will be useful,
24 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
25 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 ;; GNU General Public License for more details.
28 ;; You should have received a copy of the GNU General Public License
29 ;; along with this program. If not, see
30 ;; <http://www.gnu.org/licenses/> or write to the Free Software
31 ;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
32 ;; 02110-1301, USA.
34 ;;; Commentary:
36 ;; This provides an extension to org-mode for syncing issues with JIRA
37 ;; issue servers.
39 ;;; News:
41 ;;;; Changes in 4.0.0:
42 ;; - Introduce SDK type for handling records vs random alist structures.
44 ;;;; Changes since 3.1.0:
45 ;; - Fix how we were ruining the kill-ring with kill calls.
47 ;;;; Changes since 3.0.0:
48 ;; - Add new org-jira-add-comment call (C-c c c)
50 ;;;; Changes since 2.8.0:
51 ;; - New version 3.0.0 deprecates old filing mechanism and files
52 ;; all of the changes under the top level ticket header.
53 ;; - If you want other top level headers in the same file, this should
54 ;; work now, as long as they come after the main project one.
56 ;;;; Changes since 2.7.0:
57 ;; - Clean up multi-buffer handling, disable attachments call until
58 ;; - refresh is compatible with it.
60 ;;;; Changes since 2.6.3:
61 ;; - Insert worklog import filter in the existing org-jira-update-worklogs-for-current-issue function
62 ;; - Sync up org-clocks and worklogs! Set org-jira-worklog-sync-p to nil to avoid.
64 ;;;; Changes since 2.6.1:
65 ;; - Fix bug with getting all issues when worklog is an error trigger.
67 ;;;; Changes since 2.5.4:
68 ;; - Added new org-jira-refresh-issues-in-buffer call and binding
70 ;;;; Changes since 2.5.3:
71 ;; - Re-introduce the commit that introduced a break into Emacs 25.1.1 list/array push
72 ;; The commit caused updates/comment updates to fail when a blank list of components
73 ;; was present - it will now handle both cases (full list, empty list).
75 ;;;; Changes since 2.5.2:
76 ;; - Revert a commit that introduced a break into Emacs 25.1.1 list/array push
77 ;; The commit caused updates/comment updates to fail
79 ;;;; Changes since 2.5.1:
80 ;; - Only set duedate if a DEADLINE is present in the tags and predicate is t
82 ;;;; Changes since 2.5.0:
83 ;; - Allow overriding the org property names with new defcustom
85 ;;;; Changes since 2.4.0:
86 ;; - Fix many deprecation/warning issues
87 ;; - Fix error with allow-other-keys not being wrapped in cl-function
89 ;;;; Changes since 2.3.0:
90 ;; - Integration with org deadline and Jira due date fields
92 ;;;; Changes since 2.2.0:
93 ;; - Selecting issue type based on project key for creating issues
95 ;;;; Changes since 2.1.0:
96 ;; - Allow changing to unassigned user
97 ;; - Add new shortcut for quick assignment
99 ;;;; Changes since 2.0.0:
100 ;; - Change statusCategory to status value
101 ;; - Clean out some redundant code
102 ;; - Add ELPA tags in keywords
104 ;;;; Changes since 1.0.1:
105 ;; - Converted many calls to async
106 ;; - Removed minor annoyances (position resets etc.)
108 ;;; Code:
110 (require 'org)
111 (require 'org-clock)
112 (require 'cl-lib)
113 (require 'url)
114 (require 'ls-lisp)
115 (require 'dash)
116 (require 's)
117 (require 'jiralib)
118 (require 'org-jira-sdk)
120 (defconst org-jira-version "3.0.0"
121 "Current version of org-jira.el.")
123 (defgroup org-jira nil
124 "Customization group for org-jira."
125 :tag "Org JIRA"
126 :group 'org)
128 (defvar org-jira-working-dir "~/.org-jira"
129 "Folder under which to store org-jira working files.")
131 (defcustom org-jira-default-jql
132 "assignee = currentUser() and resolution = unresolved ORDER BY
133 priority DESC, created ASC"
134 "Default jql for querying your Jira tickets."
135 :group 'org-jira
136 :type 'string)
138 (defcustom org-jira-ignore-comment-user-list
139 '("admin")
140 "Jira usernames that should have comments ignored."
141 :group 'org-jira
142 :type '(repeat (string :tag "Jira username:")))
144 (defcustom org-jira-reverse-comment-order nil
145 "If non-nil, order comments from most recent to least recent."
146 :group 'org-jira
147 :type 'boolean)
149 (defcustom org-jira-done-states
150 '("Closed" "Resolved" "Done")
151 "Jira states that should be considered as DONE for `org-mode'."
152 :group 'org-jira
153 :type '(repeat (string :tag "Jira state name:")))
155 (defcustom org-jira-users
156 '(("Full Name" . "username"))
157 "A list of displayName and key pairs."
158 :group 'org-jira
159 :type 'list)
161 (defcustom org-jira-progress-issue-flow
162 '(("To Do" . "In Progress")
163 ("In Progress" . "Done"))
164 "Quickly define a common issue flow."
165 :group 'org-jira
166 :type 'list)
168 (defcustom org-jira-property-overrides (list)
169 "An assoc list of property tag overrides.
171 The KEY . VAL pairs should both be strings.
173 For instance, to change the :assignee: property in the org :PROPERTIES:
174 block to :WorkedBy:, you can set this as such:
176 (setq org-jira-property-overrides (list (cons \"assignee\" \"WorkedBy\")))
178 or simply:
180 (add-to-list (quote org-jira-property-overrides)
181 (cons (\"assignee\" \"WorkedBy\")))
183 This will work for most any of the properties, with the
184 exception of ones with unique functionality, such as:
186 - deadline
187 - summary
188 - description"
189 :group 'org-jira
190 :type 'list)
192 (defcustom org-jira-serv-alist nil
193 "Association list to set information for each jira server.
194 Each element of the alist is a jira server name. The CAR of each
195 element is a string, uniquely identifying the server. The CDR of
196 each element is a well-formed property list with an even number
197 of elements, alternating keys and values, specifying parameters
198 for the server.
200 (:property value :property value ... )
202 When a property is given a value in org-jira-serv-alist, its
203 setting overrides the value of the corresponding user
204 variable (if any) during syncing.
206 Most properties are optional, but some should always be set:
208 :url soap url of the jira server.
209 :username username to be used.
210 :host hostname of the jira server (TODO: compute it from ~url~).
212 All the other properties are optional. They override the global
213 variables.
215 :password password to be used, will be prompted if missing."
216 :group 'org-jira
217 :type '(alist :value-type plist))
219 (defcustom org-jira-use-status-as-todo nil
220 "Use the JIRA status as the TODO tag value."
221 :group 'org-jira)
223 (defcustom org-jira-coding-system nil
224 "Use custom coding system on org-jira buffers."
225 :group 'org-jira)
227 (defcustom org-jira-deadline-duedate-sync-p t
228 "Keep org deadline and jira duedate fields synced.
229 You may wish to set this to nil if you track org deadlines in
230 your buffer that you do not want to send back to your Jira
231 instance."
232 :group 'org-jira
233 :type 'boolean)
235 (defcustom org-jira-worklog-sync-p t
236 "Keep org clocks and jira worklog fields synced.
237 You may wish to set this to nil if you track org clocks in
238 your buffer that you do not want to send back to your Jira
239 instance."
240 :group 'org-jira
241 :type 'boolean)
243 (defcustom org-jira-download-dir "~/Downloads"
244 "Name of the default jira download library."
245 :group 'org-jira
246 :type 'string)
248 (defcustom org-jira-download-ask-override t
249 "Ask before overriding tile."
250 :group 'org-jira
251 :type 'boolean)
253 (defcustom org-jira-jira-status-to-org-keyword-alist nil
254 "Custom alist of jira status stored in car and `org-mode' keyword stored in cdr."
255 :group 'org-jira
256 :type '(alist :key-type string :value-type string))
258 (defcustom org-jira-priority-to-org-priority-omit-default-priority nil
259 "Whether to omit insertion of priority when it matches the default.
261 When set to t, will omit the insertion of the matched value from
262 `org-jira-priority-to-org-priority-alist' when it matches the `org-default-priority'."
263 :group 'org-jira
264 :type 'boolean)
266 (defcustom org-jira-priority-to-org-priority-alist nil
267 "Alist mapping jira priority keywords to `org-mode' priority cookies.
269 A sample value might be
270 (list (cons \"High\" ?A)
271 (cons \"Medium\" ?B)
272 (cons \"Low\" ?C)).
274 See `org-default-priority' for more info."
275 :group 'org-jira
276 :type '(alist :key-type string :value-type character))
278 (defcustom org-jira-boards-default-limit 50
279 "Default limit for number of issues retrieved from agile boards."
280 :group 'org-jira
281 :type 'integer)
283 (defvar org-jira-serv nil
284 "Parameters of the currently selected blog.")
286 (defvar org-jira-serv-name nil
287 "Name of the blog, to pick from `org-jira-serv-alist'.")
289 (defvar org-jira-projects-list nil
290 "List of jira projects.")
292 (defvar org-jira-current-project nil
293 "Currently selected (i.e., active project).")
295 (defvar org-jira-issues-list nil
296 "List of jira issues under the current project.")
298 (defvar org-jira-server-rpc-url nil
299 "Jira server soap URL.")
301 (defvar org-jira-server-userid nil
302 "Jira server user id.")
304 (defvar org-jira-proj-id nil
305 "Jira project ID.")
307 (defvar org-jira-logged-in nil
308 "Flag whether user is logged-in or not.")
310 (defvar org-jira-buffer-name "*org-jira-%s*"
311 "Name of the jira buffer.")
313 (defvar org-jira-buffer-kill-prompt t
314 "Ask before killing buffer.")
316 (make-variable-buffer-local 'org-jira-buffer-kill-prompt)
318 (defvar org-jira-mode-hook nil
319 "Hook to run upon entry into mode.")
321 (defvar org-jira-issue-id-history '()
322 "Prompt history for issue id.")
324 (defvar org-jira-fixversion-id-history '()
325 "Prompt history for fixversion id.")
327 (defvar org-jira-verbosity 'debug)
329 (defun org-jira-log (s) (when (eq 'debug org-jira-verbosity) (message (format "%s" s))))
331 (defmacro ensure-on-issue (&rest body)
332 "Make sure we are on an issue heading, before executing BODY."
333 (declare (debug t))
334 (declare (indent 'defun))
335 `(save-excursion
336 (save-restriction
337 (widen)
338 (unless (looking-at "^\\*\\* ")
339 (search-backward-regexp "^\\*\\* " nil t)) ; go to top heading
340 (let ((org-jira-id (org-jira-id)))
341 (unless (and org-jira-id (string-match (jiralib-get-issue-regexp) (downcase org-jira-id)))
342 (error "Not on an issue region!")))
343 ,@body)))
345 (defmacro org-jira-with-callback (&rest body)
346 "Simpler way to write the data callbacks."
347 (declare (debug t))
348 (declare (indent 'defun))
349 `(lambda (&rest request-response)
350 (declare (ignore cb-data))
351 (let ((cb-data (cl-getf request-response :data)))
352 ,@body)))
354 (defmacro org-jira-freeze-ui (&rest body)
355 "Freeze the UI layout for the user as much as possible."
356 (declare (debug t))
357 (declare (indent 'defun))
358 `(save-excursion
359 (save-restriction
360 (widen)
361 (org-save-outline-visibility t
362 (outline-show-all)
363 ,@body))))
365 (defmacro ensure-on-issue-id (issue-id &rest body)
366 "Just do some work on ISSUE-ID, execute BODY."
367 (declare (debug t))
368 (declare (indent 'defun))
369 `(let* ((proj-key (replace-regexp-in-string "-.*" "" issue-id))
370 (project-file (expand-file-name (concat proj-key ".org") org-jira-working-dir))
371 (project-buffer (or (find-buffer-visiting project-file)
372 (find-file project-file))))
373 (with-current-buffer project-buffer
374 (org-jira-freeze-ui
375 (let ((p (org-find-entry-with-id ,issue-id)))
376 (unless p (error "Issue %s not found!" ,issue-id))
377 (goto-char p)
378 (org-narrow-to-subtree)
379 ,@body)))))
381 (defmacro ensure-on-todo (&rest body)
382 "Make sure we are on an todo heading, before executing BODY."
383 (declare (debug t))
384 (declare (indent 'defun))
385 `(save-excursion
386 (save-restriction
387 (let ((continue t)
388 (on-todo nil))
389 (while continue
390 (when (org-get-todo-state)
391 (setq continue nil on-todo t))
392 (unless (and continue (org-up-heading-safe))
393 (setq continue nil)))
394 (if (not on-todo)
395 (error "TODO not found")
396 (org-narrow-to-subtree)
397 ,@body)))))
399 (defmacro ensure-on-comment (&rest body)
400 "Make sure we are on a comment heading, before executing BODY."
401 (declare (debug t))
402 (declare (indent 'defun))
403 `(save-excursion
404 (org-back-to-heading)
405 (forward-thing 'whitespace)
406 (unless (looking-at "Comment:")
407 (error "Not on a comment region!"))
408 (save-restriction
409 (org-narrow-to-subtree)
410 ,@body)))
412 (defmacro ensure-on-worklog (&rest body)
413 "Make sure we are on a worklog heading, before executing BODY."
414 (declare (debug t))
415 (declare (indent 'defun))
416 `(save-excursion
417 (org-back-to-heading)
418 (forward-thing 'whitespace)
419 (unless (looking-at "Worklog:")
420 (error "Not on a worklog region!"))
421 (save-restriction
422 (org-narrow-to-subtree)
423 ,@body)))
425 (defvar org-jira-entry-mode-map
426 (let ((org-jira-map (make-sparse-keymap)))
427 (define-key org-jira-map (kbd "C-c pg") 'org-jira-get-projects)
428 (define-key org-jira-map (kbd "C-c bg") 'org-jira-get-boards)
429 (define-key org-jira-map (kbd "C-c iv") 'org-jira-get-issues-by-board)
430 (define-key org-jira-map (kbd "C-c ib") 'org-jira-browse-issue)
431 (define-key org-jira-map (kbd "C-c ig") 'org-jira-get-issues)
432 (define-key org-jira-map (kbd "C-c ih") 'org-jira-get-issues-headonly)
433 ;;(define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-from-filter-headonly)
434 ;;(define-key org-jira-map (kbd "C-c iF") 'org-jira-get-issues-from-filter)
435 (define-key org-jira-map (kbd "C-c iu") 'org-jira-update-issue)
436 (define-key org-jira-map (kbd "C-c iw") 'org-jira-progress-issue)
437 (define-key org-jira-map (kbd "C-c in") 'org-jira-progress-issue-next)
438 (define-key org-jira-map (kbd "C-c ia") 'org-jira-assign-issue)
439 (define-key org-jira-map (kbd "C-c ir") 'org-jira-refresh-issue)
440 (define-key org-jira-map (kbd "C-c iR") 'org-jira-refresh-issues-in-buffer)
441 (define-key org-jira-map (kbd "C-c ic") 'org-jira-create-issue)
442 (define-key org-jira-map (kbd "C-c ik") 'org-jira-copy-current-issue-key)
443 (define-key org-jira-map (kbd "C-c sc") 'org-jira-create-subtask)
444 (define-key org-jira-map (kbd "C-c sg") 'org-jira-get-subtasks)
445 (define-key org-jira-map (kbd "C-c cc") 'org-jira-add-comment)
446 (define-key org-jira-map (kbd "C-c cu") 'org-jira-update-comment)
447 (define-key org-jira-map (kbd "C-c wu") 'org-jira-update-worklogs-from-org-clocks)
448 (define-key org-jira-map (kbd "C-c tj") 'org-jira-todo-to-jira)
449 (define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-by-fixversion)
450 org-jira-map))
452 ;;;###autoload
453 (define-minor-mode org-jira-mode
454 "Toggle org-jira mode.
455 With no argument, the mode is toggled on/off.
456 Non-nil argument turns mode on.
457 Nil argument turns mode off.
459 Commands:
460 \\{org-jira-entry-mode-map}
462 Entry to this mode calls the value of `org-jira-mode-hook'."
464 :init-value nil
465 :lighter " jira"
466 :group 'org-jira
467 :keymap org-jira-entry-mode-map
469 (if org-jira-mode
470 (run-mode-hooks 'org-jira-mode-hook)))
472 (defun org-jira-get-project-name (proj)
473 (org-jira-find-value proj 'key))
475 (defun org-jira-find-value (l &rest keys)
476 (let* (key exists)
477 (while (and keys (listp l))
478 (setq key (car keys))
479 (setq exists nil)
480 (mapc (lambda (item)
481 (when (equal key (car item))
482 (setq exists t)))
483 (if (and (listp l)
484 (listp (car l)))
486 nil))
487 (setq keys (cdr keys))
488 (if exists
489 (setq l (cdr (assoc key l)))
490 (setq l (or (cdr (assoc key l)) l))))
493 (defun org-jira-get-project-lead (proj)
494 (org-jira-find-value proj 'lead 'name))
496 (defun org-jira-get-assignable-users (project-key)
497 "Get the list of assignable users for PROJECT-KEY, adding user set jira-users first."
498 (append
499 '(("Unassigned" . ""))
500 org-jira-users
501 (mapcar (lambda (user)
502 (cons (org-jira-decode (cdr (assoc 'displayName user)))
503 (org-jira-decode (cdr (assoc 'name user)))))
504 (jiralib-get-users project-key))))
506 (defun org-jira-entry-put (pom property value)
507 "Similar to org-jira-entry-put, but with an optional alist of overrides.
509 At point-or-marker POM, set PROPERTY to VALUE.
511 Look at customizing org-jira-property-overrides if you want
512 to change the property names this sets."
513 (unless (stringp property)
514 (setq property (symbol-name property)))
515 (let ((property (or (assoc-default property org-jira-property-overrides)
516 property)))
517 (org-entry-put pom property (org-jira-decode value))))
519 (defun org-jira-kill-line ()
520 "Kill the line, without 'kill-line' side-effects of altering kill ring."
521 (interactive)
522 (delete-region (point) (line-end-position)))
524 ;; Appropriated from org.el
525 (defun org-jira-org-kill-line (&optional _arg)
526 "Kill line, to tags or end of line."
527 (cond
528 ((or (not org-special-ctrl-k)
529 (bolp)
530 (not (org-at-heading-p)))
531 (when (and (get-char-property (min (point-max) (point-at-eol)) 'invisible)
532 org-ctrl-k-protect-subtree
533 (or (eq org-ctrl-k-protect-subtree 'error)
534 (not (y-or-n-p "Kill hidden subtree along with headline? "))))
535 (user-error "C-k aborted as it would kill a hidden subtree"))
536 (call-interactively
537 (if (bound-and-true-p visual-line-mode) 'kill-visual-line 'org-jira-kill-line)))
538 ((looking-at ".*?\\S-\\([ \t]+\\(:[[:alnum:]_@#%:]+:\\)\\)[ \t]*$")
539 (delete-region (point) (match-beginning 1))
540 (org-set-tags nil t))
541 (t (delete-region (point) (point-at-eol)))))
543 ;;;###autoload
544 (defun org-jira-get-projects ()
545 "Get list of projects."
546 (interactive)
547 (let ((projects-file (expand-file-name "projects-list.org" org-jira-working-dir)))
548 (or (find-buffer-visiting projects-file)
549 (find-file projects-file))
550 (org-jira-mode t)
551 (save-excursion
552 (let* ((oj-projs (jiralib-get-projects)))
553 (mapc (lambda (proj)
554 (let* ((proj-key (org-jira-find-value proj 'key))
555 (proj-headline (format "Project: [[file:%s.org][%s]]" proj-key proj-key)))
556 (save-restriction
557 (widen)
558 (goto-char (point-min))
559 (outline-show-all)
560 (setq p (org-find-exact-headline-in-buffer proj-headline))
561 (if (and p (>= p (point-min))
562 (<= p (point-max)))
563 (progn
564 (goto-char p)
565 (org-narrow-to-subtree)
566 (end-of-line))
567 (goto-char (point-max))
568 (unless (looking-at "^")
569 (insert "\n"))
570 (insert "* ")
571 (org-jira-insert proj-headline)
572 (org-narrow-to-subtree))
573 (org-jira-entry-put (point) "name" (org-jira-get-project-name proj))
574 (org-jira-entry-put (point) "key" (org-jira-find-value proj 'key))
575 (org-jira-entry-put (point) "lead" (org-jira-get-project-lead proj))
576 (org-jira-entry-put (point) "ID" (org-jira-find-value proj 'id))
577 (org-jira-entry-put (point) "url" (format "%s/browse/%s" (replace-regexp-in-string "/*$" "" jiralib-url) (org-jira-find-value proj 'key))))))
578 oj-projs)))))
580 (defun org-jira-get-issue-components (issue)
581 "Return the components the ISSUE belongs to."
582 (mapconcat
583 (lambda (comp)
584 (org-jira-find-value comp 'name))
585 (org-jira-find-value issue 'fields 'components) ", "))
587 (defun org-jira-decode (data)
588 "Decode text DATA.
590 It must receive a coercion to string, as not every time will it
591 be populated."
592 (let ((coding-system (or org-jira-coding-system
593 (when (boundp 'buffer-file-coding-system)
594 buffer-file-coding-system 'utf-8))))
595 (decode-coding-string
596 (string-make-unibyte (cl-coerce data 'string)) coding-system)))
598 (defun org-jira-insert (&rest args)
599 "Set coding to text provide by `ARGS' when insert in buffer."
600 (insert (org-jira-decode (apply #'concat args))))
602 (defun org-jira-transform-time-format (jira-time-str)
603 "Convert JIRA-TIME-STR to format \"%Y-%m-%d %T\".
605 Example: \"2012-01-09T08:59:15.000Z\" becomes \"2012-01-09
606 16:59:15\", with the current timezone being +0800."
607 (condition-case ()
608 (format-time-string
609 "%Y-%m-%d %T"
610 (apply
611 'encode-time
612 (parse-time-string (replace-regexp-in-string "T\\|\\.000" " " jira-time-str))))
613 (error jira-time-str)))
615 (defun org-jira--fix-encode-time-args (arg)
616 "Fix ARG for 3 nil values at the head."
617 (cl-loop
618 for n from 0 to 2 by 1 do
619 (when (not (nth n arg))
620 (setcar (nthcdr n arg) 0)))
621 arg)
623 (defun org-jira-time-format-to-jira (org-time-str)
624 "Convert ORG-TIME-STR back to jira time format."
625 (condition-case ()
626 (format-time-string
627 "%Y-%m-%dT%T.000Z"
628 (apply 'encode-time
629 (org-jira--fix-encode-time-args (parse-time-string org-time-str)))
631 (error org-time-str)))
633 (defun org-jira-get-comment-val (key comment)
634 "Return the value associated with KEY of COMMENT."
635 (org-jira-get-issue-val key comment))
637 (defun org-jira-time-stamp-to-org-clock (time-stamp)
638 "Convert TIME-STAMP into org-clock format."
639 (format-time-string "%Y-%m-%d %a %H:%M" time-stamp))
641 (defun org-jira-date-strip-letter-t (date)
642 "Convert DATE into a time stamp and then into org-clock format.
643 Expects a date in format such as: 2017-02-26T00:08:00.000-0500 and
644 returns in format 2017-02-26 00:08:00-0500."
645 (replace-regexp-in-string
646 "\\.000\\([-+]\\)" "\\1"
647 (replace-regexp-in-string "^\\(.*?\\)T" "\\1 " date)))
649 (defun org-jira-date-to-org-clock (date)
650 "Convert DATE into a time stamp and then into org-clock format.
651 Expects a date in format such as: 2017-02-26T00:08:00.000-0500."
652 (org-jira-time-stamp-to-org-clock
653 (date-to-time
654 (org-jira-date-strip-letter-t date))))
656 (defun org-jira-worklogs-to-org-clocks (worklogs)
657 "Get a list of WORKLOGS and convert to org-clocks."
658 (mapcar
659 (lambda (worklog)
660 (let ((wl-start (cdr (assoc 'started worklog)))
661 (wl-time (cdr (assoc 'timeSpentSeconds worklog)))
662 (wl-end))
663 (setq wl-start (org-jira-date-to-org-clock wl-start))
664 (setq wl-end (org-jira-time-stamp-to-org-clock (time-add (date-to-time wl-start) wl-time)))
665 (list
666 wl-start
667 wl-end
668 (cdr (assoc 'comment worklog))
669 (cdr (assoc 'id worklog))
672 worklogs)
675 (defun org-jira-format-clock (clock-entry)
676 "Format a CLOCK-ENTRY given the (list start end).
677 This format is typically generated from org-jira-worklogs-to-org-clocks call."
678 (format "CLOCK: [%s]--[%s]" (car clock-entry) (cadr clock-entry)))
680 (defun org-jira-insert-clock (clock-entry)
681 "Insert a CLOCK-ENTRY given the (list start end).
682 This format is typically generated from org-jira-worklogs-to-org-clocks call."
683 (insert (org-jira-format-clock clock-entry))
684 (org-beginning-of-line)
685 (org-ctrl-c-ctrl-c) ;; @todo Maybe not call directly? does it matter? - used to resync the clock estimate
686 (org-end-of-line)
687 (insert "\n")
688 (insert (format " :id: %s\n" (cadddr clock-entry)))
689 (when (caddr clock-entry) (insert (format " %s\n" (org-jira-decode (caddr clock-entry))))) ;; No comment is nil, so don't print it
692 (defun org-jira-logbook-reset (issue-id &optional clocks)
693 "Find logbook for ISSUE-ID, delete it.
694 Re-create it with CLOCKS. This is used for worklogs."
695 (interactive)
696 (let ((existing-logbook-p nil))
697 ;; See if the LOGBOOK already exists or not.
698 (ensure-on-issue-id
699 issue-id
700 (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK")))
701 (when (search-forward (format ":%s:" drawer-name) nil 1 1)
702 (setq existing-logbook-p t))))
703 (ensure-on-issue-id
704 issue-id
705 (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK")))
706 (if existing-logbook-p
707 (progn ;; If we had a logbook, drop it and re-create in a bit.
708 (search-forward (format ":%s:" drawer-name) nil 1 1)
709 (org-beginning-of-line)
710 (delete-region (point) (search-forward ":END:" nil 1 1))
712 (progn ;; Otherwise, create a new one at the end of properties list
713 (search-forward ":END:" nil 1 1)
714 (forward-line)))
715 (org-insert-drawer nil (format "%s" drawer-name)) ;; Doc says non-nil, but this requires nil
716 (mapc #'org-jira-insert-clock clocks)
717 ;; Clean up leftover newlines (we left 2 behind)
718 (dotimes (n 2)
719 (search-forward-regexp "^$" nil 1 1)
720 (delete-region (point) (min (point-max) (1+ (point)))))
721 ))))
723 (defun org-jira-get-worklog-val (key WORKLOG)
724 "Return the value associated with KEY of WORKLOG."
725 (org-jira-get-comment-val key WORKLOG))
727 (defun org-jira-get-issue-val (key issue)
728 "Return the value associated with key KEY of issue ISSUE."
729 (let ((tmp (or (org-jira-find-value issue 'fields key 'key) ""))) ;; For project, we need a key, not the name...
730 (unless (stringp tmp)
731 (setq tmp (org-jira-find-value issue key)))
732 (unless (stringp tmp)
733 (setq tmp (org-jira-find-value issue 'fields key 'displayName)))
734 (unless (stringp tmp)
735 (setq tmp ""))
736 (cond ((eq key 'components)
737 (org-jira-get-issue-components issue))
738 ((member key '(created updated startDate))
739 (org-jira-transform-time-format tmp))
740 ((eq key 'status)
741 (if jiralib-use-restapi
742 (org-jira-find-value issue 'fields 'status 'name)
743 (org-jira-find-value (jiralib-get-statuses) tmp)))
744 ((eq key 'resolution)
745 (if jiralib-use-restapi
747 (if (string= tmp "")
749 (org-jira-find-value (jiralib-get-resolutions) tmp))))
750 ((eq key 'type)
751 (if jiralib-use-restapi
752 (org-jira-find-value issue 'fields 'issuetype 'name)
753 (org-jira-find-value (jiralib-get-issue-types) tmp)))
754 ((eq key 'priority)
755 (if jiralib-use-restapi
756 (org-jira-find-value issue 'fields 'priority 'name)
757 (org-jira-find-value (jiralib-get-priorities) tmp)))
758 ((eq key 'description)
759 (org-jira-strip-string tmp))
761 tmp))))
763 (defvar org-jira-jql-history nil)
765 ;;;###autoload
766 (defun org-jira-get-issue-list (&optional callback)
767 "Get list of issues, using jql (jira query language), invoke CALLBACK after.
769 Default is unresolved issues assigned to current login user; with
770 a prefix argument you are given the chance to enter your own
771 jql."
772 (org-jira-log (format "I was called, was it with a callback? %s" (if callback "yes" "no")))
773 (let ((jql org-jira-default-jql))
774 (when current-prefix-arg
775 (setq jql (read-string "Jql: "
776 (if org-jira-jql-history
777 (car org-jira-jql-history)
778 "assignee = currentUser() and resolution = unresolved")
779 'org-jira-jql-history
780 "assignee = currentUser() and resolution = unresolved")))
781 (list (jiralib-do-jql-search jql nil callback))))
783 (defun org-jira-get-issue-by-id (id)
784 "Get an issue by its ID."
785 (push id org-jira-issue-id-history)
786 (let ((jql (format "id = %s" id)))
787 (jiralib-do-jql-search jql)))
789 (defun org-jira-get-issue-by-fixversion (fixversion-id)
790 "Get an issue by its FIXVERSION-ID."
791 (push fixversion-id org-jira-fixversion-id-history)
792 (let ((jql (format "fixVersion = \"%s\"" fixversion-id)))
793 (jiralib-do-jql-search jql)))
795 ;;;###autoload
796 (defun org-jira-get-summary ()
797 "Get issue summary from point and place next to issue id from jira"
798 (interactive)
799 (let ((jira-id (thing-at-point 'symbol)))
800 (forward-symbol 1)
801 (insert (format " - %s"
802 (cdr (assoc 'summary (car (org-jira-get-issue-by-id jira-id))))))))
804 ;;;###autoload
805 (defun org-jira-get-summary-url ()
806 "Get issue summary from point and place next to issue id from jira, and make issue id a link"
807 (interactive)
808 (let ((jira-id (thing-at-point 'symbol)))
809 (insert (format "[[%s][%s]] - %s"
810 (concatenate 'string jiralib-url "browse/" jira-id) jira-id
811 (cdr (assoc 'summary (car (org-jira-get-issue-by-id jira-id))))))))
813 ;;;###autoload
814 (defun org-jira-get-issues-headonly (issues)
815 "Get list of ISSUES, head only.
817 The default behavior is to return issues assigned to you and unresolved.
819 With a prefix argument, allow you to customize the jql. See
820 `org-jira-get-issue-list'."
822 (interactive
823 (org-jira-get-issue-list))
825 (let* ((issues-file (expand-file-name "issues-headonly.org" org-jira-working-dir))
826 (issues-headonly-buffer (or (find-buffer-visiting issues-file)
827 (find-file issues-file))))
828 (with-current-buffer issues-headonly-buffer
829 (widen)
830 (delete-region (point-min) (point-max))
832 (mapc (lambda (issue)
833 (let ((issue-id (org-jira-get-issue-key issue))
834 (issue-summary (org-jira-get-issue-summary issue)))
835 (org-jira-insert (format "- [jira:%s] %s\n" issue-id issue-summary))))
836 issues))
837 (switch-to-buffer issues-headonly-buffer)))
839 ;;;###autoload
840 (defun org-jira-get-issue (id)
841 "Get a JIRA issue, allowing you to enter the issue-id first."
842 (interactive (list (read-string "Issue ID: " "" 'org-jira-issue-id-history)))
843 (org-jira-get-issues (org-jira-get-issue-by-id id))
844 (let ((issue-pos (org-find-entry-with-id id)))
845 (when issue-pos
846 (goto-char issue-pos)
847 (recenter 0))))
849 ;;;###autoload
850 (defun org-jira-get-issues-by-fixversion (fixversion)
851 "Get list of issues by FIXVERSION."
852 (interactive (list (read-string "Fixversion ID: " ""
853 'org-jira-fixversion-id-history)))
854 (org-jira-get-issues (org-jira-get-issue-by-fixversion fixversion)))
856 ;;;###autoload
857 (defun org-jira-get-issue-project (issue)
858 (org-jira-find-value issue 'fields 'project 'key))
860 (defun org-jira-get-issue-key (issue)
861 (org-jira-find-value issue 'key))
863 (defun org-jira-get-issue-summary (issue)
864 (org-jira-find-value issue 'fields 'summary))
866 (defvar org-jira-get-issue-list-callback
867 (cl-function
868 (lambda (&key data &allow-other-keys)
869 "Callback for async, DATA is the response from the request call.
871 Will send a list of org-jira-sdk-issue objects to the list printer."
872 (org-jira-log "Received data for org-jira-get-issue-list-callback.")
873 (--> data
874 (org-jira-sdk-path it '(issues))
875 (append it nil) ; convert the conses into a proper list.
876 org-jira-sdk-create-issues-from-data-list
877 org-jira-get-issues))))
879 ;;;###autoload
880 (defun org-jira-get-issues (issues)
881 "Get list of ISSUES into an org buffer.
883 Default is get unfinished issues assigned to you, but you can
884 customize jql with a prefix argument.
885 See`org-jira-get-issue-list'"
886 ;; If the user doesn't provide a default, async call to build an issue list
887 ;; from the JQL style query
888 (interactive
889 (org-jira-get-issue-list org-jira-get-issue-list-callback))
890 (org-jira-log "Fetching issues...")
891 (when (> (length issues) 0)
892 (org-jira--render-issues-from-issue-list issues)))
894 (defun org-jira--get-project-buffer (Issue)
895 (with-slots (proj-key) Issue
896 (let* ((project-file (expand-file-name (concat proj-key ".org") org-jira-working-dir))
897 (project-buffer (find-file-noselect project-file)))
898 project-buffer)))
900 (defun org-jira--render-issue (Issue)
901 "Render single ISSUE."
902 (org-jira-log "Rendering issue from issue list")
903 (org-jira-sdk-dump Issue)
904 (with-slots (proj-key issue-id summary status priority headline id) Issue
905 (let (p)
906 (with-current-buffer (org-jira--get-project-buffer Issue)
907 (org-jira-freeze-ui
908 (org-jira-mode t)
909 (goto-char (point-min))
910 (unless (looking-at (format "^* %s-Tickets" proj-key))
911 (insert (format "* %s-Tickets\n" proj-key)))
912 (setq p (org-find-entry-with-id issue-id))
913 (save-restriction
914 (if (and p (>= p (point-min))
915 (<= p (point-max)))
916 (progn
917 (goto-char p)
918 (forward-thing 'whitespace)
919 (org-jira-kill-line))
920 (goto-char (point-max))
921 (unless (looking-at "^")
922 (insert "\n"))
923 (insert "** "))
924 (let ((status (org-jira-decode status)))
925 (org-jira-insert
926 (concat (org-jira-get-org-keyword-from-status status)
928 (org-jira-get-org-priority-cookie-from-issue priority)
929 headline)))
930 (save-excursion
931 (unless (search-forward "\n" (point-max) 1)
932 (insert "\n")))
933 (org-narrow-to-subtree)
934 (save-excursion
935 (org-back-to-heading t)
936 (org-set-tags-to (replace-regexp-in-string "-" "_" issue-id)))
937 (mapc (lambda (entry)
938 (let ((val (slot-value Issue entry)))
939 (when (or (and val (not (string= val "")))
940 (eq entry 'assignee)) ;; Always show assignee
941 (org-jira-entry-put (point) (symbol-name entry) val))))
942 '(assignee reporter type priority resolution status components created updated))
944 (org-jira-entry-put (point) "ID" issue-id)
945 (org-jira-entry-put (point) "CUSTOM_ID" issue-id)
947 ;; Insert the duedate as a deadline if it exists
948 (when org-jira-deadline-duedate-sync-p
949 (let ((duedate (oref Issue duedate)))
950 (when (> (length duedate) 0)
951 (org-deadline nil duedate))))
953 (mapc
954 (lambda (heading-entry)
955 (ensure-on-issue-id
956 issue-id
957 (let* ((entry-heading
958 (concat (symbol-name heading-entry)
959 (format ": [[%s][%s]]"
960 (concat jiralib-url "/browse/" issue-id) issue-id))))
961 (setq p (org-find-exact-headline-in-buffer entry-heading))
962 (if (and p (>= p (point-min))
963 (<= p (point-max)))
964 (progn
965 (goto-char p)
966 (org-narrow-to-subtree)
967 (goto-char (point-min))
968 (forward-line 1)
969 (delete-region (point) (point-max)))
970 (if (org-goto-first-child)
971 (org-insert-heading)
972 (goto-char (point-max))
973 (org-insert-subheading t))
974 (org-jira-insert entry-heading "\n"))
976 ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup
977 (org-jira-insert
978 (replace-regexp-in-string
979 "^" " "
980 (format "%s" (slot-value Issue heading-entry)))))))
981 '(description))
983 (org-jira-update-comments-for-issue issue-id)
985 ;; FIXME: Re-enable when attachments are not erroring.
986 ;;(org-jira-update-attachments-for-current-issue)
988 ;; only sync worklog clocks when the user sets it to be so.
989 (when org-jira-worklog-sync-p
990 (org-jira-update-worklogs-for-issue issue-id))))))))
992 (defun org-jira--render-issues-from-issue-list (Issues)
993 "Add the issues from ISSUES list into the org file(s).
995 ISSUES is a list of `org-jira-sdk-issue' records."
996 ;; FIXME: Some type of loading error - the first async callback does not know about
997 ;; the issues existing as a class, so we may need to instantiate here if we have none.
998 (when (eq 0 (->> Issues (cl-remove-if-not #'org-jira-sdk-isa-issue?) length))
999 (setq Issues (org-jira-sdk-create-issues-from-data-list Issues)))
1001 ;; First off, we never ever want to run on non-issues, so check our types early.
1002 (setq Issues (cl-remove-if-not #'org-jira-sdk-isa-issue? Issues))
1003 (org-jira-log (format "About to render %d issues." (length Issues)))
1005 ;; If we have any left, we map over them.
1006 (mapc 'org-jira--render-issue Issues)
1007 (switch-to-buffer (org-jira--get-project-buffer (-last-item Issues))))
1009 ;;;###autoload
1010 (defun org-jira-update-comment ()
1011 "Update a comment for the current issue."
1012 (interactive)
1013 (let* ((issue-id (org-jira-get-from-org 'issue 'key))
1014 (comment-id (org-jira-get-from-org 'comment 'id))
1015 (comment (replace-regexp-in-string "^ " "" (org-jira-get-comment-body comment-id)))
1016 (callback-edit
1017 (cl-function
1018 (lambda (&key data &allow-other-keys)
1019 (org-jira-update-comments-for-current-issue))))
1020 (callback-add
1021 (cl-function
1022 (lambda (&key data &allow-other-keys)
1023 ;; @todo :optim: Has to be a better way to do this than delete region (like update the unmarked one)
1024 (org-jira-delete-current-comment)
1025 (org-jira-update-comments-for-current-issue)))))
1026 (if comment-id
1027 (jiralib-edit-comment issue-id comment-id comment callback-edit)
1028 (jiralib-add-comment issue-id comment callback-add))))
1030 (defun org-jira-add-comment (issue-id comment)
1031 "For ISSUE-ID, add a new COMMENT string to the issue region."
1032 (interactive
1033 (let* ((issue-id (org-jira-id))
1034 (comment (read-string (format "Comment (%s): " issue-id))))
1035 (list issue-id comment)))
1036 (lexical-let ((issue-id issue-id))
1037 (ensure-on-issue-id
1038 issue-id
1039 (goto-char (point-max))
1040 (jiralib-add-comment
1041 issue-id comment
1042 (cl-function
1043 (lambda (&key data &allow-other-keys)
1044 (ensure-on-issue-id issue-id (org-jira-update-comments-for-current-issue))))))))
1046 (defun org-jira-org-clock-to-date (org-time)
1047 "Convert ORG-TIME formatted date into a plain date string."
1048 (format-time-string
1049 "%Y-%m-%dT%H:%M:%S.000%z"
1050 (date-to-time org-time)))
1052 (defun org-jira-worklog-time-from-org-time (org-time)
1053 "Take in an ORG-TIME and convert it into the portions of a worklog time.
1054 Expects input in format such as: [2017-04-05 Wed 01:00]--[2017-04-05 Wed 01:46] => 0:46"
1055 (let ((start (replace-regexp-in-string "^\\[\\(.*?\\)\\].*" "\\1" org-time))
1056 (end (replace-regexp-in-string ".*--\\[\\(.*?\\)\\].*" "\\1" org-time)))
1057 `((started . ,(org-jira-org-clock-to-date start))
1058 (time-spent-seconds . ,(time-to-seconds
1059 (time-subtract
1060 (date-to-time end)
1061 (date-to-time start)))))))
1063 (defun org-jira-org-clock-to-jira-worklog (org-time clock-content)
1064 "Given ORG-TIME and CLOCK-CONTENT, format a jira worklog entry."
1065 (let ((lines (split-string clock-content "\n"))
1066 worklog-id)
1067 ;; See if we look like we have an id
1068 (when (string-match ":id:" (first lines))
1069 (setq worklog-id
1070 (replace-regexp-in-string "^.*:id: \\([0-9]*\\)$" "\\1" (first lines)))
1071 (when (> (string-to-number worklog-id) 0) ;; pop off the first id line if we found it valid
1072 (setq lines (cdr lines))))
1073 (setq lines (reverse (cdr (reverse lines)))) ;; drop last line
1074 (let ((comment (org-trim (mapconcat 'identity lines "\n")))
1075 (worklog-time (org-jira-worklog-time-from-org-time org-time)))
1076 `((worklog-id . ,worklog-id)
1077 (comment . ,comment)
1078 (started . ,(cdr (assoc 'started worklog-time)))
1079 (time-spent-seconds . ,(cdr (assoc 'time-spent-seconds worklog-time)))
1080 ))))
1082 ;;;###autoload
1083 (defun org-jira-update-worklogs-from-org-clocks ()
1084 "Update or add a worklog based on the org clocks."
1085 (interactive)
1086 (let ((issue-id (org-jira-get-from-org 'issue 'key)))
1087 (ensure-on-issue-id
1088 issue-id
1089 (search-forward (format ":%s:" (or (org-clock-drawer-name) "LOGBOOK")) nil 1 1)
1090 (org-beginning-of-line)
1091 ;; (org-cycle 1)
1092 (while (search-forward "CLOCK: " nil 1 1)
1093 (let ((org-time (buffer-substring-no-properties (point) (point-at-eol))))
1094 (forward-line)
1095 ;; See where the stuff ends (what point)
1096 (let (next-clock-point)
1097 (save-excursion
1098 (search-forward-regexp "\\(CLOCK\\|:END\\):" nil 1 1)
1099 (setq next-clock-point (point)))
1100 (let ((clock-content
1101 (buffer-substring-no-properties (point) next-clock-point)))
1103 ;; @todo :optim: This is inefficient, calling the resync on each update/insert event,
1104 ;; ideally we would track and only insert/update changed entries, as well
1105 ;; only call a resync once (when the entire list is processed, which will
1106 ;; basically require a dry run to see how many items we should be updating.
1108 ;; Update via jiralib call
1109 (let* ((worklog (org-jira-org-clock-to-jira-worklog org-time clock-content))
1110 (comment-text (cdr (assoc 'comment worklog)))
1111 (comment-text (if (string= (org-trim comment-text) "") nil comment-text)))
1112 (if (cdr (assoc 'worklog-id worklog))
1113 (jiralib-update-worklog
1114 issue-id
1115 (cdr (assoc 'worklog-id worklog))
1116 (cdr (assoc 'started worklog))
1117 (cdr (assoc 'time-spent-seconds worklog))
1118 comment-text
1119 (cl-function
1120 (lambda (&key data &allow-other-keys)
1121 (org-jira-update-worklogs-for-current-issue))))
1122 ;; else
1123 (jiralib-add-worklog
1124 issue-id
1125 (cdr (assoc 'started worklog))
1126 (cdr (assoc 'time-spent-seconds worklog))
1127 comment-text
1128 (cl-function
1129 (lambda (&key data &allow-other-keys)
1130 (org-jira-update-worklogs-for-current-issue))))
1132 )))))
1135 (defun org-jira-update-worklog ()
1136 "Update a worklog for the current issue."
1137 (interactive)
1138 (error "Deprecated, use org-jira-update-worklogs-from-org-clocks instead!")
1139 (let* ((issue-id (org-jira-get-from-org 'issue 'key))
1140 (worklog-id (org-jira-get-from-org 'worklog 'id))
1141 (timeSpent (org-jira-get-from-org 'worklog 'timeSpent))
1142 (timeSpent (if timeSpent
1143 timeSpent
1144 (read-string "Input the time you spent (such as 3w 1d 2h): ")))
1145 (timeSpent (replace-regexp-in-string " \\(\\sw\\)\\sw*\\(,\\|$\\)" "\\1" timeSpent))
1146 (startDate (org-jira-get-from-org 'worklog 'startDate))
1147 (startDate (if startDate
1148 startDate
1149 (org-read-date nil nil nil "Input when did you start")))
1150 (startDate (org-jira-time-format-to-jira startDate))
1151 (comment (replace-regexp-in-string "^ " "" (org-jira-get-worklog-comment worklog-id)))
1152 (worklog `((comment . ,comment)
1153 (timeSpent . ,timeSpent)
1154 (timeSpentInSeconds . 10)
1155 (startDate . ,startDate)))
1156 (worklog (if worklog-id
1157 (cons `(id . ,(replace-regexp-in-string "^worklog-" "" worklog-id)) worklog)
1158 worklog)))
1159 (if worklog-id
1160 (jiralib-update-worklog worklog)
1161 (jiralib-add-worklog-and-autoadjust-remaining-estimate issue-id startDate timeSpent comment))
1162 (org-jira-delete-current-worklog)
1163 (org-jira-update-worklogs-for-current-issue)))
1165 (defun org-jira-delete-current-comment ()
1166 "Delete the current comment."
1167 (ensure-on-comment
1168 (delete-region (point-min) (point-max))))
1170 (defun org-jira-delete-current-worklog ()
1171 "Delete the current worklog."
1172 (ensure-on-worklog
1173 (delete-region (point-min) (point-max))))
1175 ;;;###autoload
1176 (defun org-jira-copy-current-issue-key ()
1177 "Copy the current issue's key into clipboard."
1178 (interactive)
1179 (let ((issue-id (org-jira-get-from-org 'issue 'key)))
1180 (with-temp-buffer
1181 (insert issue-id)
1182 (kill-region (point-min) (point-max)))))
1184 (defun org-jira-get-comment-id (comment)
1185 (org-jira-find-value comment 'id))
1187 (defun org-jira-get-comment-author (comment)
1188 (org-jira-find-value comment 'author 'displayName))
1190 (defun org-jira-isa-ignored-comment? (comment)
1191 (member-ignore-case (oref comment author) org-jira-ignore-comment-user-list))
1193 (defun org-jira-maybe-reverse-comments (comments)
1194 (if org-jira-reverse-comment-order (reverse comments) comments))
1196 (defun org-jira-extract-comments-from-data (data)
1197 (->> (append data nil)
1198 org-jira-sdk-create-comments-from-data-list
1199 org-jira-maybe-reverse-comments
1200 (cl-remove-if #'org-jira-isa-ignored-comment?)))
1202 (defun org-jira--render-comment (issue-id Comment)
1203 (with-slots (comment-id author headline created updated body) Comment
1204 (ensure-on-issue-id
1205 issue-id
1206 (setq p (org-find-entry-with-id comment-id))
1207 (when (and p (>= p (point-min))
1208 (<= p (point-max)))
1209 (goto-char p)
1210 (org-narrow-to-subtree)
1211 (delete-region (point-min) (point-max)))
1212 (goto-char (point-max))
1213 (unless (looking-at "^")
1214 (insert "\n"))
1215 (insert "*** ")
1216 (org-jira-insert headline "\n")
1217 (org-narrow-to-subtree)
1218 (org-jira-entry-put (point) "ID" comment-id)
1219 (org-jira-entry-put (point) "created" created)
1220 (unless (string= created updated)
1221 (org-jira-entry-put (point) "updated" updated))
1222 (goto-char (point-max))
1223 ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup
1224 (org-jira-insert (replace-regexp-in-string "^" " " (or body ""))))))
1226 (defun org-jira-update-comments-for-issue (issue-id)
1227 "Update the comments for the specified ISSUE-ID issue."
1228 (jiralib-get-comments
1229 issue-id
1230 (org-jira-with-callback
1231 (org-jira-log "In the callback for org-jira-update-comments-for-issue.")
1232 (-->
1233 (org-jira-find-value cb-data 'comments)
1234 (org-jira-extract-comments-from-data it)
1235 (mapc (lambda (Comment) (org-jira--render-comment issue-id Comment)) it)))))
1237 (defun org-jira-update-comments-for-current-issue ()
1238 "Update comments for the current issue."
1239 (org-jira-log "About to update comments for current issue.")
1240 (-> (org-jira-get-from-org 'issue 'key) org-jira-update-comments-for-issue))
1242 (defun org-jira-delete-subtree ()
1243 "Derived from org-cut-subtree.
1245 Like that function, without mangling the user's clipboard for the
1246 purpose of wiping an old subtree."
1247 (let (beg end folded (beg0 (point)))
1248 (org-back-to-heading t) ; take what is really there
1249 (setq beg (point))
1250 (skip-chars-forward " \t\r\n")
1251 (save-match-data
1252 (save-excursion (outline-end-of-heading)
1253 (setq folded (org-invisible-p))
1254 (org-end-of-subtree t t)))
1255 ;; Include the end of an inlinetask
1256 (when (and (featurep 'org-inlinetask)
1257 (looking-at-p (concat (org-inlinetask-outline-regexp)
1258 "END[ \t]*$")))
1259 (end-of-line))
1260 (setq end (point))
1261 (goto-char beg0)
1262 (when (> end beg)
1263 (setq org-subtree-clip-folded folded)
1264 (org-save-markers-in-region beg end)
1265 (delete-region beg end))))
1267 (defun org-jira-update-attachments-for-current-issue ()
1268 "Update the attachments for the current issue."
1269 (when jiralib-use-restapi
1270 (lexical-let ((issue-id (org-jira-get-from-org 'issue 'key)))
1271 ;; Run the call
1272 (jiralib-get-attachments
1273 issue-id
1274 (save-excursion
1275 (cl-function
1276 (lambda (&key data &allow-other-keys)
1277 ;; First, make sure we're in the proper buffer (logic copied from org-jira-get-issues.
1278 (let* ((proj-key (replace-regexp-in-string "-.*" "" issue-id))
1279 (project-file (expand-file-name (concat proj-key ".org") org-jira-working-dir))
1280 (project-buffer (or (find-buffer-visiting project-file)
1281 (find-file project-file))))
1282 (with-current-buffer project-buffer
1283 ;; delete old attachment node
1284 (ensure-on-issue
1285 (if (org-goto-first-child)
1286 (while (org-goto-sibling)
1287 (forward-thing 'whitespace)
1288 (when (looking-at "Attachments:")
1289 (org-jira-delete-subtree)))))
1290 (let ((attachments (org-jira-find-value data 'fields 'attachment)))
1291 (when (not (zerop (length attachments)))
1292 (ensure-on-issue
1293 (if (org-goto-first-child)
1294 (progn
1295 (while (org-goto-sibling))
1296 (org-insert-heading-after-current))
1297 (org-insert-subheading nil))
1299 (insert "Attachments:")
1300 (mapc
1301 (lambda (attachment)
1302 (let ((attachment-id (org-jira-get-comment-id attachment))
1303 (author (org-jira-get-comment-author attachment))
1304 (created (org-jira-transform-time-format
1305 (org-jira-find-value attachment 'created)))
1306 (size (org-jira-find-value attachment 'size))
1307 (mimeType (org-jira-find-value attachment 'mimeType))
1308 (content (org-jira-find-value attachment 'content))
1309 (filename (org-jira-find-value attachment 'filename)))
1310 (if (looking-back "Attachments:")
1311 (org-insert-subheading nil)
1312 (org-insert-heading-respect-content))
1313 (insert "[[" content "][" filename "]]")
1314 (org-narrow-to-subtree)
1315 (org-jira-entry-put (point) "ID" attachment-id)
1316 (org-jira-entry-put (point) "Author" author)
1317 (org-jira-entry-put (point) "Name" filename)
1318 (org-jira-entry-put (point) "Created" created)
1319 (org-jira-entry-put (point) "Size" (ls-lisp-format-file-size size t))
1320 (org-jira-entry-put (point) "Content" content)
1321 (widen)))
1322 attachments)))))))))))))
1324 (defun org-jira-sort-org-clocks (clocks)
1325 "Given a CLOCKS list, sort it by start date descending."
1326 ;; Expects data such as this:
1328 ;; ((\"2017-02-26 Sun 00:08\" \"2017-02-26 Sun 01:08\" \"Hi\" \"10101\")
1329 ;; (\"2017-03-16 Thu 22:25\" \"2017-03-16 Thu 22:57\" \"Test\" \"10200\"))
1330 (sort clocks
1331 (lambda (a b)
1332 (> (time-to-seconds (date-to-time (car a)))
1333 (time-to-seconds (date-to-time (car b)))))))
1335 (defun org-jira-update-worklogs-for-current-issue ()
1336 "Update the worklogs for the current issue."
1337 (-> (org-jira-get-from-org 'issue 'key)
1338 org-jira-update-worklogs-for-issue))
1340 (defun org-jira-update-worklogs-for-issue (issue-id)
1341 "Update the worklogs for the current issue."
1342 ;; Run the call
1343 (jiralib-get-worklogs
1344 issue-id
1345 (org-jira-with-callback
1346 (ensure-on-issue-id
1347 issue-id
1348 (let ((worklogs (org-jira-find-value cb-data 'worklogs)))
1349 (org-jira-logbook-reset
1350 issue-id
1351 (org-jira-sort-org-clocks (org-jira-worklogs-to-org-clocks
1352 (jiralib-worklog-import--filter-apply worklogs)))))))))
1354 ;;;###autoload
1355 (defun org-jira-assign-issue ()
1356 "Update an issue with interactive re-assignment."
1357 (interactive)
1358 (let ((issue-id (org-jira-parse-issue-id)))
1359 (if issue-id
1360 (let* ((project (replace-regexp-in-string "-[0-9]+" "" issue-id))
1361 (jira-users (org-jira-get-assignable-users project))
1362 (user (completing-read "Assignee: " (mapcar 'car jira-users)))
1363 (assignee (cdr (assoc user jira-users))))
1364 (org-jira-update-issue-details issue-id :assignee assignee))
1365 (error "Not on an issue"))))
1367 ;;;###autoload
1368 (defun org-jira-update-issue ()
1369 "Update an issue."
1370 (interactive)
1371 (let ((issue-id (org-jira-parse-issue-id)))
1372 (if issue-id
1373 (org-jira-update-issue-details issue-id)
1374 (error "Not on an issue"))))
1376 ;;;###autoload
1377 (defun org-jira-todo-to-jira ()
1378 "Convert an ordinary todo item to a jira ticket."
1379 (interactive)
1380 (ensure-on-todo
1381 (when (org-jira-parse-issue-id)
1382 (error "Already on jira ticket"))
1383 (save-excursion (org-jira-create-issue
1384 (org-jira-read-project)
1385 (org-jira-read-issue-type)
1386 (org-get-heading t t)
1387 (org-get-entry)))
1388 (delete-region (point-min) (point-max))))
1390 ;;;###autoload
1391 (defun org-jira-get-subtasks ()
1392 "Get subtasks for the current issue."
1393 (interactive)
1394 (ensure-on-issue
1395 (org-jira-get-issues-headonly (jiralib-do-jql-search (format "parent = %s" (org-jira-parse-issue-id))))))
1397 (defvar org-jira-project-read-history nil)
1398 (defvar org-jira-boards-read-history nil)
1399 (defvar org-jira-components-read-history nil)
1400 (defvar org-jira-priority-read-history nil)
1401 (defvar org-jira-type-read-history nil)
1403 (defun org-jira-read-project ()
1404 "Read project name."
1405 (completing-read
1406 "Project: "
1407 (jiralib-make-list (jiralib-get-projects) 'key)
1411 'org-jira-project-read-history
1412 (car org-jira-project-read-history)))
1414 (defun org-jira-read-board ()
1415 "Read board name. Returns cons pair (name . integer-id)"
1416 (let* ((boards-alist
1417 (jiralib-make-assoc-list (jiralib-get-boards) 'name 'id))
1418 (board-name
1419 (completing-read "Boards: " boards-alist
1420 nil t nil
1421 'org-jira-boards-read-history
1422 (car org-jira-boards-read-history))))
1423 (assoc board-name boards-alist)))
1425 (defun org-jira-read-component (project)
1426 "Read the components options for PROJECT such as EX."
1427 (completing-read
1428 "Components (choose Done to stop): "
1429 (append '("Done") (mapcar 'cdr (jiralib-get-components project)))
1433 'org-jira-components-read-history
1434 "Done"))
1436 ;; TODO: Finish this feature - integrate into org-jira-create-issue
1437 (defun org-jira-read-components (project)
1438 "Types: string PROJECT : string (csv of components).
1440 Get all the components for the PROJECT such as EX,
1441 that should be bound to an issue."
1442 (let (components component)
1443 (while (not (equal "Done" component))
1444 (setq component (org-jira-read-component project))
1445 (unless (equal "Done" component)
1446 (push component components)))
1447 components))
1449 (defun org-jira-read-priority ()
1450 "Read priority name."
1451 (completing-read
1452 "Priority: "
1453 (mapcar 'cdr (jiralib-get-priorities))
1457 'org-jira-priority-read-history
1458 (car org-jira-priority-read-history)))
1460 (defun org-jira-read-issue-type (&optional project)
1461 "Read issue type name. PROJECT is the optional project key."
1462 (let* ((issue-types
1463 (mapcar 'cdr (if project
1464 (jiralib-get-issue-types-by-project project)
1465 (jiralib-get-issue-types))))
1466 (initial-input (when (member (car org-jira-type-read-history) issue-types)
1467 org-jira-type-read-history)))
1468 (completing-read
1469 "Type: "
1470 issue-types
1474 initial-input
1475 (car initial-input))))
1477 (defun org-jira-read-subtask-type ()
1478 "Read issue type name."
1479 (completing-read
1480 "Type: "
1481 (mapcar 'cdr (jiralib-get-subtask-types))
1485 'org-jira-type-read-history
1486 (car org-jira-type-read-history)))
1488 (defun org-jira-get-issue-struct (project type summary description)
1489 "Create an issue struct for PROJECT, of TYPE, with SUMMARY and DESCRIPTION."
1490 (if (or (equal project "")
1491 (equal type "")
1492 (equal summary ""))
1493 (error "Must provide all information!"))
1494 (let* ((project-components (jiralib-get-components project))
1495 (jira-users (org-jira-get-assignable-users project))
1496 (user (completing-read "Assignee: " (mapcar 'car jira-users)))
1497 (priority (car (rassoc (org-jira-read-priority) (jiralib-get-priorities))))
1498 (ticket-struct
1499 `((fields
1500 (project (key . ,project))
1501 (issuetype (id . ,(car (rassoc type (if (and (boundp 'parent-id) parent-id)
1502 (jiralib-get-subtask-types)
1503 (jiralib-get-issue-types))))))
1504 (summary . ,(format "%s%s" summary
1505 (if (and (boundp 'parent-id) parent-id)
1506 (format " (subtask of [jira:%s])" parent-id)
1507 "")))
1508 (description . ,description)
1509 (priority (id . ,priority))
1510 (assignee (name . ,(or (cdr (assoc user jira-users)) user)))))))
1511 ticket-struct))
1513 ;;;###autoload
1514 (defun org-jira-create-issue (project type summary description)
1515 "Create an issue in PROJECT, of type TYPE, with given SUMMARY and DESCRIPTION."
1516 (interactive
1517 (let* ((project (org-jira-read-project))
1518 (type (org-jira-read-issue-type project))
1519 (summary (read-string "Summary: "))
1520 (description (read-string "Description: ")))
1521 (list project type summary description)))
1522 (if (or (equal project "")
1523 (equal type "")
1524 (equal summary ""))
1525 (error "Must provide all information!"))
1526 (let* ((parent-id nil)
1527 (ticket-struct (org-jira-get-issue-struct project type summary description)))
1528 (org-jira-get-issues (list (jiralib-create-issue ticket-struct)))))
1530 ;;;###autoload
1531 (defun org-jira-create-subtask (project type summary description)
1532 "Create a subtask issue for PROJECT, of TYPE, with SUMMARY and DESCRIPTION."
1533 (interactive (ensure-on-issue (list (org-jira-read-project)
1534 (org-jira-read-subtask-type)
1535 (read-string "Summary: ")
1536 (read-string "Description: "))))
1537 (if (or (equal project "")
1538 (equal type "")
1539 (equal summary ""))
1540 (error "Must provide all information!"))
1541 (let* ((parent-id (org-jira-parse-issue-id))
1542 (ticket-struct (org-jira-get-issue-struct project type summary description)))
1543 (org-jira-get-issues (list (jiralib-create-subtask ticket-struct parent-id)))))
1545 (defun org-jira-strip-string (str)
1546 "Remove the beginning and ending white space for a string STR."
1547 (s-trim str))
1549 (defun org-jira-get-issue-val-from-org (key)
1550 "Return the requested value by KEY from the current issue."
1551 (ensure-on-issue
1552 (cond ((eq key 'description)
1553 (org-goto-first-child)
1554 (forward-thing 'whitespace)
1555 (if (looking-at "description: ")
1556 (org-jira-strip-string (org-get-entry))
1557 (error "Can not find description field for this issue")))
1559 ((eq key 'summary)
1560 (ensure-on-issue
1561 (org-get-heading t t)))
1563 ;; org returns a time tuple, we need to convert it
1564 ((eq key 'deadline)
1565 (let ((encoded-time (org-get-deadline-time (point))))
1566 (when encoded-time
1567 (cl-reduce (lambda (carry segment)
1568 (format "%s-%s" carry segment))
1569 (reverse (cl-subseq (decode-time encoded-time) 3 6))))))
1571 ;; default case, just grab the value in the properties block
1573 (when (symbolp key)
1574 (setq key (symbol-name key)))
1575 (setq key (or (assoc-default key org-jira-property-overrides)
1576 key))
1577 (when (string= key "key")
1578 (setq key "ID"))
1579 ;; The variable `org-special-properties' will mess this up
1580 ;; if our search, such as 'priority' is within there, so
1581 ;; don't bother with it for this (since we only ever care
1582 ;; about the local properties, not any hierarchal or special
1583 ;; ones).
1584 (let ((org-special-properties nil))
1585 (or (org-entry-get (point) key)
1586 ""))))))
1588 (defvar org-jira-actions-history nil)
1589 (defun org-jira-read-action (actions)
1590 "Read issue workflow progress ACTIONS."
1591 (let ((action (completing-read
1592 "Action: "
1593 (mapcar 'cdr actions)
1597 'org-jira-actions-history
1598 (car org-jira-actions-history))))
1599 (car (rassoc action actions))))
1601 (defvar org-jira-fields-history nil)
1602 (defun org-jira-read-field (fields)
1603 "Read (custom) FIELDS for workflow progress."
1604 (let ((field-desc (completing-read
1605 "More fields to set: "
1606 (cons "Thanks, no more fields are *required*." (mapcar 'org-jira-decode (mapcar 'cdr fields)))
1610 'org-jira-fields-history))
1611 field-name)
1612 (setq field-name (car (rassoc field-desc fields)))
1613 (if field-name
1614 (intern field-name)
1615 field-name)))
1618 (defvar org-jira-rest-fields nil
1619 "Extra fields are held here for usage between two endpoints.
1620 Used in org-jira-read-resolution and org-jira-progress-issue calls.")
1622 (defvar org-jira-resolution-history nil)
1623 (defun org-jira-read-resolution ()
1624 "Read issue workflow progress resolution."
1625 (if (not jiralib-use-restapi)
1626 (let ((resolution (completing-read
1627 "Resolution: "
1628 (mapcar 'cdr (jiralib-get-resolutions))
1632 'org-jira-resolution-history
1633 (car org-jira-resolution-history))))
1634 (car (rassoc resolution (jiralib-get-resolutions))))
1635 (let* ((resolutions (org-jira-find-value org-jira-rest-fields 'resolution 'allowedValues))
1636 (resolution-name (completing-read
1637 "Resolution: "
1638 (mapcar (lambda (resolution)
1639 (org-jira-find-value resolution 'name))
1640 resolutions))))
1641 (cons 'name resolution-name))))
1643 ;; TODO: Refactor to just scoop all ids from buffer, run ensure-on-issue-id on
1644 ;; each using a map, and refresh them that way. That way we don't have to iterate
1645 ;; on the user headings etc.
1646 (defun org-jira-refresh-issues-in-buffer ()
1647 "Iterate across all entries in current buffer, refreshing on issue :ID:.
1648 Where issue-id will be something such as \"EX-22\"."
1649 (interactive)
1650 (save-excursion
1651 (save-restriction
1652 (widen)
1653 (outline-show-all)
1654 (outline-hide-sublevels 2)
1655 (goto-char (point-min))
1656 (outline-next-visible-heading 1)
1657 (while (not (org-next-line-empty-p))
1658 (when (outline-on-heading-p t)
1659 ;; It's possible we could be on a non-org-jira headline, but
1660 ;; that should be an exceptional case and not necessitating a
1661 ;; fix atm.
1662 (org-jira-refresh-issue))
1663 (outline-next-visible-heading 1)))))
1665 ;;;###autoload
1666 (defun org-jira-refresh-issue ()
1667 "Refresh current issue from jira to org."
1668 (interactive)
1669 (ensure-on-issue
1670 (org-jira--refresh-issue (org-jira-id))))
1672 (defun org-jira--refresh-issue (issue-id)
1673 "Refresh issue from jira to org using ISSUE-ID."
1674 (jiralib-get-issue
1675 issue-id
1676 (org-jira-with-callback
1677 (org-jira-log (format "Received refresh issue data for id: %s" issue-id))
1678 (-> cb-data list org-jira-sdk-create-issues-from-data-list org-jira--render-issues-from-issue-list))))
1680 (defun org-jira--refresh-issue-by-id (issue-id)
1681 "Refresh issue from jira to org using ISSUE-ID."
1682 (ensure-on-issue-id
1683 issue-id
1684 (org-jira--refresh-issue issue-id)))
1686 (defvar org-jira-fields-values-history nil)
1687 ;;;###autoload
1688 (defun org-jira-progress-issue ()
1689 "Progress issue workflow."
1690 (interactive)
1691 (ensure-on-issue
1692 (let* ((issue-id (org-jira-id))
1693 (actions (jiralib-get-available-actions
1694 issue-id
1695 (org-jira-get-issue-val-from-org 'status)))
1696 (action (org-jira-read-action actions))
1697 (fields (jiralib-get-fields-for-action issue-id action))
1698 (org-jira-rest-fields fields)
1699 (field-key)
1700 (custom-fields-collector nil)
1701 (custom-fields
1702 (progn
1703 ;; delete those elements in fields, which have
1704 ;; already been set in custom-fields-collector
1705 (while fields
1706 (setq fields
1707 (cl-remove-if
1708 (lambda (strstr)
1709 (cl-member-if (lambda (symstr)
1710 (string= (car strstr) (symbol-name (car symstr))))
1711 custom-fields-collector))
1712 fields))
1713 (setq field-key (org-jira-read-field fields))
1714 (if (not field-key)
1715 (setq fields nil)
1716 (setq custom-fields-collector
1717 (cons
1718 (funcall (if jiralib-use-restapi
1719 #'list
1720 #'cons)
1721 field-key
1722 (if (eq field-key 'resolution)
1723 (org-jira-read-resolution)
1724 (let ((field-value (completing-read
1725 (format "Please enter %s's value: "
1726 (cdr (assoc (symbol-name field-key) fields)))
1727 org-jira-fields-values-history
1731 'org-jira-fields-values-history)))
1732 (if jiralib-use-restapi
1733 (cons 'name field-value)
1734 field-value))))
1735 custom-fields-collector))))
1736 custom-fields-collector)))
1737 (jiralib-progress-workflow-action
1738 issue-id
1739 action
1740 custom-fields
1741 (cl-function
1742 (lambda (&key data &allow-other-keys)
1743 (org-jira-refresh-issue)))))))
1745 (defun org-jira-progress-next-action (actions current-status)
1746 "Grab the user defined 'next' action from ACTIONS, given CURRENT-STATUS."
1747 (let* ((next-action-name (cdr (assoc current-status org-jira-progress-issue-flow)))
1748 (next-action-id (caar (cl-remove-if-not
1749 (lambda (action)
1750 (equal action next-action-name)) actions :key #'cdr))))
1751 next-action-id))
1753 ;;;###autoload
1754 (defun org-jira-progress-issue-next ()
1755 "Progress issue workflow."
1756 (interactive)
1757 (ensure-on-issue
1758 (let* ((issue-id (org-jira-id))
1759 (actions (jiralib-get-available-actions
1760 issue-id
1761 (org-jira-get-issue-val-from-org 'status)))
1762 (action (org-jira-progress-next-action actions (org-jira-get-issue-val-from-org 'status)))
1763 (fields (jiralib-get-fields-for-action issue-id action))
1764 (org-jira-rest-fields fields)
1765 (field-key)
1766 (custom-fields-collector nil)
1767 (custom-fields
1768 (progn
1769 ;; delete those elements in fields, which have
1770 ;; already been set in custom-fields-collector
1771 (while fields
1772 (setq fields
1773 (cl-remove-if
1774 (lambda (strstr)
1775 (cl-member-if (lambda (symstr)
1776 (string= (car strstr) (symbol-name (car symstr))))
1777 custom-fields-collector))
1778 fields))
1779 (setq field-key (org-jira-read-field fields))
1780 (if (not field-key)
1781 (setq fields nil)
1782 (setq custom-fields-collector
1783 (cons
1784 (funcall (if jiralib-use-restapi
1785 #'list
1786 #'cons)
1787 field-key
1788 (if (eq field-key 'resolution)
1789 (org-jira-read-resolution)
1790 (let ((field-value (completing-read
1791 (format "Please enter %s's value: "
1792 (cdr (assoc (symbol-name field-key) fields)))
1793 org-jira-fields-values-history
1797 'org-jira-fields-values-history)))
1798 (if jiralib-use-restapi
1799 (cons 'name field-value)
1800 field-value))))
1801 custom-fields-collector))))
1802 custom-fields-collector)))
1803 (if action
1804 (jiralib-progress-workflow-action
1805 issue-id
1806 action
1807 custom-fields
1808 (org-jira-with-callback
1809 (ensure-on-issue-id issue-id (org-jira-refresh-issue))))
1810 (error "No action defined for that step!")))))
1812 (defun org-jira-get-id-name-alist (name ids-to-names)
1813 "Find the id corresponding to NAME in IDS-TO-NAMES and return an alist with id and name as keys."
1814 (let ((id (car (rassoc name ids-to-names))))
1815 `((id . ,id)
1816 (name . ,name))))
1818 (defun org-jira-build-components-list (project-components org-issue-components)
1819 "Given PROJECT-COMPONENTS, attempt to build a list.
1821 If the PROJECT-COMPONENTS are nil, this should return:
1823 (list components []), which will translate into the JSON:
1825 {\"components\": []}
1827 otherwise it should return:
1829 (list components (list (cons id comp-id) (cons name item-name))),
1831 which will translate into the JSON:
1833 {\"components\": [{\"id\": \"comp-id\", \"name\": \"item\"}]}"
1834 (if (not project-components) (vector) ;; Return a blank array for JSON
1835 (apply 'list
1836 (cl-mapcan
1837 (lambda (item)
1838 (let ((comp-id (car (rassoc item project-components))))
1839 (if comp-id
1840 `(((id . ,comp-id)
1841 (name . ,item)))
1842 nil)))
1843 (split-string org-issue-components ",\\s *")))))
1845 (defun org-jira-strip-priority-tags (s)
1846 (->> s (replace-regexp-in-string "\\[#.*?\\]" "") s-trim))
1848 (defun org-jira-update-issue-details (issue-id &rest rest)
1849 "Update the details of issue ISSUE-ID. REST will contain optional input."
1850 (ensure-on-issue-id
1851 issue-id
1852 ;; Set up a bunch of values from the org content
1853 (let* ((org-issue-components (org-jira-get-issue-val-from-org 'components))
1854 (org-issue-description (s-trim (org-jira-get-issue-val-from-org 'description)))
1855 (org-issue-priority (org-jira-get-issue-val-from-org 'priority))
1856 (org-issue-type (org-jira-get-issue-val-from-org 'type))
1857 (org-issue-assignee (cl-getf rest :assignee (org-jira-get-issue-val-from-org 'assignee)))
1858 (project (replace-regexp-in-string "-[0-9]+" "" issue-id))
1859 (project-components (jiralib-get-components project)))
1861 ;; Lets fire off a worklog update async with the main issue
1862 ;; update, why not? This is better to fire first, because it
1863 ;; doesn't auto-refresh any areas, while the end of the main
1864 ;; update does a callback that reloads the worklog entries (so,
1865 ;; we hope that wont occur until after this successfully syncs
1866 ;; up). Only do this sync if the user defcustom defines it as such.
1867 (when org-jira-worklog-sync-p
1868 (org-jira-update-worklogs-from-org-clocks))
1870 ;; Send the update to jira
1871 (let ((update-fields
1872 (list (cons
1873 'components
1874 (or (org-jira-build-components-list
1875 project-components
1876 org-issue-components) []))
1877 (cons 'priority (org-jira-get-id-name-alist org-issue-priority
1878 (jiralib-get-priorities)))
1879 (cons 'description org-issue-description)
1880 (cons 'assignee (jiralib-get-user org-issue-assignee))
1881 (cons 'summary (org-jira-strip-priority-tags (org-jira-get-issue-val-from-org 'summary)))
1882 (cons 'issuetype (org-jira-get-id-name-alist org-issue-type
1883 (jiralib-get-issue-types))))))
1885 ;; If we enable duedate sync and we have a deadline present
1886 (when (and org-jira-deadline-duedate-sync-p
1887 (org-jira-get-issue-val-from-org 'deadline))
1888 (setq update-fields
1889 (append update-fields
1890 (list (cons 'duedate (org-jira-get-issue-val-from-org 'deadline))))))
1892 (jiralib-update-issue
1893 issue-id
1894 update-fields
1895 ;; This callback occurs on success
1896 (org-jira-with-callback
1897 (message (format "Issue '%s' updated!" issue-id))
1898 (jiralib-get-issue
1899 issue-id
1900 (org-jira-with-callback
1901 (org-jira-log "Update get issue for refresh callback hit.")
1902 (-> cb-data list org-jira-get-issues))))
1906 (defun org-jira-parse-issue-id ()
1907 "Get issue id from org text."
1908 (save-excursion
1909 (let ((continue t)
1910 issue-id)
1911 (while continue
1912 (when (string-match (jiralib-get-issue-regexp)
1913 (or (setq issue-id (org-entry-get (point) "ID"))
1914 ""))
1915 (setq continue nil))
1916 (unless (and continue (org-up-heading-safe))
1917 (setq continue nil)))
1918 issue-id)))
1920 (defun org-jira-get-from-org (type entry)
1921 "Get an org property from the current item.
1923 TYPE is the type to of the current item, and can be 'issue, or 'comment.
1925 ENTRY will vary, and is the name of the property to return. If
1926 it is a symbol, it will be converted to string."
1927 (when (symbolp entry)
1928 (setq entry (symbol-name entry)))
1929 (cond
1930 ((eq type 'issue)
1931 (org-jira-get-issue-val-from-org entry))
1932 ((eq type 'comment)
1933 (org-jira-get-comment-val-from-org entry))
1934 ((eq type 'worklog)
1935 (org-jira-get-worklog-val-from-org entry))
1936 (t (error "Unknown type %s" type))))
1938 (defun org-jira-get-comment-val-from-org (entry)
1939 "Get the JIRA issue field value ENTRY of the current comment item."
1940 (ensure-on-comment
1941 (when (symbolp entry)
1942 (setq entry (symbol-name entry)))
1943 (when (string= entry "id")
1944 (setq entry "ID"))
1945 (org-entry-get (point) entry)))
1947 (defun org-jira-get-worklog-val-from-org (entry)
1948 "Get the JIRA issue field value ENTRY of the current worklog item."
1949 (ensure-on-worklog
1950 (when (symbolp entry)
1951 (setq entry (symbol-name entry)))
1952 (when (string= entry "id")
1953 (setq entry "ID"))
1954 (org-entry-get (point) entry)))
1956 (defun org-jira-get-comment-body (&optional comment-id)
1957 "Get the comment body of the comment with id COMMENT-ID."
1958 (ensure-on-comment
1959 (goto-char (point-min))
1960 ;; so that search for :END: won't fail
1961 (org-jira-entry-put (point) "ID" comment-id)
1962 (search-forward ":END:" nil 1 1)
1963 (forward-line)
1964 (org-jira-strip-string (buffer-substring-no-properties (point) (point-max)))))
1966 (defun org-jira-get-worklog-comment (&optional worklog-id)
1967 "Get the worklog comment of the worklog with id WORKLOG-ID."
1968 (ensure-on-worklog
1969 (goto-char (point-min))
1970 ;; so that search for :END: won't fail
1971 (org-jira-entry-put (point) "ID" worklog-id)
1972 (search-forward ":END:" nil 1 1)
1973 (forward-line)
1974 (org-jira-strip-string (buffer-substring-no-properties (point) (point-max)))))
1976 (defun org-jira-id ()
1977 "Get the ID entry for the current heading."
1978 (org-entry-get (point) "ID"))
1980 ;;;###autoload
1981 (defun org-jira-browse-issue ()
1982 "Open the current issue in external browser."
1983 (interactive)
1984 (ensure-on-issue
1985 (browse-url (concat (replace-regexp-in-string "/*$" "" jiralib-url) "/browse/" (org-jira-id)))))
1987 (defun org-jira-url-copy-file (url newname)
1988 "Similar to url-copy-file but async."
1989 (lexical-let ((newname newname))
1990 (url-retrieve
1992 (lambda (status)
1993 (let ((buffer (current-buffer))
1994 (handle nil)
1995 (filename (if (and (file-exists-p newname)
1996 org-jira-download-ask-override)
1997 (read-string "File already exists, select new name or press ENTER to override: " newname)
1998 newname)))
1999 (if (not buffer)
2000 (error "Opening input file: No such file or directory, %s" url))
2001 (with-current-buffer buffer
2002 (setq handle (mm-dissect-buffer t)))
2003 (mm-save-part-to-file handle filename)
2004 (kill-buffer buffer)
2005 (mm-destroy-parts handle))))))
2007 ;;;###autoload
2008 (defun org-jira-download-attachment ()
2009 "Download the attachment under cursor."
2010 (interactive)
2011 (when jiralib-use-restapi
2012 (save-excursion
2013 (org-up-heading-safe)
2014 (org-back-to-heading)
2015 (forward-thing 'whitespace)
2016 (unless (looking-at "Attachments:")
2017 (error "Not on a attachment region!")))
2018 (let ((filename (org-entry-get (point) "Name"))
2019 (url (org-entry-get (point) "Content"))
2020 (url-request-extra-headers `(,jiralib-token)))
2021 (org-jira-url-copy-file
2023 (concat (file-name-as-directory org-jira-download-dir) filename)))))
2025 ;;;###autoload
2026 (defun org-jira-get-issues-from-filter (filter)
2027 "Get issues from the server-side stored filter named FILTER.
2029 Provide this command in case some users are not able to use
2030 client side jql (maybe because of JIRA server version?)."
2031 (interactive
2032 (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters)))))
2033 (org-jira-get-issues (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters))))))
2035 ;;;###autoload
2036 (defun org-jira-get-issues-from-filter-headonly (filter)
2037 "Get issues *head only* from saved filter named FILTER.
2038 See `org-jira-get-issues-from-filter'."
2039 (interactive
2040 (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters)))))
2041 (org-jira-get-issues-headonly (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters))))))
2043 (org-add-link-type "jira" 'org-jira-open)
2045 ;; This was only added in org 9.0, not sure all org users will have
2046 ;; that version, so keep the deprecated one from above for now.
2048 ;;(org-link-set-parameters "jira" ((:follow . 'org-jira-open)))
2050 (defun org-jira-open (path)
2051 "Open a Jira Link from PATH."
2052 (org-jira-get-issue path))
2054 ;;;###autoload
2055 (defun org-jira-get-issues-by-board ()
2056 "Get list of ISSUES from agile board."
2057 (interactive)
2058 (let* ((board (org-jira-read-board))
2059 (board-id (cdr board)))
2060 (jiralib-get-board-issues board-id
2061 :callback org-jira-get-issue-list-callback
2062 :limit (org-jira-get-board-limit board-id)
2063 :query-params (org-jira--make-jql-queryparams board-id))))
2065 (defun org-jira-get-board-limit (id)
2066 "Get limit for number of retrieved issues for a board
2067 id - integer board id"
2068 (let ((board (org-jira--get-board-from-buffer id)))
2069 (if (and board (slot-boundp board 'limit))
2070 (oref board limit)
2071 org-jira-boards-default-limit)))
2073 (defun org-jira--make-jql-queryparams (board-id)
2074 "make GET query parameters for jql, returns nil if JQL query is not set"
2075 (let* ((board (org-jira--get-board-from-buffer board-id))
2076 (jql (if (and board (slot-boundp board 'jql))
2077 (oref board jql))))
2078 (if (and jql (not (string-blank-p jql))) `((jql ,jql)))))
2080 ;;;###autoload
2081 (defun org-jira-get-issues-by-board-headonly ()
2082 "Get list of ISSUES from agile board, head only."
2083 (interactive)
2084 (let* ((board (org-jira-read-board))
2085 (board-id (cdr board)))
2086 (org-jira-get-issues-headonly
2087 (jiralib-get-board-issues board-id
2088 :limit (org-jira-get-board-limit board-id)
2089 :query-params (org-jira--make-jql-queryparams board-id)))))
2092 (defun org-jira--render-boards-from-list (boards)
2093 "Add the boards from list into the org file.
2095 boards - list of `org-jira-sdk-board' records."
2096 (mapc 'org-jira--render-board boards))
2099 (defun org-jira--render-board (board)
2100 "Render single board"
2101 ;;(org-jira-sdk-dump board)
2102 (with-slots (id name url board-type jql limit) board
2103 (with-current-buffer (org-jira--get-boards-buffer)
2104 (org-jira-mode t)
2105 (org-jira-freeze-ui
2106 (org-save-outline-visibility t
2107 (save-restriction
2108 (outline-show-all)
2109 (widen)
2110 (goto-char (point-min))
2111 (let* ((board-headline
2112 (format "Board: [[%s][%s]]" url name))
2113 (headline-pos
2114 (org-find-exact-headline-in-buffer board-headline (current-buffer) t))
2115 (entry-exists (and headline-pos (>= headline-pos (point-min)) (<= headline-pos (point-max))))
2116 (limit-value (if (slot-boundp board 'limit) (int-to-string limit) nil))
2117 (jql-value (if (slot-boundp board 'jql) jql nil)))
2118 (if entry-exists
2119 (progn
2120 (goto-char headline-pos)
2121 (org-narrow-to-subtree)
2122 (end-of-line))
2123 (goto-char (point-max))
2124 (unless (looking-at "^")
2125 (insert "\n"))
2126 (insert "* ")
2127 (org-jira-insert board-headline)
2128 (org-narrow-to-subtree))
2129 (org-jira-entry-put (point) "name" name)
2130 (org-jira-entry-put (point) "type" board-type)
2131 (org-jira-entry-put (point) "url" url)
2132 ;; do not overwrite existing user properties with empty values
2133 (if (or (not entry-exists) limit-value)
2134 (org-jira-entry-put (point) "limit" limit-value))
2135 (if (or (not entry-exists) jql-value)
2136 (org-jira-entry-put (point) "JQL" jql-value ))
2137 (org-jira-entry-put (point) "ID" id))))))))
2139 (defun org-jira--get-boards-file ()
2140 (expand-file-name "boards-list.org" org-jira-working-dir))
2142 (defun org-jira--get-boards-buffer ()
2143 "Return buffer for list of agile boards. Create one if it does not exist."
2144 (let* ((boards-file (org-jira--get-boards-file))
2145 (existing-buffer (find-buffer-visiting boards-file)))
2146 (if existing-buffer
2147 existing-buffer
2148 (find-file-noselect boards-file))))
2150 ;;;###autoload
2151 (defun org-jira-get-boards ()
2152 "Get list of boards and their properies."
2153 (interactive)
2154 (let* ((datalist (jiralib-get-boards))
2155 (boards (org-jira-sdk-create-boards-from-data-list datalist)))
2156 (org-jira--render-boards-from-list boards))
2157 (switch-to-buffer (org-jira--get-boards-buffer)))
2159 (defun org-jira--get-board-from-buffer (id)
2160 "Parse board record from org file."
2161 (with-current-buffer (org-jira--get-boards-buffer)
2162 (org-jira-freeze-ui
2163 (let ((pos (org-find-property "ID" (int-to-string id))))
2164 (if pos
2165 (progn
2166 (goto-char pos)
2167 (apply 'org-jira-sdk-board
2168 (reduce
2169 #'(lambda (acc entry)
2170 (let* ((pname (car entry))
2171 (pval (cdr entry))
2172 (pair (and pval
2173 (not (string-empty-p pval))
2174 (cond
2175 ((equal pname "ID")
2176 (list :id pval))
2177 ((equal pname "URL")
2178 (list :url pval))
2179 ((equal pname "TYPE")
2180 (list :board-type pval))
2181 ((equal pname "NAME")
2182 (list :name pval))
2183 ((equal pname "LIMIT")
2184 (list :limit (string-to-number pval)))
2185 ((equal pname "JQL")
2186 (list :jql pval))
2187 (t nil)))))
2188 (if pair (append pair acc) acc)))
2189 (org-entry-properties) :initial-value ()))))))))
2191 (defun org-jira-get-org-keyword-from-status (status)
2192 "Gets an 'org-mode' keyword corresponding to a given jira STATUS."
2193 (if org-jira-use-status-as-todo
2194 (upcase (replace-regexp-in-string " " "-" status))
2195 (let ((known-keyword (assoc status org-jira-jira-status-to-org-keyword-alist)))
2196 (cond (known-keyword (cdr known-keyword))
2197 ((member (org-jira-decode status) org-jira-done-states) "DONE")
2198 ("TODO")))))
2200 (defun org-jira-get-org-priority-string (character)
2201 "Return an org-priority-string based on CHARACTER and user settings."
2202 (cond ((not character) "")
2203 ((and org-jira-priority-to-org-priority-omit-default-priority
2204 (eq character org-default-priority)) "")
2205 (t (format "[#%c] " character))))
2207 (defun org-jira-get-org-priority-cookie-from-issue (priority)
2208 "Get the `org-mode' [#X] PRIORITY cookie."
2209 (let ((character (cdr (assoc priority org-jira-priority-to-org-priority-alist))))
2210 (org-jira-get-org-priority-string character)))
2212 (provide 'org-jira)
2213 ;;; org-jira.el ends here