diff --git a/src/domains/rules/NoMergeCommits.tests.ts b/src/domains/rules/NoMergeCommits.tests.ts new file mode 100644 index 00000000..7f95378d --- /dev/null +++ b/src/domains/rules/NoMergeCommits.tests.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest" +import { fakeCommitFactory } from "#commits/Commit.fixtures.ts" +import { fakeConfiguration } from "#configurations/Configuration.fixtures.ts" +import { commitConcern } from "#rules/concerns/CommitConcern.ts" +import type { Concerns } from "#rules/concerns/Concern.ts" +import { noMergeCommits } from "#rules/NoMergeCommits.ts" +import { ruleContext } from "#rules/Rule.ts" +import { fakeCommitSha } from "#types/CommitSha.fixtures.ts" +import type { CommitSha } from "#types/CommitSha.ts" + +const enabled = ruleContext("noMergeCommits") + +const fakeCommit = fakeCommitFactory(fakeConfiguration()) + +describe.each` + parents | subjectLine + ${[fakeCommitSha(), fakeCommitSha()]} | ${"Merge branch 'main' into bugfix/dance-party-playlist"} + ${[fakeCommitSha(), fakeCommitSha()]} | ${"Merge pull request #440 from renovate/battle-shell-7.x"} + ${[fakeCommitSha(), fakeCommitSha(), fakeCommitSha()]} | ${"amend! Merge branch 'feature/new-coffee-machine' into feature/office-overhaul"} + ${[fakeCommitSha(), fakeCommitSha(), fakeCommitSha()]} | ${"Keep my branch up to date"} +`( + "when the commit is a merge commit with $parents.length parents and has a subject line of $subjectLine", + (props: { parents: Array; subjectLine: string }) => { + const commit = fakeCommit({ message: props.subjectLine, parents: props.parents }) + + describe("and the rule is enabled", () => { + const actualConcerns = noMergeCommits([commit], enabled.options) + + it("raises a concern about the entire commit", () => { + expect(actualConcerns).toEqual([commitConcern(enabled, commit.sha)]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = noMergeCommits([commit], null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + }, +) + +describe.each` + parents | subjectLine + ${[]} | ${"init"} + ${[]} | ${"Establish the repository"} + ${[fakeCommitSha()]} | ${"just an ordinary commit"} + ${[fakeCommitSha()]} | ${"Release the robot butler"} +`( + "when the commit is not a merge commit and has a subject line of $subjectLine", + (props: { parents: Array; subjectLine: string }) => { + const commit = fakeCommit({ message: props.subjectLine, parents: props.parents }) + + describe("and the rule is enabled", () => { + const actualConcerns = noMergeCommits([commit], enabled.options) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = noMergeCommits([commit], null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + }, +) diff --git a/src/domains/rules/NoMergeCommits.ts b/src/domains/rules/NoMergeCommits.ts index 7e31369c..0ed815f1 100644 --- a/src/domains/rules/NoMergeCommits.ts +++ b/src/domains/rules/NoMergeCommits.ts @@ -1,9 +1,16 @@ import type { Commit, Commits } from "#commits/Commit.ts" +import { commitConcern } from "#rules/concerns/CommitConcern.ts" import type { Concern, Concerns } from "#rules/concerns/Concern.ts" import { type RuleContext, ruleContext } from "#rules/Rule.ts" import type { EmptyObject } from "#types/EmptyObject.ts" import { notNullish } from "#utilities/Arrays.ts" +/** + * Verifies that the commit has at most one parent commit, thus disallowing merge commits. + * + * Avoiding merge commits makes the commit history linear and preserves the ability to rebase interactively. + * This helps to preserve the readability of the commit history and makes it easier to revert changes later. + */ export function noMergeCommits(commits: Commits, options: EmptyObject | null): Concerns { if (options === null) { return [] @@ -13,6 +20,10 @@ export function noMergeCommits(commits: Commits, options: EmptyObject | null): C return commits.map((commit) => verifyCommit(commit, rule)).filter(notNullish) } -function verifyCommit(_commit: Commit, _rule: RuleContext): Concern | null { - throw new Error("The `noMergeCommits` rule has not been implemented yet") // TODO: To be implemented. +function verifyCommit(commit: Commit, rule: RuleContext): Concern | null { + if (commit.isMergeCommit) { + return commitConcern(rule, commit.sha) + } + + return null } diff --git a/src/domains/rules/NoSquashMarkers.ts b/src/domains/rules/NoSquashMarkers.ts index 242b5307..213b95f4 100644 --- a/src/domains/rules/NoSquashMarkers.ts +++ b/src/domains/rules/NoSquashMarkers.ts @@ -7,7 +7,7 @@ import type { EmptyObject } from "#types/EmptyObject.ts" import { notNullish } from "#utilities/Arrays.ts" /** - * Verifies that the subject line does not contain squash markers. + * Verifies that the subject line does not contain any squash marker. * * Combining squash commits with their ancestors makes the commit history cleaner and easier to read, * as it omits unnecessary diffs and increases the cohesion of commits. It also makes it easier to revert changes later. diff --git a/src/domains/rules/UseConciseSubjectLines.tests.ts b/src/domains/rules/UseConciseSubjectLines.tests.ts index 205aa173..5256e16a 100644 --- a/src/domains/rules/UseConciseSubjectLines.tests.ts +++ b/src/domains/rules/UseConciseSubjectLines.tests.ts @@ -33,8 +33,9 @@ describe.each` const commit = fakeCommit({ message: props.subjectLine }) describe("and the rule is enabled with a maximum length of 20 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled20.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled20.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled20, commit.sha, { range: props.expectedRange20 }), ]) @@ -42,8 +43,9 @@ describe.each` }) describe("and the rule is enabled with a maximum length of 50 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled50.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled50.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled50, commit.sha, { range: props.expectedRange50 }), ]) @@ -51,8 +53,9 @@ describe.each` }) describe("and the rule is enabled with a maximum length of 72 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled72.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled72.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled72, commit.sha, { range: props.expectedRange72 }), ]) @@ -60,8 +63,9 @@ describe.each` }) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -84,8 +88,9 @@ describe.each` const commit = fakeCommit({ message: props.subjectLine }) describe("and the rule is enabled with a maximum length of 20 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled20.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled20.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled20, commit.sha, { range: props.expectedRange20 }), ]) @@ -93,8 +98,9 @@ describe.each` }) describe("and the rule is enabled with a maximum length of 50 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled50.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled50.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled50, commit.sha, { range: props.expectedRange50 }), ]) @@ -102,15 +108,17 @@ describe.each` }) describe("and the rule is enabled with a maximum length of 72 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled72.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled72.options) expect(actualConcerns).toEqual([]) }) }) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -135,8 +143,9 @@ describe.each` const commit = fakeCommit({ message: props.subjectLine }) describe("and the rule is enabled with a maximum length of 20 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled20.options) + it("raises a concern about the characters that exceed the limit", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled20.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled20, commit.sha, { range: props.expectedRange20 }), ]) @@ -144,22 +153,25 @@ describe.each` }) describe("and the rule is enabled with a maximum length of 50 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled50.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled50.options) expect(actualConcerns).toEqual([]) }) }) describe("and the rule is enabled with a maximum length of 72 characters", () => { + const actualConcerns = useConciseSubjectLines([commit], enabled72.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], enabled72.options) expect(actualConcerns).toEqual([]) }) }) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -185,16 +197,18 @@ describe.each` `( "and the rule is enabled with a maximum length of $options.maxLength characters", (context: RuleContext<"useConciseSubjectLines">) => { + const actualConcerns = useConciseSubjectLines([commit], context.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], context.options) expect(actualConcerns).toEqual([]) }) }, ) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -218,16 +232,18 @@ describe.each` `( "and the rule is enabled with a maximum length of $options.maxLength characters", (context: RuleContext<"useConciseSubjectLines">) => { + const actualConcerns = useConciseSubjectLines([commit], context.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], context.options) expect(actualConcerns).toEqual([]) }) }, ) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -257,16 +273,18 @@ describe.each` `( "and the rule is enabled with a maximum length of $options.maxLength characters", (context: RuleContext<"useConciseSubjectLines">) => { + const actualConcerns = useConciseSubjectLines([commit], context.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], context.options) expect(actualConcerns).toEqual([]) }) }, ) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines([commit], null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines([commit], null) expect(actualConcerns).toEqual([]) }) }) @@ -299,8 +317,9 @@ describe("when verifying a set of multiple commits and some commits have long su ] describe("and the rule is enabled with a maximum length of 20 characters", () => { + const actualConcerns = useConciseSubjectLines(commits, enabled20.options) + it("raises concerns about the commits whose subject lines exceed 20 characters", () => { - const actualConcerns = useConciseSubjectLines(commits, enabled20.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled20, commits[0].sha, { range: [20, 52] }), subjectLineConcern(enabled20, commits[1].sha, { range: [20, 24] }), @@ -315,8 +334,9 @@ describe("when verifying a set of multiple commits and some commits have long su }) describe("and the rule is enabled with a maximum length of 50 characters", () => { + const actualConcerns = useConciseSubjectLines(commits, enabled50.options) + it("raises concerns about the commits whose subject lines exceed 50 characters", () => { - const actualConcerns = useConciseSubjectLines(commits, enabled50.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled50, commits[0].sha, { range: [50, 52] }), subjectLineConcern(enabled50, commits[7].sha, { range: [50, 75] }), @@ -326,8 +346,9 @@ describe("when verifying a set of multiple commits and some commits have long su }) describe("and the rule is enabled with a maximum length of 72 characters", () => { + const actualConcerns = useConciseSubjectLines(commits, enabled72.options) + it("raises concerns about the commits whose subject lines exceed 72 characters", () => { - const actualConcerns = useConciseSubjectLines(commits, enabled72.options) expect(actualConcerns).toEqual([ subjectLineConcern(enabled72, commits[7].sha, { range: [72, 75] }), subjectLineConcern(enabled72, commits[11].sha, { range: [72, 76] }), @@ -336,8 +357,9 @@ describe("when verifying a set of multiple commits and some commits have long su }) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines(commits, null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines(commits, null) expect(actualConcerns).toEqual([]) }) }) @@ -359,16 +381,18 @@ describe("when verifying a set of multiple commits and all commits have concise `( "and the rule is enabled with a maximum length of $options.maxLength characters", (context: RuleContext<"useConciseSubjectLines">) => { + const actualConcerns = useConciseSubjectLines(commits, context.options) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines(commits, context.options) expect(actualConcerns).toEqual([]) }) }, ) describe("and the rule is disabled", () => { + const actualConcerns = useConciseSubjectLines(commits, null) + it("does not raise any concerns", () => { - const actualConcerns = useConciseSubjectLines(commits, null) expect(actualConcerns).toEqual([]) }) }) diff --git a/src/domains/rules/concerns/CommitConcern.ts b/src/domains/rules/concerns/CommitConcern.ts new file mode 100644 index 00000000..c0c31ffe --- /dev/null +++ b/src/domains/rules/concerns/CommitConcern.ts @@ -0,0 +1,12 @@ +import type { RuleContext } from "#rules/Rule.ts" +import type { CommitSha } from "#types/CommitSha.ts" + +export type CommitConcern = { + location: "commit" + rule: RuleContext + commitSha: CommitSha +} + +export function commitConcern(rule: RuleContext, commitSha: CommitSha): CommitConcern { + return { location: "commit", rule, commitSha } +} diff --git a/src/domains/rules/concerns/Concern.ts b/src/domains/rules/concerns/Concern.ts index 00b0b5f8..bbbd03c0 100644 --- a/src/domains/rules/concerns/Concern.ts +++ b/src/domains/rules/concerns/Concern.ts @@ -3,6 +3,7 @@ import type { Configuration } from "#configurations/Configuration.ts" import type { AuthorEmailAddressConcern } from "#rules/concerns/AuthorEmailAddressConcern.ts" import type { AuthorNameConcern } from "#rules/concerns/AuthorNameConcern.ts" import type { BodyLineConcern } from "#rules/concerns/BodyLineConcern.ts" +import type { CommitConcern } from "#rules/concerns/CommitConcern.ts" import type { CommitterEmailAddressConcern } from "#rules/concerns/CommitterEmailAddressConcern.ts" import type { CommitterNameConcern } from "#rules/concerns/CommitterNameConcern.ts" import type { SubjectLineConcern } from "#rules/concerns/SubjectLineConcern.ts" @@ -32,6 +33,7 @@ export type Concern = | AuthorEmailAddressConcern | AuthorNameConcern | BodyLineConcern + | CommitConcern | CommitterEmailAddressConcern | CommitterNameConcern | SubjectLineConcern diff --git a/src/domains/rules/reports/CommitwiseReport.tests.ts b/src/domains/rules/reports/CommitwiseReport.tests.ts index b6d5d008..0fb128fa 100644 --- a/src/domains/rules/reports/CommitwiseReport.tests.ts +++ b/src/domains/rules/reports/CommitwiseReport.tests.ts @@ -2,14 +2,15 @@ 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 { commitConcern } from "#rules/concerns/CommitConcern.ts" import type { Concerns } from "#rules/concerns/Concern.ts" import { subjectLineConcern } from "#rules/concerns/SubjectLineConcern.ts" import { commitwiseReport } from "#rules/reports/CommitwiseReport.ts" import { ruleContext } from "#rules/Rule.ts" +import { fakeCommitSha } from "#types/CommitSha.fixtures.ts" import type { Vector } from "#types/Vector.ts" -const configuration = fakeConfiguration() -const fakeCommit = fakeCommitFactory(configuration) +const fakeCommit = fakeCommitFactory(fakeConfiguration()) describe("when there are no concerns", () => { const commits: Commits = [] @@ -21,7 +22,49 @@ describe("when there are no concerns", () => { }) }) -describe("when 'noRevertRevertCommits' has a concern about characters 0-16", () => { +describe("when 'noMergeCommits' has a concern about the commit", () => { + const commit = fakeCommit({ + sha: "507c835ff93e38ed1540ff58fb72f7837f9af13", + parents: [fakeCommitSha(), fakeCommitSha()], + message: "Merge branch 'main' into bugfix/dance-party-playlist", + }) + const concern = commitConcern(ruleContext("noMergeCommits"), commit.sha) + + it("describes the rule violation", () => { + const actualOutput = commitwiseReport([commit], [concern]) + expect(actualOutput).toBe( + ` +507c835 Merge branch 'main' into bugfix/dance-party-playlist + ╭───────────────────────────────────────────────────── + ╰─ Merge commits are not allowed. + (noMergeCommits) +`.trim(), + ) + }) +}) + +describe("when 'noMergeCommits' has a concern about the commit", () => { + const commit = fakeCommit({ + sha: "71516e17c94c69de4eeafff60fac072764d2", + parents: [fakeCommitSha(), fakeCommitSha(), fakeCommitSha()], + message: "amend! Merge branch 'feature/new-coffee-machine' into feature/office-overhaul", + }) + const concern = commitConcern(ruleContext("noMergeCommits"), commit.sha) + + it("describes the rule violation", () => { + const actualOutput = commitwiseReport([commit], [concern]) + expect(actualOutput).toBe( + ` +71516e1 amend! Merge branch 'feature/new-coffee-machine' into feature/office-overhaul + ╭────────────────────────────────────────────────────────────────────────────── + ╰─ Merge commits are not allowed. + (noMergeCommits) +`.trim(), + ) + }) +}) + +describe("when 'noRevertRevertCommits' has a concern about characters 0-16 of the subject line", () => { const commit = fakeCommit({ sha: "d4e7a978cea34b727ea52f90c928fd535e4aee", message: 'Revert "Revert "Make the program act like a clown""', @@ -43,7 +86,7 @@ d4e7a97 Revert "Revert "Make the program act like a clown"" }) }) -describe("when 'noRevertRevertCommits' has a concern about characters 1-26", () => { +describe("when 'noRevertRevertCommits' has a concern about characters 1-26 of the subject line", () => { const commit = fakeCommit({ sha: "34aa41b818c40682cabeecd5623dfe51df7a4a5", message: ' revert "revert "revert "repair the soft ice machine """', @@ -65,7 +108,7 @@ describe("when 'noRevertRevertCommits' has a concern about characters 1-26", () }) }) -describe("when 'noSquashMarkers' has a concern about characters 0-6", () => { +describe("when 'noSquashMarkers' has a concern about characters 0-6 of the subject line", () => { const commit = fakeCommit({ sha: "ffebad193fe7d02aa9b19b70ee132a26f14f8caf", message: "amend!Apply strawberry jam to make the code sweeter", @@ -85,7 +128,7 @@ ffebad1 amend!Apply strawberry jam to make the code sweeter }) }) -describe("when 'noSquashMarkers' has a concern about characters 1-14", () => { +describe("when 'noSquashMarkers' has a concern about characters 1-14 of the subject line", () => { const commit = fakeCommit({ sha: "56c750b0811fbcad2b237b2b99fc7d3fc91b926", message: " fixup! fixup! found a funny easter egg", @@ -107,7 +150,7 @@ describe("when 'noSquashMarkers' has a concern about characters 1-14", () => { }) }) -describe("when 'useCapitalisedSubjectLines' has a concern about characters 0-1", () => { +describe("when 'useCapitalisedSubjectLines' has a concern about characters 0-1 of the subject line", () => { const commit = fakeCommit({ sha: "497de39943643a56f7a69d3d19723e3035318644", message: "release the robot butler", @@ -129,7 +172,7 @@ describe("when 'useCapitalisedSubjectLines' has a concern about characters 0-1", }) }) -describe("when 'useCapitalisedSubjectLines' has a concern about characters 7-8", () => { +describe("when 'useCapitalisedSubjectLines' has a concern about characters 7-8 of the subject line", () => { const commit = fakeCommit({ sha: "92d6b11650c6b63d64fd77522241b7f50ff5b", message: "fixup! resolve a bug that thought it was a feature", @@ -151,7 +194,7 @@ describe("when 'useCapitalisedSubjectLines' has a concern about characters 7-8", }) }) -describe("when 'useConciseSubjectLines' has a concern about characters 20-25", () => { +describe("when 'useConciseSubjectLines' has a concern about characters 20-25 of the subject line", () => { const commit = fakeCommit({ sha: "68e921648c4a19e93d72f42a5d39c3eba704e41", message: "Remove redundant call to `wrapper`", @@ -175,7 +218,7 @@ describe("when 'useConciseSubjectLines' has a concern about characters 20-25", ( }) }) -describe("when 'useConciseSubjectLines' has a concern about characters 20-67", () => { +describe("when 'useConciseSubjectLines' has a concern about characters 20-67 of the subject line", () => { const commit = fakeCommit({ sha: "9bed522bd48f0aee7574635bb23f5decdc4999", message: "revisit the boolean properties in the `IceCreamMachine` constructor", @@ -199,7 +242,7 @@ describe("when 'useConciseSubjectLines' has a concern about characters 20-67", ( }) }) -describe("when 'useConciseSubjectLines' has a concern about characters 50-52", () => { +describe("when 'useConciseSubjectLines' has a concern about characters 50-52 of the subject line", () => { const commit = fakeCommit({ sha: "e8c95d69587a51685070837aaf3a8746e3cbba8", message: "Retrieve data from the exclusive third-party service", @@ -223,7 +266,7 @@ e8c95d6 Retrieve data from the exclusive third-party service }) }) -describe("when 'useConciseSubjectLines' has a concern about characters 72-76", () => { +describe("when 'useConciseSubjectLines' has a concern about characters 72-76 of the subject line", () => { const commit = fakeCommit({ sha: "be86674322213fb408d176589fadbcd44a2df", message: "make a genuine attempt to fix the bugs that the users were complaining about", diff --git a/src/domains/rules/reports/CommitwiseReport.ts b/src/domains/rules/reports/CommitwiseReport.ts index cc2b08fe..61850456 100644 --- a/src/domains/rules/reports/CommitwiseReport.ts +++ b/src/domains/rules/reports/CommitwiseReport.ts @@ -1,6 +1,8 @@ import type { Commit, Commits } from "#commits/Commit.ts" import { formatTokenisedLine } from "#commits/tokens/Token.ts" +import type { CommitConcern } from "#rules/concerns/CommitConcern.ts" import { type Concern, type Concerns, concernedCommit } from "#rules/concerns/Concern.ts" +import type { SubjectLineConcern } from "#rules/concerns/SubjectLineConcern.ts" import type { RuleContext } from "#rules/Rule.ts" import { formatCharacterRange } from "#types/CharacterRange.ts" import { indentString } from "#utilities/Strings.ts" @@ -11,12 +13,57 @@ export function commitwiseReport(commits: Commits, concerns: Concerns): string { .join("\n\n") } +function formatConcern(concern: Concern, commit: Commit): string { + switch (concern.location) { + case "author-email-address": { + throw new Error(`Not implemented yet: ${concern.location}`) + } + case "author-name": { + throw new Error(`Not implemented yet: ${concern.location}`) + } + case "body-line": { + throw new Error(`Not implemented yet: ${concern.location}`) + } + case "commit": { + return formatCommitConcern(concern, commit) + } + case "committer-email-address": { + throw new Error(`Not implemented yet: ${concern.location}`) + } + case "committer-name": { + throw new Error(`Not implemented yet: ${concern.location}`) + } + case "subject-line": { + return formatSubjectLineConcern(concern, commit) + } + } +} + const SHORT_SHA_LENGTH = 7 +const RANGE_PREFIX = "╭─" const MESSAGE_PREFIX = "╰─" const MESSAGE_SUFFIX = "─╯" -function formatConcern(concern: Concern, commit: Commit): string { +function formatCommitConcern(concern: CommitConcern, commit: Commit): string { + const formattedSubjectLine = formatTokenisedLine(commit.subjectLine) + const commitLine = `${commit.sha.slice(0, SHORT_SHA_LENGTH)} ${formattedSubjectLine}` + const message = ruleMessage(concern.rule) + + const rangeLine = indentString( + RANGE_PREFIX + "─".repeat(formattedSubjectLine.length), + SHORT_SHA_LENGTH - RANGE_PREFIX.length + 1, + ) + + const messageLine = indentString( + `${MESSAGE_PREFIX} ${message}\n (${concern.rule.key})`, + SHORT_SHA_LENGTH - MESSAGE_PREFIX.length + 1, + ) + + return `${commitLine}\n${rangeLine}\n${messageLine}` +} + +function formatSubjectLineConcern(concern: SubjectLineConcern, commit: Commit): string { const commitLine = `${commit.sha.slice(0, SHORT_SHA_LENGTH)} ${formatTokenisedLine(commit.subjectLine)}` const [rangeStart, rangeEnd] = concern.range @@ -50,7 +97,7 @@ function ruleMessage(rule: RuleContext): string { throw new Error(`Not implemented yet: ${rule.key}`) } case "noMergeCommits": { - throw new Error(`Not implemented yet: ${rule.key}`) + return "Merge commits are not allowed." } case "noRepeatedSubjectLines": { throw new Error(`Not implemented yet: ${rule.key}`)