aboutsummaryrefslogtreecommitdiff
path: root/pearl.el
diff options
context:
space:
mode:
Diffstat (limited to 'pearl.el')
-rw-r--r--pearl.el110
1 files changed, 110 insertions, 0 deletions
diff --git a/pearl.el b/pearl.el
index 7d2210e..ae694c1 100644
--- a/pearl.el
+++ b/pearl.el
@@ -3608,6 +3608,116 @@ Surface BUFFER when any field pushed. The shared close-out for
(message "%s: %s" label (pearl--save-summary-message outcomes))
(when pushed (pearl--surface-buffer buffer))))
+(defun pearl--run-atomic-field-save (spec callback)
+ "Save one atomic (enum/relation) field per SPEC, calling CALLBACK once.
+SPEC keys: :issue-id :identifier :field :marker :label; :live and :baseline
+\(comparison values -- a nil :baseline is a legacy file with no synced
+baseline); :equal (a comparator, default `equal'); :fetch-remote (FN CB; CB
+gets the remote comparison value or `:error'); :push (FN CB; pushes :live, CB
+gets a (:success BOOL) result); :commit-local (FN; advances the baseline to
+:live, already in the buffer); :commit-remote (FN VALUE; writes VALUE into the
+buffer and advances the baseline to it); :live-display (a prompt string) and
+:remote-display (FN VALUE -> string).
+
+The atomic three-way, the sibling of `pearl--run-field-save' for non-text
+fields: a missing baseline is `skipped'/`missing-property'; live equal to
+baseline is `unchanged'; otherwise the remote is fetched (`:error' ->
+`failed'/`fetch-failed') -- remote equal to live converges (advance baseline,
+`unchanged'), remote equal to baseline is a clean push, anything else runs
+`pearl--resolve-atomic-conflict'. No SHA hash and no smerge."
+ (let* ((live (plist-get spec :live))
+ (baseline (plist-get spec :baseline))
+ (equal-fn (or (plist-get spec :equal) #'equal))
+ (push-then-commit
+ (lambda (cb)
+ (funcall (plist-get spec :push)
+ (lambda (r)
+ (when (plist-get r :success)
+ (funcall (plist-get spec :commit-local)))
+ (funcall cb (plist-get r :success)))))))
+ (cond
+ ((null baseline)
+ (funcall callback (pearl--save-outcome spec 'skipped 'missing-property)))
+ ((funcall equal-fn live baseline)
+ (funcall callback (pearl--save-outcome spec 'unchanged nil)))
+ (t
+ (funcall
+ (plist-get spec :fetch-remote)
+ (lambda (remote)
+ (cond
+ ((eq remote :error)
+ (funcall callback (pearl--save-outcome spec 'failed 'fetch-failed)))
+ ((funcall equal-fn remote live)
+ (funcall (plist-get spec :commit-local))
+ (funcall callback (pearl--save-outcome spec 'unchanged nil)))
+ ((funcall equal-fn remote baseline)
+ (funcall push-then-commit
+ (lambda (ok)
+ (funcall callback
+ (pearl--save-outcome spec (if ok 'pushed 'failed)
+ (and (not ok) 'push-failed))))))
+ (t
+ (pearl--resolve-atomic-conflict
+ (plist-get spec :label)
+ (plist-get spec :live-display)
+ (funcall (plist-get spec :remote-display) remote)
+ push-then-commit
+ (lambda () (funcall (plist-get spec :commit-remote) remote))
+ (lambda (status reason)
+ (funcall callback (pearl--save-outcome spec status reason))))))))))))
+
+(defun pearl--node-assignee-id (node)
+ "The assignee id of raw issue NODE, or \"\" when unassigned."
+ (or (cdr (assoc 'id (cdr (assoc 'assignee node)))) ""))
+
+(defun pearl--node-assignee-name (node)
+ "The assignee display name of raw issue NODE, or \"(unassigned)\"."
+ (or (cdr (assoc 'name (cdr (assoc 'assignee node)))) "(unassigned)"))
+
+(defun pearl--save-assignee-field (marker callback)
+ "Save the assignee of the issue subtree at MARKER, calling CALLBACK.
+Picker-authoritative: the live id is `LINEAR-ASSIGNEE-ID', diffed against
+`LINEAR-ASSIGNEE-ID-SYNCED'. A push sends `assigneeId' (`:null' unassigns)."
+ (org-with-point-at marker
+ (let* ((issue-id (org-entry-get nil "LINEAR-ID"))
+ (identifier (org-entry-get nil "LINEAR-IDENTIFIER"))
+ (live (or (org-entry-get nil "LINEAR-ASSIGNEE-ID") ""))
+ (remote-node nil)
+ (spec
+ (list
+ :issue-id issue-id :identifier identifier :field 'assignee
+ :marker marker
+ :label (format "%s assignee" (or identifier issue-id))
+ :live live
+ :baseline (org-entry-get nil "LINEAR-ASSIGNEE-ID-SYNCED")
+ :equal #'string=
+ :live-display (or (org-entry-get nil "LINEAR-ASSIGNEE-NAME") "(unassigned)")
+ :remote-display (lambda (_id) (pearl--node-assignee-name remote-node))
+ :fetch-remote
+ (lambda (cb)
+ (pearl--fetch-issue-async
+ issue-id
+ (lambda (node)
+ (if (memq node '(:error :missing))
+ (funcall cb :error)
+ (setq remote-node node)
+ (funcall cb (pearl--node-assignee-id node))))))
+ :push
+ (lambda (cb)
+ (pearl--update-issue-async
+ issue-id
+ (list (cons "assigneeId" (if (string-empty-p live) :null live)))
+ cb))
+ :commit-local
+ (lambda () (org-entry-put marker "LINEAR-ASSIGNEE-ID-SYNCED" live))
+ :commit-remote
+ (lambda (remote-id)
+ (org-entry-put marker "LINEAR-ASSIGNEE-ID" remote-id)
+ (org-entry-put marker "LINEAR-ASSIGNEE-NAME"
+ (pearl--node-assignee-name remote-node))
+ (org-entry-put marker "LINEAR-ASSIGNEE-ID-SYNCED" remote-id)))))
+ (pearl--run-atomic-field-save spec callback))))
+
(defun pearl--save-field-thunks (marker dirty viewer-id)
"Build the ordered save thunks for the DIRTY fields of the issue at MARKER.
Title first, then description, then each changed comment candidate (VIEWER-ID