7

I have been using org-mode for notetaking = as a wiki for a some time. Besides many short org-files I have a couple of long notes with many headings and subheadings for themes like taxes. So I can't rely on a file's mtime. Now I regularly wonder if the information I see is very dated or if it's fairly recent.

Ideally only the property of the immediate subheading would be updated and not all super-headings above it.

Does such a solution exist?


(updated with responses to comments)

  • org-capture doesn't help (as far as I understand it). Most of my files are not touched by org-capture. So I can't use the "%t" template. Even if I created the file with capture that wouldn't help with future edits that I make from within org-mode
  • Logging doesn't help (as far as I understand it). I use org-mode as a wiki so I use TODO keywords only in a few files. For most files and headings there's no reason for me to add TODO states. I could add a state but then I would have to remember to run the command org-todo each time after I change a note.
  • Time Stamp doesn't help (as far as I understand it): it refers to the whole file and not just headings.

All my effors were fruitless: I checked for packages and posts about org as a wiki. I checked the org-hooks. I have searched here but only found an unanswered question that refers only to changes in headers. I also found out that adding an org-id to each heading before saving and then running a diff for each pair org-IDs (current buffer and the last saved version) is too complicated for me and maybe error-prone.

4
  • How do you add heading and subheadings? Are you using capture or are you just typing them in?
    – NickD
    Mar 11, 2018 at 1:03
  • I can't tell from the question - are you looking to timestamp headings? You can easily do this for status headings or for the file by putting # Time-stamp: in your header and enabling time-stamp-active. If this isn't what you're looking for, perhaps clarify your question.
    – mclear
    Mar 11, 2018 at 2:29
  • What about using Logging? Then if you add notes (z) or change status, the time is captured: orgmode.org/manual/…
    – mankoff
    Mar 11, 2018 at 7:25
  • Thank you very much for your input. I just updated my question and wrote why I think that these suggestions don't solve my problem.I'm an emacs beginner and not a programmer (though I have read a lot about org). So if I miss something obvious or make stupid mistakes I'd be grateful for hints.
    – hib6xk
    Mar 12, 2018 at 21:14

1 Answer 1

4

I assume that you are talking about the property, containing last modification time of the entry, but not its subheadings.

The way to approach it may be computing and saving the hash of each entry text before, say, file save and updating the modification time of the property if the hash changes:

(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) (point)))
       (end (progn
          (forward-char)          
          (if (not (re-search-forward "^\*+ " (point-max) t))
              (point-max)
            (match-beginning 0))))
       (full-str (buffer-substring 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)))
      (sxhash str-nohash-nomod-nopropbeg-end))))

(defun yant/update-modification-time ()
  "Set the :MODIFIED: property of the current entry to NOW and update :HASH: property."
  (org-set-property "HASH" (format "%s" (yant/getentryhash)))    
  (org-set-property "MODIFIED" (format-time-string "%Y-%m-%d %H:%M")))
(defun yant/skip-nonmodified ()
  "Skip org entries, which were not modified according to the :HASH: property"
  (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)))

(add-hook 'before-save-hook (lambda ()
                  (when (eq major-mode 'org-mode)
                (org-map-entries #'yant/update-modification-time nil 'file #'yant/skip-nonmodified))))  

EDIT:

Addressing the following issues:

  1. Non-existent agenda file /path/to/test.org. [R]emove from list or [A]bort? message appears in the buffer, which is not saved to a file.

  2. The hash calculation is affected by inserting the hash info after the first hash data insertion.

Solutions:

  1. The issue is due to the implementation of org-map-entries, which calls org-check-agenda-file causing the message (see org-mode maillist). I do not know a good solution for this, but one can consider advising org-check-agenda-file just for the time when the hook above is beeing executed like the following:

    (define-advice org-check-agenda-file (:around (OLDFUN file) ignore-non-existant-files)
     "Suppress \"Non existent agenda file...\" query. Just continue."
      (if (file-exists-p file)
          (funcall OLDFUN file)
        (progn
          (org-remove-file file)
          (throw 'nextfile t))))
    (advice-remove 'org-check-agenda-file #'org-check-agenda-file@ignore-non-existant-files)
    
    (add-hook 'before-save-hook
          (lambda ()
            (when (eq major-mode 'org-mode)
              (advice-add 'org-check-agenda-file :around #'org-check-agenda-file@ignore-non-existant-files)
              (org-map-entries #'yant/update-modification-time nil 'file #'yant/skip-nonmodified)
              (advice-remove 'org-check-agenda-file #'org-check-agenda-file@ignore-non-existant-files))))
    
  2. I have changed the code in the answer to ignore :PROPERTIES: and :END: statements. Adding of these keywords were causing the hash change in the entry, which did not have any property drawers at the beginning.

EDIT2:

If one would like to ignore the text properties during hash calculation, buffer-substring in yant/getentryhash should be replaced by buffer-substring-no-properties.

8
  • this looks exactly like what I need. Thanks! But I run into a major problem (and a minor one). Here are details about my error - they don't fit in a comment. I'm an emacs beginner and not a programmer. So I have no idea how to fix it. Maybe you could have a look at it?
    – hib6xk
    Mar 12, 2018 at 21:50
  • thanks for your update. The question about the non existent agenda is no problem at all. But I still have the second problem even with the new code. The problem might be me. That's why I made a screencast that I posted here. If I made a mistake a correction would be very welcome. May I bother you again? Your solution will be extremly useful to me.
    – hib6xk
    Mar 14, 2018 at 0:48
  • I managed to reproduce the problem. You just need to update the regexs for beginning and end of the properties drawer to allow leading spaces (I updated it in the answer).
    – yantar92
    Mar 14, 2018 at 2:45
  • the update works for me now. Thank you very much!
    – hib6xk
    Mar 15, 2018 at 21:34
  • I hope to use this feature for years. You apply the hash to (buffer-substring beg end). It is my understanding that this also includes "metadata" about the current paragraph. I don't know the terminology but I think it's font lock information/properties. Is it possible that this changes between versions of org-mode and/or emacs (if so could/should this be excluded)? I just tried with emacs 24-26 and org 8/9 and it worked fine. But I couldn't compile older versions on my machine. Again thanks for your patience and help.
    – hib6xk
    Mar 15, 2018 at 21:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.