diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-25 19:54:32 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-25 19:54:32 -0500 |
| commit | 8fa0febbc3a3afe78d1a5abbbb944adf6dd3612a (patch) | |
| tree | 892acf92c1e1fffcedfeab18cb980ba7e9698c91 | |
| parent | 648893bd6ff5f4c403733d03258dbf3e91f2394b (diff) | |
| download | pearl-8fa0febbc3a3afe78d1a5abbbb944adf6dd3612a.tar.gz pearl-8fa0febbc3a3afe78d1a5abbbb944adf6dd3612a.zip | |
feat(save): add an atomic conflict gate for enum and relation fields
The text conflict gate (pearl--resolve-conflict) is built for mergeable text — it can open an smerge buffer, stash to the kill ring, and advance a SHA hash. None of that fits a state, priority, assignee, or label conflict, where the value is a single id or scalar that can't be combined.
pearl--resolve-atomic-conflict is the sibling for those fields: it prompts use-local / use-remote / cancel (no merge option), and delegates the actual push and the baseline advance to the caller's two callbacks, so it stays free of any field-specific mechanics. It maps to the same outcome contract as the text gate — cancel to conflict/cancelled, use-local to pushed or failed/push-failed, use-remote to resolved-remote.
It's not wired into a saver yet — the per-field savers in the next commit call it.
| -rw-r--r-- | pearl.el | 46 | ||||
| -rw-r--r-- | tests/test-pearl-conflict.el | 56 |
2 files changed, 102 insertions, 0 deletions
@@ -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 () |
