fabro/lib/components/fabro-checkpoint/src/trailer.rs
2026-07-23 17:59:34 -04:00

197 lines
5.5 KiB
Rust

use std::fmt::Write;
/// A git commit message trailer (key-value pair).
pub struct Trailer<'a> {
pub key: &'a str,
pub value: &'a str,
}
/// Append a trailer to a commit message, inserting a blank-line separator if
/// needed.
pub fn append(message: &str, trailer: &Trailer<'_>) -> String {
let trailer_line = format!("{}: {}", trailer.key, trailer.value);
let trimmed = message.trim_end();
if trimmed.is_empty() {
return trailer_line;
}
// Check if the message already ends with a trailer block.
// A trailer block is preceded by a blank line and every line contains ": ".
if has_trailing_trailer_block(trimmed) {
format!("{trimmed}\n{trailer_line}\n")
} else {
format!("{trimmed}\n\n{trailer_line}\n")
}
}
/// Extract a trailer value from a commit message. Scans from the end.
pub fn parse<'a>(message: &'a str, key: &str) -> Option<&'a str> {
let prefix = format!("{key}: ");
// Scan lines from end to find the trailer
for line in message.lines().rev() {
let trimmed = line.trim();
if let Some(value) = trimmed.strip_prefix(&prefix) {
return Some(value);
}
// Stop scanning if we hit a blank line (left the trailer block)
if trimmed.is_empty() {
break;
}
}
None
}
/// Build a commit message from subject, body, and trailers.
pub fn format_message(subject: &str, body: &str, trailers: &[Trailer<'_>]) -> String {
let mut msg = subject.to_string();
if !body.is_empty() {
msg.push_str("\n\n");
msg.push_str(body);
}
if !trailers.is_empty() {
msg.push_str("\n\n");
for trailer in trailers {
let _ = writeln!(msg, "{}: {}", trailer.key, trailer.value);
}
}
if !msg.ends_with('\n') {
msg.push('\n');
}
msg
}
/// Check if the trimmed message ends with a trailer block.
/// A trailer block: every line after the last blank line contains ": ".
fn has_trailing_trailer_block(trimmed: &str) -> bool {
let mut found_blank = false;
let mut trailer_lines = 0;
for line in trimmed.lines().rev() {
if line.trim().is_empty() {
found_blank = true;
break;
}
if line.contains(": ") {
trailer_lines += 1;
} else {
return false;
}
}
found_blank && trailer_lines > 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_to_simple_message() {
let result = append("Initial commit", &Trailer {
key: "My-Checkpoint",
value: "abc123",
});
assert_eq!(result, "Initial commit\n\nMy-Checkpoint: abc123\n");
}
#[test]
fn append_to_message_with_existing_trailer() {
let msg = "Initial commit\n\nSigned-off-by: Alice <alice@example.com>\n";
let result = append(msg, &Trailer {
key: "My-Checkpoint",
value: "abc123",
});
assert_eq!(
result,
"Initial commit\n\nSigned-off-by: Alice <alice@example.com>\nMy-Checkpoint: abc123\n"
);
}
#[test]
fn append_to_message_with_body_no_trailer() {
let msg = "Initial commit\n\nThis is a longer description of the change.\n";
let result = append(msg, &Trailer {
key: "My-Checkpoint",
value: "abc123",
});
assert_eq!(
result,
"Initial commit\n\nThis is a longer description of the change.\n\nMy-Checkpoint: abc123\n"
);
}
#[test]
fn parse_finds_trailer() {
let msg = "Initial commit\n\nMy-Checkpoint: abc123\n";
assert_eq!(parse(msg, "My-Checkpoint"), Some("abc123"));
}
#[test]
fn parse_finds_trailer_among_multiple() {
let msg =
"Initial commit\n\nSigned-off-by: Alice <alice@example.com>\nMy-Checkpoint: abc123\n";
assert_eq!(parse(msg, "My-Checkpoint"), Some("abc123"));
assert_eq!(
parse(msg, "Signed-off-by"),
Some("Alice <alice@example.com>")
);
}
#[test]
fn parse_returns_none_when_missing() {
let msg = "Initial commit\n\nSigned-off-by: Alice\n";
assert_eq!(parse(msg, "My-Checkpoint"), None);
}
#[test]
fn parse_returns_none_on_empty_message() {
assert_eq!(parse("", "My-Checkpoint"), None);
}
#[test]
fn format_message_subject_only() {
let result = format_message("Initial commit", "", &[]);
assert_eq!(result, "Initial commit\n");
}
#[test]
fn format_message_with_body() {
let result = format_message("Initial commit", "Detailed description", &[]);
assert_eq!(result, "Initial commit\n\nDetailed description\n");
}
#[test]
fn format_message_with_trailers() {
let result = format_message("Initial commit", "", &[
Trailer {
key: "Signed-off-by",
value: "Alice",
},
Trailer {
key: "My-Checkpoint",
value: "abc123",
},
]);
assert_eq!(
result,
"Initial commit\n\nSigned-off-by: Alice\nMy-Checkpoint: abc123\n"
);
}
#[test]
fn format_message_with_body_and_trailers() {
let result = format_message("Initial commit", "Description here", &[Trailer {
key: "My-Checkpoint",
value: "abc123",
}]);
assert_eq!(
result,
"Initial commit\n\nDescription here\n\nMy-Checkpoint: abc123\n"
);
}
}