;;; my-org-jira.el -- Extensions for org-jira -*- lexical-binding: t -*- ;; Copyright (C) 2023 Free Software Foundation. ;; Author: Yuchen Pei ;; 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 . ;;; 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))) (provide 'my-org-jira) ;;; my-org-jira.el ends here