blob: 9e2f8214fef9cc414324593a814e4316d49e1ac1 (
plain) (
tree)
|
|
;;; my-org-jira.el -- Extensions for org-jira -*- lexical-binding: t -*-
;; Copyright (C) 2023 Free Software Foundation.
;; Author: Yuchen Pei <id@ypei.org>
;; Package-Requires: ((emacs "28.2"))
;; This file is part of dotted.
;; dotted is free software: you can redistribute it and/or modify it under
;; the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; dotted 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 Affero General
;; Public License for more details.
;; You should have received a copy of the GNU Affero General Public
;; License along with dotted. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Extensions for org-jira.
;;; Code:
(require 'org-jira)
;;; override `org-jira-sdk-issue'
(defclass org-jira-sdk-issue (org-jira-sdk-record)
((affected-versions :type string :initarg :affected-versions)
(assignee :type (or null string) :initarg :assignee)
(components :type string :initarg :components)
(fix-versions :type string :initarg :fix-versions)
(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)
(related-issues :type string :initarg :related-issues)
(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.")
;;; override `org-jira-sdk-from-data'
(cl-defmethod org-jira-sdk-from-data ((rec org-jira-sdk-issue))
;; (print rec)
(cl-flet ((path (keys) (org-jira-sdk-path (oref rec data) keys)))
(org-jira-sdk-issue
:affected-versions (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields versions)) ", ")
:assignee (path '(fields assignee displayName))
:components (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields components)) ", ")
:fix-versions (mapconcat (lambda (c) (org-jira-sdk-path c '(name))) (path '(fields fixVersions)) ", ")
: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))
:related-issues (mapconcat
(lambda (c)
;; (print c)
(if (org-jira-sdk-path c '(inwardIssue))
(if (equal
(org-jira-sdk-path
c '(inwardIssue fields status name))
"Closed")
""
(format "%s: %s %s"
(org-jira-sdk-path c '(type inward))
(org-jira-sdk-path c '(inwardIssue key))
(org-jira-sdk-path c '(inwardIssue fields summary))))
(if (equal
(org-jira-sdk-path
c '(outwardIssue fields status name))
"Closed")
""
(format "%s: %s %s"
(org-jira-sdk-path c '(type outward))
(org-jira-sdk-path c '(outwardIssue key))
(org-jira-sdk-path c '(outwardIssue fields summary))))))
(path '(fields issuelinks)) "; ")
: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)
)))
;; Override `org-jira--render-issue'
;; include issue-id in the headline
(defun my-org-jira--render-issue (Issue)
"Render single ISSUE."
;; (org-jira-log "Rendering issue from issue list")
;; (org-jira-log (org-jira-sdk-dump Issue))
;; (print 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)
issue-id " " 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)))
(mapc (lambda (entry)
(let ((val (slot-value Issue entry)))
(when (or (and val (not (string= val "")))
(eq entry 'assignee)) ;; Always show assignee
(org-jira-entry-put (point) (symbol-name entry) val))))
'(assignee filename reporter type type-id priority affected-versions fix-versions labels resolution status components created updated sprint related-issues))
(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))))))))
;; Overload `org-jira-update-worklogs-from-org-clocks'.
(defun my-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)))
(unless (cdr (assoc 'worklog-id worklog))
(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 my-org-jira-comment-url (issue-id comment-id)
(format
"%s/browse/%s?focusedCommentId=%s&page=com.atlassian.jira.plugin.system.issuetabpanels%%3Acomment-tabpanel#comment-%s"
jiralib-url issue-id comment-id comment-id))
(defun my-org-jira-comment-url-at-point ()
(my-org-jira-comment-url
(org-entry-get
(save-excursion
(outline-up-heading 1)
(point))
"ID")
(org-entry-get (point) "ID")))
(defun my-org-jira-kill-comment-url-at-point ()
(interactive)
(kill-new (my-org-jira-comment-url-at-point)))
(defun my-org-jira-url-p (url)
(string-match-p (format "^%s/browse/[^/]" jiralib-url) url))
(defun my-org-jira-open-url (url)
(interactive "sJIRA issue url: ")
(when (string-match (format "^%s/browse/\\([^/]+\\)" jiralib-url) url)
(org-jira-get-issue (match-string 1 url))))
(provide 'my-org-jira)
;;; my-org-jira.el ends here
|