aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el64
1 files changed, 56 insertions, 8 deletions
diff --git a/pearl.el b/pearl.el
index b0c48ca..abd0166 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1655,14 +1655,16 @@ the mapping changes, so a mid-session `setq' or customization takes effect."
pearl-todo-states-pattern)
(defun pearl--map-linear-priority-to-org (priority-num)
- "Convert Linear PRIORITY-NUM to an Org priority cookie.
-PRIORITY-NUM is 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low."
+ "Convert Linear PRIORITY-NUM to an Org priority cookie, or \"\" for none.
+PRIORITY-NUM is 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low. None (0), nil, and
+any unknown value map to no cookie -- 1:1 with the cookie so a None issue does
+not collide with Medium, which the save-model relies on for dirty detection."
(cond
((eq priority-num 1) "[#A]") ; Urgent -> A
((eq priority-num 2) "[#B]") ; High -> B
((eq priority-num 3) "[#C]") ; Medium -> C
((eq priority-num 4) "[#D]") ; Low -> D
- (t "[#C]"))) ; Default -> C
+ (t ""))) ; None / nil / unknown -> no cookie
(defun pearl--get-linear-priority-name (priority-num)
"Convert Linear PRIORITY-NUM to a readable name.
@@ -1898,7 +1900,9 @@ identifier prefix -- so an unedited heading is a no-op on title sync)."
(plist-get issue :labels) " "))
(body-org (pearl--md-to-org description)))
(concat
- (format "** %s %s %s\n" todo priority heading-title)
+ (format "** %s %s%s\n" todo
+ (if (string-empty-p priority) "" (concat priority " "))
+ heading-title)
":PROPERTIES:\n"
(format ":LINEAR-ID: %s\n" (or (plist-get issue :id) ""))
(format ":LINEAR-IDENTIFIER: %s\n" (or (plist-get issue :identifier) ""))
@@ -3250,17 +3254,61 @@ does). A candidate only means the body differs; ownership is decided later."
candidates))))))))
(nreverse candidates)))
+(defun pearl--priority-number-at-point ()
+ "Return the Linear priority (0-4) implied by the heading cookie at point.
+A/B/C/D map to Urgent/High/Medium/Low (1-4); no cookie is None (0)."
+ (save-excursion
+ (org-back-to-heading t)
+ (let ((case-fold-search nil))
+ (if (re-search-forward "\\[#\\([A-D]\\)\\]" (line-end-position) t)
+ (cdr (assoc (match-string 1) '(("A" . 1) ("B" . 2) ("C" . 3) ("D" . 4))))
+ 0))))
+
+(defun pearl--priority-dirty-p ()
+ "Non-nil if the heading's priority cookie differs from `LINEAR-PRIORITY'.
+A missing baseline (a legacy file rendered before this field) is not dirty -- a
+refresh writes the baseline; the saver's missing-property guard covers the edge."
+ (let ((base (org-entry-get nil "LINEAR-PRIORITY")))
+ (and base
+ (/= (pearl--priority-number-at-point) (string-to-number base)))))
+
+(defun pearl--id-baseline-dirty-p (live-prop base-prop)
+ "Non-nil if LIVE-PROP differs from its present baseline BASE-PROP at point.
+A missing baseline is not dirty (see `pearl--priority-dirty-p')."
+ (let ((base (org-entry-get nil base-prop)))
+ (and base
+ (not (string= (or (org-entry-get nil live-prop) "") base)))))
+
+(defun pearl--label-ids-dirty-p ()
+ "Non-nil if `LINEAR-LABEL-IDS' differs from `LINEAR-LABEL-IDS-SYNCED' as a set.
+Order-insensitive (Linear label order is not meaningful). A missing baseline is
+not dirty."
+ (let ((base (org-entry-get nil "LINEAR-LABEL-IDS-SYNCED")))
+ (and base
+ (not (equal (sort (split-string (or (org-entry-get nil "LINEAR-LABEL-IDS") "") " " t)
+ #'string<)
+ (sort (split-string base " " t) #'string<))))))
+
(defun pearl--issue-dirty-fields ()
"Return the locally-changed fields of the issue subtree at point, no network.
-A plist: `:title' and `:description' are booleans; `:comment-candidates' is the
-list from `pearl--changed-comment-candidates'. This is Phase A of the two-phase
-save scan -- comment ownership (own vs read-only) is classified separately once
-the viewer id is known (see `pearl--classify-comment-candidates')."
+A plist: `:title', `:description', `:priority', `:state', `:assignee', and
+`:labels' are booleans; `:comment-candidates' is the list from
+`pearl--changed-comment-candidates'. The structured-field checks compare each
+live value against its synced baseline (id/scalar only, no name resolution or
+remote read). `:state' here is the picker arm only -- explicit
+`LINEAR-STATE-ID' vs `LINEAR-STATE-ID-SYNCED'; the keyword-cycle arm is gated
+on keyword derivation (a later task). This is Phase A of the two-phase save
+scan -- comment ownership is classified separately once the viewer id is known
+(see `pearl--classify-comment-candidates')."
(save-excursion
(pearl--goto-heading-or-error)
(list :title (not (string= (secure-hash 'sha256 (pearl--issue-title-at-point))
(or (org-entry-get nil "LINEAR-TITLE-SHA256") "")))
:description (pearl--subtree-dirty-p)
+ :priority (pearl--priority-dirty-p)
+ :state (pearl--id-baseline-dirty-p "LINEAR-STATE-ID" "LINEAR-STATE-ID-SYNCED")
+ :assignee (pearl--id-baseline-dirty-p "LINEAR-ASSIGNEE-ID" "LINEAR-ASSIGNEE-ID-SYNCED")
+ :labels (pearl--label-ids-dirty-p)
:comment-candidates (pearl--changed-comment-candidates))))
(defun pearl--classify-comment-candidates (candidates viewer-id)