summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/org-agenda-config.el22
-rw-r--r--modules/org-refile-config.el51
-rw-r--r--tests/test-org-agenda-config-add-files.el12
-rw-r--r--tests/test-org-refile-config--scan-dir.el57
4 files changed, 124 insertions, 18 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index 22bc569c..231eff8a 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -149,13 +149,21 @@ the file keeps precedence."
"Add todo.org files from immediate subdirectories of DIRECTORY.
Only checks DIRECTORY/*/todo.org — does not recurse deeper."
(interactive "D")
- (let ((todo-files
- (seq-filter
- #'file-exists-p
- (mapcar (lambda (dir) (expand-file-name "todo.org" dir))
- (seq-filter #'file-directory-p
- (directory-files directory t "^[^.]"))))))
- (setq org-agenda-files (append todo-files org-agenda-files))))
+ (if (not (and (file-directory-p directory) (file-readable-p directory)))
+ ;; Non-fatal: a missing or unreadable project root shouldn't crash the
+ ;; whole agenda build — surface it and carry on with the other files.
+ (display-warning
+ 'org-agenda
+ (format "Agenda scan: project directory missing or unreadable, skipped: %s"
+ directory)
+ :warning)
+ (let ((todo-files
+ (seq-filter
+ #'file-exists-p
+ (mapcar (lambda (dir) (expand-file-name "todo.org" dir))
+ (seq-filter #'file-directory-p
+ (directory-files directory t "^[^.]"))))))
+ (setq org-agenda-files (append todo-files org-agenda-files)))))
;; ---------------------------- Rebuild Org Agenda ---------------------------
;; builds the org agenda list from all agenda targets with caching.
diff --git a/modules/org-refile-config.el b/modules/org-refile-config.el
index 16b37bf9..3c3cc7da 100644
--- a/modules/org-refile-config.el
+++ b/modules/org-refile-config.el
@@ -16,6 +16,13 @@
(require 'system-lib)
(require 'cj-cache-lib)
+;; Forward-declare org-refile's dynamic var so byte-compiled code treats our
+;; `let'/`setq' on it as dynamic. Without this, compiling the module turns
+;; `cj/org-refile-in-file's (let ((org-refile-targets ...)) ...) into a
+;; lexical binding that never reaches `org-refile', silently breaking
+;; in-file refiling under `make compile'.
+(defvar org-refile-targets)
+
;; ----------------------------- Org Refile Targets ----------------------------
;; sets refile targets
;; - adds project files in org-roam to the refile targets
@@ -43,6 +50,34 @@ This prevents issues where:
(org-mode)))
buf))
+(defun cj/--org-refile-scan-dir (dir)
+ "Return the todo.org files under DIR, or nil with a warning if unusable.
+A missing, unreadable, or permission-denied DIR is non-fatal: it logs a
+`display-warning' and returns nil so the rest of the refile-target scan
+continues, rather than silently swallowing the failure or crashing the
+whole scan on a missing directory."
+ (cond
+ ((not (file-directory-p dir))
+ (display-warning 'org-refile
+ (format "Refile scan: directory missing, skipped: %s" dir)
+ :warning)
+ nil)
+ ((not (file-readable-p dir))
+ (display-warning 'org-refile
+ (format "Refile scan: directory unreadable, skipped: %s" dir)
+ :warning)
+ nil)
+ (t
+ (condition-case _
+ (directory-files-recursively
+ dir "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$" nil
+ (lambda (d) (not (string-match-p "airootfs" d))))
+ (permission-denied
+ (display-warning 'org-refile
+ (format "Refile scan: permission denied, skipped: %s" dir)
+ :warning)
+ nil)))))
+
(defun cj/--org-refile-scan-targets ()
"Scan disk for the refile-targets list. Pure-ish: no caching, no logging.
Returns the list to assign to `org-refile-targets'. Slow -- walks
@@ -61,17 +96,11 @@ Returns the list to assign to `org-refile-targets'. Slow -- walks
(dolist (file project-and-topic-files)
(unless (assoc file new-files)
(push (cons file file-rule) new-files)))))
- (dolist (dir (list user-emacs-directory code-dir projects-dir))
- (condition-case nil
- (let* ((todo-files (directory-files-recursively
- dir "^[Tt][Oo][Dd][Oo]\\.[Oo][Rr][Gg]$"
- nil
- (lambda (d) (not (string-match-p "airootfs" d)))))
- (file-rule '(:maxlevel . 1)))
- (dolist (file todo-files)
- (unless (assoc file new-files)
- (push (cons file file-rule) new-files))))
- (permission-denied nil)))
+ (let ((file-rule '(:maxlevel . 1)))
+ (dolist (dir (list user-emacs-directory code-dir projects-dir))
+ (dolist (file (cj/--org-refile-scan-dir dir))
+ (unless (assoc file new-files)
+ (push (cons file file-rule) new-files)))))
(nreverse new-files)))
(defun cj/build-org-refile-targets (&optional force-rebuild)
diff --git a/tests/test-org-agenda-config-add-files.el b/tests/test-org-agenda-config-add-files.el
index e977cc68..8c6f2a69 100644
--- a/tests/test-org-agenda-config-add-files.el
+++ b/tests/test-org-agenda-config-add-files.el
@@ -7,6 +7,7 @@
;;; Code:
(require 'ert)
+(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(require 'testutil-general)
@@ -129,5 +130,16 @@
(should (member "/existing/file.org" org-agenda-files))))
(cj/delete-test-base-dir)))
+(ert-deftest test-org-agenda-config-add-files-boundary-missing-dir-warns-not-errors ()
+ "Boundary: a missing project directory warns and adds nothing, without erroring.
+Previously `directory-files' on a missing dir crashed the agenda build."
+ (let ((org-agenda-files '("/existing/file.org"))
+ (warned nil))
+ (cl-letf (((symbol-function 'display-warning)
+ (lambda (&rest _) (setq warned t))))
+ (cj/add-files-to-org-agenda-files-list "/no/such/cj-agenda/dir/"))
+ (should warned)
+ (should (equal org-agenda-files '("/existing/file.org")))))
+
(provide 'test-org-agenda-config-add-files)
;;; test-org-agenda-config-add-files.el ends here
diff --git a/tests/test-org-refile-config--scan-dir.el b/tests/test-org-refile-config--scan-dir.el
new file mode 100644
index 00000000..184fa98e
--- /dev/null
+++ b/tests/test-org-refile-config--scan-dir.el
@@ -0,0 +1,57 @@
+;;; test-org-refile-config--scan-dir.el --- Tests for refile dir scan -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/--org-refile-scan-dir, which lists the todo.org files
+;; under one root for the refile-target scan. A missing, unreadable, or
+;; permission-denied root must be non-fatal: it logs a warning and returns
+;; nil so the rest of the scan continues, instead of silently swallowing the
+;; failure or crashing on a missing directory.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-refile-config)
+
+;;; Normal Cases
+
+(ert-deftest test-org-refile-scan-dir-normal-finds-todo ()
+ "Normal: a readable directory with a todo.org returns it."
+ (let ((dir (make-temp-file "cj-refile-scan-" t)))
+ (unwind-protect
+ (progn
+ (write-region "* TODO x\n" nil (expand-file-name "todo.org" dir))
+ (let ((found (cj/--org-refile-scan-dir dir)))
+ (should (= 1 (length found)))
+ (should (string-suffix-p "todo.org" (car found)))))
+ (delete-directory dir t))))
+
+;;; Boundary Cases
+
+(ert-deftest test-org-refile-scan-dir-boundary-missing-warns-and-returns-nil ()
+ "Boundary: a missing directory warns and returns nil, without erroring."
+ (let ((warned nil))
+ (cl-letf (((symbol-function 'display-warning)
+ (lambda (&rest _) (setq warned t))))
+ (should (null (cj/--org-refile-scan-dir "/no/such/cj-refile/dir/")))
+ (should warned))))
+
+;;; Error Cases
+
+(ert-deftest test-org-refile-scan-dir-error-permission-denied-warns-and-returns-nil ()
+ "Error: a permission-denied during the scan warns and returns nil."
+ (let ((dir (make-temp-file "cj-refile-perm-" t))
+ (warned nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'directory-files-recursively)
+ (lambda (&rest _) (signal 'permission-denied (list dir))))
+ ((symbol-function 'display-warning)
+ (lambda (&rest _) (setq warned t))))
+ (should (null (cj/--org-refile-scan-dir dir)))
+ (should warned))
+ (delete-directory dir t))))
+
+(provide 'test-org-refile-config--scan-dir)
+;;; test-org-refile-config--scan-dir.el ends here