Almost three years ago I wrote about a very simple Emacs-Jira integration I have been using since then. As is often the case, I noticed some inconveniences with it, and decided to improve it.
Here is my problem: it’s difficult for me to remember about marking tasks as “done” in Org-mode. I change the status in Jira (since this is what the team expects and needs), but it’s usually not “done”, but “in code review” – when I finish a task, I submit it for review, and it is not “done” yet then. On the other hand, I don’t always even mark my tasks as “done” – in our workflow, this is often done by someone else (the person who deployed the code to production, which might or might not be me).
Of course, having over 700 tasks in the “todo” state is not very nice. My Org-mode file related to my job is pretty large (over 3 MB), and Org-mode can be a bit slow with large files. What I’d like to do is to archive done tasks (that is, move them to another file which is not normally used or manipulated, so it can be really large and it won’t bother me).
Obviously, the first step to achieve that is to mark done tasks as “done”. I don’t want to map all statuses we have in Jira to Org-mode statuses – after all, it’s Jira, not Org which is the “single source of truth” about the status of our tasks, and the reason I use Org-mode for them is not to keep their state, but to be able to clock them and to store personal notes (as opposed to Jira comments which I use for things the team should know about).
So, let’s use jira-terminal again. I can get the status of the task with jira-terminal detail -f status <task number>
. The drawback is that jira-terminal
prepends the word Status:
to its output (which doesn’t make much sense), but that can be remedied very easily with a bit of Elisp.
(defun jira-key-to-status (key)
"Return the status of Jira task KEY."
(when key
(trim-trailing-whitespace
(with-temp-buffer
(unless
(zerop
(call-process "jira-terminal" nil t nil
"detail" "-f" "status" key))
(error "Could not retrieve task data from Jira"))
(goto-char (point-min))
(search-forward "Status: ")
(buffer-substring-no-properties (point) (point-max))))))
Note how I support the case where key
is nil – as we will see, it may happen that this function will be called on a headline with no Jira task key, and I want it to silently do nothing then instead of throwing an error.
Now that we have the Jira status of the task, let’s convert it to Org status. I don’t want to get too fancy here and just map all statuses we have in Jira to TODO
and DONE
.
(defconst jira-statuses
'(("Todo" . "TODO")
("Blocked" . "TODO")
("Done" . "DONE"))
"Alist of Jira statuses and their Org counterparts.")
(In reality, we have twice as many, but that is irrelevant here.)
Now, getting the Org-compatible status of a Jira task is easy.
(defun jira-key-to-org-status (key)
"Return the Org status of Jira task KEY."
(when key
(let ((jira-status (jira-key-to-status key)))
(or (alist-get jira-status
jira-statuses
nil nil
#'string=)
(error "Jira status \"%s\" not found in `jira-statuses'" jira-status)))))
Again, I added (when key ...)
to this function, since otherwise it could complain that nil
is not a valid Jira status – not what I want. On the other hand, if the status is non-nil and not known to jira-statuses
, I explicitly want to be notified that I need to add it there, hence the error
.
The next step is updating the status of the task at point.
(defun jira-update-org-task-at-point ()
"Update the status of the task at point according to Jira."
(interactive)
(unless (org-at-heading-p)
(error "Not at a heading"))
(let* ((from-state (org-get-todo-state))
(heading (org-get-heading t t t t))
(key (when (string-match
"\\([A-Z]\\)+-\\([0-9]\\)+"
heading)
(match-string 0 heading)))
(to-state (jira-key-to-org-status key)))
(unless (or (null from-state)
(null to-state)
(string= from-state to-state))
(org-todo to-state)
(org-add-log-note))))
Here again I take care to check for possible error conditions. First of all, for this function to work, the point needs to be on an Org heading. This is actually a bit debatable. On the one hand, when calling jira-update-task-at-point
interactively on a large Org entry with a lot of notes, it might happen that the status is not even visible on the screen. Updating something without a clear visual indication does not seem a good idea, and if I bind jira-update-org-task-at-point
to some key I could press by accident, this could happen. On the other hand, it seems safe to assume that if the task is marked “Done” on Jira, updating it as DONE
in Org is fine even if the user does not see/notice that. Still, I decided to be on the safe side.
The next thing here is that the status is only updated if both from-state
and to-state
are non-nil and not equal. If from-state
is nil, it means that the current headline does not even have a TODO status, so it probably does not correspond to a Jira task. (All my headlines which do correspond to Jira tasks have a TODO status.) If to-state
is nil, it means that the Jira task could not be found – this may happen for example if there is no task key (string of the form LT-1337
) in the headline. If both from-state
and to-state
are non-nil, we compare them and if they are equal, we don’t do anything, and we update the Org TODO state otherwise.
Finally, I run org-add-log-note
. The reason I want to do this is that this is how Org mode puts a note saying that the state was changed from TODO
to DONE
when configured to do so. This is one of the darker corners of Org mode: without this invocation, the note was stored, too, but if I ran jira-update-org-task-at-point
in a loop (I’ll show how in a minute), only the last task had the note. The reason is clear – there is some hackery involving post-command-hook
here. I would very much prefer if there was some argument to org-todo
telling Emacs to add a note about the state change, but it is what it is. Note: I’m not complaining too much, Org mode is a very complicated piece of software and it’s quite probable that the complexity is there for a reason – but it would be much simpler if org-todo
could insert that note itself.
Now the next thing I want to do is to update all TODO tasks in my Org file automatically. What I want to do is to iterate over all not-DONE
headlines and run jira-update-org-task-at-point
on them. Of course, the go-to solution when you want to iterate over headlines in an Org file is org-map-entries
:
(org-map-entries #'jira-update-org-task-at-point "TODO<>*\"DONE\"" nil 'archive 'comment)
As you can see, I skip headlines which have no TODO state (that’s what the star does); I also skip archived and commented out subtrees. Of course, I don’t want to run this manually, so let’s make it into a command.
(defun jira-update-all-todos ()
"Update the status of all tasks in the region or buffer."
(interactive)
(save-restriction
(when (region-active-p)
(narrow-to-region (region-beginning) (region-end)))
(let ((inhibit-message t)
(message-log-max nil))
(org-map-entries #'jira-update-org-task-at-point "TODO<>*\"DONE\"" nil 'archive 'comment))))
Since I don’t want to see the “Note stored” message repeated as many times as many headline states were updated, I suppress both displaying it in the echo area and in the **Messages** buffer.
And that’s it for today! As usual, Emacs (and Org mode) turn out to be ideal for users like me who like to mould them into exactly what they need.
CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode