From 287f6a6aa1605c254de93eb92e0b3bd90ac1aadf Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 26 Jan 2026 02:00:22 +0000 Subject: [PATCH] Rewrap PR bodies and custom squash messages to 72 characters There is a bit of a mismatch in the logs since commit messages written by users are typically hardwrapped, while the PR bodies that become Bors commit messages are not. Use a markdown formatter ([comrak](https://docs.rs/comrak/latest/comrak/)) to rewrap things. This matches what GitHub does by default for squash merges. --- Cargo.lock | 96 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/bors/handlers/squash.rs | 38 +++++++++++++-- src/bors/merge_queue.rs | 42 ++++++++++++++++ src/bors/mod.rs | 19 +++++++- 5 files changed, 191 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89ca8637..af7c39af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "base64", "chrono", "clap", + "comrak", "futures", "graphql-parser", "hex", @@ -480,6 +481,15 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + [[package]] name = "cc" version = "1.2.48" @@ -590,6 +600,23 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "comrak" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321d20bf105b6871a49da44c5fbb93e90a7cd6178ea5a9fe6cbc1e6d4504bc5e" +dependencies = [ + "caseless", + "entities", + "jetscii", + "phf", + "phf_codegen", + "rustc-hash", + "smallvec", + "typed-arena", + "unicode_categories", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -871,6 +898,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + [[package]] name = "env_home" version = "0.1.0" @@ -1552,6 +1585,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + [[package]] name = "js-sys" version = "0.3.83" @@ -1957,6 +1996,45 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2714,6 +2792,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -3428,6 +3512,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.19.0" @@ -3473,6 +3563,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 134335dd..e911ea5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ tempfile = "3" which = "8" # Text processing +comrak = { version = "0.50.0", default-features = false } pulldown-cmark = "0.13" regex = "1" diff --git a/src/bors/handlers/squash.rs b/src/bors/handlers/squash.rs index 7139aa07..b0d56e62 100644 --- a/src/bors/handlers/squash.rs +++ b/src/bors/handlers/squash.rs @@ -3,7 +3,9 @@ use crate::bors::gitops_queue::{ GitOpsCommand, GitOpsQueueSender, PullRequestId, PushCallback, PushCommand, }; use crate::bors::handlers::{PullRequestData, unapprove_pr}; -use crate::bors::{CommandPrefix, Comment, RepositoryState, bors_commit_author}; +use crate::bors::{ + CommandPrefix, Comment, RepositoryState, bors_commit_author, format_commit_message, +}; use crate::database::BuildStatus; use crate::github::api::CommitAuthor; use crate::github::api::operations::Commit; @@ -117,8 +119,10 @@ pub(super) async fn command_squash( // Create the squashed commit on the source repository. // We take the parents of the first commit, and the tree of the last commit, to create the // squashed commit. - let commit_msg = - commit_message.unwrap_or_else(|| generate_squashed_commit_msg(&pr.github.title, &commits)); + let commit_msg = match commit_message { + Some(msg) => format_commit_message(&msg), + None => generate_squashed_commit_msg(&pr.github.title, &commits), + }; let commit = match repo_state .client .create_commit( @@ -528,6 +532,34 @@ mod tests { .await; } + #[sqlx::test] + async fn squash_long_message(pool: sqlx::PgPool) { + run_test((pool, squash_state()), async |ctx: &mut BorsTester| { + ctx.modify_pr_in_gh((), |pr| { + pr.add_commits(vec![Commit::from_sha("sha2")]); + }); + ctx.approve(()).await?; + ctx.post_comment( + "@bors squash msg=\"This is a squashed commit.\n\nLorem ipsum dolor sit amet, \ + consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et \ + dolore\"", + ) + .await?; + ctx.run_gitop_queue().await?; + ctx.expect_comments((), 1).await; + insta::assert_snapshot!( + ctx.pr(()).await.get_gh_pr().head_branch_copy().get_commit().message(), + @"This is a squashed commit + + Lorem ipsum dolor sit amet, + " + ); + + Ok(()) + }) + .await; + } + fn squash_state() -> GitHub { let gh = GitHub::default(); let pr_author = User::default_pr_author(); diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index de571f6c..9da040f8 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -1667,6 +1667,48 @@ also include this pls" .await; } + #[sqlx::test] + async fn commit_message_reflow(pool: sqlx::PgPool) { + run_test(pool, async |ctx: &mut BorsTester| { + ctx.edit_pr((), |pr| { + pr.description = "This is a very good PR, but it has a rather long description. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut \ +labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \ +nisi ut aliquip ex ea commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \ +pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ +mollit anim id est laborum. +" + .to_string(); + }) + .await?; + + ctx.approve(()).await?; + ctx.start_and_finish_auto_build(()).await?; + + insta::assert_snapshot!(ctx.auto_branch().get_commit().message(), @" + Auto merge of #1 - default-user:pr/1, r=default-user + + Title of PR 1 + + This is a very good PR, but it has a rather long description. + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea + commodo consequat. + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum + dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non + proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + "); + Ok(()) + }) + .await; + } + #[sqlx::test] async fn cancel_try_again(pool: sqlx::PgPool) { run_test(pool, async |ctx: &mut BorsTester| { diff --git a/src/bors/mod.rs b/src/bors/mod.rs index 7d8d3de2..ca7f3d4b 100644 --- a/src/bors/mod.rs +++ b/src/bors/mod.rs @@ -316,7 +316,7 @@ pub fn make_text_ignored_by_bors(text: &str) -> String { format!("{IGNORE_BLOCK_START}\n{text}\n{IGNORE_BLOCK_END}") } -/// Remove homu-ignore blocks from the merge message +/// Remove homu-ignore blocks from the merge message, then rewrap the text. pub fn normalize_merge_message(message: &str) -> String { static IGNORE_REGEX: LazyLock = LazyLock::new(|| { RegexBuilder::new(r".*?") @@ -326,7 +326,22 @@ pub fn normalize_merge_message(message: &str) -> String { .build() .unwrap() }); - IGNORE_REGEX.replace_all(message, "").to_string() + let cleaned = IGNORE_REGEX.replace_all(message, ""); + format_commit_message(&cleaned) +} + +/// Rewrap PR bodies to standard commit message width. +pub fn format_commit_message(message: &str) -> String { + let mut opts = comrak::Options::default(); + opts.render.width = 72; + opts.render.list_style = comrak::options::ListStyleType::Star; + + let mut out = String::new(); + let arena = comrak::Arena::new(); + let node = comrak::parse_document(&arena, message, &opts); + comrak::format_commonmark(&node, &opts, &mut out).unwrap(); + + out } pub fn create_merge_commit_message(pr: handlers::PullRequestData, merge_type: MergeType) -> String {