Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions @commitlint/rules/src/body-max-line-length.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { bodyMaxLineLength } from "./body-max-line-length.js";

const short = "a";
const long = "ab";
const longFooter = `Signed-off-by: ${"a".repeat(110)}`;
const url = "https://example.com/URL/with/a/very/long/path";

const value = short.length;
Expand All @@ -13,6 +14,7 @@ const messages = {
empty: "test: subject",
short: `test: subject\n${short}`,
long: `test: subject\n${long}`,
longFooter: `test: subject\n\n${short}\n\n${longFooter}`,
shortMultipleLines: `test:subject\n${short}\n${short}\n${short}`,
longMultipleLines: `test:subject\n${short}\n${long}\n${short}`,
urlStandalone: `test:subject\n${short}\n${url}\n${short}`,
Expand Down Expand Up @@ -57,6 +59,12 @@ test("with long should fail", async () => {
expect(actual).toEqual(expected);
});

test("with long footer should succeed", async () => {
const [actual] = bodyMaxLineLength(await parsed.longFooter, undefined, value);
const expected = true;
expect(actual).toEqual(expected);
});

test("with short with multiple lines should succeed", async () => {
const [actual] = bodyMaxLineLength(await parsed.shortMultipleLines, undefined, value);
const expected = true;
Expand Down
41 changes: 39 additions & 2 deletions @commitlint/rules/src/body-max-line-length.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
import { maxLineLength } from "@commitlint/ensure";
import toLines from "@commitlint/to-lines";
import { SyncRule } from "@commitlint/types";

const TRAILER_TOKEN = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+:\s\S/;

function withoutTrailingFooter(body: string, footer?: string | null): string {
const footerStart = footer ? body.length - footer.length : -1;
const input =
footer &&
footerStart >= 0 &&
body.endsWith(footer) &&
(footerStart === 0 || body[footerStart - 1] === "\n")
? body.slice(0, footerStart).replace(/(\r?\n)+$/, "")
: body;

const lines = toLines(input);
let last = lines.length - 1;

while (last >= 0 && lines[last] === "") {
last--;
}

let first = last;

while (first >= 0 && lines[first] !== "") {
first--;
}

const footerLines = lines.slice(first + 1, last + 1);

if (footerLines.length === 0 || !footerLines.every((line) => TRAILER_TOKEN.test(line))) {
return input;
Comment on lines +5 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. trailer_token excludes valid footers 📎 Requirement gap ≡ Correctness

withoutTrailingFooter() only treats the last paragraph as a footer if every line matches
TRAILER_TOKEN, but the regex requires a hyphenated token and will miss valid trailers like
Fixes:/Refs:/BREAKING CHANGE:. As a result, some footer lines can still be checked by
body-max-line-length, violating the requirement that footer line-length is not enforced by the
body rule.
Agent Prompt
## Issue description
`body-max-line-length` is intended to ignore footer/trailer lines so they are not validated against the body max length. The current trailer detection regex (`TRAILER_TOKEN`) only matches hyphenated tokens (e.g., `Signed-off-by:`), which means other valid trailer/footer tokens (e.g., `Fixes:`, `Refs:`, `BREAKING CHANGE:`) may remain in the body input and still trigger `body-max-line-length` failures.

## Issue Context
Compliance requires that footer lines are not validated by `body-max-line-length` (they should be governed separately, e.g. by `footer-max-line-length`). The current implementation gates footer stripping on `TRAILER_TOKEN`, so missed trailer formats can violate that requirement.

## Fix Focus Areas
- @commitlint/rules/src/body-max-line-length.ts[5-37]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

return lines.slice(0, first).join("\n");
}
Comment on lines +24 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Multi-trailer body not removed 🐞 Bug ≡ Correctness

withoutTrailingFooter() removes a detected trailer paragraph via lines.slice(0, first), but when
the body contains only multiple trailer lines (no blank separator), first becomes -1 and the
function returns all but the last trailer line. This can still trigger bodyMaxLineLength failures
for long trailer lines in commits that contain only trailers (e.g., Signed-off-by +
Co-authored-by).
Agent Prompt
## Issue description
`withoutTrailingFooter()` can detect that the entire body is made of trailer lines, but when there is no blank line separator, `first` ends up as `-1` and `lines.slice(0, first)` drops only the last trailer line (because `slice(0, -1)` keeps all-but-last). This leaves remaining trailer lines in the body, so `body-max-line-length` can still fail on long trailer lines.

## Issue Context
This happens when the commit has no body and contains multiple trailers (e.g., `Signed-off-by: ...` followed by `Co-authored-by: ...`) that are present in `parsed.body` (trailers are commonly handled via raw parsing / `git interpret-trailers`).

## Fix Focus Areas
- @commitlint/rules/src/body-max-line-length.ts[24-37]

### Suggested change
Special-case `first < 0` to remove the entire input (return empty string) when the last paragraph is detected as trailers.

### Add regression test
Add a test where the commit message has *no body* and *multiple* trailer lines, and ensure `bodyMaxLineLength` returns `true` when the trailers exceed the body limit (since they should be ignored).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


export const bodyMaxLineLength: SyncRule<number> = (parsed, _when = undefined, value = 0) => {
const input = parsed.body;
const body = parsed.body;

if (!input) {
if (!body) {
return [true];
}

const input = withoutTrailingFooter(body, parsed.footer);

return [maxLineLength(input, value), `body's lines must not be longer than ${value} characters`];
};
Loading