From 389a4005b48d186fe4956f0455605b6fdb1dbb65 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 4 Jul 2026 17:48:49 -0500 Subject: fix(org): log state changes into LOGBOOK and drop no-op transitions State-change log lines were written inline (org-log-into-drawer nil), where a line landing between a heading and its DEADLINE/SCHEDULED line broke org's planning-line parser and dropped the entry from agenda views. org also logged no-op "State X from X" transitions that carry no information. Set org-log-into-drawer to t so state changes land in :LOGBOOK: drawers, and add an around-advice on org-add-log-setup that skips identical from/to transitions. The suppression decision lives in a pure predicate so it's tested without driving org-todo. This removes the cause rather than stripping the lines after they appear. --- modules/org-config.el | 26 ++++++++++++++- tests/test-org-config-noop-state-log.el | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/test-org-config-noop-state-log.el diff --git a/modules/org-config.el b/modules/org-config.el index 6f25752f..a9fc4811 100644 --- a/modules/org-config.el +++ b/modules/org-config.el @@ -263,6 +263,23 @@ whole row line." ;; ----------------------------- Org TODO Settings --------------------------- +(defun cj/org--noop-state-log-p (purpose state prev-state) + "Return non-nil when a state-change log carries no information. +PURPOSE is the `org-add-log-setup' purpose symbol; STATE and PREV-STATE +are the new and previous TODO states. A \\='state transition whose new +and previous states are identical (and non-nil) is a no-op worth +suppressing." + (and (eq purpose 'state) + state prev-state + (equal state prev-state))) + +(defun cj/org--suppress-noop-state-log (orig-fn &optional purpose state prev-state how extra) + "Around-advice for `org-add-log-setup' that drops no-op state logs. +Call ORIG-FN with PURPOSE STATE PREV-STATE HOW EXTRA unless the entry is +a no-op identical-state transition (see `cj/org--noop-state-log-p')." + (unless (cj/org--noop-state-log-p purpose state prev-state) + (funcall orig-fn purpose state prev-state how extra))) + (defun cj/org-todo-settings () "All org-todo related settings are grouped and set in this function." @@ -283,9 +300,16 @@ whole row line." (setq org-enforce-todo-checkbox-dependencies t) (setq org-deadline-warning-days 7) ;; warn me w/in a week of deadlines (setq org-treat-insert-todo-heading-as-state-change nil) ;; log task creation - (setq org-log-into-drawer nil) ;; don't log into drawer + ;; state changes log into :LOGBOOK: drawers, never inline: an inline log + ;; line can wedge between a heading and its planning line and break org's + ;; planning-line parser (dropping the entry from agenda views) + (setq org-log-into-drawer t) (setq org-log-done 'time) ;; record a CLOSED timestamp on TODO->DONE + ;; drop no-op "State X from X" transitions (identical from/to) that carry + ;; no information; the advice dedups, so re-running this is safe + (advice-add 'org-add-log-setup :around #'cj/org--suppress-noop-state-log) + ;; inherit parents properties (sadly not schedules or deadlines) (setq org-use-property-inheritance t)) diff --git a/tests/test-org-config-noop-state-log.el b/tests/test-org-config-noop-state-log.el new file mode 100644 index 00000000..125eb300 --- /dev/null +++ b/tests/test-org-config-noop-state-log.el @@ -0,0 +1,59 @@ +;;; test-org-config-noop-state-log.el --- Suppress no-op state-change logs -*- lexical-binding: t; -*- + +;;; Commentary: +;; org's state-change logging writes "- State X from X" lines for no-op +;; transitions (identical from/to state), which carry no information. With +;; `org-log-into-drawer' nil those lines also land inline, where they wedge +;; between a heading and its planning line and break org's parser. Two fixes +;; in `cj/org-todo-settings': log state changes into :LOGBOOK: drawers +;; (`org-log-into-drawer' t), and suppress no-op state logs at the +;; `org-add-log-setup' choke point via `cj/org--suppress-noop-state-log'. + +;;; Code: + +(require 'ert) +(require 'org) ;; declares the org-log-* vars special +(require 'org-config) + +(ert-deftest test-org-config-log-into-drawer-enabled () + "Normal: cj/org-todo-settings enables org-log-into-drawer (LOGBOOK)." + (let ((org-log-into-drawer nil)) + (cj/org-todo-settings) + (should (eq org-log-into-drawer t)))) + +(ert-deftest test-org-config-noop-state-log-p-identical-is-noop () + "Normal: identical from/to state on a 'state purpose is a no-op." + (should (cj/org--noop-state-log-p 'state "TODO" "TODO"))) + +(ert-deftest test-org-config-noop-state-log-p-real-change-not-noop () + "Normal: a genuine state change is not a no-op." + (should-not (cj/org--noop-state-log-p 'state "DONE" "TODO"))) + +(ert-deftest test-org-config-noop-state-log-p-nil-prev-not-noop () + "Boundary: a nil previous state (initial log) is not a no-op." + (should-not (cj/org--noop-state-log-p 'state "TODO" nil))) + +(ert-deftest test-org-config-noop-state-log-p-non-state-purpose-not-noop () + "Boundary: identical strings under a non-state purpose are not suppressed." + (should-not (cj/org--noop-state-log-p 'note "x" "x"))) + +(ert-deftest test-org-config-noop-state-log-p-nil-both-not-noop () + "Error: a nil/nil state pair is not a suppressible no-op." + (should-not (cj/org--noop-state-log-p 'state nil nil))) + +(ert-deftest test-org-config-suppress-advice-skips-noop () + "Normal: the advice does NOT call through for a no-op state transition." + (let ((called nil)) + (cj/org--suppress-noop-state-log + (lambda (&rest _) (setq called t)) 'state "TODO" "TODO") + (should-not called))) + +(ert-deftest test-org-config-suppress-advice-passes-real-change () + "Normal: the advice calls through for a genuine state change." + (let ((called nil)) + (cj/org--suppress-noop-state-log + (lambda (&rest _) (setq called t)) 'state "DONE" "TODO") + (should called))) + +(provide 'test-org-config-noop-state-log) +;;; test-org-config-noop-state-log.el ends here -- cgit v1.2.3