Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/domains/commits/tokens/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function formatTokenisedLine(tokens: TokenisedLine): string {
return tokens.map((token) => token.value).join("")
}

export function isNonBlankTokenisedLine(tokens: TokenisedLine): boolean {
return tokens.some((token) => token.type === "text" && token.value.trim() !== "")
}

export function splitTextTokens(
tokens: TokenisedLine,
regex: RegExp,
Expand Down
227 changes: 227 additions & 0 deletions src/domains/rules/UseEmptyLineBeforeBodyLines.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { describe, expect, it } from "vitest"
import { fakeCommitFactory } from "#commits/Commit.fixtures.ts"
import type { Commit } from "#commits/Commit.ts"
import { fakeConfiguration } from "#configurations/Configuration.fixtures.ts"
import { bodyLineConcern } from "#rules/concerns/BodyLineConcern.ts"
import type { Concerns } from "#rules/concerns/Concern.ts"
import type { RuleKey, RuleOptions } from "#rules/Rule.ts"
import { useEmptyLineBeforeBodyLines } from "#rules/UseEmptyLineBeforeBodyLines.ts"
import type { CharacterRange } from "#types/CharacterRange.ts"
import type { Vector } from "#types/Vector.ts"

const rule = "useEmptyLineBeforeBodyLines" satisfies RuleKey
const enabled: RuleOptions<typeof rule> = {}

const fakeCommit = fakeCommitFactory(fakeConfiguration())

describe.each`
message
${"init"}
${"fix some bugs"}
${"Upgrade Vite to 8.0.0"}
${"Consolidate `BadgeFactory` with `BadgeService` and count to ten"}
${'#1 Revert "Add an amazing feature"'}
${" squash! organise the bookshelf"}
`("when the commit message of $message does not have a body", (props: { message: string }) => {
const commit = fakeCommit({ message: props.message })

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], enabled)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
})

describe.each`
message
${"\n\n"}
${" \n\n\n "}
${"Establish the repository\n"}
${"GH-34 open the hotel room\n\n"}
${"a majestic bottle of water \n \n "}
${"fixup! Apply strawberry jam to make the code sweeter\n\n\n"}
`("when the commit message of $message only has a blank body", (props: { message: string }) => {
const commit = fakeCommit({ message: props.message })

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], enabled)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
})

describe.each`
message | expectedLine | expectedRange
${"bugfix #614\nattempt 2"} | ${0} | ${[0, 1]}
${"Write unit tests\n Finally..."} | ${0} | ${[0, 1]}
${"Refactor the taxi module\nThe roof sign now flashes politely."} | ${0} | ${[0, 1]}
${"Install React\n```\npnpm add --save \\\n @types/react \\\n @types/react-dom \\\n react \\\n react-dom\n```"} | ${0} | ${[0, 1]}
${'Revert "Repair the soft ice machine"\nThis commit undoes the changes made in revision 6227ca0.'} | ${0} | ${[0, 1]}
${"squash! serve the tiny sandwiches\n guests get fewer crumbs in the keyboard"} | ${0} | ${[0, 1]}
`(
"when the commit message $message does not have an empty line before the body",
(props: { message: string; expectedLine: number; expectedRange: CharacterRange }) => {
const commit = fakeCommit({ message: props.message })

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], enabled)

it("raises a concern about the first body line", () => {
expect(actualConcerns).toEqual<Concerns>([
bodyLineConcern(rule, commit.sha, {
line: props.expectedLine,
range: props.expectedRange,
}),
])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
},
)

describe.each`
message | expectedLine | expectedRange
${"Bring it on \n\n\nThe code is prepared for anything."} | ${1} | ${[0, 1]}
${"Replace guesswork with a chart\n\n\n\nThe chart uses three excellent dots."} | ${2} | ${[0, 1]}
${"Update src/main.ts\n \n \nCo-Authored-By: Everloving Easter Bunny <everloving.easter.bunny@example.com>"} | ${1} | ${[0, 1]}
${"Bump @typescript-eslint/parser from 5.52.0 to 5.59.1\n\n\n\n\nBumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.52.0 to 5.59.1.\n- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)\n- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)"} | ${3} | ${[0, 1]}
${"transport the fragile and expensive goods\n\n\n\n \n\nThis commit moves the goods from one place to another via the `RapidTransportService`."} | ${4} | ${[0, 1]}
${"Install Vite\n\n\n```shell\npnpm add --save-dev vite\n```\n\nThis commit also uses Vite as bundler."} | ${1} | ${[0, 1]}
`(
"when the commit message $message has multiple blank lines before the body",
(props: { message: string; expectedLine: number; expectedRange: CharacterRange }) => {
const commit = fakeCommit({ message: props.message })

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], enabled)

it("raises a concern about the first non-blank body line", () => {
expect(actualConcerns).toEqual<Concerns>([
bodyLineConcern(rule, commit.sha, {
line: props.expectedLine,
range: props.expectedRange,
}),
])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
},
)

describe.each`
message
${"fix a longstanding bug\n\nit was just a matter of time before it would cause customers to complain "}
${"Refactor the taxi module\n\nThe meter now accepts exact change."}
${"Resolve the conflicts\n\nConflicts:\n\n src/grumpy-cat.ts\n src/summer-vacation-plans.ts"}
${"Teach the changelog to whisper\n\nThe noisy bits moved to the release notes.\n\nFooter: charming"}
`(
"when the commit message $message has exactly one empty line before the body",
(props: { message: string }) => {
const commit = fakeCommit({ message: props.message })

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], enabled)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines([commit], null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
},
)

describe("when verifying a set of multiple commits and some commits do not have exactly one empty line before the body", () => {
const commits: Vector<Commit, 5> = [
fakeCommit({ message: "Refactor the taxi module" }),
fakeCommit({ message: "Install a quieter keyboard\nThe old one sounded like hail." }),
fakeCommit({ message: "Update the docs\n\nThe example now uses a bowl of fresh hot soup." }),
fakeCommit({ message: "Clean the tiny dashboard\n\n\nThe widgets sparkle." }),
fakeCommit({ message: "Use precise names\n" }),
]

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines(commits, enabled)

it("raises concerns about the commits with unexpected body lines", () => {
expect(actualConcerns).toEqual<Concerns>([
bodyLineConcern(rule, commits[1].sha, { line: 0, range: [0, 1] }),
bodyLineConcern(rule, commits[3].sha, { line: 1, range: [0, 1] }),
])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines(commits, null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
})

describe("when verifying a set of multiple commits and all commits has exactly one empty line before the body", () => {
const commits: Vector<Commit, 5> = [
fakeCommit({ message: "Refactor the taxi module" }),
fakeCommit({ message: "Install a quieter keyboard\n\nThe old one sounded like hail." }),
fakeCommit({ message: "Update the docs\n\nThe example now uses a bowl of fresh hot soup." }),
fakeCommit({ message: "Clean the tiny dashboard\n\nThe widgets sparkle." }),
fakeCommit({ message: "Use precise names\n" }),
]

describe("and the rule is enabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines(commits, enabled)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})

describe("and the rule is disabled", () => {
const actualConcerns = useEmptyLineBeforeBodyLines(commits, null)

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
})
35 changes: 32 additions & 3 deletions src/domains/rules/UseEmptyLineBeforeBodyLines.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
import type { Commit, Commits } from "#commits/Commit.ts"
import { isNonBlankTokenisedLine } from "#commits/tokens/Token.ts"
import { bodyLineConcern } from "#rules/concerns/BodyLineConcern.ts"
import type { Concern, Concerns } from "#rules/concerns/Concern.ts"
import type { RuleKey } from "#rules/Rule.ts"
import type { EmptyObject } from "#types/EmptyObject.ts"
import { notNullish } from "#utilities/Arrays.ts"

const _rule = "useEmptyLineBeforeBodyLines" satisfies RuleKey
const rule = "useEmptyLineBeforeBodyLines" satisfies RuleKey

/**
* Verifies that the subject line and message body is separated by exactly one empty line.
*
* Standardising the commit message format helps to preserve the readability of the commit history in various Git clients.
*/
export function useEmptyLineBeforeBodyLines(
commits: Commits,
options: EmptyObject | null,
): Concerns {
return options !== null ? commits.map(verifyCommit).filter(notNullish) : []
}

function verifyCommit(_commit: Commit): Concern | null {
throw new Error("The `useEmptyLineBeforeBodyLines` rule has not been implemented yet") // TODO: To be implemented.
function verifyCommit(commit: Commit): Concern | null {
let firstNonBlankLineNumber: number | null = null
let lineNumber = 0

for (const bodyLine of commit.bodyLines) {
if (isNonBlankTokenisedLine(bodyLine)) {
firstNonBlankLineNumber = lineNumber
break
}

lineNumber += 1
}

if (firstNonBlankLineNumber === null) {
return null
}
if (firstNonBlankLineNumber === 0) {
return bodyLineConcern(rule, commit.sha, { line: 0, range: [0, 1] })
}
if (firstNonBlankLineNumber > 1) {
return bodyLineConcern(rule, commit.sha, { line: firstNonBlankLineNumber - 1, range: [0, 1] })
}

return null
}
61 changes: 61 additions & 0 deletions src/domains/rules/reports/CommitwiseReport.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
import { fakeCommitFactory } from "#commits/Commit.fixtures.ts"
import type { Commit, Commits } from "#commits/Commit.ts"
import { fakeConfiguration } from "#configurations/Configuration.fixtures.ts"
import { bodyLineConcern } from "#rules/concerns/BodyLineConcern.ts"
import { commitConcern } from "#rules/concerns/CommitConcern.ts"
import type { Concerns } from "#rules/concerns/Concern.ts"
import { subjectLineConcern } from "#rules/concerns/SubjectLineConcern.ts"
Expand Down Expand Up @@ -798,6 +799,66 @@ be86674 make a genuine attempt to fix the bugs that the users were complaining a
})
})

describe("when 'useEmptyLineBeforeBodyLines' has a concern about the first body line", () => {
const configuration = fakeConfiguration()
const fakeCommit = fakeCommitFactory(configuration)

const commit = fakeCommit({
sha: "f163851c187a568acc631fff402fcca43f41968d",
message: "Install a quieter keyboard\nThe old one sounded like hail.",
})
const concern = bodyLineConcern("useEmptyLineBeforeBodyLines", commit.sha, {
line: 0,
range: [0, 1],
})

it("describes the rule violation in the body line", () => {
const actualOutput = commitwiseReport([concern], [commit], configuration)
expect(actualOutput).toBe(
`
f163851 Install a quieter keyboard
╭──
∙ 1 │ The old one sounded like hail.
· ┬
· ╰─ Subject lines and message bodies must be separated by exactly one empty line.
· (useEmptyLineBeforeBodyLines)
╰──
`.trim(),
)
})
})

describe("when 'useEmptyLineBeforeBodyLines' has a concern about an extra empty body line", () => {
const configuration = fakeConfiguration()
const fakeCommit = fakeCommitFactory(configuration)

const commit = fakeCommit({
sha: "30fd57b19c55b3746a157f5981838e26865637f2",
message: "Clean the tiny dashboard\n\n\nThe widgets sparkle.\nAnd the birds are joyful.",
})
const concern = bodyLineConcern("useEmptyLineBeforeBodyLines", commit.sha, {
line: 1,
range: [0, 1],
})

it("describes the rule violation in the body line", () => {
const actualOutput = commitwiseReport([concern], [commit], configuration)
expect(actualOutput).toBe(
`
30fd57b Clean the tiny dashboard
╭──
1 │
∙ 2 │
· ┬
· ╰─ Subject lines and message bodies must be separated by exactly one empty line.
· (useEmptyLineBeforeBodyLines)
3 │ The widgets sparkle.
╰──
`.trim(),
)
})
})

describe("when 'useIssueLinks' with position 'anywhere' has a concern about characters 0-1 of the subject line", () => {
const configuration = fakeConfiguration({
rules: { useIssueLinks: { position: "anywhere" } },
Expand Down
Loading
Loading