From 9ecb59816f168647340776d72b735e60efc3b978 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 2 Nov 2026 11:46:57 +0100 Subject: document: put user in commit message trailer --- src/document/replica.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 5 deletions(-) (limited to 'src/document/replica.rs') diff --git a/src/document/replica.rs b/src/document/replica.rs index 0d1998a..03325eb 100644 --- a/src/document/replica.rs +++ b/src/document/replica.rs @@ -339,6 +339,26 @@ mod tests { assert_eq!(store::read_path(&loaded, &path).unwrap(), Some(json!(2))); } + #[test] + fn messages_name_their_user_in_a_trailer() { + let writer = Writer { + node: "node", + user: Some("tv"), + }; + let message = writer.message().unwrap(); + assert_eq!(message, "User: tv"); + assert_eq!(user_of(&message), Some("tv")); + assert_eq!( + user_of("Moved the settings.\n\nUser: tv\nSigned-off-by: someone"), + Some("tv") + ); + // Before trailers: the user itself. + assert_eq!(user_of("tv"), Some("tv")); + assert_eq!(user_of("Moved the settings."), None); + assert_eq!(user_of("Moved: the settings\nand more"), None); + assert_eq!(user_of("Signed-off-by: someone"), None); + } + #[test] fn display() { assert_eq!(Epoch::GENESIS.to_string(), "0:genesis"); @@ -346,14 +366,59 @@ mod tests { } } -/// Who makes a change, as it records: the node, as the change's author (its node ID), -/// and the user it wrote for through the API, if any, as its message. Every node holds -/// every change, so who wrote what is known swarm-wide (`GET /admin/changes`). +/// Who makes a change, as it records: the node that commits it, as its Automerge author +/// (its node ID), and the user it wrote for through the API, if any, in its message +/// (`User:`, `USER_TRAILER`). That user, at that node, is who wrote it: user names are +/// each node's own (`--htpasswd`). Every node holds every change, so who wrote what is +/// known swarm-wide (`GET /admin/changes`). pub(crate) struct Writer<'a> { pub(crate) node: &'a str, pub(crate) user: Option<&'a str>, } +/// The trailer of a change's message naming its user (`Writer`), as Git's +/// `Signed-off-by:` names who signed off. +pub(crate) const USER_TRAILER: &str = "User"; + +impl Writer<'_> { + /// The change's message: its trailers (`trailers`), if it has any. + fn message(&self) -> Option { + self.user.map(|user| format!("{USER_TRAILER}: {user}")) + } +} + +/// The trailers of a change's message, as in Git: the `Key: value` lines of its last +/// paragraph, if each of its lines is one. +pub(crate) fn trailers(message: &str) -> Vec<(&str, &str)> { + let last = message.trim_end().rsplit("\n\n").next().unwrap_or(""); + let trailers: Option> = last + .lines() + .map(|line| { + let (key, value) = line.split_once(": ")?; + (!key.is_empty() && key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')) + .then(|| (key, value.trim())) + }) + .collect(); + trailers.unwrap_or_default() +} + +/// The user a change's message names (`USER_TRAILER`). Changes made before messages had +/// trailers have the user as their whole message: one word, without a `:` +/// (`--htpasswd` names have none). +pub(crate) fn user_of(message: &str) -> Option<&str> { + let trailers = trailers(message); + if trailers.is_empty() { + let message = message.trim(); + let word = + !message.is_empty() && !message.contains(|c: char| c == ':' || c.is_whitespace()); + return word.then_some(message); + } + trailers + .into_iter() + .find(|(key, _)| key.eq_ignore_ascii_case(USER_TRAILER)) + .map(|(_, user)| user) +} + /// The writes `f` makes to `doc`, as one change stamped `at` by `writer` if it succeeds, /// dropped if not ([`Replica::transact`], and writes into the document before a rejoin). pub(crate) fn transact( @@ -403,8 +468,8 @@ fn run( } let mut options = automerge::transaction::CommitOptions::default().with_time(at.as_u64() as i64); - if let Some(user) = writer.user { - options.set_message(user); + if let Some(message) = writer.message() { + options.set_message(message); } let (_, logged) = transaction.commit_with(options); (out, Some(logged)) -- cgit v1.3.1