;;; forge-issue.el --- Issue support -*- lexical-binding:t -*- ;; Copyright (C) 2018-2026 Jonas Bernoulli ;; Author: Jonas Bernoulli ;; Maintainer: Jonas Bernoulli ;; SPDX-License-Identifier: GPL-3.0-or-later ;; This file 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 file 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 file. If not, see . ;;; Code: (require 'forge) (require 'forge-post) (require 'forge-topic) ;;; Classes (defclass forge-issue (forge-topic) ((closql-table :initform 'issue) (closql-primary-key :initform 'id) (closql-order-by :initform [(desc number)]) (closql-foreign-key :initform 'repository) (closql-class-prefix :initform "forge-") (id :initarg :id) (repository :initarg :repository) (number :initarg :number) (state :initarg :state) (author :initarg :author) (title :initarg :title) (created :initarg :created) (updated :initarg :updated :initform nil) (closed :initarg :closed) (status :initarg :status :initform nil) (locked-p :initarg :locked-p) (milestone :initarg :milestone) (body :initarg :body) (assignees :closql-tables (issue-assignee assignee)) (project-cards) ; projectsCards (edits) ; userContentEdits (labels :closql-tables (issue-label label)) (participants) (posts :closql-class forge-issue-post) (reactions) (timeline) (marks :closql-tables (issue-mark mark)) (note :initarg :note :initform nil) (their-id :initarg :their-id) (slug :initarg :slug) (saved-p :initarg :saved-p :initform nil) )) (cl-defmethod closql-dref ((obj forge-issue) (_(eql assignees))) (forge-sql-cdr [:select assignee:* :from assignee :join issue-assignee :on (= issue-assignee:id assignee:id) :where (= issue-assignee:issue $s1) :order-by [(asc login)]] (closql--oref obj 'id))) (cl-defmethod closql-dref ((obj forge-issue) (_(eql labels))) (forge-sql-cdr [:select label:* :from label :join issue-label :on (= issue-label:id label:id) :where (= issue-label:issue $s1) :order-by [(asc name)]] (closql--oref obj 'id))) (cl-defmethod closql-dref ((obj forge-issue) (_(eql marks))) (forge-sql-cdr [:select mark:* :from mark :join issue-mark :on (= issue-mark:id mark:id) :where (= issue-mark:issue $s1) :order-by [(asc name)]] (closql--oref obj 'id))) (defclass forge-issue-post (forge-post) ((closql-table :initform 'issue-post) (closql-primary-key :initform 'id) (closql-order-by :initform [(asc number)]) (closql-foreign-key :initform 'issue) (closql-class-prefix :initform "forge-issue-") (id :initarg :id) (issue :initarg :issue) (number :initarg :number) (author :initarg :author) (created :initarg :created) (updated :initarg :updated) (body :initarg :body) (edits) (reactions) )) ;;; Query ;;;; Get (cl-defmethod forge-get-repository ((post forge-issue-post)) (forge-get-repository (forge-get-issue post))) (cl-defmethod forge-get-topic ((post forge-issue-post)) (forge-get-issue post)) (cl-defmethod forge-get-issue ((issue forge-issue)) issue) (cl-defmethod forge-get-issue ((repo forge-repository) number) (cl-assert (numberp number) t) (closql-get (forge-db) (forge--object-id 'forge-issue repo number) 'forge-issue)) (cl-defmethod forge-get-issue ((number integer)) (and$ (forge-get-repository :tracked nil 'notatpt) (forge-get-issue $ number))) (cl-defmethod forge-get-issue ((id string)) (closql-get (forge-db) id 'forge-issue)) (cl-defmethod forge-get-issue ((post forge-issue-post)) (closql-get (forge-db) (oref post issue) 'forge-issue)) ;;;; Current (defun forge-current-issue (&optional demand) "Return the issue at point or being visited. If there is no such issue and DEMAND is non-nil, then signal an error." (or (forge-issue-at-point) (and (forge-issue-p forge-buffer-topic) forge-buffer-topic) (and demand (user-error "No current issue")))) (defun forge-issue-at-point (&optional demand) "Return the issue at point. If there is no such issue and DEMAND is non-nil, then signal an error." (or (thing-at-point 'forge-issue) (magit-section-value-if 'issue) (and demand (user-error "No issue at point")))) (put 'forge-issue 'thing-at-point #'forge-thingatpt--issue) (defun forge-thingatpt--issue () (and-let ((_(thing-at-point-looking-at "#\\([0-9]+\\)\\_>")) (number (string-to-number (match-string 1))) (repo (forge--repo-for-thingatpt))) (forge-get-issue repo number))) ;;; Read (defun forge-read-issue (prompt) "Read an active issue with completion using PROMPT. Open, unread and pending issues are considered active. Default to the current issue, even if it isn't active. \\While completion is in \ progress, \\[forge-read-topic-lift-limit] lifts the limit, extending the completion candidates to include all issues. If `forge-limit-topic-choices' is nil, then all candidates can be selected from the start." (forge--read-topic prompt #'forge-current-issue (forge--topics-spec :type 'issue :active t) (forge--topics-spec :type 'issue :active nil :state nil :limit nil))) (defun forge-read-open-issue (prompt) "Read an open issue with completion using PROMPT." (let* ((current (forge-current-issue)) (repo (forge-get-repository (or current :tracked))) (default (and current (forge--format-topic-line current))) (alist (forge--topic-collection (forge--list-topics (forge--topics-spec :type 'issue :state 'open) repo))) (choices (mapcar #'car alist)) (choice (magit-completing-read prompt choices nil t nil nil default))) (cdr (assoc choice alist)))) ;;; Insert (defvar-keymap forge-issues-section-map :parent forge-common-map " " #'forge-browse-issues " " #'forge-list-issues " " #'forge-topics-menu " " #'forge-topic-menu "C-c C-n" #'forge-create-issue) (defvar-keymap forge-issue-section-map :parent forge-common-map " " #'forge-visit-this-topic " " #'forge-topics-menu " " #'forge-topic-menu) (cl-defun forge-insert-issues (&optional (spec nil sspec) heading) "Insert a list of issues, according to `forge--buffer-topics-spec'. Optional SPEC can be used to override that filtering specification, and optional HEADING to change the section heading." (when-let* ((_(forge-db t)) (repo (forge-get-repository :tracked?)) (_(oref repo issues-p)) (spec (if sspec spec (forge--clone-buffer-topics-spec))) (_(memq (oref spec type) '(topic issue)))) (oset spec type 'issue) (forge--insert-topics 'issues (or heading "Issues") (forge--list-topics spec repo)))) ;;; _ ;; Local Variables: ;; read-symbol-shorthands: ( ;; ("and$" . "cond-let--and$") ;; ("thread$" . "cond-let--thread$") ;; ("when$" . "cond-let--when$") ;; ("and-let*" . "cond-let--and-let*") ;; ("and-let" . "cond-let--and-let") ;; ("if-let*" . "cond-let--if-let*") ;; ("if-let" . "cond-let--if-let") ;; ("when-let*" . "cond-let--when-let*") ;; ("when-let" . "cond-let--when-let") ;; ("while-let*" . "cond-let--while-let*") ;; ("while-let" . "cond-let--while-let")) ;; End: (provide 'forge-issue) ;;; forge-issue.el ends here