gemtext in tissue issue tracker
2024-06-01 @rrobin
The tissue issue tracker stores issues as gemtext files in a git repository, it serves them as a web page and provides a CLI tool to manage them.
Gemtext is not a very powerfull format, it only supports text, bullet points, titles, links and blocks.
Tissue overcomes its limitations with some conventions. Metadata is encoded in bullet points, these have special meaning:
* tags: important * assigned: bruce * status: closed
The other convention encodes tasks, their description and state:
* [ ] buy groceries * [x] fix the car
There are also some shorthand forms e.g. this is also valid to close a ticket
* closed
Issues
Issues are read by the read-gemtext-issue function:
(define (read-gemtext-issue file)
"Read issue from gemtext @var{file} and return an @code{}
object."
(let* ((file-document (read-gemtext-document file))
I am not familiar with guile's define-class, but these seem to be the components of an issue:
(define-class( ) (assigned #:accessor issue-assigned #:init-keyword #:assigned) (keywords #:accessor issue-keywords #:init-keyword #:keywords) (open? #:accessor issue-open? #:init-keyword #:open?) (tasks #:accessor issue-tasks #:init-keyword #:tasks) (completed-tasks #:accessor issue-completed-tasks #:init-keyword #:completed-tasks))
that is
- a gemtext document as read by the read-gemtext-document function
- who is assigned to the issue
- a list of keywords
- number of tasks and completed tasks
I find this easier to read in the issue read function
(make
#:path file
;; Fallback to filename if title has no alphabetic characters.
#:title (let ((title (hashtable-ref file-details 'title "")))
(if (string-any char-set:letter title) title file))
#:assigned (hashtable-ref file-details 'assigned '())
;; "closed" is a special keyword to indicate the open/closed
;; status of an issue.
#:keywords (delete "closed" all-keywords)
#:open? (not (member "closed" all-keywords))
#:tasks (hashtable-ref file-details 'tasks 0)
#:completed-tasks (hashtable-ref file-details 'completed-tasks 0)
#:commits (file-document-commits file-document)
#:snippet-source-text (document-snippet-source-text file-document))))
That is the following information
- the path of the file
- a title whith fallback to filename
- the assigned handler for the ticket
- a list of keywords (from which "closed" is excluded)
- the status of the ticket i.e. it is open unless the closed keywork exists
- the number of tasks and completed tasks
- the commits associated with this issue
- a text snippet
The function that extracts this metatadata from the file is file-details
file-details
As the documentation states this function returns an hash-table, generated from the file contents:
(define (file-details port) "Return a hashtable of details extracted from input PORT reading a gemtext file." (let ((result (make-eq-hashtable))
I am not familiar with transducers in guile/scheme but port-transduce seems to be processing one line at a time (get-line-dos-or-unix) from the input and using the provided lambda on each line:
(port-transduce (tmap (lambda (line)
(cond
;; Toggle preformatted state.
((string=? "```" line)
(set! in-preformatted (not in-preformatted)))
;; Ignore preformatted blocks.
(in-preformatted #t)
;; Checkbox lists are tasks. If the
;; checkbox has any character other
;; than space in it, the task is
;; completed.
this function goes along mutating the result hashtable
- preformatted blocks of lines are ignored
- tasks are counted
- keywords are added to the keywords list (see hashtable-prepend!)
- the first title in the file is used as title
There are some special rules in place as well
- 'assign' and 'assigned' are equivalent (stored as status)
- likewise for 'keyword', 'keywords', 'severity', 'status' (stored as keywords)
;; Insert values based on
;; their keys.
(for-each (match-lambda
(((or 'assign 'assigned) . values)
(hashtable-prepend! result 'assigned
(map (cut resolve-alias <> (%aliases))
values)))
(((or 'keyword 'keywords 'severity 'status
'priority 'tag 'tags 'type)
. values)
(hashtable-prepend! result 'keywords values))
Finally there is a search for keywords in lists of keywords separated by commas:
(string-contains element keyword))
(list "request" "bug" "critical"
"enhancement" "progress"
"testing" "later" "documentation"
"help" "closed")))
file-document-commits
There also a reference to file-document-commits that is used to get a list of commits that relate to the gemtext file. This is first seen in the file-document class definition
(define-class( ) (path #:accessor file-document-path #:init-keyword #:path) ;; List of objects, oldest first. (commits #:accessor file-document-commits #:init-keyword #:commits))
and it is initialized during document creation based on commits-affecting-file
(define (commits-affecting-file file)
"Return a list of commits affecting @var{file} in current repository."
I did not look deeper into this but it seems to be derived from a map of files to related commits created by file-modification-table:
(define (file-modification-table repository) "Return a hashtable mapping files to the list of commits in REPOSITORY that modified them."
References
Metadata
- commit: 62116be7fca25c78d1a72ddbc19282676b2ac729
- keywords: review, scheme