pub fn format_squash(message: Option<&str>, task_title: &str, id: &str) -> String {
let raw = message.map(str::trim).filter(|s| !s.is_empty()).unwrap_or(task_title);
let (title, body) = split_title_body(raw);
let tagged = format!("{} [{}]", title.trim_end(), id);
match body {
Some(b) if !b.is_empty() => format!("{tagged}\n\n{b}"),
_ => tagged,
}
}
pub fn join_messages(parts: &[String]) -> Option<String> {
let joined = parts
.iter()
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join("\n\n");
(!joined.is_empty()).then_some(joined)
}
fn split_title_body(s: &str) -> (&str, Option<String>) {
let s = s.trim_end_matches('\n');
let Some(nl) = s.find('\n') else {
return (s, None);
};
let title = &s[..nl];
let body = s[nl + 1..].trim_start_matches('\n').to_string();
(title, Some(body))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_uses_task_title() {
assert_eq!(
format_squash(None, "Add feature", "bl-abcd"),
"Add feature [bl-abcd]"
);
}
#[test]
fn single_line_message_is_title_only() {
assert_eq!(
format_squash(Some("Short title"), "ignored", "bl-1234"),
"Short title [bl-1234]"
);
}
#[test]
fn title_and_body_split_on_first_newline() {
let out = format_squash(
Some("Short title\nExplanation paragraph."),
"ignored",
"bl-1234",
);
assert_eq!(out, "Short title [bl-1234]\n\nExplanation paragraph.");
}
#[test]
fn title_and_body_with_existing_blank_line_preserved() {
let out = format_squash(
Some("Short title\n\nExplanation paragraph."),
"ignored",
"bl-1234",
);
assert_eq!(out, "Short title [bl-1234]\n\nExplanation paragraph.");
}
#[test]
fn multi_paragraph_body_is_preserved() {
let out = format_squash(
Some("Short title\n\nFirst paragraph.\n\nSecond paragraph."),
"ignored",
"bl-1234",
);
assert_eq!(
out,
"Short title [bl-1234]\n\nFirst paragraph.\n\nSecond paragraph."
);
}
#[test]
fn trailing_newlines_stripped() {
let out = format_squash(Some("Short title\n\n"), "ignored", "bl-1234");
assert_eq!(out, "Short title [bl-1234]");
}
#[test]
fn join_messages_none_when_empty_or_blank() {
assert_eq!(join_messages(&[]), None);
assert_eq!(join_messages(&[" ".into(), "\n".into()]), None);
}
#[test]
fn join_messages_single_value_passes_through() {
assert_eq!(join_messages(&["fix foo".into()]).as_deref(), Some("fix foo"));
}
#[test]
fn join_messages_repeated_values_become_paragraphs() {
let parts = vec!["Short title".into(), "First para.".into(), "Second para.".into()];
assert_eq!(
join_messages(&parts).as_deref(),
Some("Short title\n\nFirst para.\n\nSecond para.")
);
}
#[test]
fn join_messages_drops_blanks_between_real_values() {
let parts = vec!["Title".into(), " ".into(), "Body.".into()];
assert_eq!(join_messages(&parts).as_deref(), Some("Title\n\nBody."));
}
#[test]
fn repeated_m_then_format_squash_yields_50_72_shape() {
let msg = join_messages(&["Short title".into(), "Body paragraph.".into()]);
assert_eq!(
format_squash(msg.as_deref(), "ignored", "bl-1234"),
"Short title [bl-1234]\n\nBody paragraph."
);
}
#[test]
fn empty_message_falls_back_to_task_title() {
assert_eq!(
format_squash(Some(""), "Fallback title", "bl-abcd"),
"Fallback title [bl-abcd]"
);
assert_eq!(
format_squash(Some(" "), "Fallback title", "bl-abcd"),
"Fallback title [bl-abcd]"
);
}
}