From: Andreas Gerler Date: Sun, 22 Dec 2024 12:27:50 +0000 (+0100) Subject: updae org-jira X-Git-Url: http://git.bundesbrandschatzamt.de/?a=commitdiff_plain;h=4b7ee02a150f687ea4716ab0a28dacaf3febc804;p=emacsconfig.git updae org-jira Signed-off-by: Andreas Gerler --- diff --git a/elpa/org-jira-20230915.57/jiralib.el b/elpa/org-jira-20230915.57/jiralib.el deleted file mode 100644 index 4c1907b3..00000000 --- a/elpa/org-jira-20230915.57/jiralib.el +++ /dev/null @@ -1,1332 +0,0 @@ -;;; jiralib.el -- Provide connectivity to JIRA SOAP/REST services. - -;; Copyright (C) 2016-2022 Matthew Carter -;; Copyright (C) 2011 Bao Haojun -;; original Copyright (C) 2009 Alex Harsanyi - -;; Also, used some code from jira.el, which use xml-rpc instead of soap. -;; Thus Copyright (C) for jira.el related code: -;; Brian Zwahr -;; Dave Benjamin - -;; Authors: -;; Matthew Carter -;; Bao Haojun -;; Alex Harsanyi - -;; Maintainer: Matthew Carter -;; Version: 3.0.0 -;; Homepage: https://github.com/ahungry/org-jira - -;; This file is not part of GNU Emacs. - -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see -;; or write to the Free Software -;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -;; 02110-1301, USA. - -;; Author: Alexandru Harsanyi (AlexHarsanyi@gmail.com) -;; Created: December, 2009 -;; Keywords: soap, web-services, jira -;; Homepage: http://code.google.com/p/emacs-soap-client - -;;; Commentary: - -;; This file provides a programmatic interface to JIRA. It provides access to -;; JIRA from other programs, but no user level functionality. - -;; Jira References: - -;; Primary reference (on current Jira, only REST is supported): -;; https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis - -;; Full API list reference: -;; https://docs.atlassian.com/jira/REST/cloud/ - -;; Legacy reference (unsupported and deprecated/unavailable): -;; http://confluence.atlassian.com/display/JIRA/Creating+a+SOAP+Client - -;; JavaDoc for the Jira SOAP service -;; http://docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html - -;;; News: - -;;;; Changes since 2.6.3: -;; - Add worklog import filter and control variable for external worklogs. -;; - Add the worklog related endpoint/calls. - -;;;; Changes since 2.1.0: -;; - Remove os_username / os_password manual http request as part of sign in process -;; This produces sysadmin level warnings on Jira when these are used under the latest Jira. -;; - Remove unused function jiralib-link-issue -;; - Bring version up to match org-jira version so they can share tag - -;;;; Changes since 2.0.0: -;; - Allow issue type query by project - -;;;; Changes since 0.0.0: -;; - Converted many calls to async -;; - Converted many calls to make use of caching - -;;; Code: - -(eval-when-compile (require 'cl)) -(require 'cl-seq) -(require 'soap-client) -(require 'request) -(require 'json) -(require 'url-parse) -(require 'url-util) - -(defconst jiralib-version "3.0.0" - "Current version of jiralib.el.") - -(defgroup jiralib nil - "Jiralib customization group." - :group 'applications) - -(defgroup jiralib-faces nil - "Faces for displaying Jiralib information." - :group 'jiralib) - -(defcustom jiralib-use-restapi t - "Use restapi instead of soap." - :group 'jiralib - :type 'boolean - :initialize 'custom-initialize-set) - -(defcustom jiralib-coding-system 'utf-8 - "Use custom coding system for Jiralib." - :group 'jiralib) - -(defcustom jiralib-host "" - "User customizable host name of the Jiralib server. - -This will be used with USERNAME to compute password from -.authinfo file. Will be calculated from jiralib-url if not set." - :group 'jiralib - :type 'string - :initialize 'custom-initialize-set) - -(defcustom jiralib-user "" - "User customizable user name of the Jiralib server. - -This will be used with USERNAME to compute password from -.authinfo file. Will be calculated from jiralib-url if not set." - :group 'jiralib - :type 'string - :initialize 'custom-initialize-set) - -(defface jiralib-issue-info-face - '((t (:foreground "black" :background "yellow4"))) - "Base face for issue information." - :group 'jiralib-faces) - -(defface jiralib-issue-info-header-face - '((t (:bold t :inherit 'jiralib-issue-info-face))) - "Base face for issue headers." - :group 'jiralib-faces) - -(defface jiralib-issue-summary-face - '((t (:bold t))) - "Base face for issue summary." - :group 'jiralib-faces) - -(defface jiralib-comment-face - '((t (:background "gray23"))) - "Base face for comments." - :group 'jiralib-faces) - -(defface jiralib-comment-header-face - '((t (:bold t))) - "Base face for comment headers." - :group 'jiralib-faces) - -(defface jiralib-link-issue-face - '((t (:underline t))) - "Face for linked issues." - :group 'jiralib-faces) - -(defface jiralib-link-project-face - '((t (:underline t))) - "Face for linked projects" - :group 'jiralib-faces) - -(defface jiralib-link-filter-face - '((t (:underline t))) - "Face for linked filters" - :group 'jiralib-faces) - -(defvar jiralib-mode-hook nil) -(defvar jiralib-mode-map nil) -(defvar jiralib-issue-regexp "\\<\\(?:[A-Za-z0-9]+\\)-[0-9]+\\>") - -(defcustom jiralib-wsdl-descriptor-url - "" - "The location for the WSDL descriptor for the JIRA service. -This is specific to your local JIRA installation. The URL is -typically: - - http://YOUR_INSTALLATION/rpc/soap/jirasoapservice-v2?wsdl - -The default value works if JIRA is located at a hostname named -'jira'." - :type 'string - :group 'jiralib) - -(defcustom jiralib-url - "http://localhost:8081/" - "The address of the jira host." - :type 'string - :group 'jiralib) - -(defcustom jiralib-agile-page-size - 50 - "Page size for agile API retrieve. Limited by server property jira.search.views.default.max" - :type 'integer - :group 'jiralib) - -(defvar jiralib-token nil - "JIRA token used for authentication.") - -(defvar jiralib-user-login-name nil - "The name of the user logged into JIRA. -This is maintained by `jiralib-login'.") - -(defvar jiralib-wsdl nil) - -(defcustom jiralib-worklog-import--filters-alist - (list - '(nil "WorklogUpdatedByCurrentUser" - (lambda (wl) - (let-alist wl - (when - (and wl - (string-equal - (downcase - (or jiralib-user-login-name user-login-name "")) - (downcase (or .updateAuthor.name - (car (split-string (or .updateAuthor.emailAddress "") "@")) - "")))) - wl)))) - '(nil "WorklogAuthoredByCurrentUser" - (lambda (wl) - (let-alist wl - (when - (and wl - (string-equal - (downcase - (or jiralib-user-login-name user-login-name)) - (downcase (or .author.name - (car (split-string (or .author.emailAddress "") "@")))))) - wl))))) - "A list of triplets: ('Global-Enable 'Descriptive-Label 'Function-Definition) -that apply worklog predicate filters during import. - -Example: (list '('t \"descriptive-predicate-label\" (lambda (x) x)))" - :type '(repeat (list boolean string function)) - :group 'org-jira) - - -(defcustom jiralib-update-issue-fields-exclude-list nil - "A list of symbols to check for exclusion on updates based on matching key. -Key names should be one of components, description, assignee, reporter, summary, issuetype." - :type '(set (const :tag "Exclude components" components) - (const :tag "Exclude description" description) - (const :tag "Exclude assignee" assignee) - (const :tag "Exclude reporter" reporter) - (const :tag "Exclude summary" summary) - (const :tag "Exclude priority" priority) - (const :tag "Exclude issue type" issuetype)) - :group 'org-jira) - -(defun jiralib-load-wsdl () - "Load the JIRA WSDL descriptor." - (setq jiralib-wsdl (soap-load-wsdl-from-url (if (string-equal jiralib-wsdl-descriptor-url "") - (concat jiralib-url "/rpc/soap/jirasoapservice-v2?wsdl") - jiralib-wsdl-descriptor-url)))) - -(defun jiralib-login (username password) - "Login into JIRA as user USERNAME with PASSWORD. - -After a successful login, store the authentication token in -`jiralib-token'." - ;; NOTE that we cannot rely on `jiralib-call' because `jiralib-call' relies on - ;; us ;-) - (interactive - (if (> 24 emacs-major-version) - (let ((user (read-string "Username for Jira server login? ")) - (password (read-passwd "Password for Jira server login? "))) - (list user password)) - (let ((found (nth 0 (auth-source-search :max 1 - :host (if (string= jiralib-host "") - (url-host (url-generic-parse-url jiralib-url)) - jiralib-host) - :user (if (string= jiralib-user "") - (url-user (url-generic-parse-url jiralib-url)) - jiralib-user) - ;; secrets.el wouldn’t accept a number. - :port (list (number-to-string (url-port (url-generic-parse-url jiralib-url))) - (url-type (url-generic-parse-url jiralib-url))) - :require '(:user :secret) - :create t))) - user secret) - (when found - (setq user (plist-get found :user) - secret - (let ((sec (plist-get found :secret))) - (if (functionp sec) - (funcall sec) - sec))) - (list user secret))))) - (if jiralib-use-restapi - (setq jiralib-token `("Authorization" . , (format "Basic %s" (base64-encode-string (concat username ":" password) t)))) - (unless jiralib-wsdl - (jiralib-load-wsdl)) - (setq jiralib-token - (car (soap-invoke jiralib-wsdl "jirasoapservice-v2" "login" username password)))) - (setq jiralib-user-login-name username)) - -(defvar jiralib-complete-callback nil) - -(defun jiralib-call (method callback &rest params) - "Invoke the Jira METHOD, then CALLBACK with supplied PARAMS. - -This function should be used for all JIRA interface calls, as the -method ensures the user is logged in and invokes `soap-invoke' -with the correct service name and authentication token. - -All JIRA interface methods take an authentication token as the -first argument. The authentication token is supplied by this -function, so PARAMS should omit this parameter. For example, the -\"getIssue\" method takes two parameters: auth and key, however, -when invoking it through `jiralib-call', the call should be: - - (jiralib-call \"getIssue\" KEY) - -CALLBACK should be the post processing function to run with the -completed data from the request result, which can be accessed with: - - (cl-getf data :data) - -as such, the CALLBACK should follow this type of form: - - (cl-function - (lambda (&rest data &allow-other-keys) - (print (cl-getf data :data)))) - -If CALLBACK is set to nil then the request will occur with sync. -This produces a noticeable slowdown and is not recommended by -request.el, so if at all possible, it should be avoided." - ;; @TODO :auth: Probably pass this all the way down, but I think - ;; it may be OK at the moment to just set the variable each time. - - (setq jiralib-complete-callback - ;; Don't run with async if we don't have a login token yet. - (if jiralib-token callback nil)) - - ;; If we don't have a regex set, ensure it is set BEFORE any async - ;; calls are processing, or we're going to have a bad time. - ;; This should only end up running once per session. - (unless jiralib-issue-regexp - (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e)))) - (append (jiralib--rest-call-it - "/rest/api/2/project" - :params '((expand . "description,lead,url,projectKeys"))) - nil) - ))) - (when projects - (setq jiralib-issue-regexp - (concat "\\<" (regexp-opt projects) "-[0-9]+\\>"))))) - - (if (not jiralib-use-restapi) - (car (apply 'jiralib--call-it method params)) - (unless jiralib-token - (call-interactively 'jiralib-login)) - (cl-case (intern method) - ('getStatuses (jiralib--rest-call-it "/rest/api/2/status")) - ('getIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype")) - ('getSubTaskIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype")) - ('getIssueTypesByProject - (let ((response (jiralib--rest-call-it (format "/rest/api/2/project/%s" (first params))))) - (cl-coerce (cdr (assoc 'issueTypes response)) 'list))) - ('getUser (jiralib--rest-call-it "/rest/api/2/user" :params `((accountId . ,(first params))))) - ('getVersions (jiralib--rest-call-it (format "/rest/api/2/project/%s/versions" (first params)))) - - ;; Worklog calls - ('getWorklogs - (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)))) - - ('addWorklog - (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)) - :type "POST" - :data (json-encode (second params)))) - - ('updateWorklog - (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog/%s" (first params) (second params)) - :type "PUT" - :data (json-encode (third params)))) - - ('addWorklogAndAutoAdjustRemainingEstimate - (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)) - :type "POST" - :data (json-encode (second params)))) - - ('addComment (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/comment" (first params)) - :type "POST" - :data (json-encode (second params)))) - ('createIssue - ;; Creating the issue doesn't return it, a second call must be - ;; made to pull it in by using the self key in response. - (let ((response (jiralib--rest-call-it - "/rest/api/2/issue" - :type "POST" - :data (json-encode (first params))))) - (jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET") - )) - ('createIssueWithParent - (let ((response (jiralib--rest-call-it - "/rest/api/2/issue" - :type "POST" - :data (json-encode (first params))))) - (jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET") - )) - ('editComment (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params)) - :data (json-encode `((body . ,(third params)))) - :type "PUT")) - ('getBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s" (first params)))) - ('getBoards (apply 'jiralib--agile-call-it "/rest/agile/1.0/board" 'values params)) - ('getComment (org-jira-find-value - (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params))) - 'comments)) - ('getComments (org-jira-find-value - (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/comment" (first params))) - 'comments)) - ('getAttachmentsFromIssue (org-jira-find-value - (jiralib--rest-call-it - (format "/rest/api/2/issue/%s?fields=attachment" (first params))) - 'comments)) - ('getComponents (jiralib--rest-call-it - (format "/rest/api/2/project/%s/components" (first params)))) - ('getIssue (jiralib--rest-call-it - (format "/rest/api/2/issue/%s" (first params)))) - ('getIssuesFromBoard (apply 'jiralib--agile-call-it - (format "rest/agile/1.0/board/%d/issue" (first params)) - 'issues - (cdr params))) - ('getSprintsFromBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s/sprint" (first params)))) - ('getIssuesFromSprint (apply 'jiralib--agile-call-it - (format "rest/agile/1.0/sprint/%d/issue" (first params)) - 'issues - (cdr params))) - ('getIssuesFromJqlSearch (append (cdr ( assoc 'issues (jiralib--rest-call-it - "/rest/api/2/search" - :type "POST" - :data (json-encode `((jql . ,(first params)) - (maxResults . ,(second params))))))) - nil)) - ('getPriorities (jiralib--rest-call-it - "/rest/api/2/priority")) - ('getProjects (jiralib--rest-call-it "rest/api/2/project")) - ('getProjectsNoSchemes (append (jiralib--rest-call-it - "/rest/api/2/project" - :params '((expand . "description,lead,url,projectKeys"))) nil)) - ('getResolutions (append (jiralib--rest-call-it - "/rest/api/2/resolution") nil)) - ('getAvailableActions - (mapcar - (lambda (trans) - `(,(assoc 'name trans) ,(assoc 'id trans))) - (cdadr (jiralib--rest-call-it (format "/rest/api/2/issue/%s/transitions" (first params)))))) - ('getFieldsForAction (org-jira-find-value (car (let ((issue (first params)) - (action (second params))) - (seq-filter (lambda (trans) - (or (string-equal action (org-jira-find-value trans 'id)) - (string-equal action (org-jira-find-value trans 'name)))) - (cdadr (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/transitions" (first params)) - :params '((expand . "transitions.fields"))))))) - 'fields)) - ('progressWorkflowAction (jiralib--rest-call-it - (format "/rest/api/2/issue/%s/transitions" (first params)) - :type "POST" - :data (json-encode `(,(car (second params)) ,(car (third params)))))) - ('getUsers - (jiralib--rest-call-it (format "/rest/api/2/user/assignable/search?project=%s&maxResults=10000" (first params)) - :type "GET")) - ('updateIssue (jiralib--rest-call-it - (format "/rest/api/2/issue/%s" (first params)) - :type "PUT" - :data (json-encode `((fields . ,(second params))))))))) - -(defun jiralib--soap-call-it (&rest args) - "Deprecated SOAP call endpoint. Will be removed soon. -Pass ARGS to jiralib-call." - (let ((jiralib-token nil) - (jiralib-use-restapi nil)) - (apply #'jiralib-call args))) - -(defun jiralib--json-read () - "Read with json, force utf-8" - (decode-coding-region (point) (point-max) jiralib-coding-system) - (json-read)) - -(defun jiralib--rest-call-it (api &rest args) - "Invoke the corresponding jira rest method API. -Invoking COMPLETE-CALLBACK when the -JIRALIB-COMPLETE-CALLBACK is non-nil, request finishes, and -passing ARGS to REQUEST." - (unless api (error "jiralib--rest-call-it was called with a NIL api value.")) - (setq args - (mapcar - (lambda (arg) - (if (stringp arg) - (encode-coding-string arg jiralib-coding-system) - arg)) - args)) - (append (request-response-data - (apply #'request (if (string-match "^http[s]*://" api) api ;; If an absolute path, use it - (concat (replace-regexp-in-string "/*$" "/" jiralib-url) - (replace-regexp-in-string "^/*" "" api))) - :sync (not jiralib-complete-callback) - :headers `(,jiralib-token ("Content-Type" . "application/json")) - :parser 'jiralib--json-read - :complete jiralib-complete-callback - ;; Ensure we have useful errors - :error - (lexical-let - ((my-api api) - (my-args args)) - (cl-function - (lambda (&key data &allow-other-keys) - (print "JIRA_ERROR - see your *Messages* buffer for more details.") - (print "JIRA_ERROR REQUEST: ") - (print my-api) - (print my-args) - (print "JIRA_ERROR RESPONSE: ") - (print data) - (error "JIRA_ERROR - see your *Messages* buffer for more details.") - ))) - args)) - nil)) - -(defun jiralib--call-it (method &rest params) - "Invoke the JIRA METHOD with supplied PARAMS. - -Internal use, returns a list of responses, of which only the -first is normally used." - (when (symbolp method) - (setq method (symbol-name method))) - (unless jiralib-token - (call-interactively 'jiralib-login)) - (condition-case data - (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2" - method jiralib-token params) - (soap-error - ;; If we are here, we had a token, but it expired. Re-login and try - ;; again. - (setq jiralib-token nil) - (call-interactively 'jiralib-login) - (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2" - method jiralib-token params)))) - - -;;;; Some utility functions - -(defun jiralib-make-list (data field) - "Map all assoc elements in DATA to the value of FIELD in that element." - (cl-loop for element in data - collect (cdr (assoc field element)))) - -(defun jiralib-make-assoc-list (data key-field value-field) - "Create an association list from a SOAP structure array. - -DATA is a list of association lists (a SOAP array-of type) -KEY-FIELD is the field to use as the key in the returned alist -VALUE-FIELD is the field to use as the value in the returned alist" - (cl-loop for element in data - collect (cons (cdr (assoc key-field element)) - (cdr (assoc value-field element))))) - -(defun jiralib-make-remote-field-values (fields) - "Transform the (KEY . VALUE) list FIELDS into a RemoteFieldValue structure. - -Each (KEY . VALUE) pair is transformed into - ((id . KEY) (values . (VALUE))) - -This method exists because Several JIRA methods require a -RemoteFieldValue list, but it is easier to work with ALISTS in -emacs-lisp" - (let ((remote-field-values)) - - ;; we accept an ALIST of field-name field-values parameter, but we need to - ;; construct a structure that encodes as a RemoteFieldValue which is what - ;; updateIssue wants - (dolist (field fields) - (let ((name (car field)) - (value (cdr field))) - (when (symbolp name) - (setq name (symbol-name name))) - ;; Value must be an "array" (for which soap-client accepts lists) even - ;; if it is just one value - (unless (vectorp value) - (setq value (vector value))) - (push `((id . ,name) (values . ,value)) - remote-field-values))) - - (apply 'vector (nreverse remote-field-values)))) - - -;;;; Wrappers around JIRA methods - -(defun jiralib--rest-api-for-issue-key (key) - "Return jira rest api for issue KEY." - (concat "rest/api/2/issue/" key)) - -(defun jiralib-filter-fields-by-exclude-list (exclude-list fields) - (cl-remove-if - (lambda (el) (cl-member (car el) exclude-list)) fields)) - -(defun jiralib-update-issue (key fields &optional callback) - "Update the issue with id KEY with the values in FIELDS, invoking CALLBACK." - (let ((filtered-fields (jiralib-filter-fields-by-exclude-list - jiralib-update-issue-fields-exclude-list - fields))) - (jiralib-call - "updateIssue" - callback - key (if jiralib-use-restapi - filtered-fields - (jiralib-make-remote-field-values filtered-fields))))) - -(defvar jiralib-status-codes-cache nil) - -(defun jiralib-get-statuses () - "Return an assoc list mapping a status code to its name. -NOTE: Status codes are stored as strings, not numbers. - -This function will only ask JIRA for the list of codes once, then -will cache it." - (unless jiralib-status-codes-cache - (setq jiralib-status-codes-cache - (jiralib-make-assoc-list (jiralib-call "getStatuses" nil) 'id 'name))) - jiralib-status-codes-cache) - -(defvar jiralib-issue-types-cache nil) - -(defun jiralib-get-issue-types () - "Return an assoc list mapping an issue type code to its name. -NOTE: Issue type codes are stored as strings, not numbers. - -This function will only ask JIRA for the list of codes once, than -will cache it. - -The issue types returned via getIssueTypes are all the ones -available to the user, but not necessarily available to the given -project. - -This endpoint is essentially a master reference for when issue -types need a name lookup when given an id. - -For applying issue types to a given project that is being created, see -the #'jiralib-get-issue-types-by-project call." - (unless jiralib-issue-types-cache - (setq jiralib-issue-types-cache - (jiralib-make-assoc-list (jiralib-call "getIssueTypes" nil) 'id 'name))) - jiralib-issue-types-cache) - -(defvar jiralib-issue-types-by-project-cache nil "An alist of available issue types.") - -(defun jiralib-get-issue-types-by-project (project) - "Return the available issue types for PROJECT. - -PROJECT should be the key, such as `EX' or `DEMO'." - (unless (assoc project jiralib-issue-types-by-project-cache) - (push (cons project - (jiralib-make-assoc-list - (jiralib-call "getIssueTypesByProject" nil project) - 'id 'name)) - jiralib-issue-types-by-project-cache)) - (cdr (assoc project jiralib-issue-types-by-project-cache))) - -(defvar jiralib-priority-codes-cache nil) - -(defun jiralib-get-priorities () - "Return an assoc list mapping a priority code to its name. -NOTE: Priority codes are stored as strings, not numbers. - -This function will only ask JIRA for the list of codes once, than -will cache it." - (unless jiralib-priority-codes-cache - (setq jiralib-priority-codes-cache - (jiralib-make-assoc-list (jiralib-call "getPriorities" nil) 'id 'name))) - jiralib-priority-codes-cache) - -(defvar jiralib-resolution-code-cache nil) - -(defun jiralib-get-resolutions () - "Return an assoc list mapping a resolution code to its name. -NOTE: Resolution codes are stored as strings, not numbers. - -This function will only ask JIRA for the list of codes once, than -will cache it." - (unless jiralib-resolution-code-cache - (setq jiralib-resolution-code-cache - (jiralib-make-assoc-list (jiralib-call "getResolutions" nil) 'id 'name))) - jiralib-resolution-code-cache) - -;; NOTE: it is not such a good idea to use this, as it needs a JIRA -;; connection to construct the regexp (the user might be prompted for a JIRA -;; username and password). -;; -;; The best use of this function is to generate the regexp once-off and -;; persist it somewhere. -;; -;; FIXME: Probably just deprecate/remove this, we can assert we're on -;; an issue with a general regexp that matches the common format, vs -;; needing to know specific user project list. -(defun jiralib-get-issue-regexp () - "Return a regexp that will match an issue id. - -The regexp is constructed from the project keys in the JIRA -database. An issue is assumed to be in the format KEY-NUMBER, -where KEY is a project key and NUMBER is the issue number." - (unless jiralib-issue-regexp - (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e)))) - (jiralib-call "getProjectsNoSchemes" nil)))) - (when projects - (setq jiralib-issue-regexp - (concat "\\<" (regexp-opt projects) "-[0-9]+\\>"))))) - jiralib-issue-regexp) - -(defun jiralib-do-jql-search (jql &optional limit callback) - "Run a JQL query and return the list of issues that matched. -LIMIT is the maximum number of queries to return. Note that JIRA -has an internal limit of how many queries to return, as such, it -might not be possible to find *ALL* the issues that match a -query." - (unless (or limit (numberp limit)) - (setq limit 100)) - (jiralib-call "getIssuesFromJqlSearch" callback jql limit)) - -(defcustom jiralib-available-actions-cache-p t - "Set to t to enable caching for jiralib-get-available-actions. - -If nil, will disable caching for this endpoint. - -Possible side-effects: - - - If the server has the project workflow updated, the cache -saved here will be incorrect. - - - If the issue is not up to date with the remote, the wrong -cache key may be queried." - :type 'boolean - :group 'jiralib) - -(defvar jiralib-available-actions-cache nil "An alist of available actions.") - -(defun jiralib-get-available-actions (issue-key &optional status) - "Return the available workflow actions for ISSUE-KEY. -This uses STATUS as the cache key. -This runs the getAvailableActions SOAP method." - (if (and jiralib-available-actions-cache-p status) - (progn - (unless (assoc status jiralib-available-actions-cache) - (push (cons status - (jiralib-make-assoc-list - (mapcar (lambda (x) - (let ((namestring (cdr (car x))) - (id (cdr x))) - (cons - (cons 'name (org-jira-decode namestring)) - id))) - (jiralib-call "getAvailableActions" nil issue-key)) - 'id 'name)) - jiralib-available-actions-cache)) - (cdr (assoc status jiralib-available-actions-cache))) - (progn - (jiralib-make-assoc-list - (mapcar (lambda (x) - (let ((namestring (cdr (car x))) - (id (cdr x))) - (cons - (cons 'name (org-jira-decode namestring)) - id))) - (jiralib-call "getAvailableActions" nil issue-key)) - 'id 'name)))) - -(defcustom jiralib-fields-for-action-cache-p t - "Set to t to enable caching for jiralib-get-fields-for-action. - -If nil, will disable caching for this endpoint. - -Possible side-effects: - - - If many tasks have different workflows, you may want to disable this." - :type 'boolean - :group 'jiralib) - -(defvar jiralib-fields-for-action-cache nil "An alist of available fields.") - -(defun jiralib-get-fields-for-action-with-cache (issue-key action-id) - "Return the required fields to change ISSUE-KEY to ACTION-ID." - (if (and jiralib-fields-for-action-cache-p action-id) - (progn - (unless (assoc action-id jiralib-fields-for-action-cache) - (push (cons action-id - (jiralib-call "getFieldsForAction" nil issue-key action-id)) - jiralib-fields-for-action-cache)) - (cdr (assoc action-id jiralib-fields-for-action-cache))) - (jiralib-call "getFieldsForAction" nil issue-key action-id))) - -(defun jiralib-get-fields-for-action (issue-key action-id) - "Return the required fields to change ISSUE-KEY to ACTION-ID." - (if jiralib-use-restapi - (let ((fields (jiralib-get-fields-for-action-with-cache issue-key action-id))) - (mapcar (lambda (field) - (cons (symbol-name (car field)) - (format "%s (required: %s)" - (org-jira-find-value field 'name) - (if (eq (org-jira-find-value field 'required) :json-false) - "nil" - "t")))) - fields)) - (jiralib-make-assoc-list - (jiralib-get-fields-for-action-with-cache issue-key action-id) - 'id 'name))) - -(defun jiralib-progress-workflow-action (issue-key action-id params &optional callback) - "Progress issue with ISSUE-KEY to action ACTION-ID, and provide the needed PARAMS. - -When CALLBACK is present, this will run async." - (if jiralib-use-restapi - (jiralib-call "progressWorkflowAction" - callback issue-key `((transition (id . ,action-id))) - `((fields . ,params))) - (jiralib-call "progressWorkflowAction" - callback issue-key action-id (jiralib-make-remote-field-values params)))) - - -(defun jiralib-format-datetime (&optional datetime) - "Convert a mixed DATETIME format into the Jira required datetime format. - -This will produce a datetime string such as: - - 2010-02-05T14:30:00.000+0000 - -for being consumed in the Jira API. - -If DATETIME is not passed in, it will default to the current time." - (let* ((defaults (format-time-string "%Y-%m-%d %H:%M:%S" (current-time))) - (datetime (concat datetime (subseq defaults (length datetime)))) - (parts (parse-time-string datetime))) - (format "%04d-%02d-%02dT%02d:%02d:%02d.000+0000" - (nth 5 parts) - (nth 4 parts) - (nth 3 parts) - (nth 2 parts) - (nth 1 parts) - (nth 0 parts)))) - -(defvar jiralib-worklog-coming-soon-message - "WORKLOG FEATURES ARE NOT IMPLEMENTED YET, COMING SOON!") - -(defun jiralib-add-worklog-and-autoadjust-remaining-estimate (issue-key start-date time-spent comment) - "Log time spent on ISSUE-KEY to its worklog. -The time worked begins at START-DATE and has a TIME-SPENT -duration. JIRA will automatically update the remaining estimate -by subtracting TIME-SPENT from it. - -START-DATE should be in the format 2010-02-05T14:30:00Z - -TIME-SPENT can be in one of the following formats: 10m, 120m -hours; 10h, 120h days; 10d, 120d weeks. - -COMMENT will be added to this worklog." - (let ((formatted-start-date (jiralib-format-datetime start-date))) - (jiralib-call "addWorklogAndAutoAdjustRemainingEstimate" - nil - issue-key - ;; Expects data such as: '{"timeSpent":"1h", "started":"2017-02-21T00:00:00.000+0000", "comment":"woot!"}' - ;; and only that format will work (no loose formatting on the started date) - `((started . ,formatted-start-date) - (timeSpent . ,time-spent) - (comment . ,comment))))) - - -;;;; Issue field accessors - -(defun jiralib-issue-key (issue) - "Return the key of ISSUE." - (cdr (assoc 'key issue))) - -(defun jiralib-issue-owner (issue) - "Return the owner of ISSUE." - (cdr (assq 'assignee issue))) - -(defun jiralib-issue-status (issue) - "Return the status of ISSUE as a status string (not as a number!)." - (let ((status-code (cdr (assq 'status issue)))) - (cdr (assoc status-code (jiralib-get-statuses))))) - -(defun jiralib-custom-field-value (custom-field issue) - "Return the value of CUSTOM-FIELD for ISSUE. -Return nil if the field is not found" - (catch 'found - (dolist (field (cdr (assq 'customFieldValues issue))) - (when (equal (cdr (assq 'customfieldId field)) custom-field) - (throw 'found (cadr (assq 'values field))))))) - -(defvar jiralib-current-issue nil - "This holds the currently selected issue.") - -(defvar jiralib-projects-list nil - "This holds a list of projects and their details.") - -(defvar jiralib-types nil - "This holds a list of issues types.") - -(defvar jiralib-priorities nil - "This holds a list of priorities.") - -(defvar jiralib-user-fullnames nil - "This holds a list of user fullnames.") - -(defun jiralib-get-project-name (key) - "Return the name of the JIRA project with id KEY." - (let ((projects jiralib-projects-list) - (name nil)) - (dolist (project projects) - (if (equal (cdr (assoc 'key project)) key) - (setf name (cdr (assoc 'name project))))) - name)) - -(defun jiralib-get-type-name (id) - "Return the name of the issue type with ID." - (let ((types jiralib-types) - (name nil)) - (dolist (type types) - (if (equal (cdr (assoc 'id type)) id) - (setf name (cdr (assoc 'name type))))) - name)) - -;; (defun jiralib-get-user-fullname (account-id) -;; "Return the full name (display name) of the user with USERNAME." - ;; (if (assoc account-id jiralib-user-fullnames) - ;; (cdr (assoc account-id jiralib-user-fullnames)) - ;; (progn - ;; (let ((user (jiralib-get-user account-id))) - ;; (setf jiralib-user-fullnames (append jiralib-user-fullnames (list (cons account-id (cdr (assoc 'fullname user)))))) - ;; (cdr (assoc 'fullname user)))))) - -(defun jiralib-get-user-fullname (account-id) - "Return the full name (displayName) of the user with ACCOUNT-ID." - (cl-loop for user in (jiralib-get-users nil) - when (rassoc account-id user) - return (cdr (assoc 'displayName user)))) - -(defun jiralib-get-user-account-id (project full-name) - "Return the account-id (accountId) of the user with FULL-NAME (displayName) in PROJECT." - (cl-loop for user in (jiralib-get-users project) - when (rassoc full-name user) - return (cdr (assoc 'accountId user)))) - -(defun jiralib-get-filter (filter-id) - "Return a filter given its FILTER-ID." - (cl-flet ((id-match (filter) - (equal filter-id (cdr (assoc 'id filter))))) - (cl-find-if 'id-match (jiralib-get-saved-filters)))) - -(defun jiralib-get-filter-alist () - "Return an association list mapping filter names to IDs." - (mapcar (lambda (filter) - (cons (cdr (assoc 'name filter)) - (cdr (assoc 'id filter)))) - (jiralib-get-saved-filters))) - -(defun jiralib-add-comment (issue-key comment &optional callback) - "Add to issue with ISSUE-KEY the given COMMENT, invoke CALLBACK." - (jiralib-call "addComment" callback issue-key `((body . ,comment)))) - -(defun jiralib-edit-comment (issue-id comment-id comment &optional callback) - "Edit ISSUE-ID's comment COMMENT-ID to reflect the new COMMENT, invoke CALLBACK." - (if (not jiralib-use-restapi) - (jiralib-call "editComment" callback `((id . ,comment-id) - (body . ,comment))) - (jiralib-call "editComment" callback issue-id comment-id comment))) - -(defun jiralib-create-issue (issue) - "Create a new ISSUE in JIRALIB. - -ISSUE is a Hashtable object." - (jiralib-call "createIssue" nil issue)) - -(defun jiralib-create-subtask (subtask) - "Create SUBTASK for issue with PARENT-ISSUE-ID. - -SUBTASK is a Hashtable object." - (jiralib-call "createIssueWithParent" nil subtask)) - -(defvar jiralib-subtask-types-cache nil) - -(defun jiralib-get-subtask-types () - "Return an assoc list mapping an issue type code to its name. -NOTE: Issue type codes are stored as strings, not numbers. - -This function will only ask JIRA for the list of codes once, than -will cache it." - (unless jiralib-subtask-types-cache - (setq jiralib-subtask-types-cache - (jiralib-make-assoc-list (jiralib-call "getSubTaskIssueTypes" nil) 'id 'name))) - jiralib-subtask-types-cache) - -(defun jiralib-get-comment (issue-key comment-id &optional callback) - "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK." - (jiralib-call "getComment" callback issue-key comment-id)) - -(defun jiralib-get-comments (issue-key &optional callback) - "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK." - (jiralib-call "getComments" callback issue-key)) - -(defun jiralib-get-attachments (issue-key &optional callback) - "Return all attachments associated with issue ISSUE-KEY, invoking CALLBACK." - (jiralib-call "getAttachmentsFromIssue" callback issue-key)) - -(defun jiralib-get-worklogs (issue-key &optional callback) - "Return all worklogs associated with issue ISSUE-KEY, invoking CALLBACK." - (jiralib-call "getWorklogs" callback issue-key)) - -(defun jiralib-add-worklog (issue-id started time-spent-seconds comment &optional callback) - "Add the worklog linked to ISSUE-ID. - -Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT. -CALLBACK will be invoked if passed in upon endpoint completion." - ;; Call will fail if 0 seconds are set as the time, so always do at least one min. - (setq time-spent-seconds (max 60 time-spent-seconds)) - (let ((worklog `((started . ,started) - ;; @TODO :worklog: timeSpentSeconds changes into incorrect values - ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days... - ;; We should convert to a Xd Xh Xm format from our seconds ourselves. - (timeSpentSeconds . ,time-spent-seconds) - (comment . ,comment)))) - (jiralib-call "addWorklog" callback issue-id worklog))) - -(defun jiralib-update-worklog (issue-id worklog-id started time-spent-seconds comment &optional callback) - "Update the worklog linked to ISSUE-ID and WORKLOG-ID. - -Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT. -CALLBACK will be invoked if passed in upon endpoint completion." - ;; Call will fail if 0 seconds are set as the time, so always do at least one min. - (setq time-spent-seconds (max 60 time-spent-seconds)) - (let ((worklog `((started . ,started) - ;; @TODO :worklog: timeSpentSeconds changes into incorrect values - ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days... - ;; We should convert to a Xd Xh Xm format from our seconds ourselves. - (timeSpentSeconds . ,time-spent-seconds) - (comment . ,comment)))) - (jiralib-call "updateWorklog" callback issue-id worklog-id worklog))) - -(defvar jiralib-components-cache nil "An alist of project components.") - -(defun jiralib-get-components (project-key) - "Return all components available in the project PROJECT-KEY." - (unless (assoc project-key jiralib-components-cache) - (push (cons project-key - (jiralib-make-assoc-list - (jiralib-call "getComponents" nil project-key) 'id 'name)) - jiralib-components-cache)) - (cdr (assoc project-key jiralib-components-cache))) - -(defun jiralib-get-issue (issue-key &optional callback) - "Get the issue with key ISSUE-KEY, running CALLBACK after." - (jiralib-call "getIssue" callback issue-key)) - -(defun jiralib-get-issues-from-filter (filter-id) - "Get the issues from applying saved filter FILTER-ID." - (message "jiralib-get-issues-from-filter is NOT IMPLEMENTED!! Do not use!") - (jiralib-call "getIssuesFromFilter" nil filter-id)) - -(defun jiralib-get-issues-from-text-search (search-terms) - "Find issues using free text search SEARCH-TERMS." - (jiralib-call "getIssuesFromTextSearch" nil search-terms)) - -(defun jiralib-get-issues-from-text-search-with-project - (project-keys search-terms max-num-results) - "Find issues in projects PROJECT-KEYS, using free text search SEARCH-TERMS. - -Return no more than MAX-NUM-RESULTS." - (jiralib-call "getIssuesFromTextSearchWithProject" - nil - (apply 'vector project-keys) search-terms max-num-results)) - -;; Modified by Brian Zwahr to use getProjectsNoSchemes instead of getProjects -(defun jiralib-get-projects () - "Return a list of projects available to the user." - (if jiralib-projects-list - jiralib-projects-list - (setq jiralib-projects-list - (if jiralib-use-restapi - (jiralib-call "getProjects" nil) - (jiralib-call "getProjectsNoSchemes" nil))))) - -(defun jiralib-get-saved-filters () - "Get all saved filters available for the currently logged in user." - (jiralib-make-assoc-list (jiralib-call "getSavedFilters" nil) 'id 'name)) - -(defun jiralib-get-server-info () - "Return the Server information such as baseUrl, version, edition, buildDate, buildNumber." - (jiralib-call "getServerInfo" nil)) - -(defun jiralib-get-sub-task-issue-types () - "Return all visible subtask issue types in the system." - (jiralib-call "getSubTaskIssueTypes" nil)) - -(defun jiralib-get-user (account-id) - "Return a user's information given their full name." - (cond ((eq 0 (length account-id)) nil) ;; Unassigned - (t (jiralib-call "getUser" nil account-id)))) - -(defvar jiralib-users-cache nil "Cached list of users.") - -(defun jiralib-get-users (project-key) - "Return assignable users information given the PROJECT-KEY." - (unless jiralib-users-cache - (setq jiralib-users-cache - (jiralib-call "getUsers" nil project-key)) - (cl-loop for (name . id) in org-jira-users do - (setf jiralib-users-cache (append (list (jiralib-get-user id)) jiralib-users-cache)))) - jiralib-users-cache) - -(defun jiralib-get-versions (project-key) - "Return all versions available in project PROJECT-KEY." - (jiralib-call "getVersions" nil project-key)) - -(defun jiralib-strip-cr (string) - "Remove carriage returns from STRING." - (when string (replace-regexp-in-string "\r" "" string))) - -(defun jiralib-worklog-import--filter-apply - (worklog-obj &optional predicate-fn-lst unwrap-worklog-records-fn rewrap-worklog-records-fn) - "Remove non-matching org-jira issue worklogs. - -Variables: - WORKLOG-OBJ is the passed in object - PREDICATE-FN-LST is the list of lambdas used as match predicates. - UNWRAP-WORKLOG-RECORDS-FN is the function used to produce the list of worklog records from within the worklog-obj - REWRAP-WORKLOG-RECORDS-FN is the function used to reshape the worklog records back into the form they were received in. - -Auxiliary Notes: - Only the WORKLOG-OBJ variable is required. - The value of PPREDICATE-FN-LST is filled from the jiralib-worklog-import--filters-alist variable by default. - If PREDICATE-FN-LST is empty the unmodified value of WORKLOG-OBJ is returned. - If PREDICATE-FN-LST contains multiple predicate functions, each predicate filters operates as a clause in an AND match. In effect, a worklog must match all predicates to be returned. - The variable 'jiralib-user-login-name is used by many lambda filters." - - (let - ((unwrap-worklog-records-fn) - (rewrap-worklog-records-fn) - (predicate-fn-lst) - (worklogs worklog-obj) - (predicate-fn)) - ;; let-body - (progn - (setq unwrap-worklog-records-fn - (if (and - (boundp 'unwrap-worklog-records-fn) - (functionp unwrap-worklog-records-fn)) - unwrap-worklog-records-fn - (lambda (x) (cl-coerce x 'list)))) - (setq rewrap-worklog-records-fn - (if (and - (boundp 'rewrap-worklog-records-fn) - (functionp rewrap-worklog-records-fn)) - rewrap-worklog-records-fn - (lambda (x) (remove 'nil (cl-coerce x 'vector))))) - (setq predicate-fn-lst - (if (and (boundp 'predicate-fn-lst) - (not (null predicate-fn-lst)) - (listp predicate-fn-lst)) - predicate-fn-lst - (mapcar 'caddr - (remove 'nil - (mapcar (lambda (x) (unless (null (car x)) x)) - jiralib-worklog-import--filters-alist))))) - ;; final condition/sanity checks before processing - (cond - ;; pass cases, don't apply filters, return unaltered worklog-obj - ((or (not (boundp 'predicate-fn-lst)) (not (listp predicate-fn-lst)) (null predicate-fn-lst)) - worklog-obj) - ;; default-case, apply worklog filters and return only matching worklogs - (t - (setq worklogs (funcall unwrap-worklog-records-fn worklogs)) - (while (setq predicate-fn (pop predicate-fn-lst)) - (setq worklogs (mapcar predicate-fn worklogs))) - (funcall rewrap-worklog-records-fn worklogs)))))) - - -(defun jiralib-get-board (id &optional callback) - "Return details on given board" - (jiralib-call "getBoard" nil id)) - -(defun jiralib-get-boards () - "Return list of jira boards" - (jiralib-call "getBoards" nil)) - -(defun jiralib-get-board-sprints (id) - "Return list of jira sprints in the specified jira board" - (jiralib-call "getSprintsFromBoard" nil id)) - -(defun jiralib-get-sprint-issues (id &rest params) - "Return list of issues in the specified sprint" - (apply 'jiralib-call "getIssuesFromSprint" - (cl-getf params :callback) id params)) - -(defun jiralib-get-board-issues (board-id &rest params) - "Return list of jira issues in the specified jira board" - (apply 'jiralib-call "getIssuesFromBoard" - (cl-getf params :callback) board-id params)) - -(defun jiralib--agile-not-last-entry (num-entries total start-at limit) - "Return true if need to retrieve next page from agile api" - (and (> num-entries 0) - (or (not limit) ; not required to be set - (< limit 1) ; ignore invalid limit - (> limit start-at)) - (or (not total) ; not always returned - (> total start-at)))) - -(defun jiralib--agile-limit-page-size (page-size start-at limit) - (if (and limit - (> (+ start-at page-size) limit)) - (- limit start-at) - page-size)) - - -(defun jiralib--agile-rest-call-it (api max-results start-at limit query-params) - (let ((callurl - (format "%s?%s" api - (url-build-query-string - (append `((maxResults ,(jiralib--agile-limit-page-size max-results start-at limit)) - (startAt ,start-at)) - query-params))))) - (jiralib--rest-call-it callurl))) - -(defun jiralib--agile-call-it (api values-key &rest params) - "Invoke Jira agile method api and retrieve the results using -paging. - -If JIRALIB-COMPLETE-CALLBACK is non-nil, then the call will be -performed asynchronously and JIRALIB-COMPLETE-CALLBACK will be -called when all data are retrieved. - -If JIRALIB-COMPLETE-CALLBACK is nil, then the call will be -performed synchronously and this function will return the -retrieved data. - -API - path to called API that must start with /rest/agile/1.0. - -VALUES-KEY - key of the actual reply data in the reply assoc list. - -PARAMS - optional additional parameters. -:limit - limit total number of retrieved entries. -:query-params - extra query parameters in the format of url-build-query-string. -" - (if jiralib-complete-callback - (apply 'jiralib--agile-call-async api values-key params) - (apply 'jiralib--agile-call-sync api values-key params))) - -(defun jiralib--agile-call-sync (api values-key &rest params) - "Syncroniously invoke Jira agile method api retrieve all the -results using paging and return results. - -VALUES-KEY - key of the actual reply data in the reply assoc list. - -PARAMS - extra parameters (as keyword arguments), the supported parameters are: - -:limit - limit total number of retrieved entries. -:query-params - extra query parameters in the format of url-build-query-string. -" - (setq jiralib-complete-callback nil) - (let ((not-last t) - (start-at 0) - (limit (cl-getf params :limit)) - (query-params (cl-getf params :query-params)) - ;; maximum page size, 50 is server side maximum - (max-results jiralib-agile-page-size) - (values ())) - (while not-last - (let* ((reply-alist - (jiralib--agile-rest-call-it api max-results start-at limit query-params)) - (values-array (cdr (assoc values-key reply-alist))) - (num-entries (length values-array)) - (total (cdr (assq 'total reply-alist)))) - (setf values (append values (append values-array nil))) - (setf start-at (+ start-at num-entries)) - (setf not-last (jiralib--agile-not-last-entry num-entries total start-at limit)))) - values)) - -(defun jiralib--agile-call-async (api values-key &rest params) - "Asyncroniously invoke Jira agile method api, -retrieve all the results using paging and call -JIRALIB-COMPLETE_CALLBACK when all the data are retrieved. - -VALUES-KEY - key of the actual reply data in the reply assoc list. - -PARAMS - extra parameters (as keyword arguments), the supported parameters are: - -limit - limit total number of retrieved entries." - (lexical-let - ((start-at 0) - (limit (cl-getf params :limit)) - (query-params (cl-getf params :query-params)) - ;; maximum page size, 50 is server side maximum - (max-results jiralib-agile-page-size) - (values-list ()) - (vk values-key) - (url api) - ;; save the call back to be called later after the last page - (complete-callback jiralib-complete-callback)) - ;; setup new callback to be called after each page - (setf jiralib-complete-callback - (cl-function - (lambda (&rest data &allow-other-keys) - (condition-case err - (let* ((reply-alist (cl-getf data :data)) - (values-array (cdr (assoc vk reply-alist))) - (num-entries (length values-array)) - (total (cdr (assq 'total reply-alist)))) - (setf values-list (append values-list (append values-array nil))) - (setf start-at (+ start-at num-entries)) - (message "jiralib agile retrieve: got %d values%s%s" - start-at - (if total " of " "") - (if total (int-to-string total) "")) - (if (jiralib--agile-not-last-entry num-entries total start-at limit) - (jiralib--agile-rest-call-it url max-results start-at limit query-params) - ;; last page: call originall callback - (message "jiralib agile retrieve: calling callback") - (setf jiralib-complete-callback complete-callback) - (funcall jiralib-complete-callback - :data (list (cons vk values-list))) - (message "jiralib agile retrieve: all done"))) - ('error (message (format "jiralib agile retrieve: caught error: %s" err))))))) - (jiralib--agile-rest-call-it api max-results start-at limit query-params))) - -(provide 'jiralib) -;;; jiralib.el ends here diff --git a/elpa/org-jira-20230915.57/org-jira-autoloads.el b/elpa/org-jira-20230915.57/org-jira-autoloads.el deleted file mode 100644 index cf50172b..00000000 --- a/elpa/org-jira-20230915.57/org-jira-autoloads.el +++ /dev/null @@ -1,176 +0,0 @@ -;;; org-jira-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- -;; Generated by the `loaddefs-generate' function. - -;; This file is part of GNU Emacs. - -;;; Code: - -(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) - - - -;;; Generated autoloads from jiralib.el - -(register-definition-prefixes "jiralib" '("jiralib-")) - - -;;; Generated autoloads from org-jira.el - -(autoload 'org-jira-mode "org-jira" "\ -Toggle org-jira mode. - -With no argument, the mode is toggled on/off. -Non-nil argument turns mode on. -Nil argument turns mode off. - -Commands: -\\{org-jira-entry-mode-map} - -Entry to this mode calls the value of `org-jira-mode-hook'. - -This is a minor mode. If called interactively, toggle the -`Org-jira mode' mode. If the prefix argument is positive, enable -the mode, and if it is zero or negative, disable the mode. - -If called from Lisp, toggle the mode if ARG is `toggle'. Enable -the mode if ARG is nil, omitted, or is a positive number. -Disable the mode if ARG is a negative number. - -To check whether the minor mode is enabled in the current buffer, -evaluate `org-jira-mode'. - -The mode's hook is called both when the mode is enabled and when -it is disabled. - -(fn &optional ARG)" t) -(autoload 'org-jira-get-projects "org-jira" "\ -Get list of projects." t) -(autoload 'org-jira-get-issue-list "org-jira" "\ -Get list of issues, using jql (jira query language), invoke CALLBACK after. - -Default is unresolved issues assigned to current login user; with -a prefix argument you are given the chance to enter your own -jql. - -(fn &optional CALLBACK)") -(autoload 'org-jira-get-summary "org-jira" "\ -Get issue summary from point and place next to issue id from jira" t) -(autoload 'org-jira-get-summary-url "org-jira" "\ -Get issue summary from point and place next to issue id from jira, and make issue id a link" t) -(autoload 'org-jira-get-issues-headonly "org-jira" "\ -Get list of ISSUES, head only. - -The default behavior is to return issues assigned to you and unresolved. - -With a prefix argument, allow you to customize the jql. See -`org-jira-get-issue-list'. - -(fn ISSUES)" t) -(autoload 'org-jira-get-issue "org-jira" "\ -Get a JIRA issue, allowing you to enter the issue-id first. - -(fn ID)" t) -(autoload 'org-jira-get-issues-by-fixversion "org-jira" "\ -Get list of issues by FIXVERSION. - -(fn FIXVERSION)" t) -(autoload 'org-jira-get-issue-project "org-jira" "\ - - -(fn ISSUE)") -(autoload 'org-jira-get-issues "org-jira" "\ -Get list of ISSUES into an org buffer. - -Default is get unfinished issues assigned to you, but you can -customize jql with a prefix argument. -See`org-jira-get-issue-list' - -(fn ISSUES)" t) -(autoload 'org-jira-get-issues-from-custom-jql "org-jira" "\ -Get JQL-LIST list of issues from a custom JQL and PROJ-KEY. - -The PROJ-KEY will act as the file name, while the JQL will be any -valid JQL to populate a file to store PROJ-KEY results in. - -Please note that this is *not* concurrent or race condition -proof. If you try to run multiple calls to this function, it -will mangle things badly, as they rely on globals DEFAULT-JQL and -ORG-JIRA-PROJ-KEY-OVERRIDE being set before and after running. - -(fn &optional JQL-LIST)" t) -(autoload 'org-jira-update-comment "org-jira" "\ -Update a comment for the current issue." t) -(autoload 'org-jira-update-worklogs-from-org-clocks "org-jira" "\ -Update or add a worklog based on the org clocks." t) -(autoload 'org-jira-copy-current-issue-key "org-jira" "\ -Copy the current issue's key into clipboard." t) -(autoload 'org-jira-unassign-issue "org-jira" "\ -Update an issue to be unassigned." t) -(autoload 'org-jira-set-issue-reporter "org-jira" "\ -Update an issue's reporter interactively." t) -(autoload 'org-jira-assign-issue "org-jira" "\ -Update an issue with interactive re-assignment." t) -(autoload 'org-jira-update-issue "org-jira" "\ -Update an issue." t) -(autoload 'org-jira-todo-to-jira "org-jira" "\ -Convert an ordinary todo item to a jira ticket." t) -(autoload 'org-jira-get-subtasks "org-jira" "\ -Get subtasks for the current issue." t) -(autoload 'org-jira-create-issue "org-jira" "\ -Create an issue in PROJECT, of type TYPE, with given SUMMARY and DESCRIPTION. - -(fn PROJECT TYPE SUMMARY DESCRIPTION)" t) -(autoload 'org-jira-create-subtask "org-jira" "\ -Create a subtask issue for PROJECT, of TYPE, with SUMMARY and DESCRIPTION. - -(fn PROJECT TYPE SUMMARY DESCRIPTION)" t) -(autoload 'org-jira-refresh-issue "org-jira" "\ -Refresh current issue from jira to org." t) -(autoload 'org-jira-progress-issue "org-jira" "\ -Progress issue workflow." t) -(autoload 'org-jira-progress-issue-next "org-jira" "\ -Progress issue workflow." t) -(autoload 'org-jira-browse-issue "org-jira" "\ -Open the current issue in external browser." t) -(autoload 'org-jira-download-attachment "org-jira" "\ -Download the attachment under cursor." t) -(autoload 'org-jira-get-issues-from-filter "org-jira" "\ -Get issues from the server-side stored filter named FILTER. - -Provide this command in case some users are not able to use -client side jql (maybe because of JIRA server version?). - -(fn FILTER)" t) -(autoload 'org-jira-get-issues-from-filter-headonly "org-jira" "\ -Get issues *head only* from saved filter named FILTER. -See `org-jira-get-issues-from-filter'. - -(fn FILTER)" t) -(autoload 'org-jira-get-issues-by-board "org-jira" "\ -Get list of ISSUES from agile board." t) -(autoload 'org-jira-get-issues-by-sprint "org-jira" "\ -Get list of ISSUES from sprint." t) -(autoload 'org-jira-get-issues-by-board-headonly "org-jira" "\ -Get list of ISSUES from agile board, head only." t) -(autoload 'org-jira-get-boards "org-jira" "\ -Get list of boards and their properties." t) -(register-definition-prefixes "org-jira" '("ensure-on-" "org-jira-")) - - -;;; Generated autoloads from org-jira-sdk.el - -(register-definition-prefixes "org-jira-sdk" '("org-jira-sdk-")) - -;;; End of scraped data - -(provide 'org-jira-autoloads) - -;; Local Variables: -;; version-control: never -;; no-byte-compile: t -;; no-update-autoloads: t -;; no-native-compile: t -;; coding: utf-8-emacs-unix -;; End: - -;;; org-jira-autoloads.el ends here diff --git a/elpa/org-jira-20230915.57/org-jira-pkg.el b/elpa/org-jira-20230915.57/org-jira-pkg.el deleted file mode 100644 index bbda076e..00000000 --- a/elpa/org-jira-20230915.57/org-jira-pkg.el +++ /dev/null @@ -1,15 +0,0 @@ -(define-package "org-jira" "20230915.57" "Syncing between Jira and Org-mode." - '((emacs "24.5") - (cl-lib "0.5") - (request "0.2.0") - (dash "2.14.1")) - :commit "295b01ede42952c848bd8d76bc8c456a87876cbc" :maintainers - '(("Matthew Carter" . "m@ahungry.com")) - :maintainer - '("Matthew Carter" . "m@ahungry.com") - :keywords - '("ahungry" "jira" "org" "bug" "tracker") - :url "https://github.com/ahungry/org-jira") -;; Local Variables: -;; no-byte-compile: t -;; End: diff --git a/elpa/org-jira-20230915.57/org-jira-sdk.el b/elpa/org-jira-20230915.57/org-jira-sdk.el deleted file mode 100644 index 864de376..00000000 --- a/elpa/org-jira-20230915.57/org-jira-sdk.el +++ /dev/null @@ -1,228 +0,0 @@ -;;; org-jira-sdk.el -- SDK Layer for entities - -;; Copyright (C) 2018 Matthew Carter -;; -;; Authors: -;; Matthew Carter -;; -;; Maintainer: Matthew Carter -;; URL: https://github.com/ahungry/org-jira -;; Version: 3.1.1 -;; Keywords: ahungry jira org bug tracker -;; Package-Requires: ((emacs "24.5") (cl-lib "0.5") (request "0.2.0") (s "0.0.0")) - -;; This file is not part of GNU Emacs. - -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see -;; or write to the Free Software -;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -;; 02110-1301, USA. - -;;; Commentary: - -;; This provides an SDK wrapper for more strictly defining the entities we interact with. - -;;; Code: - -(require 'jiralib) -(require 'cl-lib) -(require 'eieio) -(require 'dash) -(require 'cl-generic) - -(defclass org-jira-sdk-record () - ((id :initarg :id :type string :required t) - (data :initarg :data :documentation "The area to hold a big alist of data.") - (hydrate-fn :initform (lambda (id) (message "Not implemented.")))) - "The ID of the record.") - -(defun org-jira-sdk-string-but-first (s) (cl-subseq s 1)) - -(defun org-jira-sdk-to-string (s) (format "%s" s)) - -(defun org-jira-sdk-to-prefixed-string (s) (format "org-jira-sdk-%s" s)) - -(defun org-jira-sdk-record-type-to-symbol (record-type) - (-> record-type symbol-name org-jira-sdk-string-but-first org-jira-sdk-to-prefixed-string intern)) - -(defun org-jira-sdk-create-from-id (record-type id &optional parent-id callback) - (let ((rec (funcall (org-jira-sdk-record-type-to-symbol record-type) - :id (format "%s" id) - :parent-id parent-id))) - (with-slots (data) rec - (setf data (org-jira-sdk-hydrate rec callback)) - (org-jira-sdk-from-data rec)))) - -(defun org-jira-sdk-create-from-data (record-type data) - (let ((rec (funcall (org-jira-sdk-record-type-to-symbol record-type) :data data))) - (org-jira-sdk-from-data rec))) - -(cl-defmethod org-jira-sdk-hydrate ((rec org-jira-sdk-record) &optional callback) - "Populate the record with data from the remote endpoint." - (with-slots (id hydrate-fn) rec - (funcall hydrate-fn id callback))) - -(cl-defgeneric org-jira-sdk-from-data ((rec org-jira-sdk-record))) - -(cl-defmethod org-jira-sdk-dump ((rec org-jira-sdk-record)) - "A decent pretty print/object dump for working with the class items." - (let ((slots (mapcar (lambda (slot) (aref slot 1)) (eieio-class-slots (eieio-object-class rec))))) - (setq slots (cl-remove-if (lambda (s) (not (slot-boundp rec s))) slots)) - (apply #'concat - (mapcar (lambda (slot) - (let ((slot (intern (org-jira-sdk-to-string slot)))) - (format "\n%+16s: %s" slot (slot-value rec (intern (org-jira-sdk-to-string slot))))) - ) - slots)))) - -(defun org-jira-sdk-path (alist key-chain) - "Query a nested path in some type of ALIST by traversing down the keys of KEY-CHAIN." - (cl-reduce (lambda (a k) (alist-get k a)) key-chain :initial-value alist)) - -(defclass org-jira-sdk-issue (org-jira-sdk-record) - ((assignee :type (or null string) :initarg :assignee) - (components :type string :initarg :components) - (labels :type string :initarg :labels) - (created :type string :initarg :created) - (description :type (or null string) :initarg :description) - (duedate :type (or null string) :initarg :duedate) - (headline :type string :initarg :headline) - (id :type string :initarg :id) ; TODO: Probably remove me - (issue-id :type string :initarg :issue-id :documentation "The common ID/key, such as EX-1.") - (issue-id-int :type string :initarg :issue-id-int :documentation "The internal Jira ID, such as 12345.") - (filename :type (or null string) :initarg :filename :documentation "The filename to write issue to.") - (priority :type (or null string) :initarg :priority) - (proj-key :type string :initarg :proj-key) - (reporter :type (or null string) :initarg :reporter) - (resolution :type (or null string) :initarg :resolution) - (sprint :type (or null string) :initarg :sprint) - (start-date :type (or null string) :initarg :start-date) - (status :type string :initarg :status) - (summary :type string :initarg :summary) - (type :type string :initarg :type) - (type-id :type string :initarg :type-id) - (updated :type string :initarg :updated) - (data :initarg :data :documentation "The remote Jira data object (alist).") - (hydrate-fn :initform #'jiralib-get-issue :initarg :hydrate-fn)) - "An issue on the end. ID of the form EX-1, or a numeric such as 10000.") - -(defclass org-jira-sdk-comment (org-jira-sdk-record) - ((author :type string :initarg :author) - (body :type string :initarg :body) - (comment-id :type string :initarg :comment-id :documentation "The comment ID, such as 12345.") - (created :type string :initarg :created) - (headline :type string :initarg :headline) - (parent-id :type string :initarg :parent-id :documentation "The parent issue-id such as EX-1.") - (updated :type string :initarg :updated) - (data :initarg :data :documentation "The reomte Jira data object (alist).") - (hydrate-fn :initform #'jiralib-get-comment :initarg :hydrate-fn))) - -(defclass org-jira-sdk-board (org-jira-sdk-record) - ((name :type string :initarg :name :required t) - (url :type string :initarg :url :required t) - (board-type :type string :initarg :board-type) - (jql :type string :initarg :jql) - (limit :type integer :initarg :limit) - ;; unused - (parent-id :type string :initarg :parent-id) - (hydrate-fn :initform #'jiralib-get-board :initarg :hydrate-fn))) - -(cl-defmethod org-jira-sdk-hydrate ((rec org-jira-sdk-comment) &optional callback) - "Populate the record with data from the remote endpoint." - (with-slots (id proj-key hydrate-fn) rec - (funcall hydrate-fn proj-key id callback))) - -(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-issue)) - (cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys))) - (org-jira-sdk-issue - :assignee (path '(fields assignee displayName)) - :components (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields components)) ", ") - :labels (mapconcat (lambda (c) (format "%s" c)) (mapcar #'identity (path '(fields labels))) ", ") - :created (path '(fields created)) ; confirm - :description (or (path '(fields description)) "") - :duedate (or (path '(fields sprint endDate)) (path '(fields duedate))) ; confirm - :filename (path '(fields project key)) - :headline (path '(fields summary)) ; Duplicate of summary, maybe different. - :id (path '(key)) - :issue-id (path '(key)) - :issue-id-int (path '(id)) - :priority (path '(fields priority name)) - :proj-key (path '(fields project key)) - :reporter (path '(fields reporter displayName)) ; reporter could be an object of its own slot values - :resolution (path '(fields resolution name)) ; confirm - :sprint (path '(fields sprint name)) - :start-date (path '(fields start-date)) ; confirm - :status (org-jira-decode (path '(fields status name))) - :summary (path '(fields summary)) - :type (path '(fields issuetype name)) - :type-id (path '(fields issuetype id)) - :updated (path '(fields updated)) ; confirm - ;; TODO: Remove this - ;; :data (oref rec data) - ))) - -(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-comment)) - (cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys))) - (org-jira-sdk-comment - :author (path '(author displayName)) - :body (path '(body)) - :comment-id (path '(id)) - :created (path '(created)) - :headline (format "Comment: %s" (path '(author displayName))) - :parent-id (if (slot-boundp rec 'parent-id) (oref rec parent-id) "") - :updated (path '(updated)) - ;; TODO: Remove this - ;; :data (oref rec data) - ))) - -(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-board)) - (with-slots (data) rec - (org-jira-sdk-board - :id (int-to-string (alist-get 'id data)) - :name (alist-get 'name data) - :url (alist-get 'self data) - ;; TODO: remove it? used by org-jira-sdk-create-from-id - :parent-id "" - :board-type (alist-get 'type data)))) - -;; Issue -(defun org-jira-sdk-create-issue-from-data (d) (org-jira-sdk-create-from-data :issue d)) -(defun org-jira-sdk-create-issues-from-data-list (ds) (mapcar #'org-jira-sdk-create-issue-from-data ds)) - -;; Issue with custom filename setting -(defun org-jira-sdk-create-issue-from-data-with-filename (filename) - (lambda (d) - (let ((Issue (org-jira-sdk-create-from-data :issue d))) - (setf (oref Issue filename) filename) - Issue))) - -(defun org-jira-sdk-create-issues-from-data-list-with-filename (filename ds) - (let ((fn (org-jira-sdk-create-issue-from-data-with-filename filename))) - (mapcar fn ds))) - -;; Comment -(defun org-jira-sdk-create-comment-from-data (d) (org-jira-sdk-create-from-data :comment d)) -(defun org-jira-sdk-create-comments-from-data-list (ds) (mapcar #'org-jira-sdk-create-comment-from-data ds)) - -(defun org-jira-sdk-isa-record? (i) (cl-typep i 'org-jira-sdk-record)) -(defun org-jira-sdk-isa-issue? (i) (cl-typep i 'org-jira-sdk-issue)) -(defun org-jira-sdk-isa-comment? (i) (cl-typep i 'org-jira-sdk-comment)) - -;; Board -(defun org-jira-sdk-create-board-from-data (d) (org-jira-sdk-create-from-data :board d)) -(defun org-jira-sdk-create-boards-from-data-list (ds) (mapcar #'org-jira-sdk-create-board-from-data ds)) - -(provide 'org-jira-sdk) - -;;; org-jira-sdk.el ends here diff --git a/elpa/org-jira-20230915.57/org-jira.el b/elpa/org-jira-20230915.57/org-jira.el deleted file mode 100644 index 0f45c414..00000000 --- a/elpa/org-jira-20230915.57/org-jira.el +++ /dev/null @@ -1,2583 +0,0 @@ -;;; org-jira.el --- Syncing between Jira and Org-mode. -*- lexical-binding: t -*- - -;; Copyright (C) 2016-2022 Matthew Carter -;; Copyright (C) 2011 Bao Haojun -;; -;; Authors: -;; Matthew Carter -;; Bao Haojun -;; -;; Maintainer: Matthew Carter -;; URL: https://github.com/ahungry/org-jira -;; Version: 4.3.3 -;; Keywords: ahungry jira org bug tracker -;; Package-Requires: ((emacs "24.5") (cl-lib "0.5") (request "0.2.0") (dash "2.14.1")) - -;; This file is not part of GNU Emacs. - -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see -;; or write to the Free Software -;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -;; 02110-1301, USA. - -;;; Commentary: - -;; This provides an extension to org-mode for syncing issues with JIRA -;; issue servers. - -;;; News: - -;;;; Changes in 4.4.1 -;; - Fix tag (4.3.3 was out of order - we had a 4.4.0 on repo) -;; - Fix for some crazy scoping issue in the org-jira-get-issue-val-from-org function - -;;;; Changes in 4.3.3: -;; - Address issue with assignee property being removed when Unassigned - -;;;; Changes in 4.3.2: -;; - Fixes issues with org-jira-add-comment and org-jira-update-comment - -;;;; Changes in 4.3.1: -;; - Fix to make custom-jql results sync worklogs properly. - -;;;; Changes in 4.3.0: -;; - Allow org-jira-set-issue-reporter call to dynamically set this value. - -;;;; Changes in 4.1.0: -;; - Allow custom-jql to be specified and render in special files (see: README.md). - -;;;; Changes in 4.0.0: -;; - Introduce SDK type for handling records vs random alist structures. - -;;;; Changes since 3.1.0: -;; - Fix how we were ruining the kill-ring with kill calls. - -;;;; Changes since 3.0.0: -;; - Add new org-jira-add-comment call (C-c c c) - -;;;; Changes since 2.8.0: -;; - New version 3.0.0 deprecates old filing mechanism and files -;; all of the changes under the top level ticket header. -;; - If you want other top level headers in the same file, this should -;; work now, as long as they come after the main project one. - -;;;; Changes since 2.7.0: -;; - Clean up multi-buffer handling, disable attachments call until -;; - refresh is compatible with it. - -;;;; Changes since 2.6.3: -;; - Insert worklog import filter in the existing org-jira-update-worklogs-for-current-issue function -;; - Sync up org-clocks and worklogs! Set org-jira-worklog-sync-p to nil to avoid. - -;;;; Changes since 2.6.1: -;; - Fix bug with getting all issues when worklog is an error trigger. - -;;;; Changes since 2.5.4: -;; - Added new org-jira-refresh-issues-in-buffer call and binding - -;;;; Changes since 2.5.3: -;; - Re-introduce the commit that introduced a break into Emacs 25.1.1 list/array push -;; The commit caused updates/comment updates to fail when a blank list of components -;; was present - it will now handle both cases (full list, empty list). - -;;;; Changes since 2.5.2: -;; - Revert a commit that introduced a break into Emacs 25.1.1 list/array push -;; The commit caused updates/comment updates to fail - -;;;; Changes since 2.5.1: -;; - Only set duedate if a DEADLINE is present in the tags and predicate is t - -;;;; Changes since 2.5.0: -;; - Allow overriding the org property names with new defcustom - -;;;; Changes since 2.4.0: -;; - Fix many deprecation/warning issues -;; - Fix error with allow-other-keys not being wrapped in cl-function - -;;;; Changes since 2.3.0: -;; - Integration with org deadline and Jira due date fields - -;;;; Changes since 2.2.0: -;; - Selecting issue type based on project key for creating issues - -;;;; Changes since 2.1.0: -;; - Allow changing to unassigned user -;; - Add new shortcut for quick assignment - -;;;; Changes since 2.0.0: -;; - Change statusCategory to status value -;; - Clean out some redundant code -;; - Add ELPA tags in keywords - -;;;; Changes since 1.0.1: -;; - Converted many calls to async -;; - Removed minor annoyances (position resets etc.) - -;;; Code: - -(eval-when-compile (require 'cl)) -(require 'org) -(require 'org-clock) -(require 'cl-lib) -(require 'url) -(require 'ls-lisp) -(require 'dash) -(require 'jiralib) -(require 'org-jira-sdk) - -(defconst org-jira-version "4.3.1" - "Current version of org-jira.el.") - -(defgroup org-jira nil - "Customization group for org-jira." - :tag "Org JIRA" - :group 'org) - -(defcustom org-jira-working-dir "~/.org-jira" - "Folder under which to store org-jira working files." - :group 'org-jira - :type 'directory) - -(defcustom org-jira-project-filename-alist nil - "Alist translating project keys to filenames. - -Each element has a structure: - - (PROJECT-KEY . NEW-FILE-NAME) - -where both are strings. NEW-FILE-NAME is relative to -`org-jira-working-dir'." - :group 'org-jira - :type '(alist :key-type (string :tag "Project key") - :value-type (string :tag "File name for this project"))) - -(defcustom org-jira-default-jql - "assignee = currentUser() and resolution = unresolved ORDER BY - priority DESC, created ASC" - "Default jql for querying your Jira tickets." - :group 'org-jira - :type 'string) - -(defcustom org-jira-ignore-comment-user-list - '("admin") - "Jira usernames that should have comments ignored." - :group 'org-jira - :type '(repeat (string :tag "Jira username:"))) - -(defcustom org-jira-reverse-comment-order nil - "If non-nil, order comments from most recent to least recent." - :group 'org-jira - :type 'boolean) - -(defcustom org-jira-done-states - '("Closed" "Resolved" "Done") - "Jira states that should be considered as DONE for `org-mode'." - :group 'org-jira - :type '(repeat (string :tag "Jira state name:"))) - -(defcustom org-jira-users - '(("Full Name" . "account-id")) - "A list of displayName and key pairs." - :group 'org-jira - :type 'list) - -(defcustom org-jira-progress-issue-flow - '(("To Do" . "In Progress") - ("In Progress" . "Done")) - "Quickly define a common issue flow." - :group 'org-jira - :type 'list) - -(defcustom org-jira-property-overrides (list) - "An assoc list of property tag overrides. - -The KEY . VAL pairs should both be strings. - -For instance, to change the :assignee: property in the org :PROPERTIES: -block to :WorkedBy:, you can set this as such: - - (setq org-jira-property-overrides (list (cons \"assignee\" \"WorkedBy\"))) - -or simply: - - (add-to-list (quote org-jira-property-overrides) - (cons (\"assignee\" \"WorkedBy\"))) - -This will work for most any of the properties, with the -exception of ones with unique functionality, such as: - - - deadline - - summary - - description" - :group 'org-jira - :type 'list) - -(defcustom org-jira-serv-alist nil - "Association list to set information for each jira server. -Each element of the alist is a jira server name. The CAR of each -element is a string, uniquely identifying the server. The CDR of -each element is a well-formed property list with an even number -of elements, alternating keys and values, specifying parameters -for the server. - - (:property value :property value ... ) - -When a property is given a value in org-jira-serv-alist, its -setting overrides the value of the corresponding user -variable (if any) during syncing. - -Most properties are optional, but some should always be set: - - :url soap url of the jira server. - :username username to be used. - :host hostname of the jira server (TODO: compute it from ~url~). - -All the other properties are optional. They override the global -variables. - - :password password to be used, will be prompted if missing." - :group 'org-jira - :type '(alist :value-type plist)) - -(defcustom org-jira-use-status-as-todo nil - "Use the JIRA status as the TODO tag value." - :group 'org-jira) - -(defcustom org-jira-deadline-duedate-sync-p t - "Keep org deadline and jira duedate fields synced. -You may wish to set this to nil if you track org deadlines in -your buffer that you do not want to send back to your Jira -instance." - :group 'org-jira - :type 'boolean) - -(defcustom org-jira-worklog-sync-p t - "Keep org clocks and jira worklog fields synced. -You may wish to set this to nil if you track org clocks in -your buffer that you do not want to send back to your Jira -instance." - :group 'org-jira - :type 'boolean) - -(defcustom org-jira-download-dir "~/Downloads" - "Name of the default jira download library." - :group 'org-jira - :type 'string) - -(defcustom org-jira-download-ask-override t - "Ask before overriding tile." - :group 'org-jira - :type 'boolean) - -(defcustom org-jira-jira-status-to-org-keyword-alist nil - "Custom alist of jira status stored in car and `org-mode' keyword stored in cdr." - :group 'org-jira - :type '(alist :key-type string :value-type string)) - -(defcustom org-jira-priority-to-org-priority-omit-default-priority nil - "Whether to omit insertion of priority when it matches the default. - -When set to t, will omit the insertion of the matched value from -`org-jira-priority-to-org-priority-alist' when it matches the `org-default-priority'." - :group 'org-jira - :type 'boolean) - -(defcustom org-jira-priority-to-org-priority-alist nil - "Alist mapping jira priority keywords to `org-mode' priority cookies. - -A sample value might be - (list (cons \"High\" ?A) - (cons \"Medium\" ?B) - (cons \"Low\" ?C)). - -See `org-default-priority' for more info." - :group 'org-jira - :type '(alist :key-type string :value-type character)) - -(defcustom org-jira-boards-default-limit 50 - "Default limit for number of issues retrieved from agile boards." - :group 'org-jira - :type 'integer) - -;; FIXME: Issue with using this - issues are grouped under a headline incorrectly. -(defcustom org-jira-custom-jqls - '( - (:jql " assignee = currentUser() and createdDate < '2019-01-01' order by created DESC " - :limit 100 - :filename "last-years-work") - (:jql " assignee = currentUser() and createdDate >= '2019-01-01' order by created DESC " - :limit 100 - :filename "this-years-work") - ) - "A list of plists with :jql and :filename keys to run arbitrary user JQL." - :group 'org-jira - :type '(alist :value-type plist)) - -(defcustom org-jira-download-comments t - "Set to nil if you don't want to update comments during issue rendering." - :group 'org-jira - :type 'boolean) - -(defvar org-jira-serv nil - "Parameters of the currently selected blog.") - -(defvar org-jira-serv-name nil - "Name of the blog, to pick from `org-jira-serv-alist'.") - -(defvar org-jira-projects-list nil - "List of jira projects.") - -(defvar org-jira-current-project nil - "Currently selected (i.e., active project).") - -(defvar org-jira-issues-list nil - "List of jira issues under the current project.") - -(defvar org-jira-server-rpc-url nil - "Jira server soap URL.") - -(defvar org-jira-server-userid nil - "Jira server user id.") - -(defvar org-jira-proj-id nil - "Jira project ID.") - -(defvar org-jira-logged-in nil - "Flag whether user is logged-in or not.") - -(defvar org-jira-buffer-name "*org-jira-%s*" - "Name of the jira buffer.") - -(defvar org-jira-buffer-kill-prompt t - "Ask before killing buffer.") - -(make-variable-buffer-local 'org-jira-buffer-kill-prompt) - -(defvar org-jira-mode-hook nil - "Hook to run upon entry into mode.") - -(defvar org-jira-issue-id-history '() - "Prompt history for issue id.") - -(defvar org-jira-fixversion-id-history '() - "Prompt history for fixversion id.") - -(defvar org-jira-verbosity 'debug) - -(defun org-jira-log (s) (when (eq 'debug org-jira-verbosity) (message "%s" s))) - -(defmacro ensure-on-issue (&rest body) - "Make sure we are on an issue heading, before executing BODY." - (declare (debug t) - (indent 0)) - `(save-excursion - (save-restriction - (widen) - (unless (looking-at "^\\*\\* ") - (search-backward-regexp "^\\*\\* " nil t)) ; go to top heading - (let ((org-jira-id (org-jira-id))) - (unless (and org-jira-id (string-match (jiralib-get-issue-regexp) (downcase org-jira-id))) - (error "Not on an issue region!"))) - ,@body))) - -(defmacro org-jira-with-callback (&rest body) - "Simpler way to write the data BODY callbacks." - (declare (debug t) - (indent 0)) - `(lambda (&rest request-response) - (declare (ignore cb-data)) - (let ((cb-data (cl-getf request-response :data))) - ,@body))) - -(defmacro org-jira-freeze-ui (&rest body) - "Freeze the UI layout for the user as much as possible." - (declare (debug t) - (indent 0)) - `(save-excursion - (save-restriction - (widen) - (org-save-outline-visibility t - (outline-show-all) - ,@body)))) - -(defvar org-jira-proj-key-override nil - "String. An override for the proj-key. Set to nil to restore old behavior.") - -;; We want some hooking system to override default-jql + this. -(defun org-jira--get-proj-key (issue-id) - "Get the proper proj-key. Typically derived from ISSUE-ID." - (if org-jira-proj-key-override org-jira-proj-key-override - (replace-regexp-in-string "-.*" "" issue-id))) - -(defun org-jira--get-proj-key-from-issue (Issue) - "Get the proper proj-key from an ISSUE instance." - (oref Issue filename)) - -;; TODO: Merge these 3 ensure macros (or, scrap all but ones that work on Issue) -(defmacro ensure-on-issue-id (issue-id &rest body) - "Just do some work on ISSUE-ID, execute BODY." - (declare (debug t) - (indent 1)) - (let ((issue-id-var (make-symbol "issue-id"))) - `(let* ((,issue-id-var ,issue-id) - (proj-key (org-jira--get-proj-key ,issue-id-var)) - (project-file (org-jira--get-project-file-name proj-key)) - (project-buffer (or (find-buffer-visiting project-file) - (find-file project-file)))) - (with-current-buffer project-buffer - (org-jira-freeze-ui - (let ((p (org-find-entry-with-id ,issue-id-var))) - (unless p (error "Issue %s not found!" ,issue-id-var)) - (goto-char p) - (org-narrow-to-subtree) - ,@body)))))) - -(defmacro ensure-on-issue-id-with-filename (issue-id filename &rest body) - "Just do some work on ISSUE-ID, execute BODY." - (declare (debug t) - (indent 1)) - (let ((issue-id-var (make-symbol "issue-id")) - (filename-var (make-symbol "filename"))) - `(let* ((,issue-id-var ,issue-id) - (,filename-var ,filename) - (proj-key ,filename-var) - (project-file (org-jira--get-project-file-name proj-key)) - (project-buffer (or (find-buffer-visiting project-file) - (find-file project-file)))) - (with-current-buffer project-buffer - (org-jira-freeze-ui - (let ((p (org-find-entry-with-id ,issue-id-var))) - (unless p (error "Issue %s not found!" ,issue-id-var)) - (goto-char p) - (org-narrow-to-subtree) - ,@body)))))) - -(defmacro ensure-on-issue-Issue (Issue &rest body) - "Just do some work on ISSUE, execute BODY." - (declare (debug t) - (indent 1)) - (let ((Issue-var (make-symbol "Issue"))) - `(let ((,Issue-var ,Issue)) - (with-slots (issue-id) ,Issue-var - (let* ((proj-key (org-jira--get-proj-key-from-issue ,Issue-var)) - (project-file (org-jira--get-project-file-name proj-key)) - (project-buffer (or (find-buffer-visiting project-file) - (find-file project-file)))) - (with-current-buffer project-buffer - (org-jira-freeze-ui - (let ((p (org-find-entry-with-id issue-id))) - (unless p (error "Issue %s not found!" issue-id)) - (goto-char p) - (org-narrow-to-subtree) - ,@body)))))))) - -(defmacro ensure-on-todo (&rest body) - "Make sure we are on an todo heading, before executing BODY." - (declare (debug t) - (indent 0)) - `(save-excursion - (save-restriction - (let ((continue t) - (on-todo nil)) - (while continue - (when (org-get-todo-state) - (setq continue nil on-todo t)) - (unless (and continue (org-up-heading-safe)) - (setq continue nil))) - (if (not on-todo) - (error "TODO not found") - (org-narrow-to-subtree) - ,@body))))) - -(defmacro ensure-on-comment (&rest body) - "Make sure we are on a comment heading, before executing BODY." - (declare (debug t) - (indent 0)) - `(save-excursion - (org-back-to-heading) - (forward-thing 'whitespace) - (unless (looking-at "Comment:") - (error "Not on a comment region!")) - (save-restriction - (org-narrow-to-subtree) - ,@body))) - -(defmacro ensure-on-worklog (&rest body) - "Make sure we are on a worklog heading, before executing BODY." - (declare (debug t) - (indent 0)) - `(save-excursion - (org-back-to-heading) - (forward-thing 'whitespace) - (unless (looking-at "Worklog:") - (error "Not on a worklog region!")) - (save-restriction - (org-narrow-to-subtree) - ,@body))) - -(defun org-jira--ensure-working-dir () - "Ensure that the org-jira-working-dir exists" - (unless (file-exists-p org-jira-working-dir) - (error (format "org-jira directory does not exist! Run (make-directory \"%s\")" org-jira-working-dir)) - ) - org-jira-working-dir - ) - -(defvar org-jira-entry-mode-map - (let ((org-jira-map (make-sparse-keymap))) - (define-key org-jira-map (kbd "C-c pg") 'org-jira-get-projects) - (define-key org-jira-map (kbd "C-c bg") 'org-jira-get-boards) - (define-key org-jira-map (kbd "C-c iv") 'org-jira-get-issues-by-board) - (define-key org-jira-map (kbd "C-c ib") 'org-jira-browse-issue) - (define-key org-jira-map (kbd "C-c ig") 'org-jira-get-issues) - (define-key org-jira-map (kbd "C-c ij") 'org-jira-get-issues-from-custom-jql) - (define-key org-jira-map (kbd "C-c ih") 'org-jira-get-issues-headonly) - ;;(define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-from-filter-headonly) - ;;(define-key org-jira-map (kbd "C-c iF") 'org-jira-get-issues-from-filter) - (define-key org-jira-map (kbd "C-c iu") 'org-jira-update-issue) - (define-key org-jira-map (kbd "C-c iw") 'org-jira-progress-issue) - (define-key org-jira-map (kbd "C-c in") 'org-jira-progress-issue-next) - (define-key org-jira-map (kbd "C-c ia") 'org-jira-assign-issue) - ;(define-key org-jira-map (kbd "C-c isr") 'org-jira-set-issue-reporter) - (define-key org-jira-map (kbd "C-c ir") 'org-jira-refresh-issue) - (define-key org-jira-map (kbd "C-c iR") 'org-jira-refresh-issues-in-buffer) - (define-key org-jira-map (kbd "C-c ic") 'org-jira-create-issue) - (define-key org-jira-map (kbd "C-c ik") 'org-jira-copy-current-issue-key) - (define-key org-jira-map (kbd "C-c sc") 'org-jira-create-subtask) - (define-key org-jira-map (kbd "C-c sg") 'org-jira-get-subtasks) - (define-key org-jira-map (kbd "C-c cc") 'org-jira-add-comment) - (define-key org-jira-map (kbd "C-c cu") 'org-jira-update-comment) - (define-key org-jira-map (kbd "C-c wu") 'org-jira-update-worklogs-from-org-clocks) - (define-key org-jira-map (kbd "C-c tj") 'org-jira-todo-to-jira) - (define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-by-fixversion) - org-jira-map)) - -;;;###autoload -(define-minor-mode org-jira-mode - "Toggle org-jira mode. -With no argument, the mode is toggled on/off. -Non-nil argument turns mode on. -Nil argument turns mode off. - -Commands: -\\{org-jira-entry-mode-map} - -Entry to this mode calls the value of `org-jira-mode-hook'." - - :init-value nil - :lighter " jira" - :group 'org-jira - :keymap org-jira-entry-mode-map - - (if org-jira-mode - (progn - (set (make-local-variable 'org-element-use-cache) nil) - (run-mode-hooks 'org-jira-mode-hook)) - (progn - (kill-local-variable 'org-element-use-cache)))) - -(defun org-jira-maybe-activate-mode () - "Having hooks can be an expensive operation, and they are invoked each -time the mode is started - we should only ever re-activate the mode if -it isn't already on." - (unless (bound-and-true-p org-jira-mode) (org-jira-mode t))) - -(defun org-jira-get-project-name (proj) - (org-jira-find-value proj 'key)) - -(defun org-jira-find-value (l &rest keys) - (let* (key exists) - (while (and keys (listp l)) - (setq key (car keys)) - (setq exists nil) - (mapc (lambda (item) - (when (equal key (car item)) - (setq exists t))) - (if (and (listp l) - (listp (car l))) - l - nil)) - (setq keys (cdr keys)) - (if exists - (setq l (cdr (assoc key l))) - (setq l (or (cdr (assoc key l)) l)))) - l)) - -(defun org-jira--get-project-file-name (project-key) - "Translate PROJECT-KEY into filename." - (-if-let (translation (cdr (assoc project-key org-jira-project-filename-alist))) - (expand-file-name translation (org-jira--ensure-working-dir)) - (expand-file-name (concat project-key ".org") (org-jira--ensure-working-dir)))) - -(defun org-jira-get-project-lead (proj) - (org-jira-find-value proj 'lead 'name)) - -;; This is mapped to accountId and not username, so we need nil not blank string. -(defun org-jira-get-assignable-users (project-key) - "Get the list of assignable users for PROJECT-KEY, adding user set jira-users first." - (append - '(("Unassigned" . nil)) - org-jira-users - (mapcar (lambda (user) - (cons (org-jira-decode (cdr (assoc 'displayName user))) - (org-jira-decode (cdr (assoc 'accountId user))))) - (jiralib-get-users project-key)))) - -(defun org-jira-get-reporter-candidates (project-key) - "Get the list of assignable users for PROJECT-KEY, adding user set jira-users first." - (append - org-jira-users - (mapcar (lambda (user) - (cons (org-jira-decode (cdr (assoc 'displayName user))) - (org-jira-decode (cdr (assoc 'accountId user))))) - (jiralib-get-users project-key)))) - -(defun org-jira-entry-put (pom property value) - "Similar to org-jira-entry-put, but with an optional alist of overrides. - -At point-or-marker POM, set PROPERTY to VALUE. - -Look at customizing org-jira-property-overrides if you want -to change the property names this sets." - (unless (stringp property) - (setq property (symbol-name property))) - (let ((property (or (assoc-default property org-jira-property-overrides) - property))) - (org-entry-put pom property (org-jira-decode value)))) - -(defun org-jira-kill-line () - "Kill the line, without 'kill-line' side-effects of altering kill ring." - (interactive) - (delete-region (point) (line-end-position))) - -;; Appropriated from org.el -(defun org-jira-org-kill-line (&optional _arg) - "Kill line, to tags or end of line." - (cond - ((or (not org-special-ctrl-k) - (bolp) - (not (org-at-heading-p))) - (when (and (get-char-property (min (point-max) (point-at-eol)) 'invisible) - org-ctrl-k-protect-subtree - (or (eq org-ctrl-k-protect-subtree 'error) - (not (y-or-n-p "Kill hidden subtree along with headline? ")))) - (user-error "C-k aborted as it would kill a hidden subtree")) - (call-interactively - (if (bound-and-true-p visual-line-mode) 'kill-visual-line 'org-jira-kill-line))) - ((looking-at ".*?\\S-\\([ \t]+\\(:[[:alnum:]_@#%:]+:\\)\\)[ \t]*$") - (delete-region (point) (match-beginning 1)) - (org-set-tags nil t)) - (t (delete-region (point) (point-at-eol))))) - -;;;###autoload -(defun org-jira-get-projects () - "Get list of projects." - (interactive) - (let ((projects-file (expand-file-name "projects-list.org" (org-jira--ensure-working-dir)))) - (or (find-buffer-visiting projects-file) - (find-file projects-file)) - (org-jira-maybe-activate-mode) - (save-excursion - (let* ((oj-projs (jiralib-get-projects))) - (mapc (lambda (proj) - (let* ((proj-key (org-jira-find-value proj 'key)) - (proj-headline (format "Project: [[file:%s.org][%s]]" proj-key proj-key))) - (save-restriction - (widen) - (goto-char (point-min)) - (outline-show-all) - (setq p (org-find-exact-headline-in-buffer proj-headline)) - (if (and p (>= p (point-min)) - (<= p (point-max))) - (progn - (goto-char p) - (org-narrow-to-subtree) - (end-of-line)) - (goto-char (point-max)) - (unless (looking-at "^") - (insert "\n")) - (insert "* ") - (org-jira-insert proj-headline) - (org-narrow-to-subtree)) - (org-jira-entry-put (point) "name" (org-jira-get-project-name proj)) - (org-jira-entry-put (point) "key" (org-jira-find-value proj 'key)) - (org-jira-entry-put (point) "lead" (org-jira-get-project-lead proj)) - (org-jira-entry-put (point) "ID" (org-jira-find-value proj 'id)) - (org-jira-entry-put (point) "url" (format "%s/browse/%s" (replace-regexp-in-string "/*$" "" jiralib-url) (org-jira-find-value proj 'key)))))) - oj-projs))))) - -(defun org-jira-get-issue-components (issue) - "Return the components the ISSUE belongs to." - (mapconcat - (lambda (comp) - (org-jira-find-value comp 'name)) - (org-jira-find-value issue 'fields 'components) ", ")) - -(defun org-jira-get-issue-labels (issue) - "Return the labels the ISSUE belongs to." - (org-jira-find-value issue 'fields 'labels)) - -(defun org-jira-decode (data) - "Decode text DATA. - -It must receive a coercion to string, as not every time will it -be populated." - (decode-coding-string - (cl-coerce data 'string) jiralib-coding-system)) - -(defun org-jira-insert (&rest args) - "Set coding to text provide by `ARGS' when insert in buffer." - (insert (org-jira-decode (apply #'concat args)))) - -(defun org-jira-transform-time-format (jira-time-str) - "Convert JIRA-TIME-STR to format \"%Y-%m-%d %T\". - -Example: \"2012-01-09T08:59:15.000Z\" becomes \"2012-01-09 -16:59:15\", with the current timezone being +0800." - (condition-case () - (format-time-string - "%Y-%m-%d %T" - (apply - 'encode-time - (parse-time-string (replace-regexp-in-string "T\\|\\.000" " " jira-time-str)))) - (error jira-time-str))) - -(defun org-jira--fix-encode-time-args (arg) - "Fix ARG for 3 nil values at the head." - (cl-loop - for n from 0 to 2 by 1 do - (when (not (nth n arg)) - (setcar (nthcdr n arg) 0))) - arg) - -(defun org-jira-time-format-to-jira (org-time-str) - "Convert ORG-TIME-STR back to jira time format." - (condition-case () - (format-time-string - "%Y-%m-%dT%T.000Z" - (apply 'encode-time - (org-jira--fix-encode-time-args (parse-time-string org-time-str))) - t) - (error org-time-str))) - -(defun org-jira-get-comment-val (key comment) - "Return the value associated with KEY of COMMENT." - (org-jira-get-issue-val key comment)) - -(defun org-jira-time-stamp-to-org-clock (time-stamp) - "Convert TIME-STAMP into org-clock format." - (format-time-string "%Y-%m-%d %a %H:%M" time-stamp)) - -(defun org-jira-date-to-org-clock (date) - "Convert DATE into a time stamp and then into org-clock format. -Expects a date in format such as: 2017-02-26T00:08:00.000-0500." - (org-jira-time-stamp-to-org-clock (date-to-time date))) - -(defun org-jira-worklogs-to-org-clocks (worklogs) - "Get a list of WORKLOGS and convert to org-clocks." - (mapcar - (lambda (worklog) - (let ((wl-start (cdr (assoc 'started worklog))) - (wl-time (cdr (assoc 'timeSpentSeconds worklog))) - (wl-end)) - (setq wl-start (org-jira-date-to-org-clock wl-start)) - (setq wl-end (org-jira-time-stamp-to-org-clock (time-add (date-to-time wl-start) wl-time))) - (list - wl-start - wl-end - (cdr (assoc 'comment worklog)) - (cdr (assoc 'id worklog)) - ) - )) - worklogs) - ) - -(defun org-jira-format-clock (clock-entry) - "Format a CLOCK-ENTRY given the (list start end). -This format is typically generated from org-jira-worklogs-to-org-clocks call." - (format "CLOCK: [%s]--[%s]" (car clock-entry) (cadr clock-entry))) - -(defun org-jira-insert-clock (clock-entry) - "Insert a CLOCK-ENTRY given the (list start end). -This format is typically generated from org-jira-worklogs-to-org-clocks call." - (insert (org-jira-format-clock clock-entry)) - (org-beginning-of-line) - (org-ctrl-c-ctrl-c) ;; @TODO Maybe not call directly? does it matter? - used to resync the clock estimate - (org-end-of-line) - (insert "\n") - (insert (format " :id: %s\n" (cadddr clock-entry))) - (when (caddr clock-entry) (insert (replace-regexp-in-string "^\\*" "-" (format " %s\n" (org-jira-decode (caddr clock-entry)))))) ;; No comment is nil, so don't print it - ) - -(defun org-jira-logbook-reset (issue-id filename &optional clocks) - "Find logbook for ISSUE-ID in FILENAME, delete it. -Re-create it with CLOCKS. This is used for worklogs." - (interactive) - (let ((existing-logbook-p nil)) - ;; See if the LOGBOOK already exists or not. - (ensure-on-issue-id-with-filename issue-id filename - (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK"))) - (when (search-forward (format ":%s:" drawer-name) nil 1 1) - (setq existing-logbook-p t)))) - (ensure-on-issue-id-with-filename issue-id filename - (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK"))) - (if existing-logbook-p - (progn ;; If we had a logbook, drop it and re-create in a bit. - (search-forward (format ":%s:" drawer-name) nil 1 1) - (org-beginning-of-line) - (delete-region (point) (search-forward ":END:" nil 1 1)) - ) - (progn ;; Otherwise, create a new one at the end of properties list - (search-forward ":END:" nil 1 1) - (forward-line))) - (org-insert-drawer nil (format "%s" drawer-name)) ;; Doc says non-nil, but this requires nil - (mapc #'org-jira-insert-clock clocks) - ;; Clean up leftover newlines (we left 2 behind) - (dotimes (n 2) - (search-forward-regexp "^$" nil 1 1) - (delete-region (point) (min (point-max) (1+ (point))))))))) - -(defun org-jira-get-worklog-val (key WORKLOG) - "Return the value associated with KEY of WORKLOG." - (org-jira-get-comment-val key WORKLOG)) - -(defun org-jira-get-issue-val (key issue) - "Return the value associated with key KEY of issue ISSUE." - (let ((tmp (or (org-jira-find-value issue 'fields key 'key) ""))) ;; For project, we need a key, not the name... - (unless (stringp tmp) - (setq tmp (org-jira-find-value issue key))) - (unless (stringp tmp) - (setq tmp (org-jira-find-value issue 'fields key 'displayName))) - (unless (stringp tmp) - (setq tmp "")) - (cond ((eq key 'components) - (org-jira-get-issue-components issue)) - ((eq key 'labels) - (org-jira-get-issue-labels issue)) - ((member key '(created updated startDate)) - (org-jira-transform-time-format tmp)) - ((eq key 'status) - (if jiralib-use-restapi - (org-jira-find-value issue 'fields 'status 'name) - (org-jira-find-value (jiralib-get-statuses) tmp))) - ((eq key 'resolution) - (if jiralib-use-restapi - tmp - (if (string= tmp "") - "" - (org-jira-find-value (jiralib-get-resolutions) tmp)))) - ((eq key 'type) - (if jiralib-use-restapi - (org-jira-find-value issue 'fields 'issuetype 'name) - (org-jira-find-value (jiralib-get-issue-types) tmp))) - ((eq key 'priority) - (if jiralib-use-restapi - (org-jira-find-value issue 'fields 'priority 'name) - (org-jira-find-value (jiralib-get-priorities) tmp))) - ((eq key 'description) - (org-trim tmp)) - (t - tmp)))) - -(defvar org-jira-jql-history nil) - -;;;###autoload -(defun org-jira-get-issue-list (&optional callback) - "Get list of issues, using jql (jira query language), invoke CALLBACK after. - -Default is unresolved issues assigned to current login user; with -a prefix argument you are given the chance to enter your own -jql." - (org-jira-log (format "I was called, was it with a callback? %s" (if callback "yes" "no"))) - (let ((jql org-jira-default-jql)) - (when current-prefix-arg - (setq jql (read-string "Jql: " - (if org-jira-jql-history - (car org-jira-jql-history) - "assignee = currentUser() and resolution = unresolved") - 'org-jira-jql-history - "assignee = currentUser() and resolution = unresolved"))) - (list (jiralib-do-jql-search jql nil callback)))) - -(defun org-jira-get-issue-by-id (id) - "Get an issue by its ID." - (push id org-jira-issue-id-history) - (let ((jql (format "id = %s" id))) - (jiralib-do-jql-search jql))) - -(defun org-jira-get-issue-by-fixversion (fixversion-id) - "Get an issue by its FIXVERSION-ID." - (push fixversion-id org-jira-fixversion-id-history) - (let ((jql (format "fixVersion = \"%s\"" fixversion-id))) - (jiralib-do-jql-search jql))) - -;;;###autoload -(defun org-jira-get-summary () - "Get issue summary from point and place next to issue id from jira" - (interactive) - (let ((jira-id (thing-at-point 'symbol))) - (unless jira-id (error "ORG_JIRA_ERROR: JIRA-ID missing in org-jira-get-summary!")) - (forward-symbol 1) - (insert (format " - %s" - (cdr (assoc 'summary (assoc 'fields (car (org-jira-get-issue-by-id jira-id))))))))) - -;;;###autoload -(defun org-jira-get-summary-url () - "Get issue summary from point and place next to issue id from jira, and make issue id a link" - (interactive) - (let ((jira-id (thing-at-point 'symbol))) - (insert (format "[[%s][%s]] - %s" - (concatenate 'string jiralib-url "browse/" jira-id) jira-id - (cdr (assoc 'summary (car (org-jira-get-issue-by-id jira-id)))))))) - -;;;###autoload -(defun org-jira-get-issues-headonly (issues) - "Get list of ISSUES, head only. - -The default behavior is to return issues assigned to you and unresolved. - -With a prefix argument, allow you to customize the jql. See -`org-jira-get-issue-list'." - - (interactive - (org-jira-get-issue-list)) - - (let* ((issues-file (expand-file-name "issues-headonly.org" (org-jira--ensure-working-dir))) - (issues-headonly-buffer (or (find-buffer-visiting issues-file) - (find-file issues-file)))) - (with-current-buffer issues-headonly-buffer - (widen) - (delete-region (point-min) (point-max)) - - (mapc (lambda (issue) - (let ((issue-id (org-jira-get-issue-key issue)) - (issue-summary (org-jira-get-issue-summary issue))) - (org-jira-insert (format "- [jira:%s] %s\n" issue-id issue-summary)))) - issues)) - (switch-to-buffer issues-headonly-buffer))) - -;;;###autoload -(defun org-jira-get-issue (id) - "Get a JIRA issue, allowing you to enter the issue-id first." - (interactive (list (read-string "Issue ID: " "" 'org-jira-issue-id-history))) - (org-jira-get-issues (org-jira-get-issue-by-id id)) - (let ((issue-pos (org-find-entry-with-id id))) - (when issue-pos - (goto-char issue-pos) - (recenter 0)))) -;;;###autoload -(defun org-jira-get-issues-by-fixversion (fixversion) - "Get list of issues by FIXVERSION." - (interactive (list (read-string "Fixversion ID: " "" - 'org-jira-fixversion-id-history))) - (org-jira-get-issues (org-jira-get-issue-by-fixversion fixversion))) - -;;;###autoload -(defun org-jira-get-issue-project (issue) - (org-jira-find-value issue 'fields 'project 'key)) - -(defun org-jira-get-issue-key (issue) - (org-jira-find-value issue 'key)) - -(defun org-jira-get-issue-summary (issue) - (org-jira-find-value issue 'fields 'summary)) - -(defvar org-jira-get-issue-list-callback - (cl-function - (lambda (&key data &allow-other-keys) - "Callback for async, DATA is the response from the request call. - -Will send a list of org-jira-sdk-issue objects to the list printer." - (org-jira-log "Received data for org-jira-get-issue-list-callback.") - (--> data - (org-jira-sdk-path it '(issues)) - (append it nil) ; convert the conses into a proper list. - org-jira-sdk-create-issues-from-data-list - org-jira-get-issues)))) - -(defvar org-jira-get-sprint-list-callback - (cl-function - (lambda (&key data &allow-other-keys) - "Callback for async, DATA is the response from the request call. - -Will send a list of org-jira-sdk-issue objects to the list printer." - (org-jira-log "Received data for org-jira-get-sprint-list-callback.") - (--> data - (org-jira-sdk-path it '(sprint)) - (append it nil) ; convert the conses into a proper list. - org-jira-sdk-create-issues-from-data-list - org-jira-get-issues)))) - - -;;;###autoload -(defun org-jira-get-issues (issues) - "Get list of ISSUES into an org buffer. - -Default is get unfinished issues assigned to you, but you can -customize jql with a prefix argument. -See`org-jira-get-issue-list'" - ;; If the user doesn't provide a default, async call to build an issue list - ;; from the JQL style query - (interactive - (org-jira-get-issue-list org-jira-get-issue-list-callback)) - (org-jira-log "Fetching issues...") - (when (> (length issues) 0) - (org-jira--render-issues-from-issue-list issues))) - -(defvar org-jira-original-default-jql nil) - -(defun org-jira-get-issues-from-custom-jql-callback (filename list) - "Generate a function that we can iterate over FILENAME and LIST with when callback finishes." - (cl-function - (lambda (&key data &allow-other-keys) - "Callback for async, DATA is the response from the request call. - -Will send a list of org-jira-sdk-issue objects to the list printer." - (org-jira-log "Received data for org-jira-get-issues-from-custom-jql-callback.") - (--> data - (org-jira-sdk-path it '(issues)) - (append it nil) ; convert the conses into a proper list. - (org-jira-sdk-create-issues-from-data-list-with-filename filename it) - org-jira-get-issues) - (setq org-jira-proj-key-override nil) - (let ((next (rest list))) - (when next - (org-jira-get-issues-from-custom-jql next)))))) - -;;;###autoload -(defun org-jira-get-issues-from-custom-jql (&optional jql-list) - "Get JQL-LIST list of issues from a custom JQL and PROJ-KEY. - -The PROJ-KEY will act as the file name, while the JQL will be any -valid JQL to populate a file to store PROJ-KEY results in. - -Please note that this is *not* concurrent or race condition -proof. If you try to run multiple calls to this function, it -will mangle things badly, as they rely on globals DEFAULT-JQL and -ORG-JIRA-PROJ-KEY-OVERRIDE being set before and after running." - (interactive) - (let* ((jl (or jql-list org-jira-custom-jqls)) - (uno (car jl)) - (filename (cl-getf uno :filename)) - (limit (cl-getf uno :limit)) - (jql (replace-regexp-in-string "[\n]" " " (cl-getf uno :jql)))) - (setq org-jira-proj-key-override filename) - (jiralib-do-jql-search jql limit (org-jira-get-issues-from-custom-jql-callback filename jl)))) - -(defun org-jira--get-project-buffer (Issue) - (let* ((proj-key (org-jira--get-proj-key-from-issue Issue)) - (project-file (org-jira--get-project-file-name proj-key)) - (project-buffer (find-file-noselect project-file))) - project-buffer)) - -(defun org-jira--is-top-headline? (proj-key) - "For PROJ-KEY, check if it is a top headline or not." - (let ((elem (org-element-at-point))) - (and (eq 'headline (car elem)) - (equal (format "%s-Tickets" proj-key) - (org-element-property :title elem)) - (= 1 (org-element-property :level elem))))) - -(defun org-jira--maybe-render-top-heading (proj-key) - "Ensure that there is a headline for PROJ-KEY at the top of the file." - (goto-char (point-min)) - (let ((top-heading (format ".*%s-Tickets" proj-key)) - (th-found? nil)) - (while (and (not (eobp)) - (not th-found?)) - (beginning-of-line) - (when (org-jira--is-top-headline? proj-key) (setq th-found? t)) - (re-search-forward top-heading nil 1 1)) - (beginning-of-line) - (unless (looking-at top-heading) - (insert (format "\n* %s-Tickets\n" proj-key))))) - -(defun org-jira--render-issue (Issue) - "Render single ISSUE." -;; (org-jira-log "Rendering issue from issue list") -;; (org-jira-log (org-jira-sdk-dump Issue)) - (with-slots (filename proj-key issue-id summary status priority headline id) Issue - (let (p) - (with-current-buffer (org-jira--get-project-buffer Issue) - (org-jira-freeze-ui - (org-jira-maybe-activate-mode) - (org-jira--maybe-render-top-heading proj-key) - (setq p (org-find-entry-with-id issue-id)) - (save-restriction - (if (and p (>= p (point-min)) - (<= p (point-max))) - (progn - (goto-char p) - (forward-thing 'whitespace) - (org-jira-kill-line)) - (goto-char (point-max)) - (unless (looking-at "^") - (insert "\n")) - (insert "** ")) - (org-jira-insert - (concat (org-jira-get-org-keyword-from-status status) - " " - (org-jira-get-org-priority-cookie-from-issue priority) - headline)) - (save-excursion - (unless (search-forward "\n" (point-max) 1) - (insert "\n"))) - (org-narrow-to-subtree) - (save-excursion - (org-back-to-heading t) - (org-set-tags-to (replace-regexp-in-string "-" "_" issue-id))) - (org-jira-entry-put (point) "assignee" (or (slot-value Issue 'assignee) "Unassigned")) - (mapc (lambda (entry) - (let ((val (slot-value Issue entry))) - (when (and val (not (string= val ""))) - (org-jira-entry-put (point) (symbol-name entry) val)))) - '(filename reporter type type-id priority labels resolution status components created updated sprint)) - - (org-jira-entry-put (point) "ID" issue-id) - (org-jira-entry-put (point) "CUSTOM_ID" issue-id) - - ;; Insert the duedate as a deadline if it exists - (when org-jira-deadline-duedate-sync-p - (let ((duedate (oref Issue duedate))) - (when (> (length duedate) 0) - (org-deadline nil duedate)))) - - (mapc - (lambda (heading-entry) - (ensure-on-issue-id-with-filename issue-id filename - (let* ((entry-heading - (concat (symbol-name heading-entry) - (format ": [[%s][%s]]" - (concat jiralib-url "/browse/" issue-id) issue-id)))) - (setq p (org-find-exact-headline-in-buffer entry-heading)) - (if (and p (>= p (point-min)) - (<= p (point-max))) - (progn - (goto-char p) - (org-narrow-to-subtree) - (goto-char (point-min)) - (forward-line 1) - (delete-region (point) (point-max))) - (if (org-goto-first-child) - (org-insert-heading) - (goto-char (point-max)) - (org-insert-subheading t)) - (org-jira-insert entry-heading "\n")) - - ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup - (org-jira-insert - (replace-regexp-in-string - "^" " " - (format "%s" (slot-value Issue heading-entry))))))) - '(description)) - - (when org-jira-download-comments - (org-jira-update-comments-for-issue Issue) - - ;; FIXME: Re-enable when attachments are not erroring. - ;;(org-jira-update-attachments-for-current-issue) - ) - - ;; only sync worklog clocks when the user sets it to be so. - (when org-jira-worklog-sync-p - (org-jira-update-worklogs-for-issue issue-id filename)))))))) - -(defun org-jira--render-issues-from-issue-list (Issues) - "Add the issues from ISSUES list into the org file(s). - -ISSUES is a list of `org-jira-sdk-issue' records." - ;; FIXME: Some type of loading error - the first async callback does not know about - ;; the issues existing as a class, so we may need to instantiate here if we have none. - (when (eq 0 (->> Issues (cl-remove-if-not #'org-jira-sdk-isa-issue?) length)) - (setq Issues (org-jira-sdk-create-issues-from-data-list Issues))) - - ;; First off, we never ever want to run on non-issues, so check our types early. - (setq Issues (cl-remove-if-not #'org-jira-sdk-isa-issue? Issues)) - (org-jira-log (format "About to render %d issues." (length Issues))) - - ;; If we have any left, we map over them. - (mapc 'org-jira--render-issue Issues) - - ;; Prior text: "Oh, are you the culprit?" - Not sure if this caused an issue at some point. - ;; We want to ensure we fix broken org narrowing though, by doing org-show-all and then org-cycle. - (switch-to-buffer (org-jira--get-project-buffer (-last-item Issues))) - (org-show-all) - (org-cycle)) - -;;;###autoload -(defun org-jira-update-comment () - "Update a comment for the current issue." - (interactive) - (let* ((issue-id (org-jira-get-from-org 'issue 'key)) ; Really the key - (filename (org-jira-filename)) - (comment-id (org-jira-get-from-org 'comment 'id)) - (comment (replace-regexp-in-string "^ " "" (org-jira-get-comment-body comment-id)))) - (lexical-let ((issue-id issue-id) - (filename filename)) - (let ((callback-edit - (cl-function - (lambda (&key _data &allow-other-keys) - (ensure-on-issue-id-with-filename - issue-id filename - (org-jira-update-comments-for-current-issue))))) - (callback-add - (cl-function - (lambda (&key _data &allow-other-keys) - (ensure-on-issue-id-with-filename - issue-id filename - ;; @TODO :optim: Has to be a better way to do this - ;; than delete region (like update the unmarked - ;; one) - (org-jira-delete-current-comment) - (org-jira-update-comments-for-current-issue)))))) - (if comment-id - (jiralib-edit-comment issue-id comment-id comment callback-edit) - (jiralib-add-comment issue-id comment callback-add)))))) - -(defun org-jira-add-comment (issue-id filename comment) - "For ISSUE-ID in FILENAME, add a new COMMENT string to the issue region." - (interactive - (let* ((issue-id (org-jira-get-from-org 'issue 'id)) - (filename (org-jira-filename)) - (comment (read-string (format "Comment (%s): " issue-id)))) - (list issue-id filename comment))) - (lexical-let ((issue-id issue-id) - (filename filename)) - (ensure-on-issue-id-with-filename issue-id filename - (goto-char (point-max)) - (jiralib-add-comment - issue-id comment - (cl-function - (lambda (&key _data &allow-other-keys) - (ensure-on-issue-id-with-filename issue-id filename - (org-jira-update-comments-for-current-issue)))))))) - -(defun org-jira-org-clock-to-date (org-time) - "Convert ORG-TIME formatted date into a plain date string." - (format-time-string - "%Y-%m-%dT%H:%M:%S.000%z" - (date-to-time org-time))) - -(defun org-jira-worklog-time-from-org-time (org-time) - "Take in an ORG-TIME and convert it into the portions of a worklog time. -Expects input in format such as: [2017-04-05 Wed 01:00]--[2017-04-05 Wed 01:46] => 0:46" - (let ((start (replace-regexp-in-string "^\\[\\(.*?\\)\\].*" "\\1" org-time)) - (end (replace-regexp-in-string ".*--\\[\\(.*?\\)\\].*" "\\1" org-time))) - `((started . ,(org-jira-org-clock-to-date start)) - (time-spent-seconds . ,(time-to-seconds - (time-subtract - (date-to-time end) - (date-to-time start))))))) - -(defun org-jira-org-clock-to-jira-worklog (org-time clock-content) - "Given ORG-TIME and CLOCK-CONTENT, format a jira worklog entry." - (let ((lines (split-string clock-content "\n")) - worklog-id) - ;; See if we look like we have an id - (when (string-match ":id:" (first lines)) - (setq worklog-id - (replace-regexp-in-string "^.*:id: \\([0-9]*\\)$" "\\1" (first lines))) - (when (> (string-to-number worklog-id) 0) ;; pop off the first id line if we found it valid - (setq lines (cdr lines)))) - (setq lines (reverse (cdr (reverse lines)))) ;; drop last line - (let ((comment (org-trim (mapconcat 'identity lines "\n"))) - (worklog-time (org-jira-worklog-time-from-org-time org-time))) - `((worklog-id . ,worklog-id) - (comment . ,comment) - (started . ,(cdr (assoc 'started worklog-time))) - (time-spent-seconds . ,(cdr (assoc 'time-spent-seconds worklog-time))) - )))) - -(defun org-jira-worklog-to-hashtable (issue-id) - "Given ISSUE-ID, return a hashtable of worklog-id -> jira worklog." - (let ((worklog-hashtable (make-hash-table :test 'equal))) - (mapc - (lambda (worklog) - (let ((worklog-id (cdr (assoc 'id worklog)))) - (puthash worklog-id worklog worklog-hashtable))) - (jiralib-worklog-import--filter-apply - (org-jira-find-value - (jiralib-get-worklogs - issue-id) - 'worklogs))) - worklog-hashtable)) - -;;;###autoload -(defun org-jira-update-worklogs-from-org-clocks () - "Update or add a worklog based on the org clocks." - (interactive) - (let* ((issue-id (org-jira-get-from-org 'issue 'key)) - (filename (org-jira-filename)) - ;; Fetch all workflogs for this issue - (jira-worklogs-ht (org-jira-worklog-to-hashtable issue-id))) - (org-jira-log (format "About to sync worklog for issue: %s in file: %s" - issue-id filename)) - (ensure-on-issue-id-with-filename issue-id filename - (search-forward (format ":%s:" (or (org-clock-drawer-name) "LOGBOOK")) nil 1 1) - (org-beginning-of-line) - ;; (org-cycle 1) - (while (search-forward "CLOCK: " nil 1 1) - (let ((org-time (buffer-substring-no-properties (point) (point-at-eol)))) - (forward-line) - ;; See where the stuff ends (what point) - (let (next-clock-point) - (save-excursion - (search-forward-regexp "\\(CLOCK\\|:END\\):" nil 1 1) - (setq next-clock-point (point))) - (let ((clock-content - (buffer-substring-no-properties (point) next-clock-point))) - ;; Update via jiralib call - (let* ((worklog (org-jira-org-clock-to-jira-worklog org-time clock-content)) - (comment-text (cdr (assoc 'comment worklog))) - (comment-text (if (string= (org-trim comment-text) "") nil comment-text))) - (if (cdr (assoc 'worklog-id worklog)) - ;; If there is a worklog in jira for this ID, check if the worklog has changed. - ;; If it has changed, update the worklog. - ;; If it has not changed, skip. - (let ((jira-worklog (gethash (cdr (assoc 'worklog-id worklog)) jira-worklogs-ht))) - (when (and jira-worklog - ;; Check if the entries are differing lengths. - (or (not (= (cdr (assoc 'timeSpentSeconds jira-worklog)) - (cdr (assoc 'time-spent-seconds worklog)))) - ;; Check if the entries start at different times. - (not (string= (cdr (assoc 'started jira-worklog)) - (cdr (assoc 'started worklog)))))) - (jiralib-update-worklog - issue-id - (cdr (assoc 'worklog-id worklog)) - (cdr (assoc 'started worklog)) - (cdr (assoc 'time-spent-seconds worklog)) - comment-text - nil))) ; no callback - synchronous - ;; else - (jiralib-add-worklog - issue-id - (cdr (assoc 'started worklog)) - (cdr (assoc 'time-spent-seconds worklog)) - comment-text - nil) ; no callback - synchronous - ) - ))))) - (org-jira-log (format "Updating worklog from org-jira-update-worklogs-from-org-clocks call")) - (org-jira-update-worklogs-for-issue issue-id filename) - ))) - -(defun org-jira-update-worklog () - "Update a worklog for the current issue." - (interactive) - (error "Deprecated, use org-jira-update-worklogs-from-org-clocks instead!") - (let* ((issue-id (org-jira-get-from-org 'issue 'key)) - (worklog-id (org-jira-get-from-org 'worklog 'id)) - (timeSpent (org-jira-get-from-org 'worklog 'timeSpent)) - (timeSpent (if timeSpent - timeSpent - (read-string "Input the time you spent (such as 3w 1d 2h): "))) - (timeSpent (replace-regexp-in-string " \\(\\sw\\)\\sw*\\(,\\|$\\)" "\\1" timeSpent)) - (startDate (org-jira-get-from-org 'worklog 'startDate)) - (startDate (if startDate - startDate - (org-read-date nil nil nil "Input when did you start"))) - (startDate (org-jira-time-format-to-jira startDate)) - (comment (replace-regexp-in-string "^ " "" (org-jira-get-worklog-comment worklog-id))) - (worklog `((comment . ,comment) - (timeSpent . ,timeSpent) - (timeSpentInSeconds . 10) - (startDate . ,startDate))) - (worklog (if worklog-id - (cons `(id . ,(replace-regexp-in-string "^worklog-" "" worklog-id)) worklog) - worklog))) - (if worklog-id - (jiralib-update-worklog worklog) - (jiralib-add-worklog-and-autoadjust-remaining-estimate issue-id startDate timeSpent comment)) - (org-jira-delete-current-worklog) - (org-jira-update-worklogs-for-current-issue))) - -(defun org-jira-delete-current-comment () - "Delete the current comment." - (ensure-on-comment - (delete-region (point-min) (point-max)))) - -(defun org-jira-delete-current-worklog () - "Delete the current worklog." - (ensure-on-worklog - (delete-region (point-min) (point-max)))) - -;;;###autoload -(defun org-jira-copy-current-issue-key () - "Copy the current issue's key into clipboard." - (interactive) - (let ((issue-id (org-jira-get-from-org 'issue 'key))) - (with-temp-buffer - (insert issue-id) - (kill-region (point-min) (point-max))))) - -(defun org-jira-get-comment-id (comment) - (org-jira-find-value comment 'id)) - -(defun org-jira-get-comment-author (comment) - (org-jira-find-value comment 'author 'displayName)) - -(defun org-jira-isa-ignored-comment? (comment) - (member-ignore-case (oref comment author) org-jira-ignore-comment-user-list)) - -(defun org-jira-maybe-reverse-comments (comments) - (if org-jira-reverse-comment-order (reverse comments) comments)) - -(defun org-jira-extract-comments-from-data (data) - (->> (append data nil) - org-jira-sdk-create-comments-from-data-list - org-jira-maybe-reverse-comments - (cl-remove-if #'org-jira-isa-ignored-comment?))) - -(defun org-jira--render-comment (Issue Comment) - (with-slots (issue-id) Issue - (with-slots (comment-id author headline created updated body) Comment - (org-jira-log (format "Rendering a comment: %s" body)) - (ensure-on-issue-Issue Issue - (setq p (org-find-entry-with-id comment-id)) - (when (and p (>= p (point-min)) - (<= p (point-max))) - (goto-char p) - (org-narrow-to-subtree) - (delete-region (point-min) (point-max))) - (goto-char (point-max)) - (unless (looking-at "^") - (insert "\n")) - (insert "*** ") - (org-jira-insert headline "\n") - (org-narrow-to-subtree) - (org-jira-entry-put (point) "ID" comment-id) - (org-jira-entry-put (point) "created" created) - (unless (string= created updated) - (org-jira-entry-put (point) "updated" updated)) - (goto-char (point-max)) - ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup - (org-jira-insert (replace-regexp-in-string "^" " " (or body ""))))))) - -(defun org-jira-update-comments-for-issue (Issue) - "Update the comments for the specified ISSUE issue." - (with-slots (issue-id) Issue - (jiralib-get-comments - issue-id - (org-jira-with-callback - (org-jira-log "In the callback for org-jira-update-comments-for-issue.") - (--> - (org-jira-find-value cb-data 'comments) - (org-jira-extract-comments-from-data it) - (mapc (lambda (Comment) (org-jira--render-comment Issue Comment)) it)))))) - -(defun org-jira-update-comments-for-current-issue () - "Update comments for the current issue." - (org-jira-log "About to update comments for current issue.") - (let ((Issue (make-instance 'org-jira-sdk-issue - :issue-id (org-jira-get-from-org 'issue 'key) - :filename (org-jira-filename)))) - (-> Issue org-jira-update-comments-for-issue))) - -(defun org-jira-delete-subtree () - "Derived from org-cut-subtree. - -Like that function, without mangling the user's clipboard for the -purpose of wiping an old subtree." - (let (beg end folded (beg0 (point))) - (org-back-to-heading t) ; take what is really there - (setq beg (point)) - (skip-chars-forward " \t\r\n") - (save-match-data - (save-excursion (outline-end-of-heading) - (setq folded (org-invisible-p)) - (org-end-of-subtree t t))) - ;; Include the end of an inlinetask - (when (and (featurep 'org-inlinetask) - (looking-at-p (concat (org-inlinetask-outline-regexp) - "END[ \t]*$"))) - (end-of-line)) - (setq end (point)) - (goto-char beg0) - (when (> end beg) - (setq org-subtree-clip-folded folded) - (org-save-markers-in-region beg end) - (delete-region beg end)))) - -(defun org-jira-update-attachments-for-current-issue () - "Update the attachments for the current issue." - (when jiralib-use-restapi - (lexical-let ((issue-id (org-jira-get-from-org 'issue 'key))) - ;; Run the call - (jiralib-get-attachments - issue-id - (save-excursion - (cl-function - (lambda (&key data &allow-other-keys) - ;; First, make sure we're in the proper buffer (logic copied from org-jira-get-issues. - (let* ((proj-key (replace-regexp-in-string "-.*" "" issue-id)) - (project-file (org-jira--get-project-file-name proj-key)) - (project-buffer (or (find-buffer-visiting project-file) - (find-file project-file)))) - (with-current-buffer project-buffer - ;; delete old attachment node - (ensure-on-issue - (if (org-goto-first-child) - (while (org-goto-sibling) - (forward-thing 'whitespace) - (when (looking-at "Attachments:") - (org-jira-delete-subtree))))) - (let ((attachments (org-jira-find-value data 'fields 'attachment))) - (when (not (zerop (length attachments))) - (ensure-on-issue - (if (org-goto-first-child) - (progn - (while (org-goto-sibling)) - (org-insert-heading-after-current)) - (org-insert-subheading nil)) - - (insert "Attachments:") - (mapc - (lambda (attachment) - (let ((attachment-id (org-jira-get-comment-id attachment)) - (author (org-jira-get-comment-author attachment)) - (created (org-jira-transform-time-format - (org-jira-find-value attachment 'created))) - (size (org-jira-find-value attachment 'size)) - (mimeType (org-jira-find-value attachment 'mimeType)) - (content (org-jira-find-value attachment 'content)) - (filename (org-jira-find-value attachment 'filename))) - (if (looking-back "Attachments:") - (org-insert-subheading nil) - (org-insert-heading-respect-content)) - (insert "[[" content "][" filename "]]") - (org-narrow-to-subtree) - (org-jira-entry-put (point) "ID" attachment-id) - (org-jira-entry-put (point) "Author" author) - (org-jira-entry-put (point) "Name" filename) - (org-jira-entry-put (point) "Created" created) - (org-jira-entry-put (point) "Size" (ls-lisp-format-file-size size t)) - (org-jira-entry-put (point) "Content" content) - (widen))) - attachments))))))))))))) - -(defun org-jira-sort-org-clocks (clocks) - "Given a CLOCKS list, sort it by start date descending." - ;; Expects data such as this: - - ;; ((\"2017-02-26 Sun 00:08\" \"2017-02-26 Sun 01:08\" \"Hi\" \"10101\") - ;; (\"2017-03-16 Thu 22:25\" \"2017-03-16 Thu 22:57\" \"Test\" \"10200\")) - (sort clocks - (lambda (a b) - (> (time-to-seconds (date-to-time (car a))) - (time-to-seconds (date-to-time (car b))))))) - -(defun org-jira-update-worklogs-for-current-issue () - "Update the worklogs for the current issue." - (let ((issue-id (org-jira-get-from-org 'issue 'key)) - (filename (org-jira-filename))) - (org-jira-update-worklogs-for-issue issue-id filename))) - -(defun org-jira-update-worklogs-for-issue (issue-id filename) - "Update the worklogs for the current ISSUE-ID located in FILENAME." - (org-jira-log (format "org-jira-update-worklogs-for-issue id: %s filename: %s" - issue-id filename)) - ;; Run the call - (jiralib-get-worklogs - issue-id - (org-jira-with-callback - (ensure-on-issue-id-with-filename issue-id filename - (let ((worklogs (org-jira-find-value cb-data 'worklogs))) - (org-jira-log (format "org-jira-update-worklogs-for-issue cb id: %s fn: %s" - issue-id filename)) - (org-jira-logbook-reset issue-id filename - (org-jira-sort-org-clocks (org-jira-worklogs-to-org-clocks - (jiralib-worklog-import--filter-apply worklogs))))))))) - -;;;###autoload -(defun org-jira-unassign-issue () - "Update an issue to be unassigned." - (interactive) - (let ((issue-id (org-jira-parse-issue-id)) - (filename (org-jira-parse-issue-filename))) - (org-jira-update-issue-details issue-id filename :assignee nil))) - -;;;###autoload -(defun org-jira-set-issue-reporter () - "Update an issue's reporter interactively." - (interactive) - (let ((issue-id (org-jira-parse-issue-id)) - (filename (org-jira-parse-issue-filename))) - (if issue-id - (let* ((project (replace-regexp-in-string "-[0-9]+" "" issue-id)) - (jira-users (org-jira-get-reporter-candidates project)) ;; TODO, probably a better option than org-jira-get-assignable-users here - (user (completing-read - "Reporter: " - (append (mapcar 'car jira-users) - (mapcar 'cdr jira-users)))) - (reporter (or - (cdr (assoc user jira-users)) - (cdr (rassoc user jira-users))))) - (when (null reporter) - (error "No reporter found, this should probably never happen.")) - (org-jira-update-issue-details issue-id filename :reporter (jiralib-get-user-account-id project reporter))) - (error "Not on an issue")))) - -;;;###autoload -(defun org-jira-assign-issue () - "Update an issue with interactive re-assignment." - (interactive) - (let ((issue-id (org-jira-parse-issue-id)) - (filename (org-jira-parse-issue-filename))) - (if issue-id - (let* ((project (replace-regexp-in-string "-[0-9]+" "" issue-id)) - (jira-users (org-jira-get-assignable-users project)) - (user (completing-read - "Assignee: " - (append (mapcar 'car jira-users) - (mapcar 'cdr jira-users)))) - (assignee (or - (cdr (assoc user jira-users)) - (cdr (rassoc user jira-users))))) - (when (null assignee) - (error "No assignee found, use org-jira-unassign-issue to make the issue unassigned")) - (org-jira-update-issue-details issue-id filename :assignee (jiralib-get-user-account-id project assignee))) - (error "Not on an issue")))) - -;;;###autoload -(defun org-jira-update-issue () - "Update an issue." - (interactive) - (let ((issue-id (org-jira-parse-issue-id)) - (filename (org-jira-parse-issue-filename))) - (if issue-id - (org-jira-update-issue-details issue-id filename) - (error "Not on an issue")))) - -;;;###autoload -(defun org-jira-todo-to-jira () - "Convert an ordinary todo item to a jira ticket." - (interactive) - (ensure-on-todo - (when (org-jira-parse-issue-id) - (error "Already on jira ticket")) - (save-excursion (org-jira-create-issue - (org-jira-read-project) - (org-jira-read-issue-type) - (org-get-heading t t) - (org-get-entry))) - (delete-region (point-min) (point-max)))) - -;;;###autoload -(defun org-jira-get-subtasks () - "Get subtasks for the current issue." - (interactive) - (ensure-on-issue - (org-jira-get-issues-headonly (jiralib-do-jql-search (format "parent = %s" (org-jira-parse-issue-id)))))) - -(defvar org-jira-project-read-history nil) -(defvar org-jira-boards-read-history nil) -(defvar org-jira-sprints-read-history nil) -(defvar org-jira-components-read-history nil) -(defvar org-jira-priority-read-history nil) -(defvar org-jira-type-read-history nil) - -(defun org-jira-read-project () - "Read project name." - (completing-read - "Project: " - (jiralib-make-list (jiralib-get-projects) 'key) - nil - t - nil - 'org-jira-project-read-history - (car org-jira-project-read-history))) - -(defun org-jira-read-board () - "Read board name. Returns cons pair (name . integer-id)" - (let* ((boards-alist - (jiralib-make-assoc-list (jiralib-get-boards) 'name 'id)) - (board-name - (completing-read "Boards: " boards-alist - nil t nil - 'org-jira-boards-read-history - (car org-jira-boards-read-history)))) - (assoc board-name boards-alist))) - -(defun org-jira-read-sprint (board) - "Read sprint name. Returns cons pair (name . integer-id)" - (let* ((sprints-alist - (jiralib-make-assoc-list (append (alist-get 'values (jiralib-get-board-sprints board)) nil) 'name 'id)) - (sprint-name - (completing-read "Sprints: " sprints-alist - nil t nil - 'org-jira-sprints-read-history - (car org-jira-sprints-read-history)))) - (assoc sprint-name sprints-alist))) - -(defun org-jira-read-component (project) - "Read the components options for PROJECT such as EX." - (completing-read - "Components (choose Done to stop): " - (append '("Done") (mapcar 'cdr (jiralib-get-components project))) - nil - t - nil - 'org-jira-components-read-history - "Done")) - -;; TODO: Finish this feature - integrate into org-jira-create-issue -(defun org-jira-read-components (project) - "Types: string PROJECT : string (csv of components). - -Get all the components for the PROJECT such as EX, -that should be bound to an issue." - (let (components component) - (while (not (equal "Done" component)) - (setq component (org-jira-read-component project)) - (unless (equal "Done" component) - (push component components))) - components)) - -(defun org-jira-read-priority () - "Read priority name." - (completing-read - "Priority: " - (mapcar 'cdr (jiralib-get-priorities)) - nil - t - nil - 'org-jira-priority-read-history - (car org-jira-priority-read-history))) - -(defun org-jira-read-issue-type (&optional project) - "Read issue type name. PROJECT is the optional project key." - (let* ((issue-types - (mapcar 'cdr (if project - (jiralib-get-issue-types-by-project project) - (jiralib-get-issue-types)))) - (initial-input (when (member (car org-jira-type-read-history) issue-types) - org-jira-type-read-history))) - - ;; TODO: The completing-read calls as such are all over the place, and always tend - ;; to follow this exact same call structure - we should abstract to a single fn - ;; that will allow calling with fewer or keyword args - (completing-read - "Type: " ; PROMPT - issue-types ; COLLECTION - nil ; PREDICATE - t ; REQUIRE-MATCH - nil ; INITIAL-INPUT - 'initial-input ; HIST - (car initial-input)))) ; DEF - -(defun org-jira-read-subtask-type () - "Read issue type name." - (completing-read - "Type: " - (mapcar 'cdr (jiralib-get-subtask-types)) - nil - t - nil - 'org-jira-type-read-history - (car org-jira-type-read-history))) - -(defun org-jira-get-issue-struct (project type summary description &optional parent-id) - "Create an issue struct for PROJECT, of TYPE, with SUMMARY and DESCRIPTION." - (if (or (equal project "") - (equal type "") - (equal summary "")) - (error "Must provide all information!")) - (let* ((project-components (jiralib-get-components project)) - (jira-users (org-jira-get-assignable-users project)) - (user (completing-read "Assignee: " (mapcar 'car jira-users))) - (priority (car (rassoc (org-jira-read-priority) (jiralib-get-priorities)))) - (ticket-struct - `((fields - (project (key . ,project)) - (parent (key . ,parent-id)) - (issuetype (id . ,(car (rassoc type (if (and (boundp 'parent-id) parent-id) - (jiralib-get-subtask-types) - (jiralib-get-issue-types-by-project project)))))) - (summary . ,(format "%s%s" summary - (if (and (boundp 'parent-id) parent-id) - (format " (subtask of [jira:%s])" parent-id) - ""))) - (description . ,description) - (priority (id . ,priority)) - ;; accountId should be nil if Unassigned, not the key slot. - (assignee (accountId . ,(or (cdr (assoc user jira-users)) nil))))))) - ticket-struct)) - -;;;###autoload -(defun org-jira-create-issue (project type summary description) - "Create an issue in PROJECT, of type TYPE, with given SUMMARY and DESCRIPTION." - (interactive - (let* ((project (org-jira-read-project)) - (type (org-jira-read-issue-type project)) - (summary (read-string "Summary: ")) - (description (read-string "Description: "))) - (list project type summary description))) - (if (or (equal project "") - (equal type "") - (equal summary "")) - (error "Must provide all information!")) - (let* ((parent-id nil) - (ticket-struct (org-jira-get-issue-struct project type summary description))) - (org-jira-get-issues (list (jiralib-create-issue ticket-struct))))) - -;;;###autoload -(defun org-jira-create-subtask (project type summary description) - "Create a subtask issue for PROJECT, of TYPE, with SUMMARY and DESCRIPTION." - (interactive (ensure-on-issue (list (org-jira-read-project) - (org-jira-read-subtask-type) - (read-string "Summary: ") - (read-string "Description: ")))) - (if (or (equal project "") - (equal type "") - (equal summary "")) - (error "Must provide all information!")) - (let* ((parent-id (org-jira-parse-issue-id)) - (ticket-struct (org-jira-get-issue-struct project type summary description parent-id))) - (org-jira-get-issues (list (jiralib-create-subtask ticket-struct))))) - -(defun org-jira-get-issue-val-from-org (key) - "Return the requested value by KEY from the current issue." - ;; There is some odd issue when not using any let-scoping, where myself - ;; and an array of users are hitting a snag circa 2023-03-01 time frame - ;; in which the setq portion of a when clause is being hit even when it - ;; evaluates to false - the bug only manifests on a first launch of Emacs - it - ;; doesn't occur when re-evaluating this function. However, wrapping it "fixes" - ;; the issue. - ;; - ;; The first link has the most troubleshooting/diagnosis around the particulars of - ;; this bug. - ;; - ;; See: https://github.com/ahungry/org-jira/issues/319 - ;; See: https://github.com/ahungry/org-jira/issues/296 - ;; See: https://github.com/ahungry/org-jira/issues/316 - (lexical-let ((my-key key)) - (ensure-on-issue - (cond ((eq my-key 'description) - (org-goto-first-child) - (forward-thing 'whitespace) - (if (looking-at "description: ") - (org-trim (org-get-entry)) - (error "Can not find description field for this issue"))) - - ((eq my-key 'summary) - (ensure-on-issue - (org-get-heading t t))) - - ;; org returns a time tuple, we need to convert it - ((eq my-key 'deadline) - (let ((encoded-time (org-get-deadline-time (point)))) - (when encoded-time - (cl-reduce (lambda (carry segment) - (format "%s-%s" carry segment)) - (reverse (cl-subseq (decode-time encoded-time) 3 6)))))) - - ;; default case, just grab the value in the properties block - (t - (when (symbolp my-key) - (setq my-key (symbol-name my-key))) - - (setq my-key (or (assoc-default my-key org-jira-property-overrides) - my-key)) - - ;; This is the "impossible" to hit setq that somehow gets hit without the let - ;; wrapper around the function input args. - (when (string= my-key "key") - (setq my-key "ID")) - - ;; The variable `org-special-properties' will mess this up - ;; if our search, such as 'priority' is within there, so - ;; don't bother with it for this (since we only ever care - ;; about the local properties, not any hierarchal or special - ;; ones). - (let ((org-special-properties nil)) - (or (org-entry-get (point) my-key t) - ""))))))) - -(defun org-jira-read-action (actions) - "Read issue workflow progress ACTIONS." - (let ((action (completing-read - "Action: " - (mapcar 'cdr actions) - nil - t - nil))) - (or - (car (rassoc action actions)) - (user-error "You specified an empty action, the valid actions are: %s" (mapcar 'cdr actions))))) - -(defvar org-jira-fields-history nil) -(defun org-jira-read-field (fields) - "Read (custom) FIELDS for workflow progress." - (let ((field-desc (completing-read - "More fields to set: " - (cons "Thanks, no more fields are *required*." (mapcar 'org-jira-decode (mapcar 'cdr fields))) - nil - t - nil - 'org-jira-fields-history)) - field-name) - (setq field-name (car (rassoc field-desc fields))) - (if field-name - (intern field-name) - field-name))) - - -(defvar org-jira-rest-fields nil - "Extra fields are held here for usage between two endpoints. -Used in org-jira-read-resolution and org-jira-progress-issue calls.") - -(defvar org-jira-resolution-history nil) -(defun org-jira-read-resolution () - "Read issue workflow progress resolution." - (if (not jiralib-use-restapi) - (let ((resolution (completing-read - "Resolution: " - (mapcar 'cdr (jiralib-get-resolutions)) - nil - t - nil - 'org-jira-resolution-history - (car org-jira-resolution-history)))) - (car (rassoc resolution (jiralib-get-resolutions)))) - (let* ((resolutions (org-jira-find-value org-jira-rest-fields 'resolution 'allowedValues)) - (resolution-name (completing-read - "Resolution: " - (mapcar (lambda (resolution) - (org-jira-find-value resolution 'name)) - resolutions)))) - (cons 'name resolution-name)))) - -(defun org-jira-refresh-issues-in-buffer-loose () - "Iterates over all level 1-2 headings in current buffer, refreshing on issue :ID:. -It differs with `org-jira-refresh-issues-in-buffer' in that it accepts the current buffer -and its corresponding filename, regardless of whether it has been previously registered -as an org-jira project file or not." - (interactive) - (save-excursion - (save-restriction - (widen) - (let* ((org-ids (org-map-entries 'org-id-get "LEVEL=1|LEVEL=2")) - (org-ids (delq nil org-ids)) - (file-name (file-name-sans-extension buffer-file-name))) - (mapcar (lambda (org-id) (org-jira--refresh-issue org-id file-name)) - org-ids))))) - -(defun org-jira-refresh-issues-in-buffer () - "Iterate across all level 1-2 headings in current buffer, refreshing on issue :ID:. -Where issue-id will be something such as \"EX-22\"." - (interactive) - (save-excursion - (save-restriction - (widen) - (let* ((org-ids (org-map-entries 'org-id-get "LEVEL=1|LEVEL=2")) - (org-ids (delq nil org-ids))) - ;; It's possible we could be on a non-org-jira headline, but - ;; that should be an exceptional case and not necessitating a - ;; fix atm. - (mapcar 'org-jira--refresh-issue-by-id org-ids))))) - -;;;###autoload -(defun org-jira-refresh-issue () - "Refresh current issue from jira to org." - (interactive) - (ensure-on-issue - (org-jira--refresh-issue (org-jira-id) (org-jira-filename)))) - -(defun org-jira--refresh-issue (issue-id &optional filename) - "Refresh issue from jira to org using ISSUE-ID." - (unless filename (setq filename (replace-regexp-in-string "-[0-9]+" "" issue-id))) - (jiralib-get-issue - issue-id - (org-jira-with-callback - (org-jira-log (format "Received refresh issue data for id: %s in file: %s" issue-id filename)) - (--> cb-data - list - (org-jira-sdk-create-issues-from-data-list-with-filename filename it) - org-jira--render-issues-from-issue-list)))) - -(defun org-jira--refresh-issue-by-id (issue-id) - "Refresh issue from jira to org using ISSUE-ID." - (ensure-on-issue-id issue-id - (org-jira--refresh-issue issue-id))) - -(defvar org-jira-fields-values-history nil) -;;;###autoload -(defun org-jira-progress-issue () - "Progress issue workflow." - (interactive) - (ensure-on-issue - (let* ((issue-id (org-jira-id)) - (actions (jiralib-get-available-actions - issue-id - (org-jira-get-issue-val-from-org 'status))) - (action (org-jira-read-action actions)) - (fields (jiralib-get-fields-for-action issue-id action)) - (org-jira-rest-fields fields) - (field-key) - (custom-fields-collector nil) - (custom-fields - (progn - ;; delete those elements in fields, which have - ;; already been set in custom-fields-collector - (while fields - (setq fields - (cl-remove-if - (lambda (strstr) - (cl-member-if (lambda (symstr) - (string= (car strstr) (symbol-name (car symstr)))) - custom-fields-collector)) - fields)) - (setq field-key (org-jira-read-field fields)) - (if (not field-key) - (setq fields nil) - (setq custom-fields-collector - (cons - (funcall (if jiralib-use-restapi - #'list - #'cons) - field-key - (if (eq field-key 'resolution) - (org-jira-read-resolution) - (let ((field-value (completing-read - (format "Please enter %s's value: " - (cdr (assoc (symbol-name field-key) fields))) - org-jira-fields-values-history - nil - nil - nil - 'org-jira-fields-values-history))) - (if jiralib-use-restapi - (cons 'name field-value) - field-value)))) - custom-fields-collector)))) - custom-fields-collector))) - (jiralib-progress-workflow-action - issue-id - action - custom-fields - (cl-function - (lambda (&key data &allow-other-keys) - (org-jira-refresh-issue))))))) - -(defun org-jira-progress-next-action (actions current-status) - "Grab the user defined 'next' action from ACTIONS, given CURRENT-STATUS." - (let* ((next-action-name (cdr (assoc current-status org-jira-progress-issue-flow))) - (next-action-id (caar (cl-remove-if-not - (lambda (action) - (equal action next-action-name)) actions :key #'cdr)))) - next-action-id)) - -;;;###autoload -(defun org-jira-progress-issue-next () - "Progress issue workflow." - (interactive) - (ensure-on-issue - (let* ((issue-id (org-jira-id)) - (filename (org-jira-filename)) - (actions (jiralib-get-available-actions - issue-id - (org-jira-get-issue-val-from-org 'status))) - (action (org-jira-progress-next-action actions (org-jira-get-issue-val-from-org 'status))) - (fields (jiralib-get-fields-for-action issue-id action)) - (org-jira-rest-fields fields) - (field-key) - (custom-fields-collector nil) - (custom-fields - (progn - ;; delete those elements in fields, which have - ;; already been set in custom-fields-collector - (while fields - (setq fields - (cl-remove-if - (lambda (strstr) - (cl-member-if (lambda (symstr) - (string= (car strstr) (symbol-name (car symstr)))) - custom-fields-collector)) - fields)) - (setq field-key (org-jira-read-field fields)) - (if (not field-key) - (setq fields nil) - (setq custom-fields-collector - (cons - (funcall (if jiralib-use-restapi - #'list - #'cons) - field-key - (if (eq field-key 'resolution) - (org-jira-read-resolution) - (let ((field-value (completing-read - (format "Please enter %s's value: " - (cdr (assoc (symbol-name field-key) fields))) - org-jira-fields-values-history - nil - nil - nil - 'org-jira-fields-values-history))) - (if jiralib-use-restapi - (cons 'name field-value) - field-value)))) - custom-fields-collector)))) - custom-fields-collector))) - (if action - (jiralib-progress-workflow-action - issue-id - action - custom-fields - (org-jira-with-callback - (ensure-on-issue-id-with-filename issue-id filename - (org-jira-refresh-issue)))) - (error "No action defined for that step!"))))) - -(defun org-jira-get-id-name-alist (name ids-to-names) - "Find the id corresponding to NAME in IDS-TO-NAMES and return an alist with id and name as keys." - (let ((id (car (rassoc name ids-to-names)))) - `((id . ,id) - (name . ,name)))) - -(defun org-jira-build-components-list (project-components org-issue-components) - "Given PROJECT-COMPONENTS, attempt to build a list. - -If the PROJECT-COMPONENTS are nil, this should return: - - (list components []), which will translate into the JSON: - - {\"components\": []} - -otherwise it should return: - - (list components (list (cons id comp-id) (cons name item-name))), - - which will translate into the JSON: - -{\"components\": [{\"id\": \"comp-id\", \"name\": \"item\"}]}" - (if (not project-components) (vector) ;; Return a blank array for JSON - (apply 'list - (cl-mapcan - (lambda (item) - (let ((comp-id (car (rassoc item project-components)))) - (if comp-id - `(((id . ,comp-id) - (name . ,item))) - nil))) - (split-string org-issue-components ",\\s *"))))) - -(defun org-jira-strip-priority-tags (s) - "Given string S, remove any priority tags in the brackets." - (->> s (replace-regexp-in-string "\\[#.*?\\]" "") org-trim)) - -(defun org-jira-update-issue-details (issue-id filename &rest rest) - "Update the details of issue ISSUE-ID in FILENAME. REST will contain optional input." - (ensure-on-issue-id-with-filename issue-id filename - ;; Set up a bunch of values from the org content - (let* ((org-issue-components (org-jira-get-issue-val-from-org 'components)) - (org-issue-labels (org-jira-get-issue-val-from-org 'labels)) - (org-issue-description (org-trim (org-jira-get-issue-val-from-org 'description))) - (org-issue-priority (org-jira-get-issue-val-from-org 'priority)) - (org-issue-type (org-jira-get-issue-val-from-org 'type)) - (org-issue-type-id (org-jira-get-issue-val-from-org 'type-id)) - (org-issue-assignee (cl-getf rest :assignee (org-jira-get-issue-val-from-org 'assignee))) - (org-issue-reporter (cl-getf rest :reporter (org-jira-get-issue-val-from-org 'reporter))) - (project (replace-regexp-in-string "-[0-9]+" "" issue-id)) - (project-components (jiralib-get-components project))) - - ;; Lets fire off a worklog update async with the main issue - ;; update, why not? This is better to fire first, because it - ;; doesn't auto-refresh any areas, while the end of the main - ;; update does a callback that reloads the worklog entries (so, - ;; we hope that won't occur until after this successfully syncs - ;; up). Only do this sync if the user defcustom defines it as such. - (when org-jira-worklog-sync-p - (org-jira-update-worklogs-from-org-clocks)) - - ;; Send the update to jira - (let ((update-fields - (list (cons - 'components - (or (org-jira-build-components-list - project-components - org-issue-components) [])) - (cons 'labels (split-string org-issue-labels ",\\s *")) - (cons 'priority (org-jira-get-id-name-alist org-issue-priority - (jiralib-get-priorities))) - (cons 'description org-issue-description) - (cons 'assignee (list (cons 'id (jiralib-get-user-account-id project org-issue-assignee)))) - (cons 'reporter (list (cons 'id (jiralib-get-user-account-id project org-issue-reporter)))) - (cons 'summary (org-jira-strip-priority-tags (org-jira-get-issue-val-from-org 'summary))) - (cons 'issuetype `((id . ,org-issue-type-id) - (name . ,org-issue-type)))))) - - - ;; If we enable duedate sync and we have a deadline present - (when (and org-jira-deadline-duedate-sync-p - (org-jira-get-issue-val-from-org 'deadline)) - (setq update-fields - (append update-fields - (list (cons 'duedate (org-jira-get-issue-val-from-org 'deadline)))))) - - ;; TODO: We need some way to handle things like assignee setting - ;; and refreshing the proper issue in the proper buffer/filename. - (jiralib-update-issue - issue-id - update-fields - ;; This callback occurs on success - (org-jira-with-callback - (message (format "Issue '%s' updated!" issue-id)) - (jiralib-get-issue - issue-id - (org-jira-with-callback - (org-jira-log "Update get issue for refresh callback hit.") - (-> cb-data list org-jira-get-issues)))) - )) - ))) - - - -(defun org-jira-parse-issue-id () - "Get issue id from org text." - (save-excursion - (let ((continue t) - issue-id) - (while continue - (when (string-match (jiralib-get-issue-regexp) - (or (setq issue-id (org-entry-get (point) "ID")) - "")) - (setq continue nil)) - (unless (and continue (org-up-heading-safe)) - (setq continue nil))) - issue-id))) - -(defun org-jira-parse-issue-filename () - "Get filename from org text." - (save-excursion - (let ((continue t) - filename) - (while continue - (when (setq filename (org-entry-get (point) "filename")) - (setq continue nil)) - (unless (and continue (org-up-heading-safe)) - (setq continue nil))) - filename))) - -(defun org-jira-get-from-org (type entry) - "Get an org property from the current item. - -TYPE is the type to of the current item, and can be 'issue, or 'comment. - -ENTRY will vary, and is the name of the property to return. If -it is a symbol, it will be converted to string." - (when (symbolp entry) - (setq entry (symbol-name entry))) - (cond - ((eq type 'issue) - (org-jira-get-issue-val-from-org entry)) - ((eq type 'comment) - (org-jira-get-comment-val-from-org entry)) - ((eq type 'worklog) - (org-jira-get-worklog-val-from-org entry)) - (t (error "Unknown type %s" type)))) - -(defun org-jira-get-comment-val-from-org (entry) - "Get the JIRA issue field value ENTRY of the current comment item." - (ensure-on-comment - (when (symbolp entry) - (setq entry (symbol-name entry))) - (when (string= entry "id") - (setq entry "ID")) - (org-entry-get (point) entry))) - -(defun org-jira-get-worklog-val-from-org (entry) - "Get the JIRA issue field value ENTRY of the current worklog item." - (ensure-on-worklog - (when (symbolp entry) - (setq entry (symbol-name entry))) - (when (string= entry "id") - (setq entry "ID")) - (org-entry-get (point) entry))) - -(defun org-jira-get-comment-body (&optional comment-id) - "Get the comment body of the comment with id COMMENT-ID." - (ensure-on-comment - (goto-char (point-min)) - ;; so that search for :END: won't fail - (org-jira-entry-put (point) "ID" comment-id) - (search-forward ":END:" nil 1 1) - (forward-line) - (org-trim (buffer-substring-no-properties (point) (point-max))))) - -(defun org-jira-get-worklog-comment (&optional worklog-id) - "Get the worklog comment of the worklog with id WORKLOG-ID." - (ensure-on-worklog - (goto-char (point-min)) - ;; so that search for :END: won't fail - (org-jira-entry-put (point) "ID" worklog-id) - (search-forward ":END:" nil 1 1) - (forward-line) - (org-trim (buffer-substring-no-properties (point) (point-max))))) - -(defun org-jira-id () - "Get the ID entry for the current heading." - (org-entry-get (point) "ID")) - -(defun org-jira-filename () - "Get the ID entry for the current heading." - (org-jira-get-from-org 'issue 'filename)) - -;;;###autoload -(defun org-jira-browse-issue () - "Open the current issue in external browser." - (interactive) - (ensure-on-issue - (browse-url (concat (replace-regexp-in-string "/*$" "" jiralib-url) "/browse/" (org-jira-id))))) - -(defun org-jira-url-copy-file (url newname) - "Similar to url-copy-file but async." - (lexical-let ((newname newname)) - (url-retrieve - url - (lambda (status) - (let ((buffer (current-buffer)) - (handle nil) - (filename (if (and (file-exists-p newname) - org-jira-download-ask-override) - (read-string "File already exists, select new name or press ENTER to override: " newname) - newname))) - (if (not buffer) - (error "Opening input file: No such file or directory, %s" url)) - (with-current-buffer buffer - (setq handle (mm-dissect-buffer t))) - (mm-save-part-to-file handle filename) - (kill-buffer buffer) - (mm-destroy-parts handle)))))) - -;;;###autoload -(defun org-jira-download-attachment () - "Download the attachment under cursor." - (interactive) - (when jiralib-use-restapi - (save-excursion - (org-up-heading-safe) - (org-back-to-heading) - (forward-thing 'whitespace) - (unless (looking-at "Attachments:") - (error "Not on a attachment region!"))) - (let ((filename (org-entry-get (point) "Name")) - (url (org-entry-get (point) "Content")) - (url-request-extra-headers `(,jiralib-token))) - (org-jira-url-copy-file - url - (concat (file-name-as-directory org-jira-download-dir) filename))))) - -;;;###autoload -(defun org-jira-get-issues-from-filter (filter) - "Get issues from the server-side stored filter named FILTER. - -Provide this command in case some users are not able to use -client side jql (maybe because of JIRA server version?)." - (interactive - (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters))))) - (org-jira-get-issues (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters)))))) - -;;;###autoload -(defun org-jira-get-issues-from-filter-headonly (filter) - "Get issues *head only* from saved filter named FILTER. -See `org-jira-get-issues-from-filter'." - (interactive - (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters))))) - (org-jira-get-issues-headonly (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters)))))) - -(org-add-link-type "jira" 'org-jira-open) - -;; This was only added in org 9.0, not sure all org users will have -;; that version, so keep the deprecated one from above for now. - -;;(org-link-set-parameters "jira" ((:follow . 'org-jira-open))) - -(defun org-jira-open (path) - "Open a Jira Link from PATH." - (org-jira-get-issue path)) - -;;;###autoload -(defun org-jira-get-issues-by-board () - "Get list of ISSUES from agile board." - (interactive) - (let* ((board (org-jira-read-board)) - (board-id (cdr board))) - (jiralib-get-board-issues board-id - :callback org-jira-get-issue-list-callback - :limit (org-jira-get-board-limit board-id) - :query-params (org-jira--make-jql-queryparams board-id)))) - -;;;###autoload -(defun org-jira-get-issues-by-sprint () - "Get list of ISSUES from sprint." - (interactive) - (let* ((board (org-jira-read-board)) - (board-id (cdr board)) - (sprint (org-jira-read-sprint board-id)) - (sprint-id (cdr sprint))) - (jiralib-get-sprint-issues sprint-id - :callback org-jira-get-issue-list-callback - :limit (org-jira-get-board-limit board-id) - :query-params (org-jira--make-jql-queryparams board-id)))) - -(defun org-jira-get-board-limit (id) - "Get limit for number of retrieved issues for a board -id - integer board id" - (let ((board (org-jira--get-board-from-buffer id))) - (if (and board (slot-boundp board 'limit)) - (oref board limit) - org-jira-boards-default-limit))) - -(defun org-jira--make-jql-queryparams (board-id) - "make GET query parameters for jql, returns nil if JQL query is not set" - (let* ((board (org-jira--get-board-from-buffer board-id)) - (jql (if (and board (slot-boundp board 'jql)) - (oref board jql)))) - (if (and jql (not (string-blank-p jql))) `((jql ,jql))))) - -;;;###autoload -(defun org-jira-get-issues-by-board-headonly () - "Get list of ISSUES from agile board, head only." - (interactive) - (let* ((board (org-jira-read-board)) - (board-id (cdr board))) - (org-jira-get-issues-headonly - (jiralib-get-board-issues board-id - :limit (org-jira-get-board-limit board-id) - :query-params (org-jira--make-jql-queryparams board-id))))) - - -(defun org-jira--render-boards-from-list (boards) - "Add the boards from list into the org file. - -boards - list of `org-jira-sdk-board' records." - (mapc 'org-jira--render-board boards)) - - -(defun org-jira--render-board (board) - "Render single board" - ;;(org-jira-sdk-dump board) - (with-slots (id name url board-type jql limit) board - (with-current-buffer (org-jira--get-boards-buffer) - (org-jira-maybe-activate-mode) - (org-jira-freeze-ui - (org-save-outline-visibility t - (save-restriction - (outline-show-all) - (widen) - (goto-char (point-min)) - (let* ((board-headline - (format "Board: [[%s][%s]]" url name)) - (headline-pos - (org-find-exact-headline-in-buffer board-headline (current-buffer) t)) - (entry-exists (and headline-pos (>= headline-pos (point-min)) (<= headline-pos (point-max)))) - (limit-value (if (slot-boundp board 'limit) (int-to-string limit) nil)) - (jql-value (if (slot-boundp board 'jql) jql nil))) - (if entry-exists - (progn - (goto-char headline-pos) - (org-narrow-to-subtree) - (end-of-line)) - (goto-char (point-max)) - (unless (looking-at "^") - (insert "\n")) - (insert "* ") - (org-jira-insert board-headline) - (org-narrow-to-subtree)) - (org-jira-entry-put (point) "name" name) - (org-jira-entry-put (point) "type" board-type) - (org-jira-entry-put (point) "url" url) - ;; do not overwrite existing user properties with empty values - (if (or (not entry-exists) limit-value) - (org-jira-entry-put (point) "limit" limit-value)) - (if (or (not entry-exists) jql-value) - (org-jira-entry-put (point) "JQL" jql-value )) - (org-jira-entry-put (point) "ID" id)))))))) - -(defun org-jira--get-boards-file () - (expand-file-name "boards-list.org" (org-jira--ensure-working-dir))) - -(defun org-jira--get-boards-buffer () - "Return buffer for list of agile boards. Create one if it does not exist." - (let* ((boards-file (org-jira--get-boards-file)) - (existing-buffer (find-buffer-visiting boards-file))) - (if existing-buffer - existing-buffer - (find-file-noselect boards-file)))) - -;;;###autoload -(defun org-jira-get-boards () - "Get list of boards and their properties." - (interactive) - (let* ((datalist (jiralib-get-boards)) - (boards (org-jira-sdk-create-boards-from-data-list datalist))) - (org-jira--render-boards-from-list boards)) - (switch-to-buffer (org-jira--get-boards-buffer))) - -(defun org-jira--get-board-from-buffer (id) - "Parse board record from org file." - (with-current-buffer (org-jira--get-boards-buffer) - (org-jira-freeze-ui - (let ((pos (org-find-property "ID" (int-to-string id)))) - (if pos - (progn - (goto-char pos) - (apply 'org-jira-sdk-board - (cl-reduce - #'(lambda (acc entry) - (let* ((pname (car entry)) - (pval (cdr entry)) - (pair (and pval - (not (string-empty-p pval)) - (cond - ((equal pname "ID") - (list :id pval)) - ((equal pname "URL") - (list :url pval)) - ((equal pname "TYPE") - (list :board-type pval)) - ((equal pname "NAME") - (list :name pval)) - ((equal pname "LIMIT") - (list :limit (string-to-number pval))) - ((equal pname "JQL") - (list :jql pval)) - (t nil))))) - (if pair (append pair acc) acc))) - (org-entry-properties) :initial-value ())))))))) - -(defun org-jira-get-org-keyword-from-status (status) - "Gets an 'org-mode' keyword corresponding to a given jira STATUS." - (if org-jira-use-status-as-todo - (upcase (replace-regexp-in-string " " "-" status)) - (let ((known-keyword (assoc status org-jira-jira-status-to-org-keyword-alist))) - (cond (known-keyword (cdr known-keyword)) - ((member (org-jira-decode status) org-jira-done-states) "DONE") - ("TODO"))))) - -(defun org-jira-get-org-priority-string (character) - "Return an org-priority-string based on CHARACTER and user settings." - (cond ((not character) "") - ((and org-jira-priority-to-org-priority-omit-default-priority - (eq character org-default-priority)) "") - (t (format "[#%c] " character)))) - -(defun org-jira-get-org-priority-cookie-from-issue (priority) - "Get the `org-mode' [#X] PRIORITY cookie." - (let ((character (cdr (assoc priority org-jira-priority-to-org-priority-alist)))) - (org-jira-get-org-priority-string character))) - -(provide 'org-jira) -;;; org-jira.el ends here diff --git a/elpa/org-jira-20240928.2340/jiralib.el b/elpa/org-jira-20240928.2340/jiralib.el new file mode 100644 index 00000000..23d4204d --- /dev/null +++ b/elpa/org-jira-20240928.2340/jiralib.el @@ -0,0 +1,1346 @@ +;;; jiralib.el -- Provide connectivity to JIRA SOAP/REST services. + +;; Copyright (C) 2016-2022 Matthew Carter +;; Copyright (C) 2011 Bao Haojun +;; original Copyright (C) 2009 Alex Harsanyi + +;; Also, used some code from jira.el, which use xml-rpc instead of soap. +;; Thus Copyright (C) for jira.el related code: +;; Brian Zwahr +;; Dave Benjamin + +;; Authors: +;; Matthew Carter +;; Bao Haojun +;; Alex Harsanyi + +;; Maintainer: Matthew Carter +;; Homepage: https://github.com/ahungry/org-jira + +;; This file is not part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see +;; or write to the Free Software +;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +;; 02110-1301, USA. + +;; Author: Alexandru Harsanyi (AlexHarsanyi@gmail.com) +;; Created: December, 2009 +;; Keywords: soap, web-services, jira +;; Homepage: http://code.google.com/p/emacs-soap-client + +;;; Commentary: + +;; This file provides a programmatic interface to JIRA. It provides access to +;; JIRA from other programs, but no user level functionality. + +;; Jira References: + +;; Primary reference (on current Jira, only REST is supported): +;; https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis + +;; Full API list reference: +;; https://docs.atlassian.com/jira/REST/cloud/ + +;; Legacy reference (unsupported and deprecated/unavailable): +;; http://confluence.atlassian.com/display/JIRA/Creating+a+SOAP+Client + +;; JavaDoc for the Jira SOAP service +;; http://docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html + +;;; News: + +;;;; Changes since 2.6.3: +;; - Add worklog import filter and control variable for external worklogs. +;; - Add the worklog related endpoint/calls. + +;;;; Changes since 2.1.0: +;; - Remove os_username / os_password manual http request as part of sign in process +;; This produces sysadmin level warnings on Jira when these are used under the latest Jira. +;; - Remove unused function jiralib-link-issue +;; - Bring version up to match org-jira version so they can share tag + +;;;; Changes since 2.0.0: +;; - Allow issue type query by project + +;;;; Changes since 0.0.0: +;; - Converted many calls to async +;; - Converted many calls to make use of caching + +;;; Code: + +(eval-when-compile (require 'cl)) +(require 'cl-seq) +(require 'soap-client) +(require 'request) +(require 'json) +(require 'url-parse) +(require 'url-util) + +(defconst jiralib-version "3.0.0" + "Current version of jiralib.el.") + +(defgroup jiralib nil + "Jiralib customization group." + :group 'applications) + +(defgroup jiralib-faces nil + "Faces for displaying Jiralib information." + :group 'jiralib) + +(defcustom jiralib-use-restapi t + "Use restapi instead of soap." + :group 'jiralib + :type 'boolean + :initialize 'custom-initialize-set) + +(defcustom jiralib-coding-system 'utf-8 + "Use custom coding system for Jiralib." + :group 'jiralib) + +(defcustom jiralib-host "" + "User customizable host name of the Jiralib server. + +This will be used with USERNAME to compute password from +.authinfo file. Will be calculated from jiralib-url if not set." + :group 'jiralib + :type 'string + :initialize 'custom-initialize-set) + +(defcustom jiralib-user "" + "User customizable user name of the Jiralib server. + +This will be used with USERNAME to compute password from +.authinfo file. Will be calculated from jiralib-url if not set." + :group 'jiralib + :type 'string + :initialize 'custom-initialize-set) + +(defface jiralib-issue-info-face + '((t (:foreground "black" :background "yellow4"))) + "Base face for issue information." + :group 'jiralib-faces) + +(defface jiralib-issue-info-header-face + '((t (:bold t :inherit 'jiralib-issue-info-face))) + "Base face for issue headers." + :group 'jiralib-faces) + +(defface jiralib-issue-summary-face + '((t (:bold t))) + "Base face for issue summary." + :group 'jiralib-faces) + +(defface jiralib-comment-face + '((t (:background "gray23"))) + "Base face for comments." + :group 'jiralib-faces) + +(defface jiralib-comment-header-face + '((t (:bold t))) + "Base face for comment headers." + :group 'jiralib-faces) + +(defface jiralib-link-issue-face + '((t (:underline t))) + "Face for linked issues." + :group 'jiralib-faces) + +(defface jiralib-link-project-face + '((t (:underline t))) + "Face for linked projects" + :group 'jiralib-faces) + +(defface jiralib-link-filter-face + '((t (:underline t))) + "Face for linked filters" + :group 'jiralib-faces) + +(defvar jiralib-mode-hook nil) +(defvar jiralib-mode-map nil) +(defvar jiralib-issue-regexp "\\<\\(?:[A-Za-z0-9]+\\)-[0-9]+\\>") + +(defcustom jiralib-wsdl-descriptor-url + "" + "The location for the WSDL descriptor for the JIRA service. +This is specific to your local JIRA installation. The URL is +typically: + + http://YOUR_INSTALLATION/rpc/soap/jirasoapservice-v2?wsdl + +The default value works if JIRA is located at a hostname named +'jira'." + :type 'string + :group 'jiralib) + +(defcustom jiralib-url + "http://localhost:8081/" + "The address of the jira host." + :type 'string + :group 'jiralib) + +(defcustom jiralib-agile-page-size + 50 + "Page size for agile API retrieve. Limited by server property jira.search.views.default.max" + :type 'integer + :group 'jiralib) + +(defvar jiralib-token nil + "JIRA token used for authentication.") + +(defvar jiralib-user-login-name nil + "The name of the user logged into JIRA. +This is maintained by `jiralib-login'.") + +(defvar jiralib-wsdl nil) + +(defcustom jiralib-worklog-import--filters-alist + (list + '(nil "WorklogUpdatedByCurrentUser" + (lambda (wl) + (let-alist wl + (when + (and wl + (string-equal + (downcase + (or jiralib-user-login-name user-login-name "")) + (downcase (or .updateAuthor.name + (car (split-string (or .updateAuthor.emailAddress "") "@")) + "")))) + wl)))) + '(nil "WorklogAuthoredByCurrentUser" + (lambda (wl) + (let-alist wl + (when + (and wl + (string-equal + (downcase + (or jiralib-user-login-name user-login-name)) + (downcase (or .author.name + (car (split-string (or .author.emailAddress "") "@")))))) + wl))))) + "A list of triplets: ('Global-Enable 'Descriptive-Label 'Function-Definition) +that apply worklog predicate filters during import. + +Example: (list '('t \"descriptive-predicate-label\" (lambda (x) x)))" + :type '(repeat (list boolean string function)) + :group 'org-jira) + + +(defcustom jiralib-update-issue-fields-exclude-list nil + "A list of symbols to check for exclusion on updates based on matching key. +Key names should be one of components, description, assignee, reporter, summary, issuetype." + :type '(set (const :tag "Exclude components" components) + (const :tag "Exclude description" description) + (const :tag "Exclude assignee" assignee) + (const :tag "Exclude reporter" reporter) + (const :tag "Exclude summary" summary) + (const :tag "Exclude priority" priority) + (const :tag "Exclude issue type" issuetype)) + :group 'org-jira) + +(defun jiralib-load-wsdl () + "Load the JIRA WSDL descriptor." + (setq jiralib-wsdl (soap-load-wsdl-from-url (if (string-equal jiralib-wsdl-descriptor-url "") + (concat jiralib-url "/rpc/soap/jirasoapservice-v2?wsdl") + jiralib-wsdl-descriptor-url)))) + +(defun jiralib-login (username password) + "Login into JIRA as user USERNAME with PASSWORD. + +After a successful login, store the authentication token in +`jiralib-token'." + ;; NOTE that we cannot rely on `jiralib-call' because `jiralib-call' relies on + ;; us ;-) + (interactive + (if (> 24 emacs-major-version) + (let ((user (read-string "Username for Jira server login? ")) + (password (read-passwd "Password for Jira server login? "))) + (list user password)) + (let ((found (nth 0 (auth-source-search :max 1 + :host (if (string= jiralib-host "") + (url-host (url-generic-parse-url jiralib-url)) + jiralib-host) + :user (if (string= jiralib-user "") + (url-user (url-generic-parse-url jiralib-url)) + jiralib-user) + ;; secrets.el wouldn’t accept a number. + :port (list (number-to-string (url-port (url-generic-parse-url jiralib-url))) + (url-type (url-generic-parse-url jiralib-url))) + :require '(:user :secret) + :create t))) + user secret) + (when found + (setq user (plist-get found :user) + secret + (let ((sec (plist-get found :secret))) + (if (functionp sec) + (funcall sec) + sec))) + (list user secret))))) + (if jiralib-use-restapi + (setq jiralib-token `("Authorization" . , (format "Basic %s" (base64-encode-string (concat username ":" password) t)))) + (unless jiralib-wsdl + (jiralib-load-wsdl)) + (setq jiralib-token + (car (soap-invoke jiralib-wsdl "jirasoapservice-v2" "login" username password)))) + (setq jiralib-user-login-name username)) + +(defvar jiralib-complete-callback nil) + +(defun jiralib-call (method callback &rest params) + "Invoke the Jira METHOD, then CALLBACK with supplied PARAMS. + +This function should be used for all JIRA interface calls, as the +method ensures the user is logged in and invokes `soap-invoke' +with the correct service name and authentication token. + +All JIRA interface methods take an authentication token as the +first argument. The authentication token is supplied by this +function, so PARAMS should omit this parameter. For example, the +\"getIssue\" method takes two parameters: auth and key, however, +when invoking it through `jiralib-call', the call should be: + + (jiralib-call \"getIssue\" KEY) + +CALLBACK should be the post processing function to run with the +completed data from the request result, which can be accessed with: + + (cl-getf data :data) + +as such, the CALLBACK should follow this type of form: + + (cl-function + (lambda (&rest data &allow-other-keys) + (print (cl-getf data :data)))) + +If CALLBACK is set to nil then the request will occur with sync. +This produces a noticeable slowdown and is not recommended by +request.el, so if at all possible, it should be avoided." + ;; @TODO :auth: Probably pass this all the way down, but I think + ;; it may be OK at the moment to just set the variable each time. + + (setq jiralib-complete-callback + ;; Don't run with async if we don't have a login token yet. + (if jiralib-token callback nil)) + + ;; If we don't have a regex set, ensure it is set BEFORE any async + ;; calls are processing, or we're going to have a bad time. + ;; This should only end up running once per session. + (unless jiralib-issue-regexp + (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e)))) + (append (jiralib--rest-call-it + "/rest/api/2/project" + :params '((expand . "description,lead,url,projectKeys"))) + nil) + ))) + (when projects + (setq jiralib-issue-regexp + (concat "\\<" (regexp-opt projects) "-[0-9]+\\>"))))) + + (if (not jiralib-use-restapi) + (car (apply 'jiralib--call-it method params)) + (unless jiralib-token + (call-interactively 'jiralib-login)) + (cl-case (intern method) + ('getStatuses (jiralib--rest-call-it "/rest/api/2/status")) + ('getIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype")) + ('getSubTaskIssueTypes (jiralib--rest-call-it "/rest/api/2/issuetype")) + ('getIssueTypesByProject + (let ((response (jiralib--rest-call-it (format "/rest/api/2/project/%s" (first params))))) + (cl-coerce (cdr (assoc 'issueTypes response)) 'list))) + ('getUser (jiralib--rest-call-it "/rest/api/2/user" :params `((accountId . ,(first params))))) + ('getVersions (jiralib--rest-call-it (format "/rest/api/2/project/%s/versions" (first params)))) + + ;; Worklog calls + ('getWorklogs + (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)))) + + ('addWorklog + (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)) + :type "POST" + :data (json-encode (second params)))) + + ('updateWorklog + (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog/%s" (first params) (second params)) + :type "PUT" + :data (json-encode (third params)))) + + ('addWorklogAndAutoAdjustRemainingEstimate + (jiralib--rest-call-it (format "/rest/api/2/issue/%s/worklog" (first params)) + :type "POST" + :data (json-encode (second params)))) + + ('addComment (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/comment" (first params)) + :type "POST" + :data (json-encode (second params)))) + ('createIssue + ;; Creating the issue doesn't return it, a second call must be + ;; made to pull it in by using the self key in response. + (let ((response (jiralib--rest-call-it + "/rest/api/2/issue" + :type "POST" + :data (json-encode (first params))))) + (jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET") + )) + ('createIssueWithParent + (let ((response (jiralib--rest-call-it + "/rest/api/2/issue" + :type "POST" + :data (json-encode (first params))))) + (jiralib--rest-call-it (cdr (assoc 'self response)) :type "GET") + )) + ('editComment (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params)) + :data (json-encode `((body . ,(third params)))) + :type "PUT")) + ('getBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s" (first params)))) + ('getBoards (apply 'jiralib--agile-call-it "/rest/agile/1.0/board" 'values params)) + ('getComment (org-jira-find-value + (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/comment/%s" (first params) (second params))) + 'comments)) + ('getComments (org-jira-find-value + (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/comment" (first params))) + 'comments)) + ('getAttachmentsFromIssue (org-jira-find-value + (jiralib--rest-call-it + (format "/rest/api/2/issue/%s?fields=attachment" (first params))) + 'comments)) + ('getComponents (jiralib--rest-call-it + (format "/rest/api/2/project/%s/components" (first params)))) + ('getIssue (jiralib--rest-call-it + (format "/rest/api/2/issue/%s" (first params)))) + ('getIssuesFromBoard (apply 'jiralib--agile-call-it + (format "rest/agile/1.0/board/%d/issue" (first params)) + 'issues + (cdr params))) + ('getSprintsFromBoard (jiralib--rest-call-it (format "/rest/agile/1.0/board/%s/sprint" (first params)))) + ('getIssuesFromSprint (apply 'jiralib--agile-call-it + (format "rest/agile/1.0/sprint/%d/issue" (first params)) + 'issues + (cdr params))) + ('getIssuesFromJqlSearch (append (cdr ( assoc 'issues (jiralib--rest-call-it + "/rest/api/2/search" + :type "POST" + :data (json-encode `((jql . ,(first params)) + (maxResults . ,(second params))))))) + nil)) + ('getPriorities (jiralib--rest-call-it + "/rest/api/2/priority")) + ('getProjects (jiralib--rest-call-it "rest/api/2/project")) + ('getProjectsNoSchemes (append (jiralib--rest-call-it + "/rest/api/2/project" + :params '((expand . "description,lead,url,projectKeys"))) nil)) + ('getResolutions (append (jiralib--rest-call-it + "/rest/api/2/resolution") nil)) + ('getAvailableActions + (mapcar + (lambda (trans) + `(,(assoc 'name trans) ,(assoc 'id trans))) + (cdadr (jiralib--rest-call-it (format "/rest/api/2/issue/%s/transitions" (first params)))))) + ('getFieldsForAction (org-jira-find-value (car (let ((issue (first params)) + (action (second params))) + (seq-filter (lambda (trans) + (or (string-equal action (org-jira-find-value trans 'id)) + (string-equal action (org-jira-find-value trans 'name)))) + (cdadr (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/transitions" (first params)) + :params '((expand . "transitions.fields"))))))) + 'fields)) + ('progressWorkflowAction (jiralib--rest-call-it + (format "/rest/api/2/issue/%s/transitions" (first params)) + :type "POST" + :data (json-encode `(,(car (second params)) ,(car (third params)))))) + ('getUsers + (jiralib--rest-call-it (format "/rest/api/2/user/assignable/search?project=%s&maxResults=10000" (first params)) + :type "GET")) + ('updateIssue (jiralib--rest-call-it + (format "/rest/api/2/issue/%s" (first params)) + :type "PUT" + :data (json-encode `((fields . ,(second params)))))) + ('getLabels (jiralib--rest-call-it (format "/rest/api/2/label?startAt=%s" (first params))))))) + +(defun jiralib--soap-call-it (&rest args) + "Deprecated SOAP call endpoint. Will be removed soon. +Pass ARGS to jiralib-call." + (let ((jiralib-token nil) + (jiralib-use-restapi nil)) + (apply #'jiralib-call args))) + +(defun jiralib--json-read () + "Read with json, force utf-8" + (decode-coding-region (point) (point-max) jiralib-coding-system) + (json-read)) + +(defun jiralib--rest-call-it (api &rest args) + "Invoke the corresponding jira rest method API. +Invoking COMPLETE-CALLBACK when the +JIRALIB-COMPLETE-CALLBACK is non-nil, request finishes, and +passing ARGS to REQUEST." + (unless api (error "jiralib--rest-call-it was called with a NIL api value.")) + (setq args + (mapcar + (lambda (arg) + (if (stringp arg) + (encode-coding-string arg jiralib-coding-system) + arg)) + args)) + (append (request-response-data + (apply #'request (if (string-match "^http[s]*://" api) api ;; If an absolute path, use it + (concat (replace-regexp-in-string "/*$" "/" jiralib-url) + (replace-regexp-in-string "^/*" "" api))) + :sync (not jiralib-complete-callback) + :headers `(,jiralib-token ("Content-Type" . "application/json")) + :parser 'jiralib--json-read + :complete jiralib-complete-callback + ;; Ensure we have useful errors + :error + (lexical-let + ((my-api api) + (my-args args)) + (cl-function + (lambda (&key data &allow-other-keys) + (print "JIRA_ERROR - see your *Messages* buffer for more details.") + (print "JIRA_ERROR REQUEST: ") + (print my-api) + (print my-args) + (print "JIRA_ERROR RESPONSE: ") + (print data) + (error "JIRA_ERROR - see your *Messages* buffer for more details.") + ))) + args)) + nil)) + +(defun jiralib--call-it (method &rest params) + "Invoke the JIRA METHOD with supplied PARAMS. + +Internal use, returns a list of responses, of which only the +first is normally used." + (when (symbolp method) + (setq method (symbol-name method))) + (unless jiralib-token + (call-interactively 'jiralib-login)) + (condition-case data + (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2" + method jiralib-token params) + (soap-error + ;; If we are here, we had a token, but it expired. Re-login and try + ;; again. + (setq jiralib-token nil) + (call-interactively 'jiralib-login) + (apply 'soap-invoke jiralib-wsdl "jirasoapservice-v2" + method jiralib-token params)))) + + +;;;; Some utility functions + +(defun jiralib-make-list (data field) + "Map all assoc elements in DATA to the value of FIELD in that element." + (cl-loop for element in data + collect (cdr (assoc field element)))) + +(defun jiralib-make-assoc-list (data key-field value-field) + "Create an association list from a SOAP structure array. + +DATA is a list of association lists (a SOAP array-of type) +KEY-FIELD is the field to use as the key in the returned alist +VALUE-FIELD is the field to use as the value in the returned alist" + (cl-loop for element in data + collect (cons (cdr (assoc key-field element)) + (cdr (assoc value-field element))))) + +(defun jiralib-make-remote-field-values (fields) + "Transform the (KEY . VALUE) list FIELDS into a RemoteFieldValue structure. + +Each (KEY . VALUE) pair is transformed into + ((id . KEY) (values . (VALUE))) + +This method exists because Several JIRA methods require a +RemoteFieldValue list, but it is easier to work with ALISTS in +emacs-lisp" + (let ((remote-field-values)) + + ;; we accept an ALIST of field-name field-values parameter, but we need to + ;; construct a structure that encodes as a RemoteFieldValue which is what + ;; updateIssue wants + (dolist (field fields) + (let ((name (car field)) + (value (cdr field))) + (when (symbolp name) + (setq name (symbol-name name))) + ;; Value must be an "array" (for which soap-client accepts lists) even + ;; if it is just one value + (unless (vectorp value) + (setq value (vector value))) + (push `((id . ,name) (values . ,value)) + remote-field-values))) + + (apply 'vector (nreverse remote-field-values)))) + + +;;;; Wrappers around JIRA methods + +(defun jiralib--rest-api-for-issue-key (key) + "Return jira rest api for issue KEY." + (concat "rest/api/2/issue/" key)) + +(defun jiralib-filter-fields-by-exclude-list (exclude-list fields) + (cl-remove-if + (lambda (el) (cl-member (car el) exclude-list)) fields)) + +(defun jiralib-update-issue (key fields &optional callback) + "Update the issue with id KEY with the values in FIELDS, invoking CALLBACK." + (let ((filtered-fields (jiralib-filter-fields-by-exclude-list + jiralib-update-issue-fields-exclude-list + fields))) + (jiralib-call + "updateIssue" + callback + key (if jiralib-use-restapi + filtered-fields + (jiralib-make-remote-field-values filtered-fields))))) + +(defvar jiralib-status-codes-cache nil) + +(defun jiralib-get-statuses () + "Return an assoc list mapping a status code to its name. +NOTE: Status codes are stored as strings, not numbers. + +This function will only ask JIRA for the list of codes once, then +will cache it." + (unless jiralib-status-codes-cache + (setq jiralib-status-codes-cache + (jiralib-make-assoc-list (jiralib-call "getStatuses" nil) 'id 'name))) + jiralib-status-codes-cache) + +(defvar jiralib-issue-types-cache nil) + +(defun jiralib-get-issue-types () + "Return an assoc list mapping an issue type code to its name. +NOTE: Issue type codes are stored as strings, not numbers. + +This function will only ask JIRA for the list of codes once, than +will cache it. + +The issue types returned via getIssueTypes are all the ones +available to the user, but not necessarily available to the given +project. + +This endpoint is essentially a master reference for when issue +types need a name lookup when given an id. + +For applying issue types to a given project that is being created, see +the #'jiralib-get-issue-types-by-project call." + (unless jiralib-issue-types-cache + (setq jiralib-issue-types-cache + (jiralib-make-assoc-list (jiralib-call "getIssueTypes" nil) 'id 'name))) + jiralib-issue-types-cache) + +(defvar jiralib-issue-types-by-project-cache nil "An alist of available issue types.") + +(defun jiralib-get-issue-types-by-project (project) + "Return the available issue types for PROJECT. + +PROJECT should be the key, such as `EX' or `DEMO'." + (unless (assoc project jiralib-issue-types-by-project-cache) + (push (cons project + (jiralib-make-assoc-list + (jiralib-call "getIssueTypesByProject" nil project) + 'id 'name)) + jiralib-issue-types-by-project-cache)) + (cdr (assoc project jiralib-issue-types-by-project-cache))) + +(defvar jiralib-priority-codes-cache nil) + +(defun jiralib-get-priorities () + "Return an assoc list mapping a priority code to its name. +NOTE: Priority codes are stored as strings, not numbers. + +This function will only ask JIRA for the list of codes once, than +will cache it." + (unless jiralib-priority-codes-cache + (setq jiralib-priority-codes-cache + (jiralib-make-assoc-list (jiralib-call "getPriorities" nil) 'id 'name))) + jiralib-priority-codes-cache) + +(defvar jiralib-resolution-code-cache nil) + +(defun jiralib-get-resolutions () + "Return an assoc list mapping a resolution code to its name. +NOTE: Resolution codes are stored as strings, not numbers. + +This function will only ask JIRA for the list of codes once, than +will cache it." + (unless jiralib-resolution-code-cache + (setq jiralib-resolution-code-cache + (jiralib-make-assoc-list (jiralib-call "getResolutions" nil) 'id 'name))) + jiralib-resolution-code-cache) + +;; NOTE: it is not such a good idea to use this, as it needs a JIRA +;; connection to construct the regexp (the user might be prompted for a JIRA +;; username and password). +;; +;; The best use of this function is to generate the regexp once-off and +;; persist it somewhere. +;; +;; FIXME: Probably just deprecate/remove this, we can assert we're on +;; an issue with a general regexp that matches the common format, vs +;; needing to know specific user project list. +(defun jiralib-get-issue-regexp () + "Return a regexp that will match an issue id. + +The regexp is constructed from the project keys in the JIRA +database. An issue is assumed to be in the format KEY-NUMBER, +where KEY is a project key and NUMBER is the issue number." + (unless jiralib-issue-regexp + (let ((projects (mapcar (lambda (e) (downcase (cdr (assoc 'key e)))) + (jiralib-call "getProjectsNoSchemes" nil)))) + (when projects + (setq jiralib-issue-regexp + (concat "\\<" (regexp-opt projects) "-[0-9]+\\>"))))) + jiralib-issue-regexp) + +(defun jiralib-do-jql-search (jql &optional limit callback) + "Run a JQL query and return the list of issues that matched. +LIMIT is the maximum number of queries to return. Note that JIRA +has an internal limit of how many queries to return, as such, it +might not be possible to find *ALL* the issues that match a +query." + (unless (or limit (numberp limit)) + (setq limit 100)) + (jiralib-call "getIssuesFromJqlSearch" callback jql limit)) + +(defcustom jiralib-available-actions-cache-p t + "Set to t to enable caching for jiralib-get-available-actions. + +If nil, will disable caching for this endpoint. + +Possible side-effects: + + - If the server has the project workflow updated, the cache +saved here will be incorrect. + + - If the issue is not up to date with the remote, the wrong +cache key may be queried." + :type 'boolean + :group 'jiralib) + +(defvar jiralib-available-actions-cache nil "An alist of available actions.") + +(defun jiralib-get-available-actions (issue-key &optional status) + "Return the available workflow actions for ISSUE-KEY. +This uses STATUS as the cache key. +This runs the getAvailableActions SOAP method." + (if (and jiralib-available-actions-cache-p status) + (progn + (unless (assoc status jiralib-available-actions-cache) + (push (cons status + (jiralib-make-assoc-list + (mapcar (lambda (x) + (let ((namestring (cdr (car x))) + (id (cdr x))) + (cons + (cons 'name (org-jira-decode namestring)) + id))) + (jiralib-call "getAvailableActions" nil issue-key)) + 'id 'name)) + jiralib-available-actions-cache)) + (cdr (assoc status jiralib-available-actions-cache))) + (progn + (jiralib-make-assoc-list + (mapcar (lambda (x) + (let ((namestring (cdr (car x))) + (id (cdr x))) + (cons + (cons 'name (org-jira-decode namestring)) + id))) + (jiralib-call "getAvailableActions" nil issue-key)) + 'id 'name)))) + +(defcustom jiralib-fields-for-action-cache-p t + "Set to t to enable caching for jiralib-get-fields-for-action. + +If nil, will disable caching for this endpoint. + +Possible side-effects: + + - If many tasks have different workflows, you may want to disable this." + :type 'boolean + :group 'jiralib) + +(defvar jiralib-fields-for-action-cache nil "An alist of available fields.") + +(defun jiralib-get-fields-for-action-with-cache (issue-key action-id) + "Return the required fields to change ISSUE-KEY to ACTION-ID." + (if (and jiralib-fields-for-action-cache-p action-id) + (progn + (unless (assoc action-id jiralib-fields-for-action-cache) + (push (cons action-id + (jiralib-call "getFieldsForAction" nil issue-key action-id)) + jiralib-fields-for-action-cache)) + (cdr (assoc action-id jiralib-fields-for-action-cache))) + (jiralib-call "getFieldsForAction" nil issue-key action-id))) + +(defun jiralib-get-fields-for-action (issue-key action-id) + "Return the required fields to change ISSUE-KEY to ACTION-ID." + (if jiralib-use-restapi + (let ((fields (jiralib-get-fields-for-action-with-cache issue-key action-id))) + (mapcar (lambda (field) + (cons (symbol-name (car field)) + (format "%s (required: %s)" + (org-jira-find-value field 'name) + (if (eq (org-jira-find-value field 'required) :json-false) + "nil" + "t")))) + fields)) + (jiralib-make-assoc-list + (jiralib-get-fields-for-action-with-cache issue-key action-id) + 'id 'name))) + +(defun jiralib-progress-workflow-action (issue-key action-id params &optional callback) + "Progress issue with ISSUE-KEY to action ACTION-ID, and provide the needed PARAMS. + +When CALLBACK is present, this will run async." + (if jiralib-use-restapi + (jiralib-call "progressWorkflowAction" + callback issue-key `((transition (id . ,action-id))) + `((fields . ,params))) + (jiralib-call "progressWorkflowAction" + callback issue-key action-id (jiralib-make-remote-field-values params)))) + + +(defun jiralib-format-datetime (&optional datetime) + "Convert a mixed DATETIME format into the Jira required datetime format. + +This will produce a datetime string such as: + + 2010-02-05T14:30:00.000+0000 + +for being consumed in the Jira API. + +If DATETIME is not passed in, it will default to the current time." + (let* ((defaults (format-time-string "%Y-%m-%d %H:%M:%S" (current-time))) + (datetime (concat datetime (subseq defaults (length datetime)))) + (parts (parse-time-string datetime))) + (format "%04d-%02d-%02dT%02d:%02d:%02d.000+0000" + (nth 5 parts) + (nth 4 parts) + (nth 3 parts) + (nth 2 parts) + (nth 1 parts) + (nth 0 parts)))) + +(defvar jiralib-worklog-coming-soon-message + "WORKLOG FEATURES ARE NOT IMPLEMENTED YET, COMING SOON!") + +(defun jiralib-add-worklog-and-autoadjust-remaining-estimate (issue-key start-date time-spent comment) + "Log time spent on ISSUE-KEY to its worklog. +The time worked begins at START-DATE and has a TIME-SPENT +duration. JIRA will automatically update the remaining estimate +by subtracting TIME-SPENT from it. + +START-DATE should be in the format 2010-02-05T14:30:00Z + +TIME-SPENT can be in one of the following formats: 10m, 120m +hours; 10h, 120h days; 10d, 120d weeks. + +COMMENT will be added to this worklog." + (let ((formatted-start-date (jiralib-format-datetime start-date))) + (jiralib-call "addWorklogAndAutoAdjustRemainingEstimate" + nil + issue-key + ;; Expects data such as: '{"timeSpent":"1h", "started":"2017-02-21T00:00:00.000+0000", "comment":"woot!"}' + ;; and only that format will work (no loose formatting on the started date) + `((started . ,formatted-start-date) + (timeSpent . ,time-spent) + (comment . ,comment))))) + + +;;;; Issue field accessors + +(defun jiralib-issue-key (issue) + "Return the key of ISSUE." + (cdr (assoc 'key issue))) + +(defun jiralib-issue-owner (issue) + "Return the owner of ISSUE." + (cdr (assq 'assignee issue))) + +(defun jiralib-issue-status (issue) + "Return the status of ISSUE as a status string (not as a number!)." + (let ((status-code (cdr (assq 'status issue)))) + (cdr (assoc status-code (jiralib-get-statuses))))) + +(defun jiralib-custom-field-value (custom-field issue) + "Return the value of CUSTOM-FIELD for ISSUE. +Return nil if the field is not found" + (catch 'found + (dolist (field (cdr (assq 'customFieldValues issue))) + (when (equal (cdr (assq 'customfieldId field)) custom-field) + (throw 'found (cadr (assq 'values field))))))) + +(defvar jiralib-current-issue nil + "This holds the currently selected issue.") + +(defvar jiralib-projects-list nil + "This holds a list of projects and their details.") + +(defvar jiralib-types nil + "This holds a list of issues types.") + +(defvar jiralib-priorities nil + "This holds a list of priorities.") + +(defvar jiralib-user-fullnames nil + "This holds a list of user fullnames.") + +(defun jiralib-get-project-name (key) + "Return the name of the JIRA project with id KEY." + (let ((projects jiralib-projects-list) + (name nil)) + (dolist (project projects) + (if (equal (cdr (assoc 'key project)) key) + (setf name (cdr (assoc 'name project))))) + name)) + +(defun jiralib-get-type-name (id) + "Return the name of the issue type with ID." + (let ((types jiralib-types) + (name nil)) + (dolist (type types) + (if (equal (cdr (assoc 'id type)) id) + (setf name (cdr (assoc 'name type))))) + name)) + +;; (defun jiralib-get-user-fullname (account-id) +;; "Return the full name (display name) of the user with USERNAME." + ;; (if (assoc account-id jiralib-user-fullnames) + ;; (cdr (assoc account-id jiralib-user-fullnames)) + ;; (progn + ;; (let ((user (jiralib-get-user account-id))) + ;; (setf jiralib-user-fullnames (append jiralib-user-fullnames (list (cons account-id (cdr (assoc 'fullname user)))))) + ;; (cdr (assoc 'fullname user)))))) + +(defun jiralib-get-user-fullname (account-id) + "Return the full name (displayName) of the user with ACCOUNT-ID." + (cl-loop for user in (jiralib-get-users nil) + when (rassoc account-id user) + return (cdr (assoc 'displayName user)))) + +(defun jiralib-get-user-account-id (project full-name) + "Return the account-id (accountId) of the user with FULL-NAME (displayName) in PROJECT." + (cl-loop for user in (jiralib-get-users project) + when (rassoc full-name user) + return (cdr (assoc 'accountId user)))) + +(defun jiralib-get-filter (filter-id) + "Return a filter given its FILTER-ID." + (cl-flet ((id-match (filter) + (equal filter-id (cdr (assoc 'id filter))))) + (cl-find-if 'id-match (jiralib-get-saved-filters)))) + +(defun jiralib-get-filter-alist () + "Return an association list mapping filter names to IDs." + (mapcar (lambda (filter) + (cons (cdr (assoc 'name filter)) + (cdr (assoc 'id filter)))) + (jiralib-get-saved-filters))) + +(defun jiralib-add-comment (issue-key comment &optional callback) + "Add to issue with ISSUE-KEY the given COMMENT, invoke CALLBACK." + (jiralib-call "addComment" callback issue-key `((body . ,comment)))) + +(defun jiralib-edit-comment (issue-id comment-id comment &optional callback) + "Edit ISSUE-ID's comment COMMENT-ID to reflect the new COMMENT, invoke CALLBACK." + (if (not jiralib-use-restapi) + (jiralib-call "editComment" callback `((id . ,comment-id) + (body . ,comment))) + (jiralib-call "editComment" callback issue-id comment-id comment))) + +(defun jiralib-create-issue (issue) + "Create a new ISSUE in JIRALIB. + +ISSUE is a Hashtable object." + (jiralib-call "createIssue" nil issue)) + +(defun jiralib-create-subtask (subtask) + "Create SUBTASK for issue with PARENT-ISSUE-ID. + +SUBTASK is a Hashtable object." + (jiralib-call "createIssueWithParent" nil subtask)) + +(defvar jiralib-subtask-types-cache nil) + +(defun jiralib-get-subtask-types () + "Return an assoc list mapping an issue type code to its name. +NOTE: Issue type codes are stored as strings, not numbers. + +This function will only ask JIRA for the list of codes once, than +will cache it." + (unless jiralib-subtask-types-cache + (setq jiralib-subtask-types-cache + (jiralib-make-assoc-list (jiralib-call "getSubTaskIssueTypes" nil) 'id 'name))) + jiralib-subtask-types-cache) + +(defun jiralib-get-comment (issue-key comment-id &optional callback) + "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK." + (jiralib-call "getComment" callback issue-key comment-id)) + +(defun jiralib-get-comments (issue-key &optional callback) + "Return all comments associated with issue ISSUE-KEY, invoking CALLBACK." + (jiralib-call "getComments" callback issue-key)) + +(defun jiralib-get-attachments (issue-key &optional callback) + "Return all attachments associated with issue ISSUE-KEY, invoking CALLBACK." + (jiralib-call "getAttachmentsFromIssue" callback issue-key)) + +(defun jiralib-get-worklogs (issue-key &optional callback) + "Return all worklogs associated with issue ISSUE-KEY, invoking CALLBACK." + (jiralib-call "getWorklogs" callback issue-key)) + +(defun jiralib-add-worklog (issue-id started time-spent-seconds comment &optional callback) + "Add the worklog linked to ISSUE-ID. + +Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT. +CALLBACK will be invoked if passed in upon endpoint completion." + ;; Call will fail if 0 seconds are set as the time, so always do at least one min. + (setq time-spent-seconds (max 60 time-spent-seconds)) + (let ((worklog `((started . ,started) + ;; @TODO :worklog: timeSpentSeconds changes into incorrect values + ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days... + ;; We should convert to a Xd Xh Xm format from our seconds ourselves. + (timeSpentSeconds . ,time-spent-seconds) + (comment . ,comment)))) + (jiralib-call "addWorklog" callback issue-id worklog))) + +(defun jiralib-update-worklog (issue-id worklog-id started time-spent-seconds comment &optional callback) + "Update the worklog linked to ISSUE-ID and WORKLOG-ID. + +Requires STARTED (a jira datetime), TIME-SPENT-SECONDS (integer) and a COMMENT. +CALLBACK will be invoked if passed in upon endpoint completion." + ;; Call will fail if 0 seconds are set as the time, so always do at least one min. + (setq time-spent-seconds (max 60 time-spent-seconds)) + (let ((worklog `((started . ,started) + ;; @TODO :worklog: timeSpentSeconds changes into incorrect values + ;; in the Jira API (for instance, 89600 = 1 day, but Jira thinks 3 days... + ;; We should convert to a Xd Xh Xm format from our seconds ourselves. + (timeSpentSeconds . ,time-spent-seconds) + (comment . ,comment)))) + (jiralib-call "updateWorklog" callback issue-id worklog-id worklog))) + +(defvar jiralib-components-cache nil "An alist of project components.") + +(defun jiralib-get-components (project-key) + "Return all components available in the project PROJECT-KEY." + (unless (assoc project-key jiralib-components-cache) + (push (cons project-key + (jiralib-make-assoc-list + (jiralib-call "getComponents" nil project-key) 'id 'name)) + jiralib-components-cache)) + (cdr (assoc project-key jiralib-components-cache))) + +(defun jiralib-get-issue (issue-key &optional callback) + "Get the issue with key ISSUE-KEY, running CALLBACK after." + (jiralib-call "getIssue" callback issue-key)) + +(defun jiralib-get-issues-from-filter (filter-id) + "Get the issues from applying saved filter FILTER-ID." + (message "jiralib-get-issues-from-filter is NOT IMPLEMENTED!! Do not use!") + (jiralib-call "getIssuesFromFilter" nil filter-id)) + +(defun jiralib-get-issues-from-text-search (search-terms) + "Find issues using free text search SEARCH-TERMS." + (jiralib-call "getIssuesFromTextSearch" nil search-terms)) + +(defun jiralib-get-issues-from-text-search-with-project + (project-keys search-terms max-num-results) + "Find issues in projects PROJECT-KEYS, using free text search SEARCH-TERMS. + +Return no more than MAX-NUM-RESULTS." + (jiralib-call "getIssuesFromTextSearchWithProject" + nil + (apply 'vector project-keys) search-terms max-num-results)) + +;; Modified by Brian Zwahr to use getProjectsNoSchemes instead of getProjects +(defun jiralib-get-projects () + "Return a list of projects available to the user." + (if jiralib-projects-list + jiralib-projects-list + (setq jiralib-projects-list + (if jiralib-use-restapi + (jiralib-call "getProjects" nil) + (jiralib-call "getProjectsNoSchemes" nil))))) + +(defun jiralib-get-saved-filters () + "Get all saved filters available for the currently logged in user." + (jiralib-make-assoc-list (jiralib-call "getSavedFilters" nil) 'id 'name)) + +(defun jiralib-get-server-info () + "Return the Server information such as baseUrl, version, edition, buildDate, buildNumber." + (jiralib-call "getServerInfo" nil)) + +(defun jiralib-get-sub-task-issue-types () + "Return all visible subtask issue types in the system." + (jiralib-call "getSubTaskIssueTypes" nil)) + +(defun jiralib-get-user (account-id) + "Return a user's information given their full name." + (cond ((eq 0 (length account-id)) nil) ;; Unassigned + (t (jiralib-call "getUser" nil account-id)))) + +(defvar jiralib-users-cache nil "Cached list of users.") + +(defun jiralib-get-users (project-key) + "Return assignable users information given the PROJECT-KEY." + (unless jiralib-users-cache + (setq jiralib-users-cache + (jiralib-call "getUsers" nil project-key)) + (cl-loop for (name . id) in org-jira-users do + (setf jiralib-users-cache (append (list (jiralib-get-user id)) jiralib-users-cache)))) + jiralib-users-cache) + +(defun jiralib-get-versions (project-key) + "Return all versions available in project PROJECT-KEY." + (jiralib-call "getVersions" nil project-key)) + +(defun jiralib-strip-cr (string) + "Remove carriage returns from STRING." + (when string (replace-regexp-in-string "\r" "" string))) + +(defun jiralib-worklog-import--filter-apply + (worklog-obj &optional predicate-fn-lst unwrap-worklog-records-fn rewrap-worklog-records-fn) + "Remove non-matching org-jira issue worklogs. + +Variables: + WORKLOG-OBJ is the passed in object + PREDICATE-FN-LST is the list of lambdas used as match predicates. + UNWRAP-WORKLOG-RECORDS-FN is the function used to produce the list of worklog records from within the worklog-obj + REWRAP-WORKLOG-RECORDS-FN is the function used to reshape the worklog records back into the form they were received in. + +Auxiliary Notes: + Only the WORKLOG-OBJ variable is required. + The value of PPREDICATE-FN-LST is filled from the jiralib-worklog-import--filters-alist variable by default. + If PREDICATE-FN-LST is empty the unmodified value of WORKLOG-OBJ is returned. + If PREDICATE-FN-LST contains multiple predicate functions, each predicate filters operates as a clause in an AND match. In effect, a worklog must match all predicates to be returned. + The variable 'jiralib-user-login-name is used by many lambda filters." + + (let + ((unwrap-worklog-records-fn) + (rewrap-worklog-records-fn) + (predicate-fn-lst) + (worklogs worklog-obj) + (predicate-fn)) + ;; let-body + (progn + (setq unwrap-worklog-records-fn + (if (and + (boundp 'unwrap-worklog-records-fn) + (functionp unwrap-worklog-records-fn)) + unwrap-worklog-records-fn + (lambda (x) (cl-coerce x 'list)))) + (setq rewrap-worklog-records-fn + (if (and + (boundp 'rewrap-worklog-records-fn) + (functionp rewrap-worklog-records-fn)) + rewrap-worklog-records-fn + (lambda (x) (remove 'nil (cl-coerce x 'vector))))) + (setq predicate-fn-lst + (if (and (boundp 'predicate-fn-lst) + (not (null predicate-fn-lst)) + (listp predicate-fn-lst)) + predicate-fn-lst + (mapcar 'caddr + (remove 'nil + (mapcar (lambda (x) (unless (null (car x)) x)) + jiralib-worklog-import--filters-alist))))) + ;; final condition/sanity checks before processing + (cond + ;; pass cases, don't apply filters, return unaltered worklog-obj + ((or (not (boundp 'predicate-fn-lst)) (not (listp predicate-fn-lst)) (null predicate-fn-lst)) + worklog-obj) + ;; default-case, apply worklog filters and return only matching worklogs + (t + (setq worklogs (funcall unwrap-worklog-records-fn worklogs)) + (while (setq predicate-fn (pop predicate-fn-lst)) + (setq worklogs (mapcar predicate-fn worklogs))) + (funcall rewrap-worklog-records-fn worklogs)))))) + + +(defun jiralib-get-board (id &optional callback) + "Return details on given board" + (jiralib-call "getBoard" nil id)) + +(defun jiralib-get-boards () + "Return list of jira boards" + (jiralib-call "getBoards" nil)) + +(defun jiralib-get-board-sprints (id) + "Return list of jira sprints in the specified jira board" + (jiralib-call "getSprintsFromBoard" nil id)) + +(defun jiralib-get-sprint-issues (id &rest params) + "Return list of issues in the specified sprint" + (apply 'jiralib-call "getIssuesFromSprint" + (cl-getf params :callback) id params)) + +(defun jiralib-get-board-issues (board-id &rest params) + "Return list of jira issues in the specified jira board" + (apply 'jiralib-call "getIssuesFromBoard" + (cl-getf params :callback) board-id params)) + +(defvar jiralib-labels-cache nil) +(defun jiralib-get-labels () + "Return assignable labels that can be added to an issue." + (unless jiralib-labels-cache + (setq jiralib-labels-start-at 0) + (while (progn + (let* ((labels (jiralib-call "getLabels" nil jiralib-labels-start-at)) + (max-results (alist-get 'maxResults labels)) + (is-last (alist-get 'isLast labels)) + (values (alist-get 'values labels))) + (setq jiralib-labels-start-at (+ max-results jiralib-labels-start-at) + jiralib-labels-cache (append values jiralib-labels-cache)) + (not (eq is-last t))))))) + +(defun jiralib--agile-not-last-entry (num-entries total start-at limit) + "Return true if need to retrieve next page from agile api" + (and (> num-entries 0) + (or (not limit) ; not required to be set + (< limit 1) ; ignore invalid limit + (> limit start-at)) + (or (not total) ; not always returned + (> total start-at)))) + +(defun jiralib--agile-limit-page-size (page-size start-at limit) + (if (and limit + (> (+ start-at page-size) limit)) + (- limit start-at) + page-size)) + + +(defun jiralib--agile-rest-call-it (api max-results start-at limit query-params) + (let ((callurl + (format "%s?%s" api + (url-build-query-string + (append `((maxResults ,(jiralib--agile-limit-page-size max-results start-at limit)) + (startAt ,start-at)) + query-params))))) + (jiralib--rest-call-it callurl))) + +(defun jiralib--agile-call-it (api values-key &rest params) + "Invoke Jira agile method api and retrieve the results using +paging. + +If JIRALIB-COMPLETE-CALLBACK is non-nil, then the call will be +performed asynchronously and JIRALIB-COMPLETE-CALLBACK will be +called when all data are retrieved. + +If JIRALIB-COMPLETE-CALLBACK is nil, then the call will be +performed synchronously and this function will return the +retrieved data. + +API - path to called API that must start with /rest/agile/1.0. + +VALUES-KEY - key of the actual reply data in the reply assoc list. + +PARAMS - optional additional parameters. +:limit - limit total number of retrieved entries. +:query-params - extra query parameters in the format of url-build-query-string. +" + (if jiralib-complete-callback + (apply 'jiralib--agile-call-async api values-key params) + (apply 'jiralib--agile-call-sync api values-key params))) + +(defun jiralib--agile-call-sync (api values-key &rest params) + "Syncroniously invoke Jira agile method api retrieve all the +results using paging and return results. + +VALUES-KEY - key of the actual reply data in the reply assoc list. + +PARAMS - extra parameters (as keyword arguments), the supported parameters are: + +:limit - limit total number of retrieved entries. +:query-params - extra query parameters in the format of url-build-query-string. +" + (setq jiralib-complete-callback nil) + (let ((not-last t) + (start-at 0) + (limit (cl-getf params :limit)) + (query-params (cl-getf params :query-params)) + ;; maximum page size, 50 is server side maximum + (max-results jiralib-agile-page-size) + (values ())) + (while not-last + (let* ((reply-alist + (jiralib--agile-rest-call-it api max-results start-at limit query-params)) + (values-array (cdr (assoc values-key reply-alist))) + (num-entries (length values-array)) + (total (cdr (assq 'total reply-alist)))) + (setf values (append values (append values-array nil))) + (setf start-at (+ start-at num-entries)) + (setf not-last (jiralib--agile-not-last-entry num-entries total start-at limit)))) + values)) + +(defun jiralib--agile-call-async (api values-key &rest params) + "Asyncroniously invoke Jira agile method api, +retrieve all the results using paging and call +JIRALIB-COMPLETE_CALLBACK when all the data are retrieved. + +VALUES-KEY - key of the actual reply data in the reply assoc list. + +PARAMS - extra parameters (as keyword arguments), the supported parameters are: + +limit - limit total number of retrieved entries." + (lexical-let + ((start-at 0) + (limit (cl-getf params :limit)) + (query-params (cl-getf params :query-params)) + ;; maximum page size, 50 is server side maximum + (max-results jiralib-agile-page-size) + (values-list ()) + (vk values-key) + (url api) + ;; save the call back to be called later after the last page + (complete-callback jiralib-complete-callback)) + ;; setup new callback to be called after each page + (setf jiralib-complete-callback + (cl-function + (lambda (&rest data &allow-other-keys) + (condition-case err + (let* ((reply-alist (cl-getf data :data)) + (values-array (cdr (assoc vk reply-alist))) + (num-entries (length values-array)) + (total (cdr (assq 'total reply-alist)))) + (setf values-list (append values-list (append values-array nil))) + (setf start-at (+ start-at num-entries)) + (message "jiralib agile retrieve: got %d values%s%s" + start-at + (if total " of " "") + (if total (int-to-string total) "")) + (if (jiralib--agile-not-last-entry num-entries total start-at limit) + (jiralib--agile-rest-call-it url max-results start-at limit query-params) + ;; last page: call originall callback + (message "jiralib agile retrieve: calling callback") + (setf jiralib-complete-callback complete-callback) + (funcall jiralib-complete-callback + :data (list (cons vk values-list))) + (message "jiralib agile retrieve: all done"))) + ('error (message (format "jiralib agile retrieve: caught error: %s" err))))))) + (jiralib--agile-rest-call-it api max-results start-at limit query-params))) + +(provide 'jiralib) +;;; jiralib.el ends here diff --git a/elpa/org-jira-20240928.2340/org-jira-autoloads.el b/elpa/org-jira-20240928.2340/org-jira-autoloads.el new file mode 100644 index 00000000..7a4569a2 --- /dev/null +++ b/elpa/org-jira-20240928.2340/org-jira-autoloads.el @@ -0,0 +1,178 @@ +;;; org-jira-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from jiralib.el + +(register-definition-prefixes "jiralib" '("jiralib-")) + + +;;; Generated autoloads from org-jira.el + +(autoload 'org-jira-mode "org-jira" "\ +Toggle org-jira mode. + +With no argument, the mode is toggled on/off. +Non-nil argument turns mode on. +Nil argument turns mode off. + +Commands: +\\{org-jira-entry-mode-map} + +Entry to this mode calls the value of `org-jira-mode-hook'. + +This is a minor mode. If called interactively, toggle the `Org-jira +mode' mode. If the prefix argument is positive, enable the mode, and if +it is zero or negative, disable the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable the +mode if ARG is nil, omitted, or is a positive number. Disable the mode +if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate the variable `org-jira-mode'. + +The mode's hook is called both when the mode is enabled and when it is +disabled. + +(fn &optional ARG)" t) +(autoload 'org-jira-get-projects "org-jira" "\ +Get list of projects." t) +(autoload 'org-jira-get-issue-list "org-jira" "\ +Get list of issues, using jql (jira query language), invoke CALLBACK after. + +Default is unresolved issues assigned to current login user; with +a prefix argument you are given the chance to enter your own +jql. + +(fn &optional CALLBACK)") +(autoload 'org-jira-get-summary "org-jira" "\ +Get issue summary from point and place next to issue id from jira" t) +(autoload 'org-jira-get-summary-url "org-jira" "\ +Get issue summary from point and place next to issue id from jira, and make issue id a link" t) +(autoload 'org-jira-get-issues-headonly "org-jira" "\ +Get list of ISSUES, head only. + +The default behavior is to return issues assigned to you and unresolved. + +With a prefix argument, allow you to customize the jql. See +`org-jira-get-issue-list'. + +(fn ISSUES)" t) +(autoload 'org-jira-get-issue "org-jira" "\ +Get a JIRA issue, allowing you to enter the issue-id first. + +(fn ID)" t) +(autoload 'org-jira-get-issues-by-fixversion "org-jira" "\ +Get list of issues by FIXVERSION. + +(fn FIXVERSION)" t) +(autoload 'org-jira-get-issue-project "org-jira" "\ + + +(fn ISSUE)") +(autoload 'org-jira-get-issues "org-jira" "\ +Get list of ISSUES into an org buffer. + +Default is get unfinished issues assigned to you, but you can +customize jql with a prefix argument. +See`org-jira-get-issue-list' + +(fn ISSUES)" t) +(autoload 'org-jira-get-issues-from-custom-jql "org-jira" "\ +Get JQL-LIST list of issues from a custom JQL and PROJ-KEY. + +The PROJ-KEY will act as the file name, while the JQL will be any +valid JQL to populate a file to store PROJ-KEY results in. + +Please note that this is *not* concurrent or race condition +proof. If you try to run multiple calls to this function, it +will mangle things badly, as they rely on globals DEFAULT-JQL and +ORG-JIRA-PROJ-KEY-OVERRIDE being set before and after running. + +(fn &optional JQL-LIST)" t) +(autoload 'org-jira-update-comment "org-jira" "\ +Update a comment for the current issue." t) +(autoload 'org-jira-update-worklogs-from-org-clocks "org-jira" "\ +Update or add a worklog based on the org clocks." t) +(autoload 'org-jira-copy-current-issue-key "org-jira" "\ +Copy the current issue's key into clipboard." t) +(autoload 'org-jira-unassign-issue "org-jira" "\ +Update an issue to be unassigned." t) +(autoload 'org-jira-set-issue-reporter "org-jira" "\ +Update an issue's reporter interactively." t) +(autoload 'org-jira-assign-issue "org-jira" "\ +Update an issue with interactive re-assignment." t) +(autoload 'org-jira-update-issue "org-jira" "\ +Update an issue." t) +(autoload 'org-jira-todo-to-jira "org-jira" "\ +Convert an ordinary todo item to a jira ticket." t) +(autoload 'org-jira-get-subtasks "org-jira" "\ +Get subtasks for the current issue." t) +(autoload 'org-jira-update-issue-labels "org-jira" "\ +Update jira issue labels." t) +(autoload 'org-jira-create-issue "org-jira" "\ +Create an issue in PROJECT, of type TYPE, with given SUMMARY and DESCRIPTION. + +(fn PROJECT TYPE SUMMARY DESCRIPTION)" t) +(autoload 'org-jira-create-subtask "org-jira" "\ +Create a subtask issue for PROJECT, of TYPE, with SUMMARY and DESCRIPTION. + +(fn PROJECT TYPE SUMMARY DESCRIPTION)" t) +(autoload 'org-jira-refresh-issue "org-jira" "\ +Refresh current issue from jira to org." t) +(autoload 'org-jira-progress-issue "org-jira" "\ +Progress issue workflow." t) +(autoload 'org-jira-progress-issue-next "org-jira" "\ +Progress issue workflow." t) +(autoload 'org-jira-browse-issue "org-jira" "\ +Open the current issue in external browser." t) +(autoload 'org-jira-download-attachment "org-jira" "\ +Download the attachment under cursor." t) +(autoload 'org-jira-get-issues-from-filter "org-jira" "\ +Get issues from the server-side stored filter named FILTER. + +Provide this command in case some users are not able to use +client side jql (maybe because of JIRA server version?). + +(fn FILTER)" t) +(autoload 'org-jira-get-issues-from-filter-headonly "org-jira" "\ +Get issues *head only* from saved filter named FILTER. +See `org-jira-get-issues-from-filter'. + +(fn FILTER)" t) +(autoload 'org-jira-get-issues-by-board "org-jira" "\ +Get list of ISSUES from agile board." t) +(autoload 'org-jira-get-issues-by-sprint "org-jira" "\ +Get list of ISSUES from sprint." t) +(autoload 'org-jira-get-issues-by-board-headonly "org-jira" "\ +Get list of ISSUES from agile board, head only." t) +(autoload 'org-jira-get-boards "org-jira" "\ +Get list of boards and their properties." t) +(register-definition-prefixes "org-jira" '("ensure-on-" "org-jira-")) + + +;;; Generated autoloads from org-jira-sdk.el + +(register-definition-prefixes "org-jira-sdk" '("org-jira-sdk-")) + +;;; End of scraped data + +(provide 'org-jira-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; org-jira-autoloads.el ends here diff --git a/elpa/org-jira-20240928.2340/org-jira-pkg.el b/elpa/org-jira-20240928.2340/org-jira-pkg.el new file mode 100644 index 00000000..1dd0f168 --- /dev/null +++ b/elpa/org-jira-20240928.2340/org-jira-pkg.el @@ -0,0 +1,12 @@ +;; -*- no-byte-compile: t; lexical-binding: nil -*- +(define-package "org-jira" "20240928.2340" + "Syncing between Jira and Org-mode." + '((emacs "24.5") + (cl-lib "0.5") + (request "0.2.0") + (dash "2.14.1")) + :url "https://github.com/ahungry/org-jira" + :commit "5f591f5f4abd0ef12c64676e38d8ec3b13eba280" + :revdesc "5f591f5f4abd" + :keywords '("ahungry" "jira" "org" "bug" "tracker") + :maintainers '(("Matthew Carter" . "m@ahungry.com"))) diff --git a/elpa/org-jira-20240928.2340/org-jira-sdk.el b/elpa/org-jira-20240928.2340/org-jira-sdk.el new file mode 100644 index 00000000..49032f50 --- /dev/null +++ b/elpa/org-jira-20240928.2340/org-jira-sdk.el @@ -0,0 +1,227 @@ +;;; org-jira-sdk.el -- SDK Layer for entities + +;; Copyright (C) 2018 Matthew Carter +;; +;; Authors: +;; Matthew Carter +;; +;; Maintainer: Matthew Carter +;; URL: https://github.com/ahungry/org-jira +;; Keywords: ahungry jira org bug tracker +;; Package-Requires: ((emacs "24.5") (cl-lib "0.5") (request "0.2.0") (s "0.0.0")) + +;; This file is not part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see +;; or write to the Free Software +;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +;; 02110-1301, USA. + +;;; Commentary: + +;; This provides an SDK wrapper for more strictly defining the entities we interact with. + +;;; Code: + +(require 'jiralib) +(require 'cl-lib) +(require 'eieio) +(require 'dash) +(require 'cl-generic) + +(defclass org-jira-sdk-record () + ((id :initarg :id :type string :required t) + (data :initarg :data :documentation "The area to hold a big alist of data.") + (hydrate-fn :initform (lambda (id) (message "Not implemented.")))) + "The ID of the record.") + +(defun org-jira-sdk-string-but-first (s) (cl-subseq s 1)) + +(defun org-jira-sdk-to-string (s) (format "%s" s)) + +(defun org-jira-sdk-to-prefixed-string (s) (format "org-jira-sdk-%s" s)) + +(defun org-jira-sdk-record-type-to-symbol (record-type) + (-> record-type symbol-name org-jira-sdk-string-but-first org-jira-sdk-to-prefixed-string intern)) + +(defun org-jira-sdk-create-from-id (record-type id &optional parent-id callback) + (let ((rec (funcall (org-jira-sdk-record-type-to-symbol record-type) + :id (format "%s" id) + :parent-id parent-id))) + (with-slots (data) rec + (setf data (org-jira-sdk-hydrate rec callback)) + (org-jira-sdk-from-data rec)))) + +(defun org-jira-sdk-create-from-data (record-type data) + (let ((rec (funcall (org-jira-sdk-record-type-to-symbol record-type) :data data))) + (org-jira-sdk-from-data rec))) + +(cl-defmethod org-jira-sdk-hydrate ((rec org-jira-sdk-record) &optional callback) + "Populate the record with data from the remote endpoint." + (with-slots (id hydrate-fn) rec + (funcall hydrate-fn id callback))) + +(cl-defgeneric org-jira-sdk-from-data ((rec org-jira-sdk-record))) + +(cl-defmethod org-jira-sdk-dump ((rec org-jira-sdk-record)) + "A decent pretty print/object dump for working with the class items." + (let ((slots (mapcar (lambda (slot) (aref slot 1)) (eieio-class-slots (eieio-object-class rec))))) + (setq slots (cl-remove-if (lambda (s) (not (slot-boundp rec s))) slots)) + (apply #'concat + (mapcar (lambda (slot) + (let ((slot (intern (org-jira-sdk-to-string slot)))) + (format "\n%+16s: %s" slot (slot-value rec (intern (org-jira-sdk-to-string slot))))) + ) + slots)))) + +(defun org-jira-sdk-path (alist key-chain) + "Query a nested path in some type of ALIST by traversing down the keys of KEY-CHAIN." + (cl-reduce (lambda (a k) (alist-get k a)) key-chain :initial-value alist)) + +(defclass org-jira-sdk-issue (org-jira-sdk-record) + ((assignee :type (or null string) :initarg :assignee) + (components :type string :initarg :components) + (labels :type string :initarg :labels) + (created :type string :initarg :created) + (description :type (or null string) :initarg :description) + (duedate :type (or null string) :initarg :duedate) + (headline :type string :initarg :headline) + (id :type string :initarg :id) ; TODO: Probably remove me + (issue-id :type string :initarg :issue-id :documentation "The common ID/key, such as EX-1.") + (issue-id-int :type string :initarg :issue-id-int :documentation "The internal Jira ID, such as 12345.") + (filename :type (or null string) :initarg :filename :documentation "The filename to write issue to.") + (priority :type (or null string) :initarg :priority) + (proj-key :type string :initarg :proj-key) + (reporter :type (or null string) :initarg :reporter) + (resolution :type (or null string) :initarg :resolution) + (sprint :type (or null string) :initarg :sprint) + (start-date :type (or null string) :initarg :start-date) + (status :type string :initarg :status) + (summary :type string :initarg :summary) + (type :type string :initarg :type) + (type-id :type string :initarg :type-id) + (updated :type string :initarg :updated) + (data :initarg :data :documentation "The remote Jira data object (alist).") + (hydrate-fn :initform #'jiralib-get-issue :initarg :hydrate-fn)) + "An issue on the end. ID of the form EX-1, or a numeric such as 10000.") + +(defclass org-jira-sdk-comment (org-jira-sdk-record) + ((author :type string :initarg :author) + (body :type string :initarg :body) + (comment-id :type string :initarg :comment-id :documentation "The comment ID, such as 12345.") + (created :type string :initarg :created) + (headline :type string :initarg :headline) + (parent-id :type string :initarg :parent-id :documentation "The parent issue-id such as EX-1.") + (updated :type string :initarg :updated) + (data :initarg :data :documentation "The reomte Jira data object (alist).") + (hydrate-fn :initform #'jiralib-get-comment :initarg :hydrate-fn))) + +(defclass org-jira-sdk-board (org-jira-sdk-record) + ((name :type string :initarg :name :required t) + (url :type string :initarg :url :required t) + (board-type :type string :initarg :board-type) + (jql :type string :initarg :jql) + (limit :type integer :initarg :limit) + ;; unused + (parent-id :type string :initarg :parent-id) + (hydrate-fn :initform #'jiralib-get-board :initarg :hydrate-fn))) + +(cl-defmethod org-jira-sdk-hydrate ((rec org-jira-sdk-comment) &optional callback) + "Populate the record with data from the remote endpoint." + (with-slots (id proj-key hydrate-fn) rec + (funcall hydrate-fn proj-key id callback))) + +(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-issue)) + (cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys))) + (org-jira-sdk-issue + :assignee (path '(fields assignee displayName)) + :components (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields components)) ", ") + :labels (mapconcat (lambda (c) (format "%s" c)) (mapcar #'identity (path '(fields labels))) ", ") + :created (path '(fields created)) ; confirm + :description (or (path '(fields description)) "") + :duedate (or (path '(fields sprint endDate)) (path '(fields duedate))) ; confirm + :filename (path '(fields project key)) + :headline (path '(fields summary)) ; Duplicate of summary, maybe different. + :id (path '(key)) + :issue-id (path '(key)) + :issue-id-int (path '(id)) + :priority (path '(fields priority name)) + :proj-key (path '(fields project key)) + :reporter (path '(fields reporter displayName)) ; reporter could be an object of its own slot values + :resolution (path '(fields resolution name)) ; confirm + :sprint (path '(fields sprint name)) + :start-date (path '(fields start-date)) ; confirm + :status (org-jira-decode (path '(fields status name))) + :summary (path '(fields summary)) + :type (path '(fields issuetype name)) + :type-id (path '(fields issuetype id)) + :updated (path '(fields updated)) ; confirm + ;; TODO: Remove this + ;; :data (oref rec data) + ))) + +(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-comment)) + (cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys))) + (org-jira-sdk-comment + :author (path '(author displayName)) + :body (path '(body)) + :comment-id (path '(id)) + :created (path '(created)) + :headline (format "Comment: %s" (path '(author displayName))) + :parent-id (if (slot-boundp rec 'parent-id) (oref rec parent-id) "") + :updated (path '(updated)) + ;; TODO: Remove this + ;; :data (oref rec data) + ))) + +(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-board)) + (with-slots (data) rec + (org-jira-sdk-board + :id (int-to-string (alist-get 'id data)) + :name (alist-get 'name data) + :url (alist-get 'self data) + ;; TODO: remove it? used by org-jira-sdk-create-from-id + :parent-id "" + :board-type (alist-get 'type data)))) + +;; Issue +(defun org-jira-sdk-create-issue-from-data (d) (org-jira-sdk-create-from-data :issue d)) +(defun org-jira-sdk-create-issues-from-data-list (ds) (mapcar #'org-jira-sdk-create-issue-from-data ds)) + +;; Issue with custom filename setting +(defun org-jira-sdk-create-issue-from-data-with-filename (filename) + (lambda (d) + (let ((Issue (org-jira-sdk-create-from-data :issue d))) + (setf (oref Issue filename) filename) + Issue))) + +(defun org-jira-sdk-create-issues-from-data-list-with-filename (filename ds) + (let ((fn (org-jira-sdk-create-issue-from-data-with-filename filename))) + (mapcar fn ds))) + +;; Comment +(defun org-jira-sdk-create-comment-from-data (d) (org-jira-sdk-create-from-data :comment d)) +(defun org-jira-sdk-create-comments-from-data-list (ds) (mapcar #'org-jira-sdk-create-comment-from-data ds)) + +(defun org-jira-sdk-isa-record? (i) (cl-typep i 'org-jira-sdk-record)) +(defun org-jira-sdk-isa-issue? (i) (cl-typep i 'org-jira-sdk-issue)) +(defun org-jira-sdk-isa-comment? (i) (cl-typep i 'org-jira-sdk-comment)) + +;; Board +(defun org-jira-sdk-create-board-from-data (d) (org-jira-sdk-create-from-data :board d)) +(defun org-jira-sdk-create-boards-from-data-list (ds) (mapcar #'org-jira-sdk-create-board-from-data ds)) + +(provide 'org-jira-sdk) + +;;; org-jira-sdk.el ends here diff --git a/elpa/org-jira-20240928.2340/org-jira.el b/elpa/org-jira-20240928.2340/org-jira.el new file mode 100644 index 00000000..6bb4a2a7 --- /dev/null +++ b/elpa/org-jira-20240928.2340/org-jira.el @@ -0,0 +1,2630 @@ +;;; org-jira.el --- Syncing between Jira and Org-mode. -*- lexical-binding: t -*- + +;; Copyright (C) 2016-2022 Matthew Carter +;; Copyright (C) 2011 Bao Haojun +;; +;; Authors: +;; Matthew Carter +;; Bao Haojun +;; +;; Maintainer: Matthew Carter +;; URL: https://github.com/ahungry/org-jira +;; Package-Version: 20240928.2340 +;; Package-Revision: 5f591f5f4abd +;; Keywords: ahungry jira org bug tracker +;; Package-Requires: ((emacs "24.5") (cl-lib "0.5") (request "0.2.0") (dash "2.14.1")) + +;; This file is not part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see +;; or write to the Free Software +;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +;; 02110-1301, USA. + +;;; Commentary: + +;; This provides an extension to org-mode for syncing issues with JIRA +;; issue servers. + +;;; News: + +;;;; Changes in 4.4.2 +;; - Fix for org-insert-subheading behavior change in org 9.7+ in render-issues + +;;;; Changes in 4.4.1 +;; - Fix tag (4.3.3 was out of order - we had a 4.4.0 on repo) +;; - Fix for some crazy scoping issue in the org-jira-get-issue-val-from-org function + +;;;; Changes in 4.3.3: +;; - Address issue with assignee property being removed when Unassigned + +;;;; Changes in 4.3.2: +;; - Fixes issues with org-jira-add-comment and org-jira-update-comment + +;;;; Changes in 4.3.1: +;; - Fix to make custom-jql results sync worklogs properly. + +;;;; Changes in 4.3.0: +;; - Allow org-jira-set-issue-reporter call to dynamically set this value. + +;;;; Changes in 4.1.0: +;; - Allow custom-jql to be specified and render in special files (see: README.md). + +;;;; Changes in 4.0.0: +;; - Introduce SDK type for handling records vs random alist structures. + +;;;; Changes since 3.1.0: +;; - Fix how we were ruining the kill-ring with kill calls. + +;;;; Changes since 3.0.0: +;; - Add new org-jira-add-comment call (C-c c c) + +;;;; Changes since 2.8.0: +;; - New version 3.0.0 deprecates old filing mechanism and files +;; all of the changes under the top level ticket header. +;; - If you want other top level headers in the same file, this should +;; work now, as long as they come after the main project one. + +;;;; Changes since 2.7.0: +;; - Clean up multi-buffer handling, disable attachments call until +;; - refresh is compatible with it. + +;;;; Changes since 2.6.3: +;; - Insert worklog import filter in the existing org-jira-update-worklogs-for-current-issue function +;; - Sync up org-clocks and worklogs! Set org-jira-worklog-sync-p to nil to avoid. + +;;;; Changes since 2.6.1: +;; - Fix bug with getting all issues when worklog is an error trigger. + +;;;; Changes since 2.5.4: +;; - Added new org-jira-refresh-issues-in-buffer call and binding + +;;;; Changes since 2.5.3: +;; - Re-introduce the commit that introduced a break into Emacs 25.1.1 list/array push +;; The commit caused updates/comment updates to fail when a blank list of components +;; was present - it will now handle both cases (full list, empty list). + +;;;; Changes since 2.5.2: +;; - Revert a commit that introduced a break into Emacs 25.1.1 list/array push +;; The commit caused updates/comment updates to fail + +;;;; Changes since 2.5.1: +;; - Only set duedate if a DEADLINE is present in the tags and predicate is t + +;;;; Changes since 2.5.0: +;; - Allow overriding the org property names with new defcustom + +;;;; Changes since 2.4.0: +;; - Fix many deprecation/warning issues +;; - Fix error with allow-other-keys not being wrapped in cl-function + +;;;; Changes since 2.3.0: +;; - Integration with org deadline and Jira due date fields + +;;;; Changes since 2.2.0: +;; - Selecting issue type based on project key for creating issues + +;;;; Changes since 2.1.0: +;; - Allow changing to unassigned user +;; - Add new shortcut for quick assignment + +;;;; Changes since 2.0.0: +;; - Change statusCategory to status value +;; - Clean out some redundant code +;; - Add ELPA tags in keywords + +;;;; Changes since 1.0.1: +;; - Converted many calls to async +;; - Removed minor annoyances (position resets etc.) + +;;; Code: + +(eval-when-compile (require 'cl)) +(require 'org) +(require 'org-clock) +(require 'cl-lib) +(require 'url) +(require 'ls-lisp) +(require 'dash) +(require 'jiralib) +(require 'org-jira-sdk) + +(defconst org-jira-version "4.4.2" + "Current version of org-jira.el.") + +(defgroup org-jira nil + "Customization group for org-jira." + :tag "Org JIRA" + :group 'org) + +(defcustom org-jira-working-dir "~/.org-jira" + "Folder under which to store org-jira working files." + :group 'org-jira + :type 'directory) + +(defcustom org-jira-project-filename-alist nil + "Alist translating project keys to filenames. + +Each element has a structure: + + (PROJECT-KEY . NEW-FILE-NAME) + +where both are strings. NEW-FILE-NAME is relative to +`org-jira-working-dir'." + :group 'org-jira + :type '(alist :key-type (string :tag "Project key") + :value-type (string :tag "File name for this project"))) + +(defcustom org-jira-default-jql + "assignee = currentUser() and resolution = unresolved ORDER BY + priority DESC, created ASC" + "Default jql for querying your Jira tickets." + :group 'org-jira + :type 'string) + +(defcustom org-jira-ignore-comment-user-list + '("admin") + "Jira usernames that should have comments ignored." + :group 'org-jira + :type '(repeat (string :tag "Jira username:"))) + +(defcustom org-jira-reverse-comment-order nil + "If non-nil, order comments from most recent to least recent." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-done-states + '("Closed" "Resolved" "Done") + "Jira states that should be considered as DONE for `org-mode'." + :group 'org-jira + :type '(repeat (string :tag "Jira state name:"))) + +(defcustom org-jira-users + '(("Full Name" . "account-id")) + "A list of displayName and key pairs." + :group 'org-jira + :type 'list) + +(defcustom org-jira-progress-issue-flow + '(("To Do" . "In Progress") + ("In Progress" . "Done")) + "Quickly define a common issue flow." + :group 'org-jira + :type 'list) + +(defcustom org-jira-property-overrides (list) + "An assoc list of property tag overrides. + +The KEY . VAL pairs should both be strings. + +For instance, to change the :assignee: property in the org :PROPERTIES: +block to :WorkedBy:, you can set this as such: + + (setq org-jira-property-overrides (list (cons \"assignee\" \"WorkedBy\"))) + +or simply: + + (add-to-list (quote org-jira-property-overrides) + (cons (\"assignee\" \"WorkedBy\"))) + +This will work for most any of the properties, with the +exception of ones with unique functionality, such as: + + - deadline + - summary + - description" + :group 'org-jira + :type 'list) + +(defcustom org-jira-serv-alist nil + "Association list to set information for each jira server. +Each element of the alist is a jira server name. The CAR of each +element is a string, uniquely identifying the server. The CDR of +each element is a well-formed property list with an even number +of elements, alternating keys and values, specifying parameters +for the server. + + (:property value :property value ... ) + +When a property is given a value in org-jira-serv-alist, its +setting overrides the value of the corresponding user +variable (if any) during syncing. + +Most properties are optional, but some should always be set: + + :url soap url of the jira server. + :username username to be used. + :host hostname of the jira server (TODO: compute it from ~url~). + +All the other properties are optional. They override the global +variables. + + :password password to be used, will be prompted if missing." + :group 'org-jira + :type '(alist :value-type plist)) + +(defcustom org-jira-use-status-as-todo nil + "Use the JIRA status as the TODO tag value." + :group 'org-jira) + +(defcustom org-jira-deadline-duedate-sync-p t + "Keep org deadline and jira duedate fields synced. +You may wish to set this to nil if you track org deadlines in +your buffer that you do not want to send back to your Jira +instance." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-worklog-sync-p t + "Keep org clocks and jira worklog fields synced. +You may wish to set this to nil if you track org clocks in +your buffer that you do not want to send back to your Jira +instance." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-download-dir "~/Downloads" + "Name of the default jira download library." + :group 'org-jira + :type 'string) + +(defcustom org-jira-download-ask-override t + "Ask before overriding tile." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-jira-status-to-org-keyword-alist nil + "Custom alist of jira status stored in car and `org-mode' keyword stored in cdr." + :group 'org-jira + :type '(alist :key-type string :value-type string)) + +(defcustom org-jira-priority-to-org-priority-omit-default-priority nil + "Whether to omit insertion of priority when it matches the default. + +When set to t, will omit the insertion of the matched value from +`org-jira-priority-to-org-priority-alist' when it matches the `org-default-priority'." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-priority-to-org-priority-alist nil + "Alist mapping jira priority keywords to `org-mode' priority cookies. + +A sample value might be + (list (cons \"High\" ?A) + (cons \"Medium\" ?B) + (cons \"Low\" ?C)). + +See `org-default-priority' for more info." + :group 'org-jira + :type '(alist :key-type string :value-type character)) + +(defcustom org-jira-boards-default-limit 50 + "Default limit for number of issues retrieved from agile boards." + :group 'org-jira + :type 'integer) + +;; FIXME: Issue with using this - issues are grouped under a headline incorrectly. +(defcustom org-jira-custom-jqls + '( + (:jql " assignee = currentUser() and createdDate < '2019-01-01' order by created DESC " + :limit 100 + :filename "last-years-work") + (:jql " assignee = currentUser() and createdDate >= '2019-01-01' order by created DESC " + :limit 100 + :filename "this-years-work") + ) + "A list of plists with :jql and :filename keys to run arbitrary user JQL." + :group 'org-jira + :type '(alist :value-type plist)) + +(defcustom org-jira-download-comments t + "Set to nil if you don't want to update comments during issue rendering." + :group 'org-jira + :type 'boolean) + +(defcustom org-jira-update-issue-details-include-reporter t + "For Jira Cloud API we will get an error if `reporter' is sent with an update request." + :group 'org-jira + :type 'string) + +(defvar org-jira-serv nil + "Parameters of the currently selected blog.") + +(defvar org-jira-serv-name nil + "Name of the blog, to pick from `org-jira-serv-alist'.") + +(defvar org-jira-projects-list nil + "List of jira projects.") + +(defvar org-jira-current-project nil + "Currently selected (i.e., active project).") + +(defvar org-jira-issues-list nil + "List of jira issues under the current project.") + +(defvar org-jira-server-rpc-url nil + "Jira server soap URL.") + +(defvar org-jira-server-userid nil + "Jira server user id.") + +(defvar org-jira-proj-id nil + "Jira project ID.") + +(defvar org-jira-logged-in nil + "Flag whether user is logged-in or not.") + +(defvar org-jira-buffer-name "*org-jira-%s*" + "Name of the jira buffer.") + +(defvar org-jira-buffer-kill-prompt t + "Ask before killing buffer.") + +(make-variable-buffer-local 'org-jira-buffer-kill-prompt) + +(defvar org-jira-mode-hook nil + "Hook to run upon entry into mode.") + +(defvar org-jira-issue-id-history '() + "Prompt history for issue id.") + +(defvar org-jira-fixversion-id-history '() + "Prompt history for fixversion id.") + +(defvar org-jira-verbosity 'debug) + +(defun org-jira-log (s) (when (eq 'debug org-jira-verbosity) (message "%s" s))) + +(defmacro ensure-on-issue (&rest body) + "Make sure we are on an issue heading, before executing BODY." + (declare (debug t) + (indent 0)) + `(save-excursion + (save-restriction + (widen) + (unless (looking-at "^\\*\\* ") + (search-backward-regexp "^\\*\\* " nil t)) ; go to top heading + (let ((org-jira-id (org-jira-id))) + (unless (and org-jira-id (string-match (jiralib-get-issue-regexp) (downcase org-jira-id))) + (error "Not on an issue region!"))) + ,@body))) + +(defmacro org-jira-with-callback (&rest body) + "Simpler way to write the data BODY callbacks." + (declare (debug t) + (indent 0)) + `(lambda (&rest request-response) + (declare (ignore cb-data)) + (let ((cb-data (cl-getf request-response :data))) + ,@body))) + +(defmacro org-jira-freeze-ui (&rest body) + "Freeze the UI layout for the user as much as possible." + (declare (debug t) + (indent 0)) + `(save-excursion + (save-restriction + (widen) + (org-save-outline-visibility t + (outline-show-all) + ,@body)))) + +(defvar org-jira-proj-key-override nil + "String. An override for the proj-key. Set to nil to restore old behavior.") + +;; We want some hooking system to override default-jql + this. +(defun org-jira--get-proj-key (issue-id) + "Get the proper proj-key. Typically derived from ISSUE-ID." + (if org-jira-proj-key-override org-jira-proj-key-override + (replace-regexp-in-string "-.*" "" issue-id))) + +(defun org-jira--get-proj-key-from-issue (Issue) + "Get the proper proj-key from an ISSUE instance." + (oref Issue filename)) + +;; TODO: Merge these 3 ensure macros (or, scrap all but ones that work on Issue) +(defmacro ensure-on-issue-id (issue-id &rest body) + "Just do some work on ISSUE-ID, execute BODY." + (declare (debug t) + (indent 1)) + (let ((issue-id-var (make-symbol "issue-id"))) + `(let* ((,issue-id-var ,issue-id) + (proj-key (org-jira--get-proj-key ,issue-id-var)) + (project-file (org-jira--get-project-file-name proj-key)) + (project-buffer (or (find-buffer-visiting project-file) + (find-file project-file)))) + (with-current-buffer project-buffer + (org-jira-freeze-ui + (let ((p (org-find-entry-with-id ,issue-id-var))) + (unless p (error "Issue %s not found!" ,issue-id-var)) + (goto-char p) + (org-narrow-to-subtree) + ,@body)))))) + +(defmacro ensure-on-issue-id-with-filename (issue-id filename &rest body) + "Just do some work on ISSUE-ID, execute BODY." + (declare (debug t) + (indent 1)) + (let ((issue-id-var (make-symbol "issue-id")) + (filename-var (make-symbol "filename"))) + `(let* ((,issue-id-var ,issue-id) + (,filename-var ,filename) + (proj-key ,filename-var) + (project-file (org-jira--get-project-file-name proj-key)) + (project-buffer (or (find-buffer-visiting project-file) + (find-file project-file)))) + (with-current-buffer project-buffer + (org-jira-freeze-ui + (let ((p (org-find-entry-with-id ,issue-id-var))) + (unless p (error "Issue %s not found!" ,issue-id-var)) + (goto-char p) + (org-narrow-to-subtree) + ,@body)))))) + +(defmacro ensure-on-issue-Issue (Issue &rest body) + "Just do some work on ISSUE, execute BODY." + (declare (debug t) + (indent 1)) + (let ((Issue-var (make-symbol "Issue"))) + `(let ((,Issue-var ,Issue)) + (with-slots (issue-id) ,Issue-var + (let* ((proj-key (org-jira--get-proj-key-from-issue ,Issue-var)) + (project-file (org-jira--get-project-file-name proj-key)) + (project-buffer (or (find-buffer-visiting project-file) + (find-file project-file)))) + (with-current-buffer project-buffer + (org-jira-freeze-ui + (let ((p (org-find-entry-with-id issue-id))) + (unless p (error "Issue %s not found!" issue-id)) + (goto-char p) + (org-narrow-to-subtree) + ,@body)))))))) + +(defmacro ensure-on-todo (&rest body) + "Make sure we are on an todo heading, before executing BODY." + (declare (debug t) + (indent 0)) + `(save-excursion + (save-restriction + (let ((continue t) + (on-todo nil)) + (while continue + (when (org-get-todo-state) + (setq continue nil on-todo t)) + (unless (and continue (org-up-heading-safe)) + (setq continue nil))) + (if (not on-todo) + (error "TODO not found") + (org-narrow-to-subtree) + ,@body))))) + +(defmacro ensure-on-comment (&rest body) + "Make sure we are on a comment heading, before executing BODY." + (declare (debug t) + (indent 0)) + `(save-excursion + (org-back-to-heading) + (forward-thing 'whitespace) + (unless (looking-at "Comment:") + (error "Not on a comment region!")) + (save-restriction + (org-narrow-to-subtree) + ,@body))) + +(defmacro ensure-on-worklog (&rest body) + "Make sure we are on a worklog heading, before executing BODY." + (declare (debug t) + (indent 0)) + `(save-excursion + (org-back-to-heading) + (forward-thing 'whitespace) + (unless (looking-at "Worklog:") + (error "Not on a worklog region!")) + (save-restriction + (org-narrow-to-subtree) + ,@body))) + +(defun org-jira--ensure-working-dir () + "Ensure that the org-jira-working-dir exists" + (unless (file-exists-p org-jira-working-dir) + (error (format "org-jira directory does not exist! Run (make-directory \"%s\")" org-jira-working-dir)) + ) + org-jira-working-dir + ) + +(defvar org-jira-entry-mode-map + (let ((org-jira-map (make-sparse-keymap))) + (define-key org-jira-map (kbd "C-c pg") 'org-jira-get-projects) + (define-key org-jira-map (kbd "C-c bg") 'org-jira-get-boards) + (define-key org-jira-map (kbd "C-c iv") 'org-jira-get-issues-by-board) + (define-key org-jira-map (kbd "C-c ib") 'org-jira-browse-issue) + (define-key org-jira-map (kbd "C-c ig") 'org-jira-get-issues) + (define-key org-jira-map (kbd "C-c ij") 'org-jira-get-issues-from-custom-jql) + (define-key org-jira-map (kbd "C-c ih") 'org-jira-get-issues-headonly) + ;;(define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-from-filter-headonly) + ;;(define-key org-jira-map (kbd "C-c iF") 'org-jira-get-issues-from-filter) + (define-key org-jira-map (kbd "C-c il") 'org-jira-update-issue-labels) + (define-key org-jira-map (kbd "C-c iu") 'org-jira-update-issue) + (define-key org-jira-map (kbd "C-c iw") 'org-jira-progress-issue) + (define-key org-jira-map (kbd "C-c in") 'org-jira-progress-issue-next) + (define-key org-jira-map (kbd "C-c ia") 'org-jira-assign-issue) + ;(define-key org-jira-map (kbd "C-c isr") 'org-jira-set-issue-reporter) + (define-key org-jira-map (kbd "C-c ir") 'org-jira-refresh-issue) + (define-key org-jira-map (kbd "C-c iR") 'org-jira-refresh-issues-in-buffer) + (define-key org-jira-map (kbd "C-c ic") 'org-jira-create-issue) + (define-key org-jira-map (kbd "C-c ik") 'org-jira-copy-current-issue-key) + (define-key org-jira-map (kbd "C-c sc") 'org-jira-create-subtask) + (define-key org-jira-map (kbd "C-c sg") 'org-jira-get-subtasks) + (define-key org-jira-map (kbd "C-c cc") 'org-jira-add-comment) + (define-key org-jira-map (kbd "C-c cu") 'org-jira-update-comment) + (define-key org-jira-map (kbd "C-c wu") 'org-jira-update-worklogs-from-org-clocks) + (define-key org-jira-map (kbd "C-c tj") 'org-jira-todo-to-jira) + (define-key org-jira-map (kbd "C-c if") 'org-jira-get-issues-by-fixversion) + org-jira-map)) + +;;;###autoload +(define-minor-mode org-jira-mode + "Toggle org-jira mode. +With no argument, the mode is toggled on/off. +Non-nil argument turns mode on. +Nil argument turns mode off. + +Commands: +\\{org-jira-entry-mode-map} + +Entry to this mode calls the value of `org-jira-mode-hook'." + + :init-value nil + :lighter " jira" + :group 'org-jira + :keymap org-jira-entry-mode-map + + (if org-jira-mode + (progn + (set (make-local-variable 'org-element-use-cache) nil) + (run-mode-hooks 'org-jira-mode-hook)) + (progn + (kill-local-variable 'org-element-use-cache)))) + +(defun org-jira-maybe-activate-mode () + "Having hooks can be an expensive operation, and they are invoked each +time the mode is started - we should only ever re-activate the mode if +it isn't already on." + (unless (bound-and-true-p org-jira-mode) (org-jira-mode t))) + +(defun org-jira-get-project-name (proj) + (org-jira-find-value proj 'key)) + +(defun org-jira-find-value (l &rest keys) + (let* (key exists) + (while (and keys (listp l)) + (setq key (car keys)) + (setq exists nil) + (mapc (lambda (item) + (when (equal key (car item)) + (setq exists t))) + (if (and (listp l) + (listp (car l))) + l + nil)) + (setq keys (cdr keys)) + (if exists + (setq l (cdr (assoc key l))) + (setq l (or (cdr (assoc key l)) l)))) + l)) + +(defun org-jira--get-project-file-name (project-key) + "Translate PROJECT-KEY into filename." + (-if-let (translation (cdr (assoc project-key org-jira-project-filename-alist))) + (expand-file-name translation (org-jira--ensure-working-dir)) + (expand-file-name (concat project-key ".org") (org-jira--ensure-working-dir)))) + +(defun org-jira-get-project-lead (proj) + (org-jira-find-value proj 'lead 'name)) + +;; This is mapped to accountId and not username, so we need nil not blank string. +(defun org-jira-get-assignable-users (project-key) + "Get the list of assignable users for PROJECT-KEY, adding user set jira-users first." + (append + '(("Unassigned" . nil)) + org-jira-users + (mapcar (lambda (user) + (cons (org-jira-decode (cdr (assoc 'displayName user))) + (org-jira-decode (cdr (assoc 'accountId user))))) + (jiralib-get-users project-key)))) + +(defun org-jira-get-reporter-candidates (project-key) + "Get the list of assignable users for PROJECT-KEY, adding user set jira-users first." + (append + org-jira-users + (mapcar (lambda (user) + (cons (org-jira-decode (cdr (assoc 'displayName user))) + (org-jira-decode (cdr (assoc 'accountId user))))) + (jiralib-get-users project-key)))) + +(defun org-jira-entry-put (pom property value) + "Similar to org-jira-entry-put, but with an optional alist of overrides. + +At point-or-marker POM, set PROPERTY to VALUE. + +Look at customizing org-jira-property-overrides if you want +to change the property names this sets." + (unless (stringp property) + (setq property (symbol-name property))) + (let ((property (or (assoc-default property org-jira-property-overrides) + property))) + (org-entry-put pom property (org-jira-decode value)))) + +(defun org-jira-kill-line () + "Kill the line, without 'kill-line' side-effects of altering kill ring." + (interactive) + (delete-region (point) (line-end-position))) + +;; Appropriated from org.el +(defun org-jira-org-kill-line (&optional _arg) + "Kill line, to tags or end of line." + (cond + ((or (not org-special-ctrl-k) + (bolp) + (not (org-at-heading-p))) + (when (and (get-char-property (min (point-max) (point-at-eol)) 'invisible) + org-ctrl-k-protect-subtree + (or (eq org-ctrl-k-protect-subtree 'error) + (not (y-or-n-p "Kill hidden subtree along with headline? ")))) + (user-error "C-k aborted as it would kill a hidden subtree")) + (call-interactively + (if (bound-and-true-p visual-line-mode) 'kill-visual-line 'org-jira-kill-line))) + ((looking-at ".*?\\S-\\([ \t]+\\(:[[:alnum:]_@#%:]+:\\)\\)[ \t]*$") + (delete-region (point) (match-beginning 1)) + (org-set-tags nil t)) + (t (delete-region (point) (point-at-eol))))) + +;;;###autoload +(defun org-jira-get-projects () + "Get list of projects." + (interactive) + (let ((projects-file (expand-file-name "projects-list.org" (org-jira--ensure-working-dir)))) + (or (find-buffer-visiting projects-file) + (find-file projects-file)) + (org-jira-maybe-activate-mode) + (save-excursion + (let* ((oj-projs (jiralib-get-projects))) + (mapc (lambda (proj) + (let* ((proj-key (org-jira-find-value proj 'key)) + (proj-headline (format "Project: [[file:%s.org][%s]]" proj-key proj-key))) + (save-restriction + (widen) + (goto-char (point-min)) + (outline-show-all) + (setq p (org-find-exact-headline-in-buffer proj-headline)) + (if (and p (>= p (point-min)) + (<= p (point-max))) + (progn + (goto-char p) + (org-narrow-to-subtree) + (end-of-line)) + (goto-char (point-max)) + (unless (looking-at "^") + (insert "\n")) + (insert "* ") + (org-jira-insert proj-headline) + (org-narrow-to-subtree)) + (org-jira-entry-put (point) "name" (org-jira-get-project-name proj)) + (org-jira-entry-put (point) "key" (org-jira-find-value proj 'key)) + (org-jira-entry-put (point) "lead" (org-jira-get-project-lead proj)) + (org-jira-entry-put (point) "ID" (org-jira-find-value proj 'id)) + (org-jira-entry-put (point) "url" (format "%s/browse/%s" (replace-regexp-in-string "/*$" "" jiralib-url) (org-jira-find-value proj 'key)))))) + oj-projs))))) + +(defun org-jira-get-issue-components (issue) + "Return the components the ISSUE belongs to." + (mapconcat + (lambda (comp) + (org-jira-find-value comp 'name)) + (org-jira-find-value issue 'fields 'components) ", ")) + +(defun org-jira-get-issue-labels (issue) + "Return the labels the ISSUE belongs to." + (org-jira-find-value issue 'fields 'labels)) + +(defun org-jira-decode (data) + "Decode text DATA. + +It must receive a coercion to string, as not every time will it +be populated." + (decode-coding-string + (cl-coerce data 'string) jiralib-coding-system)) + +(defun org-jira-insert (&rest args) + "Set coding to text provide by `ARGS' when insert in buffer." + (insert (org-jira-decode (apply #'concat args)))) + +(defun org-jira-transform-time-format (jira-time-str) + "Convert JIRA-TIME-STR to format \"%Y-%m-%d %T\". + +Example: \"2012-01-09T08:59:15.000Z\" becomes \"2012-01-09 +16:59:15\", with the current timezone being +0800." + (condition-case () + (format-time-string + "%Y-%m-%d %T" + (apply + 'encode-time + (parse-time-string (replace-regexp-in-string "T\\|\\.000" " " jira-time-str)))) + (error jira-time-str))) + +(defun org-jira--fix-encode-time-args (arg) + "Fix ARG for 3 nil values at the head." + (cl-loop + for n from 0 to 2 by 1 do + (when (not (nth n arg)) + (setcar (nthcdr n arg) 0))) + arg) + +(defun org-jira-time-format-to-jira (org-time-str) + "Convert ORG-TIME-STR back to jira time format." + (condition-case () + (format-time-string + "%Y-%m-%dT%T.000Z" + (apply 'encode-time + (org-jira--fix-encode-time-args (parse-time-string org-time-str))) + t) + (error org-time-str))) + +(defun org-jira-get-comment-val (key comment) + "Return the value associated with KEY of COMMENT." + (org-jira-get-issue-val key comment)) + +(defun org-jira-time-stamp-to-org-clock (time-stamp) + "Convert TIME-STAMP into org-clock format." + (format-time-string "%Y-%m-%d %a %H:%M" time-stamp)) + +(defun org-jira-date-to-org-clock (date) + "Convert DATE into a time stamp and then into org-clock format. +Expects a date in format such as: 2017-02-26T00:08:00.000-0500." + (org-jira-time-stamp-to-org-clock (date-to-time date))) + +(defun org-jira-worklogs-to-org-clocks (worklogs) + "Get a list of WORKLOGS and convert to org-clocks." + (mapcar + (lambda (worklog) + (let ((wl-start (cdr (assoc 'started worklog))) + (wl-time (cdr (assoc 'timeSpentSeconds worklog))) + (wl-end)) + (setq wl-start (org-jira-date-to-org-clock wl-start)) + (setq wl-end (org-jira-time-stamp-to-org-clock (time-add (date-to-time wl-start) wl-time))) + (list + wl-start + wl-end + (cdr (assoc 'comment worklog)) + (cdr (assoc 'id worklog)) + ) + )) + worklogs) + ) + +(defun org-jira-format-clock (clock-entry) + "Format a CLOCK-ENTRY given the (list start end). +This format is typically generated from org-jira-worklogs-to-org-clocks call." + (format "CLOCK: [%s]--[%s]" (car clock-entry) (cadr clock-entry))) + +(defun org-jira-insert-clock (clock-entry) + "Insert a CLOCK-ENTRY given the (list start end). +This format is typically generated from org-jira-worklogs-to-org-clocks call." + (insert (org-jira-format-clock clock-entry)) + (org-beginning-of-line) + (org-ctrl-c-ctrl-c) ;; @TODO Maybe not call directly? does it matter? - used to resync the clock estimate + (org-end-of-line) + (insert "\n") + (insert (format " :id: %s\n" (cadddr clock-entry))) + (when (caddr clock-entry) (insert (replace-regexp-in-string "^\\*" "-" (format " %s\n" (org-jira-decode (caddr clock-entry)))))) ;; No comment is nil, so don't print it + ) + +(defun org-jira-logbook-reset (issue-id filename &optional clocks) + "Find logbook for ISSUE-ID in FILENAME, delete it. +Re-create it with CLOCKS. This is used for worklogs." + (interactive) + (let ((existing-logbook-p nil)) + ;; See if the LOGBOOK already exists or not. + (ensure-on-issue-id-with-filename issue-id filename + (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK"))) + (when (search-forward (format ":%s:" drawer-name) nil 1 1) + (setq existing-logbook-p t)))) + (ensure-on-issue-id-with-filename issue-id filename + (let ((drawer-name (or (org-clock-drawer-name) "LOGBOOK"))) + (if existing-logbook-p + (progn ;; If we had a logbook, drop it and re-create in a bit. + (search-forward (format ":%s:" drawer-name) nil 1 1) + (org-beginning-of-line) + (delete-region (point) (search-forward ":END:" nil 1 1)) + ) + (progn ;; Otherwise, create a new one at the end of properties list + (search-forward ":END:" nil 1 1) + (forward-line))) + (org-insert-drawer nil (format "%s" drawer-name)) ;; Doc says non-nil, but this requires nil + (mapc #'org-jira-insert-clock clocks) + ;; Clean up leftover newlines (we left 2 behind) + (dotimes (n 2) + (search-forward-regexp "^$" nil 1 1) + (delete-region (point) (min (point-max) (1+ (point))))))))) + +(defun org-jira-get-worklog-val (key WORKLOG) + "Return the value associated with KEY of WORKLOG." + (org-jira-get-comment-val key WORKLOG)) + +(defun org-jira-get-issue-val (key issue) + "Return the value associated with key KEY of issue ISSUE." + (let ((tmp (or (org-jira-find-value issue 'fields key 'key) ""))) ;; For project, we need a key, not the name... + (unless (stringp tmp) + (setq tmp (org-jira-find-value issue key))) + (unless (stringp tmp) + (setq tmp (org-jira-find-value issue 'fields key 'displayName))) + (unless (stringp tmp) + (setq tmp "")) + (cond ((eq key 'components) + (org-jira-get-issue-components issue)) + ((eq key 'labels) + (org-jira-get-issue-labels issue)) + ((member key '(created updated startDate)) + (org-jira-transform-time-format tmp)) + ((eq key 'status) + (if jiralib-use-restapi + (org-jira-find-value issue 'fields 'status 'name) + (org-jira-find-value (jiralib-get-statuses) tmp))) + ((eq key 'resolution) + (if jiralib-use-restapi + tmp + (if (string= tmp "") + "" + (org-jira-find-value (jiralib-get-resolutions) tmp)))) + ((eq key 'type) + (if jiralib-use-restapi + (org-jira-find-value issue 'fields 'issuetype 'name) + (org-jira-find-value (jiralib-get-issue-types) tmp))) + ((eq key 'priority) + (if jiralib-use-restapi + (org-jira-find-value issue 'fields 'priority 'name) + (org-jira-find-value (jiralib-get-priorities) tmp))) + ((eq key 'description) + (org-trim tmp)) + (t + tmp)))) + +(defvar org-jira-jql-history nil) + +;;;###autoload +(defun org-jira-get-issue-list (&optional callback) + "Get list of issues, using jql (jira query language), invoke CALLBACK after. + +Default is unresolved issues assigned to current login user; with +a prefix argument you are given the chance to enter your own +jql." + (org-jira-log (format "I was called, was it with a callback? %s" (if callback "yes" "no"))) + (let ((jql org-jira-default-jql)) + (when current-prefix-arg + (setq jql (read-string "Jql: " + (if org-jira-jql-history + (car org-jira-jql-history) + "assignee = currentUser() and resolution = unresolved") + 'org-jira-jql-history + "assignee = currentUser() and resolution = unresolved"))) + (list (jiralib-do-jql-search jql nil callback)))) + +(defun org-jira-get-issue-by-id (id) + "Get an issue by its ID." + (push id org-jira-issue-id-history) + (let ((jql (format "id = %s" id))) + (jiralib-do-jql-search jql))) + +(defun org-jira-get-issue-by-fixversion (fixversion-id) + "Get an issue by its FIXVERSION-ID." + (push fixversion-id org-jira-fixversion-id-history) + (let ((jql (format "fixVersion = \"%s\"" fixversion-id))) + (jiralib-do-jql-search jql))) + +;;;###autoload +(defun org-jira-get-summary () + "Get issue summary from point and place next to issue id from jira" + (interactive) + (let ((jira-id (thing-at-point 'symbol))) + (unless jira-id (error "ORG_JIRA_ERROR: JIRA-ID missing in org-jira-get-summary!")) + (forward-symbol 1) + (insert (format " - %s" + (cdr (assoc 'summary (assoc 'fields (car (org-jira-get-issue-by-id jira-id))))))))) + +;;;###autoload +(defun org-jira-get-summary-url () + "Get issue summary from point and place next to issue id from jira, and make issue id a link" + (interactive) + (let ((jira-id (thing-at-point 'symbol))) + (insert (format "[[%s][%s]] - %s" + (concatenate 'string jiralib-url "browse/" jira-id) jira-id + (cdr (assoc 'summary (car (org-jira-get-issue-by-id jira-id)))))))) + +;;;###autoload +(defun org-jira-get-issues-headonly (issues) + "Get list of ISSUES, head only. + +The default behavior is to return issues assigned to you and unresolved. + +With a prefix argument, allow you to customize the jql. See +`org-jira-get-issue-list'." + + (interactive + (org-jira-get-issue-list)) + + (let* ((issues-file (expand-file-name "issues-headonly.org" (org-jira--ensure-working-dir))) + (issues-headonly-buffer (or (find-buffer-visiting issues-file) + (find-file issues-file)))) + (with-current-buffer issues-headonly-buffer + (widen) + (delete-region (point-min) (point-max)) + + (mapc (lambda (issue) + (let ((issue-id (org-jira-get-issue-key issue)) + (issue-summary (org-jira-get-issue-summary issue))) + (org-jira-insert (format "- [jira:%s] %s\n" issue-id issue-summary)))) + issues)) + (switch-to-buffer issues-headonly-buffer))) + +;;;###autoload +(defun org-jira-get-issue (id) + "Get a JIRA issue, allowing you to enter the issue-id first." + (interactive (list (read-string "Issue ID: " "" 'org-jira-issue-id-history))) + (org-jira-get-issues (org-jira-get-issue-by-id id)) + (let ((issue-pos (org-find-entry-with-id id))) + (when issue-pos + (goto-char issue-pos) + (recenter 0)))) +;;;###autoload +(defun org-jira-get-issues-by-fixversion (fixversion) + "Get list of issues by FIXVERSION." + (interactive (list (read-string "Fixversion ID: " "" + 'org-jira-fixversion-id-history))) + (org-jira-get-issues (org-jira-get-issue-by-fixversion fixversion))) + +;;;###autoload +(defun org-jira-get-issue-project (issue) + (org-jira-find-value issue 'fields 'project 'key)) + +(defun org-jira-get-issue-key (issue) + (org-jira-find-value issue 'key)) + +(defun org-jira-get-issue-summary (issue) + (org-jira-find-value issue 'fields 'summary)) + +(defvar org-jira-get-issue-list-callback + (cl-function + (lambda (&key data &allow-other-keys) + "Callback for async, DATA is the response from the request call. + +Will send a list of org-jira-sdk-issue objects to the list printer." + (org-jira-log "Received data for org-jira-get-issue-list-callback.") + (--> data + (org-jira-sdk-path it '(issues)) + (append it nil) ; convert the conses into a proper list. + org-jira-sdk-create-issues-from-data-list + org-jira-get-issues)))) + +(defvar org-jira-get-sprint-list-callback + (cl-function + (lambda (&key data &allow-other-keys) + "Callback for async, DATA is the response from the request call. + +Will send a list of org-jira-sdk-issue objects to the list printer." + (org-jira-log "Received data for org-jira-get-sprint-list-callback.") + (--> data + (org-jira-sdk-path it '(sprint)) + (append it nil) ; convert the conses into a proper list. + org-jira-sdk-create-issues-from-data-list + org-jira-get-issues)))) + + +;;;###autoload +(defun org-jira-get-issues (issues) + "Get list of ISSUES into an org buffer. + +Default is get unfinished issues assigned to you, but you can +customize jql with a prefix argument. +See`org-jira-get-issue-list'" + ;; If the user doesn't provide a default, async call to build an issue list + ;; from the JQL style query + (interactive + (org-jira-get-issue-list org-jira-get-issue-list-callback)) + (org-jira-log "Fetching issues...") + (when (> (length issues) 0) + (org-jira--render-issues-from-issue-list issues))) + +(defvar org-jira-original-default-jql nil) + +(defun org-jira-get-issues-from-custom-jql-callback (filename list) + "Generate a function that we can iterate over FILENAME and LIST with when callback finishes." + (cl-function + (lambda (&key data &allow-other-keys) + "Callback for async, DATA is the response from the request call. + +Will send a list of org-jira-sdk-issue objects to the list printer." + (org-jira-log "Received data for org-jira-get-issues-from-custom-jql-callback.") + (--> data + (org-jira-sdk-path it '(issues)) + (append it nil) ; convert the conses into a proper list. + (org-jira-sdk-create-issues-from-data-list-with-filename filename it) + org-jira-get-issues) + (setq org-jira-proj-key-override nil) + (let ((next (rest list))) + (when next + (org-jira-get-issues-from-custom-jql next)))))) + +;;;###autoload +(defun org-jira-get-issues-from-custom-jql (&optional jql-list) + "Get JQL-LIST list of issues from a custom JQL and PROJ-KEY. + +The PROJ-KEY will act as the file name, while the JQL will be any +valid JQL to populate a file to store PROJ-KEY results in. + +Please note that this is *not* concurrent or race condition +proof. If you try to run multiple calls to this function, it +will mangle things badly, as they rely on globals DEFAULT-JQL and +ORG-JIRA-PROJ-KEY-OVERRIDE being set before and after running." + (interactive) + (let* ((jl (or jql-list org-jira-custom-jqls)) + (uno (car jl)) + (filename (cl-getf uno :filename)) + (limit (cl-getf uno :limit)) + (jql (replace-regexp-in-string "[\n]" " " (cl-getf uno :jql)))) + (setq org-jira-proj-key-override filename) + (jiralib-do-jql-search jql limit (org-jira-get-issues-from-custom-jql-callback filename jl)))) + +(defun org-jira--get-project-buffer (Issue) + (let* ((proj-key (org-jira--get-proj-key-from-issue Issue)) + (project-file (org-jira--get-project-file-name proj-key)) + (project-buffer (find-file-noselect project-file))) + project-buffer)) + +(defun org-jira--is-top-headline? (proj-key) + "For PROJ-KEY, check if it is a top headline or not." + (let ((elem (org-element-at-point))) + (and (eq 'headline (car elem)) + (equal (format "%s-Tickets" proj-key) + (org-element-property :title elem)) + (= 1 (org-element-property :level elem))))) + +(defun org-jira--maybe-render-top-heading (proj-key) + "Ensure that there is a headline for PROJ-KEY at the top of the file." + (goto-char (point-min)) + (let ((top-heading (format ".*%s-Tickets" proj-key)) + (th-found? nil)) + (while (and (not (eobp)) + (not th-found?)) + (beginning-of-line) + (when (org-jira--is-top-headline? proj-key) (setq th-found? t)) + (re-search-forward top-heading nil 1 1)) + (beginning-of-line) + (unless (looking-at top-heading) + (insert (format "\n* %s-Tickets\n" proj-key))))) + +(defun org-jira--render-issue (Issue) + "Render single ISSUE." +;; (org-jira-log "Rendering issue from issue list") +;; (org-jira-log (org-jira-sdk-dump Issue)) + (with-slots (filename proj-key issue-id summary status priority headline id) Issue + (let (p) + (with-current-buffer (org-jira--get-project-buffer Issue) + (org-jira-freeze-ui + (org-jira-maybe-activate-mode) + (org-jira--maybe-render-top-heading proj-key) + (setq p (org-find-entry-with-id issue-id)) + (save-restriction + (if (and p (>= p (point-min)) + (<= p (point-max))) + (progn + (goto-char p) + (forward-thing 'whitespace) + (org-jira-kill-line)) + (goto-char (point-max)) + (unless (looking-at "^") + (insert "\n")) + (insert "** ")) + (org-jira-insert + (concat (org-jira-get-org-keyword-from-status status) + " " + (org-jira-get-org-priority-cookie-from-issue priority) + headline)) + (save-excursion + (unless (search-forward "\n" (point-max) 1) + (insert "\n"))) + (org-narrow-to-subtree) + (save-excursion + (org-back-to-heading t) + (org-set-tags-to (replace-regexp-in-string "-" "_" issue-id))) + (org-jira-entry-put (point) "assignee" (or (slot-value Issue 'assignee) "Unassigned")) + (mapc (lambda (entry) + (let ((val (slot-value Issue entry))) + (when (and val (not (string= val ""))) + (org-jira-entry-put (point) (symbol-name entry) val)))) + '(filename reporter type type-id priority labels resolution status components created updated sprint)) + + (org-jira-entry-put (point) "ID" issue-id) + (org-jira-entry-put (point) "CUSTOM_ID" issue-id) + + ;; Insert the duedate as a deadline if it exists + (when org-jira-deadline-duedate-sync-p + (let ((duedate (oref Issue duedate))) + (when (> (length duedate) 0) + (org-deadline nil duedate)))) + + (mapc + (lambda (heading-entry) + (ensure-on-issue-id-with-filename issue-id filename + (let* ((entry-heading + (concat (symbol-name heading-entry) + (format ": [[%s][%s]]" + (concat jiralib-url "/browse/" issue-id) issue-id)))) + (setq p (org-find-exact-headline-in-buffer entry-heading)) + (if (and p (>= p (point-min)) + (<= p (point-max))) + (progn + (goto-char p) + (org-narrow-to-subtree) + (goto-char (point-min)) + (forward-line 1) + (delete-region (point) (point-max))) + (if (org-goto-first-child) + (org-insert-heading) + (goto-char (point-max)) + (open-line 1) + (org-insert-subheading t)) + (org-jira-insert entry-heading "\n")) + + ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup + (org-jira-insert + (replace-regexp-in-string + "^" " " + (format "%s" (slot-value Issue heading-entry))))))) + '(description)) + + (when org-jira-download-comments + (org-jira-update-comments-for-issue Issue) + + ;; FIXME: Re-enable when attachments are not erroring. + ;;(org-jira-update-attachments-for-current-issue) + ) + + ;; only sync worklog clocks when the user sets it to be so. + (when org-jira-worklog-sync-p + (org-jira-update-worklogs-for-issue issue-id filename)))))))) + +(defun org-jira--render-issues-from-issue-list (Issues) + "Add the issues from ISSUES list into the org file(s). + +ISSUES is a list of `org-jira-sdk-issue' records." + ;; FIXME: Some type of loading error - the first async callback does not know about + ;; the issues existing as a class, so we may need to instantiate here if we have none. + (when (eq 0 (->> Issues (cl-remove-if-not #'org-jira-sdk-isa-issue?) length)) + (setq Issues (org-jira-sdk-create-issues-from-data-list Issues))) + + ;; First off, we never ever want to run on non-issues, so check our types early. + (setq Issues (cl-remove-if-not #'org-jira-sdk-isa-issue? Issues)) + (org-jira-log (format "About to render %d issues." (length Issues))) + + ;; If we have any left, we map over them. + (mapc 'org-jira--render-issue Issues) + + ;; Prior text: "Oh, are you the culprit?" - Not sure if this caused an issue at some point. + ;; We want to ensure we fix broken org narrowing though, by doing org-show-all and then org-cycle. + (switch-to-buffer (org-jira--get-project-buffer (-last-item Issues))) + (org-show-all) + (org-cycle)) + +;;;###autoload +(defun org-jira-update-comment () + "Update a comment for the current issue." + (interactive) + (let* ((issue-id (org-jira-get-from-org 'issue 'key)) ; Really the key + (filename (org-jira-filename)) + (comment-id (org-jira-get-from-org 'comment 'id)) + (comment (replace-regexp-in-string "^ " "" (org-jira-get-comment-body comment-id)))) + (lexical-let ((issue-id issue-id) + (filename filename)) + (let ((callback-edit + (cl-function + (lambda (&key _data &allow-other-keys) + (ensure-on-issue-id-with-filename + issue-id filename + (org-jira-update-comments-for-current-issue))))) + (callback-add + (cl-function + (lambda (&key _data &allow-other-keys) + (ensure-on-issue-id-with-filename + issue-id filename + ;; @TODO :optim: Has to be a better way to do this + ;; than delete region (like update the unmarked + ;; one) + (org-jira-delete-current-comment) + (org-jira-update-comments-for-current-issue)))))) + (if comment-id + (jiralib-edit-comment issue-id comment-id comment callback-edit) + (jiralib-add-comment issue-id comment callback-add)))))) + +(defun org-jira-add-comment (issue-id filename comment) + "For ISSUE-ID in FILENAME, add a new COMMENT string to the issue region." + (interactive + (let* ((issue-id (org-jira-get-from-org 'issue 'id)) + (filename (org-jira-filename)) + (comment (read-string (format "Comment (%s): " issue-id)))) + (list issue-id filename comment))) + (lexical-let ((issue-id issue-id) + (filename filename)) + (ensure-on-issue-id-with-filename issue-id filename + (goto-char (point-max)) + (jiralib-add-comment + issue-id comment + (cl-function + (lambda (&key _data &allow-other-keys) + (ensure-on-issue-id-with-filename issue-id filename + (org-jira-update-comments-for-current-issue)))))))) + +(defun org-jira-org-clock-to-date (org-time) + "Convert ORG-TIME formatted date into a plain date string." + (format-time-string + "%Y-%m-%dT%H:%M:%S.000%z" + (date-to-time org-time))) + +(defun org-jira-worklog-time-from-org-time (org-time) + "Take in an ORG-TIME and convert it into the portions of a worklog time. +Expects input in format such as: [2017-04-05 Wed 01:00]--[2017-04-05 Wed 01:46] => 0:46" + (let ((start (replace-regexp-in-string "^\\[\\(.*?\\)\\].*" "\\1" org-time)) + (end (replace-regexp-in-string ".*--\\[\\(.*?\\)\\].*" "\\1" org-time))) + `((started . ,(org-jira-org-clock-to-date start)) + (time-spent-seconds . ,(time-to-seconds + (time-subtract + (date-to-time end) + (date-to-time start))))))) + +(defun org-jira-org-clock-to-jira-worklog (org-time clock-content) + "Given ORG-TIME and CLOCK-CONTENT, format a jira worklog entry." + (let ((lines (split-string clock-content "\n")) + worklog-id) + ;; See if we look like we have an id + (when (string-match ":id:" (first lines)) + (setq worklog-id + (replace-regexp-in-string "^.*:id: \\([0-9]*\\)$" "\\1" (first lines))) + (when (> (string-to-number worklog-id) 0) ;; pop off the first id line if we found it valid + (setq lines (cdr lines)))) + (setq lines (reverse (cdr (reverse lines)))) ;; drop last line + (let ((comment (org-trim (mapconcat 'identity lines "\n"))) + (worklog-time (org-jira-worklog-time-from-org-time org-time))) + `((worklog-id . ,worklog-id) + (comment . ,comment) + (started . ,(cdr (assoc 'started worklog-time))) + (time-spent-seconds . ,(cdr (assoc 'time-spent-seconds worklog-time))) + )))) + +(defun org-jira-worklog-to-hashtable (issue-id) + "Given ISSUE-ID, return a hashtable of worklog-id -> jira worklog." + (let ((worklog-hashtable (make-hash-table :test 'equal))) + (mapc + (lambda (worklog) + (let ((worklog-id (cdr (assoc 'id worklog)))) + (puthash worklog-id worklog worklog-hashtable))) + (jiralib-worklog-import--filter-apply + (org-jira-find-value + (jiralib-get-worklogs + issue-id) + 'worklogs))) + worklog-hashtable)) + +;;;###autoload +(defun org-jira-update-worklogs-from-org-clocks () + "Update or add a worklog based on the org clocks." + (interactive) + (let* ((issue-id (org-jira-get-from-org 'issue 'key)) + (filename (org-jira-filename)) + ;; Fetch all workflogs for this issue + (jira-worklogs-ht (org-jira-worklog-to-hashtable issue-id))) + (org-jira-log (format "About to sync worklog for issue: %s in file: %s" + issue-id filename)) + (ensure-on-issue-id-with-filename issue-id filename + (search-forward (format ":%s:" (or (org-clock-drawer-name) "LOGBOOK")) nil 1 1) + (org-beginning-of-line) + ;; (org-cycle 1) + (while (search-forward "CLOCK: " nil 1 1) + (let ((org-time (buffer-substring-no-properties (point) (point-at-eol)))) + (forward-line) + ;; See where the stuff ends (what point) + (let (next-clock-point) + (save-excursion + (search-forward-regexp "\\(CLOCK\\|:END\\):" nil 1 1) + (setq next-clock-point (point))) + (let ((clock-content + (buffer-substring-no-properties (point) next-clock-point))) + ;; Update via jiralib call + (let* ((worklog (org-jira-org-clock-to-jira-worklog org-time clock-content)) + (comment-text (cdr (assoc 'comment worklog))) + (comment-text (if (string= (org-trim comment-text) "") nil comment-text))) + (if (cdr (assoc 'worklog-id worklog)) + ;; If there is a worklog in jira for this ID, check if the worklog has changed. + ;; If it has changed, update the worklog. + ;; If it has not changed, skip. + (let ((jira-worklog (gethash (cdr (assoc 'worklog-id worklog)) jira-worklogs-ht))) + (when (and jira-worklog + ;; Check if the entries are differing lengths. + (or (not (= (cdr (assoc 'timeSpentSeconds jira-worklog)) + (cdr (assoc 'time-spent-seconds worklog)))) + ;; Check if the entries start at different times. + (not (string= (cdr (assoc 'started jira-worklog)) + (cdr (assoc 'started worklog)))))) + (jiralib-update-worklog + issue-id + (cdr (assoc 'worklog-id worklog)) + (cdr (assoc 'started worklog)) + (cdr (assoc 'time-spent-seconds worklog)) + comment-text + nil))) ; no callback - synchronous + ;; else + (jiralib-add-worklog + issue-id + (cdr (assoc 'started worklog)) + (cdr (assoc 'time-spent-seconds worklog)) + comment-text + nil) ; no callback - synchronous + ) + ))))) + (org-jira-log (format "Updating worklog from org-jira-update-worklogs-from-org-clocks call")) + (org-jira-update-worklogs-for-issue issue-id filename) + ))) + +(defun org-jira-update-worklog () + "Update a worklog for the current issue." + (interactive) + (error "Deprecated, use org-jira-update-worklogs-from-org-clocks instead!") + (let* ((issue-id (org-jira-get-from-org 'issue 'key)) + (worklog-id (org-jira-get-from-org 'worklog 'id)) + (timeSpent (org-jira-get-from-org 'worklog 'timeSpent)) + (timeSpent (if timeSpent + timeSpent + (read-string "Input the time you spent (such as 3w 1d 2h): "))) + (timeSpent (replace-regexp-in-string " \\(\\sw\\)\\sw*\\(,\\|$\\)" "\\1" timeSpent)) + (startDate (org-jira-get-from-org 'worklog 'startDate)) + (startDate (if startDate + startDate + (org-read-date nil nil nil "Input when did you start"))) + (startDate (org-jira-time-format-to-jira startDate)) + (comment (replace-regexp-in-string "^ " "" (org-jira-get-worklog-comment worklog-id))) + (worklog `((comment . ,comment) + (timeSpent . ,timeSpent) + (timeSpentInSeconds . 10) + (startDate . ,startDate))) + (worklog (if worklog-id + (cons `(id . ,(replace-regexp-in-string "^worklog-" "" worklog-id)) worklog) + worklog))) + (if worklog-id + (jiralib-update-worklog worklog) + (jiralib-add-worklog-and-autoadjust-remaining-estimate issue-id startDate timeSpent comment)) + (org-jira-delete-current-worklog) + (org-jira-update-worklogs-for-current-issue))) + +(defun org-jira-delete-current-comment () + "Delete the current comment." + (ensure-on-comment + (delete-region (point-min) (point-max)))) + +(defun org-jira-delete-current-worklog () + "Delete the current worklog." + (ensure-on-worklog + (delete-region (point-min) (point-max)))) + +;;;###autoload +(defun org-jira-copy-current-issue-key () + "Copy the current issue's key into clipboard." + (interactive) + (let ((issue-id (org-jira-get-from-org 'issue 'key))) + (with-temp-buffer + (insert issue-id) + (kill-region (point-min) (point-max))))) + +(defun org-jira-get-comment-id (comment) + (org-jira-find-value comment 'id)) + +(defun org-jira-get-comment-author (comment) + (org-jira-find-value comment 'author 'displayName)) + +(defun org-jira-isa-ignored-comment? (comment) + (member-ignore-case (oref comment author) org-jira-ignore-comment-user-list)) + +(defun org-jira-maybe-reverse-comments (comments) + (if org-jira-reverse-comment-order (reverse comments) comments)) + +(defun org-jira-extract-comments-from-data (data) + (->> (append data nil) + org-jira-sdk-create-comments-from-data-list + org-jira-maybe-reverse-comments + (cl-remove-if #'org-jira-isa-ignored-comment?))) + +(defun org-jira--render-comment (Issue Comment) + (with-slots (issue-id) Issue + (with-slots (comment-id author headline created updated body) Comment + (org-jira-log (format "Rendering a comment: %s" body)) + (ensure-on-issue-Issue Issue + (setq p (org-find-entry-with-id comment-id)) + (when (and p (>= p (point-min)) + (<= p (point-max))) + (goto-char p) + (org-narrow-to-subtree) + (delete-region (point-min) (point-max))) + (goto-char (point-max)) + (unless (looking-at "^") + (insert "\n")) + (insert "*** ") + (org-jira-insert headline "\n") + (org-narrow-to-subtree) + (org-jira-entry-put (point) "ID" comment-id) + (org-jira-entry-put (point) "created" created) + (unless (string= created updated) + (org-jira-entry-put (point) "updated" updated)) + (goto-char (point-max)) + ;; Insert 2 spaces of indentation so Jira markup won't cause org-markup + (org-jira-insert (replace-regexp-in-string "^" " " (or body ""))))))) + +(defun org-jira-update-comments-for-issue (Issue) + "Update the comments for the specified ISSUE issue." + (with-slots (issue-id) Issue + (jiralib-get-comments + issue-id + (org-jira-with-callback + (org-jira-log "In the callback for org-jira-update-comments-for-issue.") + (--> + (org-jira-find-value cb-data 'comments) + (org-jira-extract-comments-from-data it) + (mapc (lambda (Comment) (org-jira--render-comment Issue Comment)) it)))))) + +(defun org-jira-update-comments-for-current-issue () + "Update comments for the current issue." + (org-jira-log "About to update comments for current issue.") + (let ((Issue (make-instance 'org-jira-sdk-issue + :issue-id (org-jira-get-from-org 'issue 'key) + :filename (org-jira-filename)))) + (-> Issue org-jira-update-comments-for-issue))) + +(defun org-jira-delete-subtree () + "Derived from org-cut-subtree. + +Like that function, without mangling the user's clipboard for the +purpose of wiping an old subtree." + (let (beg end folded (beg0 (point))) + (org-back-to-heading t) ; take what is really there + (setq beg (point)) + (skip-chars-forward " \t\r\n") + (save-match-data + (save-excursion (outline-end-of-heading) + (setq folded (org-invisible-p)) + (org-end-of-subtree t t))) + ;; Include the end of an inlinetask + (when (and (featurep 'org-inlinetask) + (looking-at-p (concat (org-inlinetask-outline-regexp) + "END[ \t]*$"))) + (end-of-line)) + (setq end (point)) + (goto-char beg0) + (when (> end beg) + (setq org-subtree-clip-folded folded) + (org-save-markers-in-region beg end) + (delete-region beg end)))) + +(defun org-jira-update-attachments-for-current-issue () + "Update the attachments for the current issue." + (when jiralib-use-restapi + (lexical-let ((issue-id (org-jira-get-from-org 'issue 'key))) + ;; Run the call + (jiralib-get-attachments + issue-id + (save-excursion + (cl-function + (lambda (&key data &allow-other-keys) + ;; First, make sure we're in the proper buffer (logic copied from org-jira-get-issues. + (let* ((proj-key (replace-regexp-in-string "-.*" "" issue-id)) + (project-file (org-jira--get-project-file-name proj-key)) + (project-buffer (or (find-buffer-visiting project-file) + (find-file project-file)))) + (with-current-buffer project-buffer + ;; delete old attachment node + (ensure-on-issue + (if (org-goto-first-child) + (while (org-goto-sibling) + (forward-thing 'whitespace) + (when (looking-at "Attachments:") + (org-jira-delete-subtree))))) + (let ((attachments (org-jira-find-value data 'fields 'attachment))) + (when (not (zerop (length attachments))) + (ensure-on-issue + (if (org-goto-first-child) + (progn + (while (org-goto-sibling)) + (org-insert-heading-after-current)) + (org-insert-subheading nil)) + + (insert "Attachments:") + (mapc + (lambda (attachment) + (let ((attachment-id (org-jira-get-comment-id attachment)) + (author (org-jira-get-comment-author attachment)) + (created (org-jira-transform-time-format + (org-jira-find-value attachment 'created))) + (size (org-jira-find-value attachment 'size)) + (mimeType (org-jira-find-value attachment 'mimeType)) + (content (org-jira-find-value attachment 'content)) + (filename (org-jira-find-value attachment 'filename))) + (if (looking-back "Attachments:") + (org-insert-subheading nil) + (org-insert-heading-respect-content)) + (insert "[[" content "][" filename "]]") + (org-narrow-to-subtree) + (org-jira-entry-put (point) "ID" attachment-id) + (org-jira-entry-put (point) "Author" author) + (org-jira-entry-put (point) "Name" filename) + (org-jira-entry-put (point) "Created" created) + (org-jira-entry-put (point) "Size" (ls-lisp-format-file-size size t)) + (org-jira-entry-put (point) "Content" content) + (widen))) + attachments))))))))))))) + +(defun org-jira-sort-org-clocks (clocks) + "Given a CLOCKS list, sort it by start date descending." + ;; Expects data such as this: + + ;; ((\"2017-02-26 Sun 00:08\" \"2017-02-26 Sun 01:08\" \"Hi\" \"10101\") + ;; (\"2017-03-16 Thu 22:25\" \"2017-03-16 Thu 22:57\" \"Test\" \"10200\")) + (sort clocks + (lambda (a b) + (> (time-to-seconds (date-to-time (car a))) + (time-to-seconds (date-to-time (car b))))))) + +(defun org-jira-update-worklogs-for-current-issue () + "Update the worklogs for the current issue." + (let ((issue-id (org-jira-get-from-org 'issue 'key)) + (filename (org-jira-filename))) + (org-jira-update-worklogs-for-issue issue-id filename))) + +(defun org-jira-update-worklogs-for-issue (issue-id filename) + "Update the worklogs for the current ISSUE-ID located in FILENAME." + (org-jira-log (format "org-jira-update-worklogs-for-issue id: %s filename: %s" + issue-id filename)) + ;; Run the call + (jiralib-get-worklogs + issue-id + (org-jira-with-callback + (ensure-on-issue-id-with-filename issue-id filename + (let ((worklogs (org-jira-find-value cb-data 'worklogs))) + (org-jira-log (format "org-jira-update-worklogs-for-issue cb id: %s fn: %s" + issue-id filename)) + (org-jira-logbook-reset issue-id filename + (org-jira-sort-org-clocks (org-jira-worklogs-to-org-clocks + (jiralib-worklog-import--filter-apply worklogs))))))))) + +;;;###autoload +(defun org-jira-unassign-issue () + "Update an issue to be unassigned." + (interactive) + (let ((issue-id (org-jira-parse-issue-id)) + (filename (org-jira-parse-issue-filename))) + (org-jira-update-issue-details issue-id filename :assignee nil))) + +;;;###autoload +(defun org-jira-set-issue-reporter () + "Update an issue's reporter interactively." + (interactive) + (let ((issue-id (org-jira-parse-issue-id)) + (filename (org-jira-parse-issue-filename))) + (if issue-id + (let* ((project (replace-regexp-in-string "-[0-9]+" "" issue-id)) + (jira-users (org-jira-get-reporter-candidates project)) ;; TODO, probably a better option than org-jira-get-assignable-users here + (user (completing-read + "Reporter: " + (append (mapcar 'car jira-users) + (mapcar 'cdr jira-users)))) + (reporter (or + (cdr (assoc user jira-users)) + (cdr (rassoc user jira-users))))) + (when (null reporter) + (error "No reporter found, this should probably never happen.")) + (org-jira-update-issue-details issue-id filename :reporter (jiralib-get-user-account-id project reporter))) + (error "Not on an issue")))) + +;;;###autoload +(defun org-jira-assign-issue () + "Update an issue with interactive re-assignment." + (interactive) + (let ((issue-id (org-jira-parse-issue-id)) + (filename (org-jira-parse-issue-filename))) + (if issue-id + (let* ((project (replace-regexp-in-string "-[0-9]+" "" issue-id)) + (jira-users (org-jira-get-assignable-users project)) + (user (completing-read + "Assignee: " + (append (mapcar 'car jira-users) + (mapcar 'cdr jira-users)))) + (assignee (or + (cdr (assoc user jira-users)) + (cdr (rassoc user jira-users))))) + (when (null assignee) + (error "No assignee found, use org-jira-unassign-issue to make the issue unassigned")) + (org-jira-update-issue-details issue-id filename :assignee (jiralib-get-user-account-id project assignee))) + (error "Not on an issue")))) + +;;;###autoload +(defun org-jira-update-issue () + "Update an issue." + (interactive) + (let ((issue-id (org-jira-parse-issue-id)) + (filename (org-jira-parse-issue-filename))) + (if issue-id + (org-jira-update-issue-details issue-id filename) + (error "Not on an issue")))) + +;;;###autoload +(defun org-jira-todo-to-jira () + "Convert an ordinary todo item to a jira ticket." + (interactive) + (ensure-on-todo + (when (org-jira-parse-issue-id) + (error "Already on jira ticket")) + (save-excursion (org-jira-create-issue + (org-jira-read-project) + (org-jira-read-issue-type) + (org-get-heading t t) + (org-get-entry))) + (delete-region (point-min) (point-max)))) + +;;;###autoload +(defun org-jira-get-subtasks () + "Get subtasks for the current issue." + (interactive) + (ensure-on-issue + (org-jira-get-issues-headonly (jiralib-do-jql-search (format "parent = %s" (org-jira-parse-issue-id)))))) + +;;;###autoload +(defun org-jira-update-issue-labels () + "Update jira issue labels." + (interactive) + (let* ((labels (org-jira-parse-issue-labels)) + (updated-labels (org-jira-read-labels (format "%s, " labels))) + (updated-labels-string (mapconcat 'identity updated-labels ", "))) + (org-set-property "labels" updated-labels-string) + (org-jira-update-issue))) + +(defvar org-jira-project-read-history nil) +(defvar org-jira-boards-read-history nil) +(defvar org-jira-sprints-read-history nil) +(defvar org-jira-components-read-history nil) +(defvar org-jira-priority-read-history nil) +(defvar org-jira-type-read-history nil) + +(defun org-jira-read-project () + "Read project name." + (completing-read + "Project: " + (jiralib-make-list (jiralib-get-projects) 'key) + nil + t + nil + 'org-jira-project-read-history + (car org-jira-project-read-history))) + +(defun org-jira-read-board () + "Read board name. Returns cons pair (name . integer-id)" + (let* ((boards-alist + (jiralib-make-assoc-list (jiralib-get-boards) 'name 'id)) + (board-name + (completing-read "Boards: " boards-alist + nil t nil + 'org-jira-boards-read-history + (car org-jira-boards-read-history)))) + (assoc board-name boards-alist))) + +(defun org-jira-read-sprint (board) + "Read sprint name. Returns cons pair (name . integer-id)" + (let* ((sprints-alist + (jiralib-make-assoc-list (append (alist-get 'values (jiralib-get-board-sprints board)) nil) 'name 'id)) + (sprint-name + (completing-read "Sprints: " sprints-alist + nil t nil + 'org-jira-sprints-read-history + (car org-jira-sprints-read-history)))) + (assoc sprint-name sprints-alist))) + +(defun org-jira-read-component (project) + "Read the components options for PROJECT such as EX." + (completing-read + "Components (choose Done to stop): " + (append '("Done") (mapcar 'cdr (jiralib-get-components project))) + nil + t + nil + 'org-jira-components-read-history + "Done")) + +;; TODO: Finish this feature - integrate into org-jira-create-issue +(defun org-jira-read-components (project) + "Types: string PROJECT : string (csv of components). + +Get all the components for the PROJECT such as EX, +that should be bound to an issue." + (let (components component) + (while (not (equal "Done" component)) + (setq component (org-jira-read-component project)) + (unless (equal "Done" component) + (push component components))) + components)) + +(defun org-jira-read-priority () + "Read priority name." + (completing-read + "Priority: " + (mapcar 'cdr (jiralib-get-priorities)) + nil + t + nil + 'org-jira-priority-read-history + (car org-jira-priority-read-history))) + +(defun org-jira-read-issue-type (&optional project) + "Read issue type name. PROJECT is the optional project key." + (let* ((issue-types + (mapcar 'cdr (if project + (jiralib-get-issue-types-by-project project) + (jiralib-get-issue-types)))) + (initial-input (when (member (car org-jira-type-read-history) issue-types) + org-jira-type-read-history))) + + ;; TODO: The completing-read calls as such are all over the place, and always tend + ;; to follow this exact same call structure - we should abstract to a single fn + ;; that will allow calling with fewer or keyword args + (completing-read + "Type: " ; PROMPT + issue-types ; COLLECTION + nil ; PREDICATE + t ; REQUIRE-MATCH + nil ; INITIAL-INPUT + 'initial-input ; HIST + (car initial-input)))) ; DEF + +(defun org-jira-read-subtask-type () + "Read issue type name." + (completing-read + "Type: " + (mapcar 'cdr (jiralib-get-subtask-types)) + nil + t + nil + 'org-jira-type-read-history + (car org-jira-type-read-history))) + +(defun org-jira-get-issue-struct (project type summary description &optional parent-id) + "Create an issue struct for PROJECT, of TYPE, with SUMMARY and DESCRIPTION." + (if (or (equal project "") + (equal type "") + (equal summary "")) + (error "Must provide all information!")) + (let* ((project-components (jiralib-get-components project)) + (jira-users (org-jira-get-assignable-users project)) + (user (completing-read "Assignee: " (mapcar 'car jira-users))) + (priority (car (rassoc (org-jira-read-priority) (jiralib-get-priorities)))) + (labels (org-jira-read-labels)) + (ticket-struct + `((fields + (project (key . ,project)) + (parent (key . ,parent-id)) + (issuetype (id . ,(car (rassoc type (if (and (boundp 'parent-id) parent-id) + (jiralib-get-subtask-types) + (jiralib-get-issue-types-by-project project)))))) + (summary . ,(format "%s%s" summary + (if (and (boundp 'parent-id) parent-id) + (format " (subtask of [jira:%s])" parent-id) + ""))) + (description . ,description) + (priority (id . ,priority)) + (labels . ,labels) + ;; accountId should be nil if Unassigned, not the key slot. + (assignee (accountId . ,(or (cdr (assoc user jira-users)) nil))))))) + ticket-struct)) + +;;;###autoload +(defun org-jira-create-issue (project type summary description) + "Create an issue in PROJECT, of type TYPE, with given SUMMARY and DESCRIPTION." + (interactive + (let* ((project (org-jira-read-project)) + (type (org-jira-read-issue-type project)) + (summary (read-string "Summary: ")) + (description (read-string "Description: "))) + (list project type summary description))) + (if (or (equal project "") + (equal type "") + (equal summary "")) + (error "Must provide all information!")) + (let* ((parent-id nil) + (ticket-struct (org-jira-get-issue-struct project type summary description))) + (org-jira-get-issues (list (jiralib-create-issue ticket-struct))))) + +;;;###autoload +(defun org-jira-create-subtask (project type summary description) + "Create a subtask issue for PROJECT, of TYPE, with SUMMARY and DESCRIPTION." + (interactive (ensure-on-issue (list (org-jira-read-project) + (org-jira-read-subtask-type) + (read-string "Summary: ") + (read-string "Description: ")))) + (if (or (equal project "") + (equal type "") + (equal summary "")) + (error "Must provide all information!")) + (let* ((parent-id (org-jira-parse-issue-id)) + (ticket-struct (org-jira-get-issue-struct project type summary description parent-id))) + (org-jira-get-issues (list (jiralib-create-subtask ticket-struct))))) + +(defun org-jira-get-issue-val-from-org (key) + "Return the requested value by KEY from the current issue." + ;; There is some odd issue when not using any let-scoping, where myself + ;; and an array of users are hitting a snag circa 2023-03-01 time frame + ;; in which the setq portion of a when clause is being hit even when it + ;; evaluates to false - the bug only manifests on a first launch of Emacs - it + ;; doesn't occur when re-evaluating this function. However, wrapping it "fixes" + ;; the issue. + ;; + ;; The first link has the most troubleshooting/diagnosis around the particulars of + ;; this bug. + ;; + ;; See: https://github.com/ahungry/org-jira/issues/319 + ;; See: https://github.com/ahungry/org-jira/issues/296 + ;; See: https://github.com/ahungry/org-jira/issues/316 + (lexical-let ((my-key key)) + (ensure-on-issue + (cond ((eq my-key 'description) + (org-goto-first-child) + (forward-thing 'whitespace) + (if (looking-at "description: ") + (org-trim (org-get-entry)) + (error "Can not find description field for this issue"))) + + ((eq my-key 'summary) + (ensure-on-issue + (org-get-heading t t))) + + ;; org returns a time tuple, we need to convert it + ((eq my-key 'deadline) + (let ((encoded-time (org-get-deadline-time (point)))) + (when encoded-time + (cl-reduce (lambda (carry segment) + (format "%s-%s" carry segment)) + (reverse (cl-subseq (decode-time encoded-time) 3 6)))))) + + ;; default case, just grab the value in the properties block + (t + (when (symbolp my-key) + (setq my-key (symbol-name my-key))) + + (setq my-key (or (assoc-default my-key org-jira-property-overrides) + my-key)) + + ;; This is the "impossible" to hit setq that somehow gets hit without the let + ;; wrapper around the function input args. + (when (string= my-key "key") + (setq my-key "ID")) + + ;; The variable `org-special-properties' will mess this up + ;; if our search, such as 'priority' is within there, so + ;; don't bother with it for this (since we only ever care + ;; about the local properties, not any hierarchal or special + ;; ones). + (let ((org-special-properties nil)) + (or (org-entry-get (point) my-key t) + ""))))))) + +(defun org-jira-read-action (actions) + "Read issue workflow progress ACTIONS." + (let ((action (completing-read + "Action: " + (mapcar 'cdr actions) + nil + t + nil))) + (or + (car (rassoc action actions)) + (user-error "You specified an empty action, the valid actions are: %s" (mapcar 'cdr actions))))) + +(defun org-jira-read-labels (&optional current-labels) + "Pick multiple labels which will be added or updating existing +CURRENT-LABELS and save with the jira issue." + (unless current-labels (setq current-labels nil)) + (if jiralib-labels-cache + (completing-read-multiple "Labels: " jiralib-labels-cache nil nil current-labels) + (jiralib-get-labels) + (completing-read-multiple "Labels: " jiralib-labels-cache nil nil current-labels))) + +(defvar org-jira-fields-history nil) +(defun org-jira-read-field (fields) + "Read (custom) FIELDS for workflow progress." + (let ((field-desc (completing-read + "More fields to set: " + (cons "Thanks, no more fields are *required*." (mapcar 'org-jira-decode (mapcar 'cdr fields))) + nil + t + nil + 'org-jira-fields-history)) + field-name) + (setq field-name (car (rassoc field-desc fields))) + (if field-name + (intern field-name) + field-name))) + + +(defvar org-jira-rest-fields nil + "Extra fields are held here for usage between two endpoints. +Used in org-jira-read-resolution and org-jira-progress-issue calls.") + +(defvar org-jira-resolution-history nil) +(defun org-jira-read-resolution () + "Read issue workflow progress resolution." + (if (not jiralib-use-restapi) + (let ((resolution (completing-read + "Resolution: " + (mapcar 'cdr (jiralib-get-resolutions)) + nil + t + nil + 'org-jira-resolution-history + (car org-jira-resolution-history)))) + (car (rassoc resolution (jiralib-get-resolutions)))) + (let* ((resolutions (org-jira-find-value org-jira-rest-fields 'resolution 'allowedValues)) + (resolution-name (completing-read + "Resolution: " + (mapcar (lambda (resolution) + (org-jira-find-value resolution 'name)) + resolutions)))) + (cons 'name resolution-name)))) + +(defun org-jira-refresh-issues-in-buffer-loose () + "Iterates over all level 1-2 headings in current buffer, refreshing on issue :ID:. +It differs with `org-jira-refresh-issues-in-buffer' in that it accepts the current buffer +and its corresponding filename, regardless of whether it has been previously registered +as an org-jira project file or not." + (interactive) + (save-excursion + (save-restriction + (widen) + (let* ((org-ids (org-map-entries 'org-id-get "LEVEL=1|LEVEL=2")) + (org-ids (delq nil org-ids)) + (file-name (file-name-sans-extension buffer-file-name))) + (mapcar (lambda (org-id) (org-jira--refresh-issue org-id file-name)) + org-ids))))) + +(defun org-jira-refresh-issues-in-buffer () + "Iterate across all level 1-2 headings in current buffer, refreshing on issue :ID:. +Where issue-id will be something such as \"EX-22\"." + (interactive) + (save-excursion + (save-restriction + (widen) + (let* ((org-ids (org-map-entries 'org-id-get "LEVEL=1|LEVEL=2")) + (org-ids (delq nil org-ids))) + ;; It's possible we could be on a non-org-jira headline, but + ;; that should be an exceptional case and not necessitating a + ;; fix atm. + (mapcar 'org-jira--refresh-issue-by-id org-ids))))) + +;;;###autoload +(defun org-jira-refresh-issue () + "Refresh current issue from jira to org." + (interactive) + (ensure-on-issue + (org-jira--refresh-issue (org-jira-id) (org-jira-filename)))) + +(defun org-jira--refresh-issue (issue-id &optional filename) + "Refresh issue from jira to org using ISSUE-ID." + (unless filename (setq filename (replace-regexp-in-string "-[0-9]+" "" issue-id))) + (jiralib-get-issue + issue-id + (org-jira-with-callback + (org-jira-log (format "Received refresh issue data for id: %s in file: %s" issue-id filename)) + (--> cb-data + list + (org-jira-sdk-create-issues-from-data-list-with-filename filename it) + org-jira--render-issues-from-issue-list)))) + +(defun org-jira--refresh-issue-by-id (issue-id) + "Refresh issue from jira to org using ISSUE-ID." + (ensure-on-issue-id issue-id + (org-jira--refresh-issue issue-id))) + +(defvar org-jira-fields-values-history nil) +;;;###autoload +(defun org-jira-progress-issue () + "Progress issue workflow." + (interactive) + (ensure-on-issue + (let* ((issue-id (org-jira-id)) + (actions (jiralib-get-available-actions + issue-id + (org-jira-get-issue-val-from-org 'status))) + (action (org-jira-read-action actions)) + (fields (jiralib-get-fields-for-action issue-id action)) + (org-jira-rest-fields fields) + (field-key) + (custom-fields-collector nil) + (custom-fields + (progn + ;; delete those elements in fields, which have + ;; already been set in custom-fields-collector + (while fields + (setq fields + (cl-remove-if + (lambda (strstr) + (cl-member-if (lambda (symstr) + (string= (car strstr) (symbol-name (car symstr)))) + custom-fields-collector)) + fields)) + (setq field-key (org-jira-read-field fields)) + (if (not field-key) + (setq fields nil) + (setq custom-fields-collector + (cons + (funcall (if jiralib-use-restapi + #'list + #'cons) + field-key + (if (eq field-key 'resolution) + (org-jira-read-resolution) + (let ((field-value (completing-read + (format "Please enter %s's value: " + (cdr (assoc (symbol-name field-key) fields))) + org-jira-fields-values-history + nil + nil + nil + 'org-jira-fields-values-history))) + (if jiralib-use-restapi + (cons 'name field-value) + field-value)))) + custom-fields-collector)))) + custom-fields-collector))) + (jiralib-progress-workflow-action + issue-id + action + custom-fields + (cl-function + (lambda (&key data &allow-other-keys) + (org-jira-refresh-issue))))))) + +(defun org-jira-progress-next-action (actions current-status) + "Grab the user defined 'next' action from ACTIONS, given CURRENT-STATUS." + (let* ((next-action-name (cdr (assoc current-status org-jira-progress-issue-flow))) + (next-action-id (caar (cl-remove-if-not + (lambda (action) + (equal action next-action-name)) actions :key #'cdr)))) + next-action-id)) + +;;;###autoload +(defun org-jira-progress-issue-next () + "Progress issue workflow." + (interactive) + (ensure-on-issue + (let* ((issue-id (org-jira-id)) + (filename (org-jira-filename)) + (actions (jiralib-get-available-actions + issue-id + (org-jira-get-issue-val-from-org 'status))) + (action (org-jira-progress-next-action actions (org-jira-get-issue-val-from-org 'status))) + (fields (jiralib-get-fields-for-action issue-id action)) + (org-jira-rest-fields fields) + (field-key) + (custom-fields-collector nil) + (custom-fields + (progn + ;; delete those elements in fields, which have + ;; already been set in custom-fields-collector + (while fields + (setq fields + (cl-remove-if + (lambda (strstr) + (cl-member-if (lambda (symstr) + (string= (car strstr) (symbol-name (car symstr)))) + custom-fields-collector)) + fields)) + (setq field-key (org-jira-read-field fields)) + (if (not field-key) + (setq fields nil) + (setq custom-fields-collector + (cons + (funcall (if jiralib-use-restapi + #'list + #'cons) + field-key + (if (eq field-key 'resolution) + (org-jira-read-resolution) + (let ((field-value (completing-read + (format "Please enter %s's value: " + (cdr (assoc (symbol-name field-key) fields))) + org-jira-fields-values-history + nil + nil + nil + 'org-jira-fields-values-history))) + (if jiralib-use-restapi + (cons 'name field-value) + field-value)))) + custom-fields-collector)))) + custom-fields-collector))) + (if action + (jiralib-progress-workflow-action + issue-id + action + custom-fields + (org-jira-with-callback + (ensure-on-issue-id-with-filename issue-id filename + (org-jira-refresh-issue)))) + (error "No action defined for that step!"))))) + +(defun org-jira-get-id-name-alist (name ids-to-names) + "Find the id corresponding to NAME in IDS-TO-NAMES and return an alist with id and name as keys." + (let ((id (car (rassoc name ids-to-names)))) + `((id . ,id) + (name . ,name)))) + +(defun org-jira-build-components-list (project-components org-issue-components) + "Given PROJECT-COMPONENTS, attempt to build a list. + +If the PROJECT-COMPONENTS are nil, this should return: + + (list components []), which will translate into the JSON: + + {\"components\": []} + +otherwise it should return: + + (list components (list (cons id comp-id) (cons name item-name))), + + which will translate into the JSON: + +{\"components\": [{\"id\": \"comp-id\", \"name\": \"item\"}]}" + (if (not project-components) (vector) ;; Return a blank array for JSON + (apply 'list + (cl-mapcan + (lambda (item) + (let ((comp-id (car (rassoc item project-components)))) + (if comp-id + `(((id . ,comp-id) + (name . ,item))) + nil))) + (split-string org-issue-components ",\\s *"))))) + +(defun org-jira-strip-priority-tags (s) + "Given string S, remove any priority tags in the brackets." + (->> s (replace-regexp-in-string "\\[#.*?\\]" "") org-trim)) + +(defun org-jira-update-issue-details (issue-id filename &rest rest) + "Update the details of issue ISSUE-ID in FILENAME. REST will contain optional input." + (ensure-on-issue-id-with-filename issue-id filename + ;; Set up a bunch of values from the org content + (let* ((org-issue-components (org-jira-get-issue-val-from-org 'components)) + (org-issue-labels (org-jira-get-issue-val-from-org 'labels)) + (org-issue-description (org-trim (org-jira-get-issue-val-from-org 'description))) + (org-issue-priority (org-jira-get-issue-val-from-org 'priority)) + (org-issue-type (org-jira-get-issue-val-from-org 'type)) + (org-issue-type-id (org-jira-get-issue-val-from-org 'type-id)) + (org-issue-assignee (cl-getf rest :assignee (org-jira-get-issue-val-from-org 'assignee))) + (org-issue-reporter (cl-getf rest :reporter (org-jira-get-issue-val-from-org 'reporter))) + (project (replace-regexp-in-string "-[0-9]+" "" issue-id)) + (project-components (jiralib-get-components project))) + + ;; Lets fire off a worklog update async with the main issue + ;; update, why not? This is better to fire first, because it + ;; doesn't auto-refresh any areas, while the end of the main + ;; update does a callback that reloads the worklog entries (so, + ;; we hope that won't occur until after this successfully syncs + ;; up). Only do this sync if the user defcustom defines it as such. + (when org-jira-worklog-sync-p + (org-jira-update-worklogs-from-org-clocks)) + + ;; Send the update to jira + (let ((update-fields + (list (cons + 'components + (or (org-jira-build-components-list + project-components + org-issue-components) [])) + (cons 'labels (split-string org-issue-labels ",\\s *")) + (cons 'priority (org-jira-get-id-name-alist org-issue-priority + (jiralib-get-priorities))) + (cons 'description org-issue-description) + (cons 'assignee (list (cons 'id (jiralib-get-user-account-id project org-issue-assignee)))) + (cons 'summary (org-jira-strip-priority-tags (org-jira-get-issue-val-from-org 'summary))) + (cons 'issuetype `((id . ,org-issue-type-id) + (name . ,org-issue-type)))))) + + (if org-jira-update-issue-details-include-reporter + (setq update-fields + (append update-fields + (list (cons 'reporter (list (cons 'id (jiralib-get-user-account-id project org-issue-reporter)))))))) + + ;; If we enable duedate sync and we have a deadline present + (when (and org-jira-deadline-duedate-sync-p + (org-jira-get-issue-val-from-org 'deadline)) + (setq update-fields + (append update-fields + (list (cons 'duedate (org-jira-get-issue-val-from-org 'deadline)))))) + + ;; TODO: We need some way to handle things like assignee setting + ;; and refreshing the proper issue in the proper buffer/filename. + (jiralib-update-issue + issue-id + update-fields + ;; This callback occurs on success + (org-jira-with-callback + (message (format "Issue '%s' updated!" issue-id)) + (jiralib-get-issue + issue-id + (org-jira-with-callback + (org-jira-log "Update get issue for refresh callback hit.") + (-> cb-data list org-jira-get-issues)))) + )) + ))) + + + +(defun org-jira-parse-issue-id () + "Get issue id from org text." + (save-excursion + (let ((continue t) + issue-id) + (while continue + (when (string-match (jiralib-get-issue-regexp) + (or (setq issue-id (org-entry-get (point) "ID")) + "")) + (setq continue nil)) + (unless (and continue (org-up-heading-safe)) + (setq continue nil))) + issue-id))) + +(defun org-jira-parse-issue-filename () + "Get filename from org text." + (save-excursion + (let ((continue t) + filename) + (while continue + (when (setq filename (org-entry-get (point) "filename")) + (setq continue nil)) + (unless (and continue (org-up-heading-safe)) + (setq continue nil))) + filename))) + +(defun org-jira-parse-issue-labels () + "Get issue labels from org text." + (save-excursion + (let ((continue t) + labels) + (while continue + (when (setq labels (org-entry-get (point) "labels")) + (setq continue nil)) + (unless (and continue (org-up-heading-safe)) + (setq continue nil))) + labels))) + +(defun org-jira-get-from-org (type entry) + "Get an org property from the current item. + +TYPE is the type to of the current item, and can be 'issue, or 'comment. + +ENTRY will vary, and is the name of the property to return. If +it is a symbol, it will be converted to string." + (when (symbolp entry) + (setq entry (symbol-name entry))) + (cond + ((eq type 'issue) + (org-jira-get-issue-val-from-org entry)) + ((eq type 'comment) + (org-jira-get-comment-val-from-org entry)) + ((eq type 'worklog) + (org-jira-get-worklog-val-from-org entry)) + (t (error "Unknown type %s" type)))) + +(defun org-jira-get-comment-val-from-org (entry) + "Get the JIRA issue field value ENTRY of the current comment item." + (ensure-on-comment + (when (symbolp entry) + (setq entry (symbol-name entry))) + (when (string= entry "id") + (setq entry "ID")) + (org-entry-get (point) entry))) + +(defun org-jira-get-worklog-val-from-org (entry) + "Get the JIRA issue field value ENTRY of the current worklog item." + (ensure-on-worklog + (when (symbolp entry) + (setq entry (symbol-name entry))) + (when (string= entry "id") + (setq entry "ID")) + (org-entry-get (point) entry))) + +(defun org-jira-get-comment-body (&optional comment-id) + "Get the comment body of the comment with id COMMENT-ID." + (ensure-on-comment + (goto-char (point-min)) + ;; so that search for :END: won't fail + (org-jira-entry-put (point) "ID" comment-id) + (search-forward ":END:" nil 1 1) + (forward-line) + (org-trim (buffer-substring-no-properties (point) (point-max))))) + +(defun org-jira-get-worklog-comment (&optional worklog-id) + "Get the worklog comment of the worklog with id WORKLOG-ID." + (ensure-on-worklog + (goto-char (point-min)) + ;; so that search for :END: won't fail + (org-jira-entry-put (point) "ID" worklog-id) + (search-forward ":END:" nil 1 1) + (forward-line) + (org-trim (buffer-substring-no-properties (point) (point-max))))) + +(defun org-jira-id () + "Get the ID entry for the current heading." + (org-entry-get (point) "ID")) + +(defun org-jira-filename () + "Get the ID entry for the current heading." + (org-jira-get-from-org 'issue 'filename)) + +;;;###autoload +(defun org-jira-browse-issue () + "Open the current issue in external browser." + (interactive) + (ensure-on-issue + (browse-url (concat (replace-regexp-in-string "/*$" "" jiralib-url) "/browse/" (org-jira-id))))) + +(defun org-jira-url-copy-file (url newname) + "Similar to url-copy-file but async." + (lexical-let ((newname newname)) + (url-retrieve + url + (lambda (status) + (let ((buffer (current-buffer)) + (handle nil) + (filename (if (and (file-exists-p newname) + org-jira-download-ask-override) + (read-string "File already exists, select new name or press ENTER to override: " newname) + newname))) + (if (not buffer) + (error "Opening input file: No such file or directory, %s" url)) + (with-current-buffer buffer + (setq handle (mm-dissect-buffer t))) + (mm-save-part-to-file handle filename) + (kill-buffer buffer) + (mm-destroy-parts handle)))))) + +;;;###autoload +(defun org-jira-download-attachment () + "Download the attachment under cursor." + (interactive) + (when jiralib-use-restapi + (save-excursion + (org-up-heading-safe) + (org-back-to-heading) + (forward-thing 'whitespace) + (unless (looking-at "Attachments:") + (error "Not on a attachment region!"))) + (let ((filename (org-entry-get (point) "Name")) + (url (org-entry-get (point) "Content")) + (url-request-extra-headers `(,jiralib-token))) + (org-jira-url-copy-file + url + (concat (file-name-as-directory org-jira-download-dir) filename))))) + +;;;###autoload +(defun org-jira-get-issues-from-filter (filter) + "Get issues from the server-side stored filter named FILTER. + +Provide this command in case some users are not able to use +client side jql (maybe because of JIRA server version?)." + (interactive + (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters))))) + (org-jira-get-issues (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters)))))) + +;;;###autoload +(defun org-jira-get-issues-from-filter-headonly (filter) + "Get issues *head only* from saved filter named FILTER. +See `org-jira-get-issues-from-filter'." + (interactive + (list (completing-read "Filter: " (mapcar 'cdr (jiralib-get-saved-filters))))) + (org-jira-get-issues-headonly (jiralib-get-issues-from-filter (car (rassoc filter (jiralib-get-saved-filters)))))) + +(org-add-link-type "jira" 'org-jira-open) + +;; This was only added in org 9.0, not sure all org users will have +;; that version, so keep the deprecated one from above for now. + +;;(org-link-set-parameters "jira" ((:follow . 'org-jira-open))) + +(defun org-jira-open (path) + "Open a Jira Link from PATH." + (org-jira-get-issue path)) + +;;;###autoload +(defun org-jira-get-issues-by-board () + "Get list of ISSUES from agile board." + (interactive) + (let* ((board (org-jira-read-board)) + (board-id (cdr board))) + (jiralib-get-board-issues board-id + :callback org-jira-get-issue-list-callback + :limit (org-jira-get-board-limit board-id) + :query-params (org-jira--make-jql-queryparams board-id)))) + +;;;###autoload +(defun org-jira-get-issues-by-sprint () + "Get list of ISSUES from sprint." + (interactive) + (let* ((board (org-jira-read-board)) + (board-id (cdr board)) + (sprint (org-jira-read-sprint board-id)) + (sprint-id (cdr sprint))) + (jiralib-get-sprint-issues sprint-id + :callback org-jira-get-issue-list-callback + :limit (org-jira-get-board-limit board-id) + :query-params (org-jira--make-jql-queryparams board-id)))) + +(defun org-jira-get-board-limit (id) + "Get limit for number of retrieved issues for a board +id - integer board id" + (let ((board (org-jira--get-board-from-buffer id))) + (if (and board (slot-boundp board 'limit)) + (oref board limit) + org-jira-boards-default-limit))) + +(defun org-jira--make-jql-queryparams (board-id) + "make GET query parameters for jql, returns nil if JQL query is not set" + (let* ((board (org-jira--get-board-from-buffer board-id)) + (jql (if (and board (slot-boundp board 'jql)) + (oref board jql)))) + (if (and jql (not (string-blank-p jql))) `((jql ,jql))))) + +;;;###autoload +(defun org-jira-get-issues-by-board-headonly () + "Get list of ISSUES from agile board, head only." + (interactive) + (let* ((board (org-jira-read-board)) + (board-id (cdr board))) + (org-jira-get-issues-headonly + (jiralib-get-board-issues board-id + :limit (org-jira-get-board-limit board-id) + :query-params (org-jira--make-jql-queryparams board-id))))) + + +(defun org-jira--render-boards-from-list (boards) + "Add the boards from list into the org file. + +boards - list of `org-jira-sdk-board' records." + (mapc 'org-jira--render-board boards)) + + +(defun org-jira--render-board (board) + "Render single board" + ;;(org-jira-sdk-dump board) + (with-slots (id name url board-type jql limit) board + (with-current-buffer (org-jira--get-boards-buffer) + (org-jira-maybe-activate-mode) + (org-jira-freeze-ui + (org-save-outline-visibility t + (save-restriction + (outline-show-all) + (widen) + (goto-char (point-min)) + (let* ((board-headline + (format "Board: [[%s][%s]]" url name)) + (headline-pos + (org-find-exact-headline-in-buffer board-headline (current-buffer) t)) + (entry-exists (and headline-pos (>= headline-pos (point-min)) (<= headline-pos (point-max)))) + (limit-value (if (slot-boundp board 'limit) (int-to-string limit) nil)) + (jql-value (if (slot-boundp board 'jql) jql nil))) + (if entry-exists + (progn + (goto-char headline-pos) + (org-narrow-to-subtree) + (end-of-line)) + (goto-char (point-max)) + (unless (looking-at "^") + (insert "\n")) + (insert "* ") + (org-jira-insert board-headline) + (org-narrow-to-subtree)) + (org-jira-entry-put (point) "name" name) + (org-jira-entry-put (point) "type" board-type) + (org-jira-entry-put (point) "url" url) + ;; do not overwrite existing user properties with empty values + (if (or (not entry-exists) limit-value) + (org-jira-entry-put (point) "limit" limit-value)) + (if (or (not entry-exists) jql-value) + (org-jira-entry-put (point) "JQL" jql-value )) + (org-jira-entry-put (point) "ID" id)))))))) + +(defun org-jira--get-boards-file () + (expand-file-name "boards-list.org" (org-jira--ensure-working-dir))) + +(defun org-jira--get-boards-buffer () + "Return buffer for list of agile boards. Create one if it does not exist." + (let* ((boards-file (org-jira--get-boards-file)) + (existing-buffer (find-buffer-visiting boards-file))) + (if existing-buffer + existing-buffer + (find-file-noselect boards-file)))) + +;;;###autoload +(defun org-jira-get-boards () + "Get list of boards and their properties." + (interactive) + (let* ((datalist (jiralib-get-boards)) + (boards (org-jira-sdk-create-boards-from-data-list datalist))) + (org-jira--render-boards-from-list boards)) + (switch-to-buffer (org-jira--get-boards-buffer))) + +(defun org-jira--get-board-from-buffer (id) + "Parse board record from org file." + (with-current-buffer (org-jira--get-boards-buffer) + (org-jira-freeze-ui + (let ((pos (org-find-property "ID" (int-to-string id)))) + (if pos + (progn + (goto-char pos) + (apply 'org-jira-sdk-board + (cl-reduce + #'(lambda (acc entry) + (let* ((pname (car entry)) + (pval (cdr entry)) + (pair (and pval + (not (string-empty-p pval)) + (cond + ((equal pname "ID") + (list :id pval)) + ((equal pname "URL") + (list :url pval)) + ((equal pname "TYPE") + (list :board-type pval)) + ((equal pname "NAME") + (list :name pval)) + ((equal pname "LIMIT") + (list :limit (string-to-number pval))) + ((equal pname "JQL") + (list :jql pval)) + (t nil))))) + (if pair (append pair acc) acc))) + (org-entry-properties) :initial-value ())))))))) + +(defun org-jira-get-org-keyword-from-status (status) + "Gets an 'org-mode' keyword corresponding to a given jira STATUS." + (if org-jira-use-status-as-todo + (upcase (replace-regexp-in-string " " "-" status)) + (let ((known-keyword (assoc status org-jira-jira-status-to-org-keyword-alist))) + (cond (known-keyword (cdr known-keyword)) + ((member (org-jira-decode status) org-jira-done-states) "DONE") + ("TODO"))))) + +(defun org-jira-get-org-priority-string (character) + "Return an org-priority-string based on CHARACTER and user settings." + (cond ((not character) "") + ((and org-jira-priority-to-org-priority-omit-default-priority + (eq character org-default-priority)) "") + (t (format "[#%c] " character)))) + +(defun org-jira-get-org-priority-cookie-from-issue (priority) + "Get the `org-mode' [#X] PRIORITY cookie." + (let ((character (cdr (assoc priority org-jira-priority-to-org-priority-alist)))) + (org-jira-get-org-priority-string character))) + +(provide 'org-jira) +;;; org-jira.el ends here