aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pearl.el58
-rw-r--r--tests/test-pearl-org-write.el10
-rw-r--r--tests/test-pearl-output.el59
3 files changed, 113 insertions, 14 deletions
diff --git a/pearl.el b/pearl.el
index 79c2ebd..f988668 100644
--- a/pearl.el
+++ b/pearl.el
@@ -2838,6 +2838,16 @@ yields the hardcoded default. Pure function, no side effects."
(buffer-string))))
+(defun pearl--dirty-refresh-choice (path)
+ "Ask how to refresh PATH when its buffer has unsaved changes.
+Returns one of ?d (discard the edits and rebuild), ?m (merge by LINEAR-ID,
+keeping unpushed edits), or ?q (cancel and leave the buffer untouched)."
+ (read-char-choice
+ (format (concat "%s has unsaved changes — "
+ "(d)iscard & refresh, (m)erge keeping edits, (q) cancel? ")
+ (file-name-nondirectory path))
+ '(?d ?m ?q)))
+
(defun pearl--update-org-from-issues (issues &optional source truncated)
"Update `pearl-org-file-path' with rendered ISSUES, buffer-aware.
SOURCE and TRUNCATED are threaded into the header (see
@@ -2849,8 +2859,9 @@ Behavior depends on whether the file is currently visited in a buffer:
then visit it so there is a buffer to show.
- Buffer exists and is unmodified: replace its contents in place and save,
preserving point and avoiding a modtime mismatch warning.
-- Buffer exists and has unsaved edits: do not overwrite. Log and emit a
- message asking the user to save or revert before re-running.
+- Buffer exists and has unsaved edits: a full rebuild would discard them, so
+ prompt (see `pearl--dirty-refresh-choice') to discard and rebuild, merge by
+ LINEAR-ID keeping the edits, or cancel.
In every case the resulting buffer is surfaced (see `pearl--surface-buffer'),
so a fetch run from a menu or dashboard actually shows the issues instead of
@@ -2892,15 +2903,42 @@ just writing the file and leaving it off-screen."
org-file-path (length issues))
(pearl--surface-buffer existing-buf))
- ;; Branch C: buffer is dirty -- defer, do not overwrite, but show it.
+ ;; Branch C: buffer is dirty -- a full rebuild would discard unsaved work,
+ ;; so ask before clobbering rather than silently deferring.
(t
- (pearl--log
- "Linear refresh deferred: %s has unsaved changes (%d issues not written)"
- org-file-path (length issues))
- (message
- "Linear refresh deferred: %s has unsaved changes. Save or revert, then re-run M-x pearl-list-issues."
- (file-name-nondirectory org-file-path))
- (pearl--surface-buffer existing-buf)))))
+ (pcase (pearl--dirty-refresh-choice org-file-path)
+ (?d ; discard the edits, rebuild in place
+ (with-current-buffer existing-buf
+ (let ((inhibit-read-only t))
+ (set-buffer-modified-p nil)
+ (erase-buffer)
+ (insert new-content)
+ (save-buffer)
+ (goto-char (point-min))
+ (pearl--restore-page-visibility)
+ (pearl-highlight-comments)))
+ (message "Updated Linear issues in %s with %d active issues"
+ org-file-path (length issues))
+ (pearl--surface-buffer existing-buf))
+ (?m ; merge by LINEAR-ID, keeping edits
+ (with-current-buffer existing-buf
+ (let ((counts (pearl--merge-issues-into-buffer issues)))
+ (pearl--update-source-header (length issues) truncated)
+ (pearl--update-derived-todo-header (pearl--gather-header-states issues))
+ (pearl-highlight-comments)
+ (pearl--restore-page-visibility)
+ (message "Merged into %s: %d updated, %d added, %d dropped%s"
+ (file-name-nondirectory org-file-path)
+ (plist-get counts :updated)
+ (plist-get counts :added)
+ (plist-get counts :dropped)
+ (let ((s (plist-get counts :skipped)))
+ (if (> s 0) (format ", %d kept (unpushed edits)" s) "")))))
+ (pearl--surface-buffer existing-buf))
+ (_ ; cancel
+ (message "Linear refresh cancelled; %s left unchanged"
+ (file-name-nondirectory org-file-path))
+ (pearl--surface-buffer existing-buf)))))))
(defcustom pearl-saved-queries nil
"Named local issue queries, run with `pearl-run-saved-query'.
diff --git a/tests/test-pearl-org-write.el b/tests/test-pearl-org-write.el
index 6c30380..e467436 100644
--- a/tests/test-pearl-org-write.el
+++ b/tests/test-pearl-org-write.el
@@ -69,19 +69,20 @@
(should-not (string-match-p "old content" (buffer-string)))))))
(ert-deftest test-pearl-update-org-dirty-buffer-not-overwritten ()
- "A buffer with unsaved edits is left untouched, not clobbered."
+ "Cancelling the dirty-refresh prompt leaves a buffer with unsaved edits untouched."
(test-pearl--with-org-file tmp
(let ((buf (find-file-noselect tmp)))
(with-current-buffer buf
(insert "unsaved edits"))
- (pearl--update-org-from-issues test-pearl--sample-issues)
+ (cl-letf (((symbol-function 'pearl--dirty-refresh-choice) (lambda (_p) ?q)))
+ (pearl--update-org-from-issues test-pearl--sample-issues))
(with-current-buffer buf
(should (buffer-modified-p))
(should (string-match-p "unsaved edits" (buffer-string)))
(should-not (string-match-p "ENG-1" (buffer-string)))))))
(ert-deftest test-pearl-update-org-dirty-buffer-preserves-disk-and-surfaces ()
- "A dirty buffer is left as-is, the on-disk file is untouched, and the buffer is surfaced."
+ "Cancelling the prompt leaves the dirty buffer and on-disk file as-is, and surfaces it."
(test-pearl--with-org-file tmp
(let ((buf (find-file-noselect tmp))
(surfaced nil))
@@ -92,7 +93,8 @@
(goto-char (point-max))
(insert "UNSAVED EDIT\n"))
(cl-letf (((symbol-function 'pearl--surface-buffer)
- (lambda (b) (setq surfaced b))))
+ (lambda (b) (setq surfaced b)))
+ ((symbol-function 'pearl--dirty-refresh-choice) (lambda (_p) ?q)))
(pearl--update-org-from-issues test-pearl--sample-issues))
;; buffer keeps the unsaved edit and stays modified
(with-current-buffer buf
diff --git a/tests/test-pearl-output.el b/tests/test-pearl-output.el
index eb5c671..58cd446 100644
--- a/tests/test-pearl-output.el
+++ b/tests/test-pearl-output.el
@@ -184,5 +184,64 @@
(kill-buffer buf))
(ignore-errors (delete-file tmp)))))
+;;; --update-org-from-issues on a dirty buffer (prompted: discard / merge / cancel)
+
+(defconst test-pearl-output--issue
+ '(:id "u" :identifier "ENG-1" :title "Hello" :priority 2 :state (:name "Todo"))
+ "A minimal normalized issue for the dirty-buffer branch tests.")
+
+(defmacro test-pearl-output--with-dirty-buffer (choice &rest body)
+ "Run BODY with a modified buffer visiting a temp active file and the dirty
+refresh prompt stubbed to return CHOICE. BUF and TMP are bound in BODY."
+ (declare (indent 1))
+ `(let* ((tmp (make-temp-file "pearl-dirty" nil ".org"))
+ (pearl-org-file-path tmp)
+ (buf (find-file-noselect tmp)))
+ (unwind-protect
+ (with-current-buffer buf
+ (insert "DIRTYMARKER not yet saved\n") ; buffer is now modified
+ (cl-letf (((symbol-function 'pearl--surface-buffer) #'ignore)
+ ((symbol-function 'pearl--dirty-refresh-choice)
+ (lambda (_path) ,choice)))
+ ,@body))
+ (when (buffer-live-p buf)
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf))
+ (ignore-errors (delete-file tmp)))))
+
+(ert-deftest test-pearl-update-org-dirty-discard-rebuilds ()
+ "Choosing discard rebuilds the buffer from the fetch and clears the edits."
+ (test-pearl-output--with-dirty-buffer ?d
+ (pearl--update-org-from-issues (list test-pearl-output--issue)
+ '(:type filter :name "X" :filter nil) nil)
+ (should-not (buffer-modified-p))
+ (should-not (string-match-p "DIRTYMARKER" (buffer-string)))
+ (should (string-match-p "ENG-1" (buffer-string)))))
+
+(ert-deftest test-pearl-update-org-dirty-cancel-leaves-buffer ()
+ "Choosing cancel leaves the dirty buffer untouched."
+ (test-pearl-output--with-dirty-buffer ?q
+ (pearl--update-org-from-issues (list test-pearl-output--issue)
+ '(:type filter :name "X" :filter nil) nil)
+ (should (buffer-modified-p))
+ (should (string-match-p "DIRTYMARKER" (buffer-string)))
+ (should-not (string-match-p "ENG-1" (buffer-string)))))
+
+(ert-deftest test-pearl-update-org-dirty-merge-routes-to-merge ()
+ "Choosing merge routes to the by-LINEAR-ID merge instead of a rebuild."
+ (let ((merged nil))
+ (test-pearl-output--with-dirty-buffer ?m
+ (cl-letf (((symbol-function 'pearl--merge-issues-into-buffer)
+ (lambda (_issues) (setq merged t) '(:updated 1 :added 0 :dropped 0 :skipped 0)))
+ ((symbol-function 'pearl--update-source-header) #'ignore)
+ ((symbol-function 'pearl--update-derived-todo-header) #'ignore)
+ ((symbol-function 'pearl-highlight-comments) #'ignore)
+ ((symbol-function 'pearl--restore-page-visibility) #'ignore))
+ (pearl--update-org-from-issues (list test-pearl-output--issue)
+ '(:type filter :name "X" :filter nil) nil))
+ (should merged)
+ ;; merge does not rebuild, so the marker is not wiped by a full replace
+ (should (string-match-p "DIRTYMARKER" (buffer-string))))))
+
(provide 'test-pearl-output)
;;; test-pearl-output.el ends here