pub(crate) fn strip_trailers(text: &str) -> &str {
let trimmed = text.trim_end();
if trimmed.is_empty() {
return trimmed;
}
let bytes = trimmed.as_bytes();
let mut i = bytes.len();
while i > 0 {
let line_end = i;
let line_start = trimmed[..i].rfind('\n').map_or(0, |pos| pos + 1);
let line = &trimmed[line_start..line_end];
if line.trim().is_empty() {
break;
}
i = if line_start == 0 { 0 } else { line_start - 1 };
}
if i == 0 {
return trimmed;
}
let last_paragraph_start = trimmed[i..].find(|c: char| c != '\n' && c != '\r');
let last_paragraph_start = match last_paragraph_start {
Some(offset) => i + offset,
None => return trimmed,
};
let last_paragraph = &trimmed[last_paragraph_start..];
let all_trailers = last_paragraph
.lines()
.filter(|l| !l.trim().is_empty())
.all(is_trailer_line);
if !all_trailers {
return trimmed;
}
trimmed[..i].trim_end()
}
fn is_trailer_line(line: &str) -> bool {
let Some(colon_pos) = line.find(": ") else {
return false;
};
let key = &line[..colon_pos];
!key.is_empty()
&& key.starts_with(|c: char| c.is_ascii_alphabetic())
&& key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn body_with_trailers() {
let input = "feat: add caching\n\nThis adds Redis caching.\n\nSigned-off-by: Alice \
<a@b>\nRefs: DAT-123";
assert_eq!(
strip_trailers(input),
"feat: add caching\n\nThis adds Redis caching."
);
}
#[test]
fn no_trailers() {
let input = "feat: add caching\n\nThis adds Redis caching.";
assert_eq!(strip_trailers(input), input);
}
#[test]
fn title_plus_trailers_no_body() {
let input = "feat: add caching\n\nSigned-off-by: Alice <a@b>";
assert_eq!(strip_trailers(input), "feat: add caching");
}
#[test]
fn title_only() {
let input = "feat: add caching";
assert_eq!(strip_trailers(input), input);
}
#[test]
fn mixed_trailer_and_non_trailer_last_paragraph() {
let input = "title\n\nbody\n\nSigned-off-by: Alice <a@b>\nThis is not a trailer";
assert_eq!(strip_trailers(input), input.trim_end());
}
#[test]
fn multiple_paragraphs_only_last_is_trailers() {
let input = "title\n\nbody\n\nRefs: X\n\nSigned-off-by: Alice <a@b>";
assert_eq!(strip_trailers(input), "title\n\nbody\n\nRefs: X");
}
#[test]
fn empty_input() {
assert_eq!(strip_trailers(""), "");
}
#[test]
fn single_paragraph_no_blank_lines() {
let input = "Signed-off-by: Alice <a@b>\nRefs: DAT-123";
assert_eq!(strip_trailers(input), input);
}
#[test]
fn multiple_blank_lines_before_trailers() {
let input = "feat: add caching\n\nBody text.\n\n\nSigned-off-by: Alice <a@b>";
assert_eq!(strip_trailers(input), "feat: add caching\n\nBody text.");
}
#[test]
fn trailing_whitespace_in_input() {
let input = "feat: add caching\n\nBody.\n\nRefs: DAT-123\n ";
assert_eq!(strip_trailers(input), "feat: add caching\n\nBody.");
}
#[test]
fn co_authored_by_trailer() {
let input = "fix: resolve deadlock\n\nCo-authored-by: Bob <bob@example.com>";
assert_eq!(strip_trailers(input), "fix: resolve deadlock");
}
#[test]
fn key_with_no_space_after_colon_is_not_trailer() {
let input = "title\n\nbody\n\nNot-a-trailer:no space here";
assert_eq!(strip_trailers(input), input);
}
#[test]
fn key_with_spaces_is_not_trailer() {
let input = "title\n\nbody\n\nNot a trailer: value here";
assert_eq!(strip_trailers(input), input);
}
#[test]
fn trailer_value_can_contain_colons() {
let input = "title\n\nbody\n\nRefs: https://example.com:8080/path";
assert_eq!(strip_trailers(input), "title\n\nbody");
}
}