aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el46
-rw-r--r--tests/test-pearl-conflict.el56
2 files changed, 102 insertions, 0 deletions
diff --git a/pearl.el b/pearl.el
index abd0166..7d2210e 100644
--- a/pearl.el
+++ b/pearl.el
@@ -2081,6 +2081,52 @@ push-failed), `rewrite' abort -> (conflict aborted)."
(when outcome-cb (funcall outcome-cb 'failed 'push-failed))))))
(lambda () (when outcome-cb (funcall outcome-cb 'conflict 'aborted)))))))
+(defun pearl--read-atomic-conflict-resolution (label)
+ "Prompt for resolving an atomic-field conflict on LABEL, returning a symbol.
+One of `use-local' (push mine, overwrite Linear), `use-remote' (take Linear's),
+or `cancel'. There is no merge option -- an enum or relation value can't be
+combined. A bare RET defaults to `cancel'."
+ (let* ((choices '(("cancel -- leave the field in conflict" . cancel)
+ ("use local -- push my value, overwrite Linear" . use-local)
+ ("use remote -- take Linear's value" . use-remote)))
+ (default (caar choices))
+ (pick (completing-read
+ (format "Conflict on %s (RET cancels): " label)
+ (mapcar #'car choices) nil t nil nil default)))
+ (cdr (assoc pick choices))))
+
+(defun pearl--resolve-atomic-conflict
+ (label local-display remote-display push-local-fn take-remote-fn outcome-cb)
+ "Resolve an atomic-field (enum/relation) conflict on LABEL interactively.
+The sibling of `pearl--resolve-conflict' for fields whose value is a single
+id or scalar, not mergeable text -- so there is no smerge/rewrite branch and no
+SHA hash is written.
+
+LOCAL-DISPLAY and REMOTE-DISPLAY are human-readable values for the prompt.
+PUSH-LOCAL-FN takes a callback invoked with non-nil on a successful push of the
+local value (the caller advances the baseline on success). TAKE-REMOTE-FN
+adopts Linear's value locally and advances the baseline, with no push.
+OUTCOME-CB is called once with (STATUS REASON): `cancel' -> (conflict
+cancelled); `use-local' -> (pushed nil) or (failed push-failed); `use-remote'
+-> (resolved-remote nil)."
+ (pcase (pearl--read-atomic-conflict-resolution label)
+ ('cancel
+ (message "Left %s in conflict (refresh to see Linear's %s)" label remote-display)
+ (funcall outcome-cb 'conflict 'cancelled))
+ ('use-local
+ (funcall push-local-fn
+ (lambda (ok)
+ (if ok
+ (progn
+ (message "Pushed your %s (%s) to Linear" label local-display)
+ (funcall outcome-cb 'pushed nil))
+ (message "Failed to push %s" label)
+ (funcall outcome-cb 'failed 'push-failed)))))
+ ('use-remote
+ (funcall take-remote-fn)
+ (message "Took Linear's %s (%s)" label remote-display)
+ (funcall outcome-cb 'resolved-remote nil))))
+
;;; Compose Buffer
;;
;; A focused Org buffer for composing multi-line text (comments, descriptions)
diff --git a/tests/test-pearl-conflict.el b/tests/test-pearl-conflict.el
index 138e622..56d3c84 100644
--- a/tests/test-pearl-conflict.el
+++ b/tests/test-pearl-conflict.el
@@ -224,6 +224,62 @@
(should (string= (secure-hash 'sha256 "merged text")
(org-entry-get marker "LINEAR-DESC-SHA256"))))))))
+;;; --resolve-atomic-conflict (enum/relation fields, no merge)
+
+(ert-deftest test-pearl-resolve-atomic-conflict-cancel ()
+ "Cancel pushes nothing, takes nothing, and reports conflict/cancelled."
+ (let (pushed took outcome)
+ (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution)
+ (lambda (_) 'cancel)))
+ (pearl--resolve-atomic-conflict
+ "ENG-1 state" "In Progress" "Done"
+ (lambda (cb) (setq pushed t) (funcall cb t))
+ (lambda () (setq took t))
+ (lambda (status reason) (setq outcome (list status reason))))
+ (should-not pushed)
+ (should-not took)
+ (should (equal '(conflict cancelled) outcome)))))
+
+(ert-deftest test-pearl-resolve-atomic-conflict-use-local-pushes ()
+ "Use-local runs the push; on success reports pushed."
+ (let (pushed took outcome)
+ (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution)
+ (lambda (_) 'use-local)))
+ (pearl--resolve-atomic-conflict
+ "ENG-1 state" "In Progress" "Done"
+ (lambda (cb) (setq pushed t) (funcall cb t))
+ (lambda () (setq took t))
+ (lambda (status reason) (setq outcome (list status reason))))
+ (should pushed)
+ (should-not took)
+ (should (equal '(pushed nil) outcome)))))
+
+(ert-deftest test-pearl-resolve-atomic-conflict-use-local-push-failure ()
+ "A failed local push reports failed/push-failed."
+ (let (outcome)
+ (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution)
+ (lambda (_) 'use-local)))
+ (pearl--resolve-atomic-conflict
+ "ENG-1 state" "In Progress" "Done"
+ (lambda (cb) (funcall cb nil))
+ (lambda () nil)
+ (lambda (status reason) (setq outcome (list status reason))))
+ (should (equal '(failed push-failed) outcome)))))
+
+(ert-deftest test-pearl-resolve-atomic-conflict-use-remote-takes-no-push ()
+ "Use-remote adopts Linear's value, never pushes, and reports resolved-remote."
+ (let (pushed took outcome)
+ (cl-letf (((symbol-function 'pearl--read-atomic-conflict-resolution)
+ (lambda (_) 'use-remote)))
+ (pearl--resolve-atomic-conflict
+ "ENG-1 state" "In Progress" "Done"
+ (lambda (cb) (setq pushed t) (funcall cb t))
+ (lambda () (setq took t))
+ (lambda (status reason) (setq outcome (list status reason))))
+ (should took)
+ (should-not pushed)
+ (should (equal '(resolved-remote nil) outcome)))))
+
;;; --conflict-has-markers-p
(ert-deftest test-pearl-conflict-has-markers-p ()