Skip to content

Instantly share code, notes, and snippets.

@mmarshall540
Created October 19, 2025 15:03
Show Gist options
  • Save mmarshall540/8fcefea266e041f9a54a1eb704cd9b3b to your computer and use it in GitHub Desktop.
Save mmarshall540/8fcefea266e041f9a54a1eb704cd9b3b to your computer and use it in GitHub Desktop.
Add "MODIFIED" (and "HASH") properties to Org-mode entries on save
;; Org-mode: Custom property: MODIFIED
;; Adapted from: https://emacs.stackexchange.com/a/39376/31079
(defun yant/getentryhash ()
"Get the hash sum of the text in current entry.
Except :HASH: and :MODIFIED: property texts."
(save-excursion
(let* ((beg (progn (org-back-to-heading :invisible-ok) (point)))
(end (progn
(forward-char)
(if (not (re-search-forward "^\*+ " (point-max) t))
(point-max)
(match-beginning 0))))
(full-str (buffer-substring-no-properties beg end))
(str-nohash (if (string-match "^ *:HASH:.+\n" full-str)
(replace-match "" nil nil full-str)
full-str))
(str-nohash-nomod (if (string-match
"^ *:MODIFIED:.+\n" str-nohash)
(replace-match "" nil nil str-nohash)
str-nohash))
(str-nohash-nomod-nopropbeg (if (string-match
"^ *:PROPERTIES:\n"
str-nohash-nomod)
(replace-match
"" nil nil
str-nohash-nomod)
str-nohash-nomod))
(str-nohash-nomod-nopropbeg-end
(if (string-match "^ *:END:\n" str-nohash-nomod-nopropbeg)
(replace-match "" nil nil str-nohash-nomod-nopropbeg)
str-nohash-nomod-nopropbeg))
;; Do not change hash just because whitespace was trimmed
;; from line-endings or extra newlines removed or added to
;; the end of an entry.
(final-str
(concat
(string-trim-right
(replace-regexp-in-string
"[ \t]+[\n\r]" "\n"
str-nohash-nomod-nopropbeg-end))
"\n")))
(secure-hash 'md5 final-str))))
(defun yant/update-modification-time ()
"Set the :MODIFIED: property of the current entry.
Set to NOW and update :HASH: property."
(org-set-property "HASH" (format "%s" (yant/getentryhash)))
(org-set-property "MODIFIED"
(format-time-string
(org-time-stamp-format :with-hm :inactive)))
(setq my/updated-org-entries-count
(1+ my/updated-org-entries-count)))
(defun yant/skip-nonmodified ()
"Skip org entries, which were not modified."
(let ((next-headline (save-excursion (or (outline-next-heading)
(point-max)))))
(if (string= (org-entry-get (point) "HASH" nil)
(format "%s" (yant/getentryhash)))
next-headline
nil)))
(defvar my/updated-org-entries-count)
(defun my/org-update-modified-prop ()
"Update the MODIFIED property if major-mode is `org-mode'."
(setq-local my/updated-org-entries-count 0)
(when (and (eq major-mode 'org-mode)
(equal default-directory (expand-file-name "~/org/")))
(org-map-entries #'yant/update-modification-time
nil
'file
#'yant/skip-nonmodified)
(when (> my/updated-org-entries-count 0)
(message "Saved %s and updated %s entries!"
buffer-file-name
my/updated-org-entries-count))))
(add-hook 'before-save-hook 'my/org-update-modified-prop)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment