aboutsummaryrefslogtreecommitdiffstats
path: root/src/document
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 11:46:57 +0100
committertv <tv@krebsco.de>2026-11-02 11:46:57 +0100
commit9ecb59816f168647340776d72b735e60efc3b978 (patch)
tree244763c47954d00377b61b0efc5c03f33ef1ed0a /src/document
parent6174fd28c4c7311aafdf5ea22427c80725faafac (diff)
document: put user in commit message trailer
Diffstat (limited to 'src/document')
-rw-r--r--src/document/replica.rs75
1 files changed, 70 insertions, 5 deletions
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
@@ -340,20 +340,85 @@ mod tests {
}
#[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");
assert_eq!(epoch(2, 0xab).to_string(), "2:abababababab");
}
}
-/// 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<String> {
+ 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<Vec<(&str, &str)>> = 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<T, E>(
@@ -403,8 +468,8 @@ fn run<T, E>(
}
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))