Hi, I would like to submit a small patch (see attachment) for org-id that addresses two related issues with UUID generation.
The `uuidgen' program shipped with macOS produces uppercase UUIDs, while uuidgen on Linux produces lowercase ones. Users who edit the same notes from both systems end up with inconsistently cased IDs. The patch downcases the output of org-id-uuid-program. It also changes the default of org-id-uuid-program to nil, so Org uses its internal generator instead of spawning an external process for every new ID. When the built-in uuid.el (a candidate for Emacs 32) is available, `org-id-uuid' uses `uuid-v4'; otherwise it falls back to the existing implementation. So, switching to the internal generator directly buys us: - Consistency at the root across any machine; no need for special downcasing since its output is already lowercase. - A considerable speed-up that can be relevant when several operations are batched. On my machine, `uuid-v4' is roughly 500 times faster than calling `uuidgen'. I have a copyright assignment with the FSF. Best regards, Andrea
From 5b5fce4aae00b91c36bde1f99620dc4c516c4769 Mon Sep 17 00:00:00 2001 From: Andrea Alberti <[email protected]> Date: Sun, 13 Sep 2026 01:19:06 +0200 Subject: [PATCH] org-id: Standardize UUIDs to lowercase and use built-in uuid.el This patch introduces two related fixes: 1. Apply 'downcase to the UUID generated by the CLI utility `uuidgen'. In Unix systems, `uuidgen' produces by default lowercase UUID codes; the `uuidgen' utility shipped with macOS produces uppercase UUIDs. When switching between Emacs sessions on Linux and macOS, the different casing causes inconsistencies in the org notes. 2. Uses built-in module (candidate for Emacs 32) to generate UUIDs when `uuid-v4' is bound; and as a fallback the existing old code in `org-id-uuid'. The default now changes to using internal functions rather than invoking an external process; this resolves at the root the source of inconsistencies and it is ~500x faster (measured on a Mac computer). * lisp/org-macs.el (org-id-uuid): Use `uuid-v4' from the built-in uuid.el when available; fall back to the existing implementation otherwise. * lisp/org-id.el (org-id-uuid-program): Default to nil, meaning the internal generator is used. (org-id-method): Update docstring. (org-id-new): Use the internal generator when `org-id-uuid-program' is nil; otherwise downcase the output of the external program to be consistent with Unix systems. * etc/ORG-NEWS (New and changed options): Announce the change. --- etc/ORG-NEWS | 14 ++++++++++++++ lisp/org-id.el | 26 ++++++++++++++++++-------- lisp/org-macs.el | 47 ++++++++++++++++++++++++++--------------------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 80cd17ba6..24bcac969 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -189,6 +189,20 @@ and some environments listed in the manual are not supported. # adding new customizations, or changing the interpretation of the # existing customizations. +*** ~org-id-uuid-program~ now defaults to nil; UUIDs are generated internally and lowercased + +Org no longer calls the external =uuidgen= program by default to +generate ~uuid~ IDs. Instead, ~org-id-uuid~ is used, which relies on +the built-in =uuid.el= library (~uuid-v4~) when available and falls +back to the previous Org implementation otherwise. This avoids +spawning a process for every new ID and is considerably faster (about +500x measured on a Mac laptop). + +When ~org-id-uuid-program~ is set to a program, its output is now +downcased. The =uuidgen= shipped with macOS produces uppercase UUIDs, +unlike Unix systems, which led to inconsistent IDs when sharing notes +across platforms. + *** The ~file+headline~ capture target specification now accepts omitted or nil headline ~file+headline~ is a target specification in ~org-capture-templates~. diff --git a/lisp/org-id.el b/lisp/org-id.el index 40bf5fa4b..75b7e8266 100644 --- a/lisp/org-id.el +++ b/lisp/org-id.el @@ -167,10 +167,16 @@ general option `org-link-context-for-files' and the org-id option :package-version '(Org . "9.7") :type 'boolean) -(defcustom org-id-uuid-program "uuidgen" - "The uuidgen program." +(defcustom org-id-uuid-program nil + "Program used to generate the UUID, or nil to use Emacs's own generator. + +On Unix-like systems, uuidgen is usually available as an external program +for generating UUIDs. Using the internal function avoids spawning a process +to run the external program." :group 'org-id - :type 'string) + :type '(choice (const :tag "Use built-in generator" nil) + (string :tag "Program")) + :package-version '(Org . "10.0")) (defcustom org-id-ts-format "%Y%m%dT%H%M%S.%6N" "Timestamp format for IDs generated using `ts' `org-id-method'. @@ -193,9 +199,10 @@ org Org's own internal method, using an encoding of the current time to microsecond accuracy, and optionally the current domain of the computer. See the variable `org-id-include-domain'. -uuid Create random (version 4) UUIDs. If the program defined in - `org-id-uuid-program' is available it is used to create the ID. - Otherwise an internal functions is used. +uuid Create random (version 4) UUIDs. If `org-id-uuid-program' is set to + a program generating a valid UUID, the generated UUID is used to + create the new ID; the string is forced to lower case. Otherwise an + internal function is used. ts Create ID's based on timestamps as specified in `org-id-ts-format'." :group 'org-id @@ -439,9 +446,12 @@ So a typical ID could look like \"Org:4nd91V40HI\"." (if (equal prefix ":") (setq prefix "")) (cond ((memq org-id-method '(uuidgen uuid)) - (setq unique (org-trim (shell-command-to-string org-id-uuid-program))) + (if (null org-id-uuid-program) + (setq unique (org-id-uuid)) + (setq unique (downcase (org-trim + (shell-command-to-string org-id-uuid-program))))) (unless (org-uuidgen-p unique) - (setq unique (org-id-uuid)))) + (setq unique (org-id-uuid)))) ((eq org-id-method 'org) (let* ((etime (org-reverse-string (org-id-time-to-b36))) (postfix (when org-id-include-domain diff --git a/lisp/org-macs.el b/lisp/org-macs.el index e267debba..b4d917bcf 100644 --- a/lisp/org-macs.el +++ b/lisp/org-macs.el @@ -36,6 +36,9 @@ (require 'format-spec) (eval-when-compile (require 'subr-x)) ; For `when-let*', Emacs < 29 +(declare-function uuid-v4 "uuid" (&rest args)) +(declare-function uuid-to-string "uuid" (uuid)) + ;;; Org version verification. (defvar org--inhibit-version-check nil @@ -876,27 +879,29 @@ When NEXT is non-nil, check the next line instead." (defun org-id-uuid () "Return string with random (version 4) UUID." - (let ((rnd (md5 (format "%s%s%s%s%s%s%s" - (random) - (org-time-convert-to-list nil) - (user-uid) - (emacs-pid) - (user-full-name) - user-mail-address - (recent-keys))))) - (format "%s-%s-4%s-%s%s-%s" - (substring rnd 0 8) - (substring rnd 8 12) - (substring rnd 13 16) - (format "%x" - (logior - #b10000000 - (logand - #b10111111 - (string-to-number - (substring rnd 16 18) 16)))) - (substring rnd 18 20) - (substring rnd 20 32)))) + (if (or (fboundp 'uuid-v4) (and (require 'uuid nil t) (fboundp 'uuid-v4))) + (uuid-to-string (uuid-v4)) + (let ((rnd (md5 (format "%s%s%s%s%s%s%s" + (random) + (org-time-convert-to-list nil) + (user-uid) + (emacs-pid) + (user-full-name) + user-mail-address + (recent-keys))))) + (format "%s-%s-4%s-%s%s-%s" + (substring rnd 0 8) + (substring rnd 8 12) + (substring rnd 13 16) + (format "%x" + (logior + #b10000000 + (logand + #b10111111 + (string-to-number + (substring rnd 16 18) 16)))) + (substring rnd 18 20) + (substring rnd 20 32))))) ;;; Motion -- 2.55.0
