Split commit paragraphs on any blank line

The trailing comment block was found by searching for "\n\n", so a CRLF message kept its # Conflicts: list below the trailer. Walk the lines instead and treat any blank or whitespace-only line as a paragraph break. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: siGit Code <noreply@sigit.si>

Seto Elkahfi committed Sep 29, 2026 at 08:18 UTC 182a30dc83e2d265073b2e8bf85588ca393ba828
1 file changed +32 -7
src/tools.rs
+32 -7
@@ -2042,16 +2042,25 @@ fn git_head(cwd: &Path) -> Option<String> {
2042 /// `# Conflicts:` list stays in the commit. Any trailer after it is no longer
2043 /// in the last paragraph, and GitHub stops seeing it. Only trailing
2044 /// paragraphs made entirely of `#` lines go; a `#` line inside the message
2045 -/// (a Markdown heading in a `-m` body, say) is left alone.
2045 +/// (a Markdown heading in a `-m` body, say) is left alone. Paragraphs are
2046 +/// split on any blank line, so a run of them or CRLF endings work too.
2047 fn strip_trailing_comment_block(message: &str) -> &str {
2048 let mut rest = message.trim_end();
2049 loop {
2049 - let (head, last) = match rest.rfind("\n\n") {
2050 - Some(split) => (&rest[..split], &rest[split + 2..]),
2051 - None => return rest,
2052 - };
2053 - if last.lines().all(|line| line.starts_with('#')) {
2054 - rest = head.trim_end();
2050 + // Byte offset just past the last blank line, i.e. where the final
2051 + // paragraph starts. `rest` never ends in one, so that paragraph
2052 + // is non-empty.
2053 + let mut split = None;
2054 + let mut offset = 0;
2055 + for line in rest.split_inclusive('\n') {
2056 + offset += line.len();
2057 + if line.trim().is_empty() {
2058 + split = Some(offset);
2059 + }
2060 + }
2061 + let Some(split) = split else { return rest };
2062 + if rest[split..].lines().all(|line| line.starts_with('#')) {
2063 + rest = rest[..split].trim_end();
2064 } else {
2065 return rest;
2066 }
@@ -3315,6 +3324,22 @@ mod tests {
3324 let body = "Title\n\n## Why\n\nBecause\n\n# Note\nnot a comment";
3325 assert_eq!(strip_trailing_comment_block(body), body);
3326 assert_eq!(strip_trailing_comment_block("# only"), "# only");
3327 + // Extra blank lines, whitespace-only lines, and CRLF still separate
3328 + // paragraphs.
3329 + assert_eq!(
3330 + strip_trailing_comment_block("Merge\n\n\n\n# Conflicts:\n#\tf\n"),
3331 + "Merge"
3332 + );
3333 + assert_eq!(
3334 + strip_trailing_comment_block("Merge\n \t\n# Conflicts:\n#\tf"),
3335 + "Merge"
3336 + );
3337 + assert_eq!(
3338 + strip_trailing_comment_block("Merge\r\n\r\n# Conflicts:\r\n#\tf\r\n"),
3339 + "Merge"
3340 + );
3341 + let crlf = "Title\r\n\r\n# Note\r\nnot a comment\r\n";
3342 + assert_eq!(strip_trailing_comment_block(crlf), crlf.trim_end());
3343 }
3344
3345 /// Start a merge of `side` into `main` that conflicts on file.txt and