diff --git a/src/domains/commits/Commit.tests.ts b/src/domains/commits/Commit.tests.ts index fbe1a714..76a2562c 100644 --- a/src/domains/commits/Commit.tests.ts +++ b/src/domains/commits/Commit.tests.ts @@ -116,6 +116,30 @@ describe.each` }) }) +describe("when the commit signature is non-empty", () => { + const crudeCommit = fakeCrudeCommit({ + signature: + "-----BEGIN SSH SIGNATURE-----\n" + + "MzEwM2JkMTMtNmJiMy00N2YxLWEyNTUtOWMxZmFmYTAyMGZlNDI3MWYyMmEtMjU4MS00YTky\n" + + "LWFhNTEtMjI5YjRiZWIxYzMxNTZiMzAwYmMtYmU5ZC00NjUxLWFmODAtY2U3N2I2NmZmNDIy\n" + + "-----END SSH SIGNATURE-----", + }) + + it("has a signature", () => { + const commit = mapCrudeCommitToCommit(crudeCommit, configuration) + expect(commit.hasSignature).toBe(true) + }) +}) + +describe("when the commit signature is empty", () => { + const crudeCommit = fakeCrudeCommit({ signature: "" }) + + it("does not have a signature", () => { + const commit = mapCrudeCommitToCommit(crudeCommit, configuration) + expect(commit.hasSignature).toBe(false) + }) +}) + describe("when the commit message is empty", () => { const crudeCommit = fakeCrudeCommit({ message: "" }) diff --git a/src/domains/commits/Commit.ts b/src/domains/commits/Commit.ts index 31bd2d82..adaa3397 100644 --- a/src/domains/commits/Commit.ts +++ b/src/domains/commits/Commit.ts @@ -19,6 +19,7 @@ export type Commit = { committerName: string committerEmail: string isMergeCommit: boolean + hasSignature: boolean subjectLine: TokenisedLine bodyLines: TokenisedLines } @@ -34,6 +35,7 @@ export function mapCrudeCommitToCommit( return { sha: crudeCommit.sha, isMergeCommit: crudeCommit.parents.length > 1, + hasSignature: crudeCommit.signature !== "", authorName: crudeCommit.authorName, authorEmail: crudeCommit.authorEmail, committerName: crudeCommit.committerName, diff --git a/src/domains/commits/CrudeCommit.fixtures.ts b/src/domains/commits/CrudeCommit.fixtures.ts index 4d41cb8f..93fb913e 100644 --- a/src/domains/commits/CrudeCommit.fixtures.ts +++ b/src/domains/commits/CrudeCommit.fixtures.ts @@ -12,6 +12,7 @@ export function fakeCrudeCommit(overrides: CrudeCommitTemplate = {}): CrudeCommi committerName: "Leonardo da Vinci", committerEmail: "71091436+katanaturtle@users.noreply.github.com", message: "Introduce a cool feature\n\nIt is really awesome!", + signature: "", ...overrides, } } diff --git a/src/domains/commits/CrudeCommit.ts b/src/domains/commits/CrudeCommit.ts index e188be8a..cb21a87a 100644 --- a/src/domains/commits/CrudeCommit.ts +++ b/src/domains/commits/CrudeCommit.ts @@ -14,6 +14,7 @@ export type CrudeCommit = { committerName: string committerEmail: string message: string + signature: string } export type CrudeCommits = Array diff --git a/src/domains/commits/git/GetGitBranchCrudeCommits.tests.ts b/src/domains/commits/git/GetGitBranchCrudeCommits.tests.ts index 34e130f7..7a8775d4 100644 --- a/src/domains/commits/git/GetGitBranchCrudeCommits.tests.ts +++ b/src/domains/commits/git/GetGitBranchCrudeCommits.tests.ts @@ -505,6 +505,34 @@ describe("when the committer is absent", () => { }) }) +describe.each` + signature + ${"-----BEGIN SSH SIGNATURE-----\nNjI1ZTMxMzctODU1ZS00YmVmLWI0MTMtY2Q4MmI4YjZlYzE0\n-----END SSH SIGNATURE-----"} + ${"-----BEGIN PGP SIGNATURE-----\n\nNGU2NWQ0YzctN2I0OC00YWExLWE2NmUtZjg3MTZmNGY2MWJhODM4NTI1ZTEtZWY4OS00YjVjLTgwMWItOTgxNTBmOGJiOTZk\n-----END PGP SIGNATURE-----"} +`("when the commit has a signature of $signature", (props: { signature: string }) => { + const signature = props.signature + + beforeEach(() => { + mockGitLog([{ gpgsig: [signature] }]) + }) + + it("preserves the signature", async () => { + const [commit] = await getGitBranchCrudeCommits() + expect(commit?.signature).toBe(signature) + }) +}) + +describe("when the commit does not have a signature", () => { + beforeEach(() => { + mockGitLog([{}]) + }) + + it("omits the signature", async () => { + const [commit] = await getGitBranchCrudeCommits() + expect(commit?.signature).toBe("") + }) +}) + describe("when the Git log does not have any commits", () => { const commitDtos = fakeGitLogCommitDtos(0) diff --git a/src/domains/commits/git/GetGitBranchCrudeCommits.ts b/src/domains/commits/git/GetGitBranchCrudeCommits.ts index abbd25e5..cb214fc3 100644 --- a/src/domains/commits/git/GetGitBranchCrudeCommits.ts +++ b/src/domains/commits/git/GetGitBranchCrudeCommits.ts @@ -27,6 +27,7 @@ function mapDtoToCrudeCommit(dto: GitLogCommitDto): CrudeCommit { committerName, committerEmail, message: dto.message[0], + signature: dto.gpgsig[0] ?? "", } } diff --git a/src/domains/commits/github/GetGithubPullRequestCrudeCommits.tests.ts b/src/domains/commits/github/GetGithubPullRequestCrudeCommits.tests.ts index be45befc..6dc8c3df 100644 --- a/src/domains/commits/github/GetGithubPullRequestCrudeCommits.tests.ts +++ b/src/domains/commits/github/GetGithubPullRequestCrudeCommits.tests.ts @@ -587,6 +587,45 @@ describe("when the committer is absent", () => { }) }) +describe.each` + signature + ${"-----BEGIN SSH SIGNATURE-----\nN2ViNGVjNmYtMzk2NC00YjUxLTg5ZTItZmJmYmQ2M2YyZGZk\n-----END SSH SIGNATURE-----"} + ${"-----BEGIN PGP SIGNATURE-----\n\nMmE2NDljNTctZTJkNi00YTg2LTllOGMtMjkyYjc3YjlhOGJiYzc3NGRiZGMtNjcxNC00NjI5LWI1ODMtNDFlMGI3YTI2ZmYw\n-----END PGP SIGNATURE-----"} +`("when the commit has a signature of $signature", (props: { signature: string }) => { + const signature = props.signature + + beforeEach(() => { + mockGithubPullRequestCommitDtos([{ commit: { verification: { signature } } }]) + }) + + it("preserves the signature", async () => { + const [commit] = await getGithubPullRequestCrudeCommits() + expect(commit?.signature).toBe(signature) + }) +}) + +describe("when the commit does not have a signature", () => { + beforeEach(() => { + mockGithubPullRequestCommitDtos([{ commit: { verification: { signature: null } } }]) + }) + + it("omits the signature", async () => { + const [commit] = await getGithubPullRequestCrudeCommits() + expect(commit?.signature).toBe("") + }) +}) + +describe("when the commit verification object is absent", () => { + beforeEach(() => { + mockGithubPullRequestCommitDtos([{ commit: {} }]) + }) + + it("omits the signature", async () => { + const [commit] = await getGithubPullRequestCrudeCommits() + expect(commit?.signature).toBe("") + }) +}) + describe("when the pull request does not have any commits", () => { const commitDtos = fakeGithubCommitDtos(0) diff --git a/src/domains/commits/github/GetGithubPullRequestCrudeCommits.ts b/src/domains/commits/github/GetGithubPullRequestCrudeCommits.ts index c3875c09..a909db28 100644 --- a/src/domains/commits/github/GetGithubPullRequestCrudeCommits.ts +++ b/src/domains/commits/github/GetGithubPullRequestCrudeCommits.ts @@ -25,5 +25,6 @@ function mapDtoToCrudeCommit(dto: GithubCommitDto): CrudeCommit { committerEmail: dto.commit.committer?.email ?? "", parents: dto.parents.map((parentDto) => parentDto.sha), message: dto.commit.message, + signature: dto.commit.verification?.signature ?? "", } } diff --git a/src/domains/rules/NoMergeCommits.tests.ts b/src/domains/rules/NoMergeCommits.tests.ts index a39db136..064ef714 100644 --- a/src/domains/rules/NoMergeCommits.tests.ts +++ b/src/domains/rules/NoMergeCommits.tests.ts @@ -1,5 +1,6 @@ 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 { commitConcern } from "#rules/concerns/CommitConcern.ts" import type { Concerns } from "#rules/concerns/Concern.ts" @@ -7,6 +8,7 @@ import { noMergeCommits } from "#rules/NoMergeCommits.ts" import type { RuleKey, RuleOptions } from "#rules/Rule.ts" import { fakeCommitSha } from "#types/CommitSha.fixtures.ts" import type { CommitSha } from "#types/CommitSha.ts" +import type { Vector } from "#types/Vector.ts" const rule = "noMergeCommits" satisfies RuleKey const enabled: RuleOptions = {} @@ -70,3 +72,59 @@ describe.each` }) }, ) + +describe("when verifying a set of multiple commits and some commits are merge commits", () => { + const commits: Vector = [ + fakeCommit({ parents: [fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha(), fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha(), fakeCommitSha(), fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha(), fakeCommitSha()] }), + fakeCommit({ parents: [] }), + ] + + describe("and the rule is enabled", () => { + const actualConcerns = noMergeCommits(commits, enabled) + + it("raises concerns about the merge commits", () => { + expect(actualConcerns).toEqual([ + commitConcern(rule, commits[1].sha), + commitConcern(rule, commits[3].sha), + commitConcern(rule, commits[4].sha), + ]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = noMergeCommits(commits, null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) +}) + +describe("when verifying a set of multiple commits and no commits are merge commits", () => { + const commits: Vector = [ + fakeCommit({ parents: [fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha()] }), + fakeCommit({ parents: [fakeCommitSha()] }), + fakeCommit({ parents: [] }), + ] + + describe("and the rule is enabled", () => { + const actualConcerns = noMergeCommits(commits, enabled) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = noMergeCommits(commits, null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) +}) diff --git a/src/domains/rules/UseSignedCommits.tests.ts b/src/domains/rules/UseSignedCommits.tests.ts new file mode 100644 index 00000000..0a99b05f --- /dev/null +++ b/src/domains/rules/UseSignedCommits.tests.ts @@ -0,0 +1,147 @@ +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 { commitConcern } from "#rules/concerns/CommitConcern.ts" +import type { Concerns } from "#rules/concerns/Concern.ts" +import type { RuleKey, RuleOptions } from "#rules/Rule.ts" +import { useSignedCommits } from "#rules/UseSignedCommits.ts" +import type { Vector } from "#types/Vector.ts" + +const rule = "useSignedCommits" satisfies RuleKey +const enabled: RuleOptions = {} + +const fakeCommit = fakeCommitFactory(fakeConfiguration()) + +describe.each` + subjectLine + ${"Teach the coffee machine to stop judging mugs"} + ${"shipping a tiny fix before lunch"} +`( + "when the commit does not have a signature and has a subject line of $subjectLine", + (props: { subjectLine: string }) => { + const commit = fakeCommit({ message: props.subjectLine, signature: "" }) + + describe("and the rule is enabled", () => { + const actualConcerns = useSignedCommits([commit], enabled) + + it("raises a concern about the entire commit", () => { + expect(actualConcerns).toEqual([commitConcern(rule, commit.sha)]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = useSignedCommits([commit], null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + }, +) + +describe.each` + signature | subjectLine + ${"-----BEGIN SSH SIGNATURE-----\nNWNhOTk2MWQtMWZiZS00NTg4LThiMmUtNzEzMmExMjJiM2Q5\n-----END SSH SIGNATURE-----"} | ${"Give the release notes a sensible haircut"} + ${"-----BEGIN PGP SIGNATURE-----\n\nOTI4Zjk0OWMtNjJhOS00OTk1LTkwYzEtNmU1MWYxYWE0MzdkNThmMDM4NGQtOGFjYS00ZDk5LWJjYjMtZmRiNWI1NmEzNTU4\n-----END PGP SIGNATURE-----"} | ${"put the changelog back where it belongs"} +`( + "when the commit has a signature and has a subject line of $subjectLine", + (props: { signature: string; subjectLine: string }) => { + const commit = fakeCommit({ message: props.subjectLine, signature: props.signature }) + + describe("and the rule is enabled", () => { + const actualConcerns = useSignedCommits([commit], enabled) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = useSignedCommits([commit], null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + }, +) + +describe("when verifying a set of multiple commits and some commits lack a signature", () => { + const commits: Vector = [ + fakeCommit({ + signature: + "-----BEGIN SSH SIGNATURE-----\nYzhmNjMwODktYzA3MS00YzM5LWEyYzAtNTYyYjNjMmExMjJi\n-----END SSH SIGNATURE-----", + }), + fakeCommit({ signature: "" }), + fakeCommit({ signature: "" }), + fakeCommit({ + signature: + "-----BEGIN SSH SIGNATURE-----\nN2YwNDUwYzktYzRhNi00YzNiLWFiMzMtYWEyYjg3MThjMDc0\n-----END SSH SIGNATURE-----", + }), + fakeCommit({ signature: "" }), + fakeCommit({ + signature: + "-----BEGIN PGP SIGNATURE-----\n\nZmMzMDA1ZjItYzdlNy00NzliLThmOTYtNjk1NWZkMzA0MGNjNmFhYTA2YzUtOWQ1Yi00YmViLWI0MzMtNzJjYmU3ZjA3OGVl\n-----END PGP SIGNATURE-----", + }), + fakeCommit({ signature: "" }), + ] + + describe("and the rule is enabled", () => { + const actualConcerns = useSignedCommits(commits, enabled) + + it("raises concerns about the commits without a signature", () => { + expect(actualConcerns).toEqual([ + commitConcern(rule, commits[1].sha), + commitConcern(rule, commits[2].sha), + commitConcern(rule, commits[4].sha), + commitConcern(rule, commits[6].sha), + ]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = useSignedCommits(commits, null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) +}) + +describe("when verifying a set of multiple commits and all commits have signatures", () => { + const commits: Vector = [ + fakeCommit({ + signature: + "-----BEGIN SSH SIGNATURE-----\nOTFhZDVjOGUtMWQxYS00NTY4LWFmNDgtOWU4Y2U2MzU5NGE2\n-----END SSH SIGNATURE-----", + }), + fakeCommit({ + signature: + "-----BEGIN PGP SIGNATURE-----\n\nODk5ZTUwYTAtZDIwNi00ODY2LWEyMGItOGY0NGEwYjlmZDNmMDFkOTQzOGEtNjZiNy00MTk2LTlkYzYtMTc2MDg4MTZkMjgw\n-----END PGP SIGNATURE-----", + }), + fakeCommit({ + signature: + "-----BEGIN SSH SIGNATURE-----\nMmZjYWYxMDktNWUzYS00ZmNiLWFjNTQtMmUzYzA1ODQwMGY4\n-----END SSH SIGNATURE-----", + }), + fakeCommit({ + signature: + "-----BEGIN PGP SIGNATURE-----\n\nODVlYmM5MjgtMDA3NC00MjNhLTlkYTQtYWZjNThkNTJhNzU2NjY4MThlZmYtMTFhYi00ODBmLThlOWEtYzAwNGE5MmMwNjVj\n-----END PGP SIGNATURE-----", + }), + ] + + describe("and the rule is enabled", () => { + const actualConcerns = useSignedCommits(commits, enabled) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) + + describe("and the rule is disabled", () => { + const actualConcerns = useSignedCommits(commits, null) + + it("does not raise any concerns", () => { + expect(actualConcerns).toEqual([]) + }) + }) +}) diff --git a/src/domains/rules/UseSignedCommits.ts b/src/domains/rules/UseSignedCommits.ts index 57040e25..f69dcf38 100644 --- a/src/domains/rules/UseSignedCommits.ts +++ b/src/domains/rules/UseSignedCommits.ts @@ -1,15 +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 { RuleKey } from "#rules/Rule.ts" import type { EmptyObject } from "#types/EmptyObject.ts" import { notNullish } from "#utilities/Arrays.ts" -const _rule = "useSignedCommits" satisfies RuleKey +const rule = "useSignedCommits" satisfies RuleKey export function useSignedCommits(commits: Commits, options: EmptyObject | null): Concerns { return options !== null ? commits.map(verifyCommit).filter(notNullish) : [] } -function verifyCommit(_commit: Commit): Concern | null { - throw new Error("The `useSignedCommits` rule has not been implemented yet") // TODO: To be implemented. +function verifyCommit(commit: Commit): Concern | null { + return commit.hasSignature ? null : commitConcern(rule, commit.sha) } diff --git a/src/domains/rules/reports/CommitwiseReport.tests.ts b/src/domains/rules/reports/CommitwiseReport.tests.ts index 95182079..6ef2eb68 100644 --- a/src/domains/rules/reports/CommitwiseReport.tests.ts +++ b/src/domains/rules/reports/CommitwiseReport.tests.ts @@ -79,7 +79,7 @@ describe("when 'noMergeCommits' has a concern about the commit", () => { }) const concern = commitConcern("noMergeCommits", commit.sha) - it("describes the rule violation", () => { + it("describes the rule violation in the commit", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -92,7 +92,7 @@ describe("when 'noMergeCommits' has a concern about the commit", () => { }) }) -describe("when 'noMergeCommits' has a concern about the commit", () => { +describe("when 'noMergeCommits' has a concern about the commit with a long subject line", () => { const configuration = fakeConfiguration() const fakeCommit = fakeCommitFactory(configuration) @@ -103,7 +103,7 @@ describe("when 'noMergeCommits' has a concern about the commit", () => { }) const concern = commitConcern("noMergeCommits", commit.sha) - it("describes the rule violation", () => { + it("describes the rule violation in the commit", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -116,6 +116,54 @@ describe("when 'noMergeCommits' has a concern about the commit", () => { }) }) +describe("when 'useSignedCommits' has a concern about the commit", () => { + const configuration = fakeConfiguration() + const fakeCommit = fakeCommitFactory(configuration) + + const commit = fakeCommit({ + sha: "9b9e2ab8f3248152474f41f728f1221d5bf55a16", + message: "Sign the pantry inventory app", + signature: "", + }) + const concern = commitConcern("useSignedCommits", commit.sha) + + it("describes the rule violation in the commit", () => { + const actualOutput = commitwiseReport([concern], [commit], configuration) + expect(actualOutput).toBe( + ` +9b9e2ab Sign the pantry inventory app + ╭────────────────────────────── + ╰─ Commits must be signed with a signing key. + (useSignedCommits) +`.trim(), + ) + }) +}) + +describe("when 'useSignedCommits' has a concern about the commit with a long subject line", () => { + const configuration = fakeConfiguration() + const fakeCommit = fakeCommitFactory(configuration) + + const commit = fakeCommit({ + sha: "42cefd126a47bfd368d774047a711519eadc2d5", + message: "fixup! GH-692 it's raining gold everywhere we go", + signature: "", + }) + const concern = commitConcern("useSignedCommits", commit.sha) + + it("describes the rule violation in the commit", () => { + const actualOutput = commitwiseReport([concern], [commit], configuration) + expect(actualOutput).toBe( + ` +42cefd1 fixup! GH-692 it's raining gold everywhere we go + ╭───────────────────────────────────────────────── + ╰─ Commits must be signed with a signing key. + (useSignedCommits) +`.trim(), + ) + }) +}) + describe("when 'noRevertRevertCommits' has a concern about characters 0-16 of the subject line", () => { const configuration = fakeConfiguration() const fakeCommit = fakeCommitFactory(configuration) @@ -763,7 +811,7 @@ describe("when 'useIssueLinks' with position 'anywhere' has a concern about char }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [0, 1] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -790,7 +838,7 @@ describe("when 'useIssueLinks' with position 'prefix' has a concern about charac }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [7, 8] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -817,7 +865,7 @@ describe("when 'useIssueLinks' with position 'suffix' has a concern about charac }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [49, 50] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -844,7 +892,7 @@ describe("when 'useIssueLinks' with position 'suffix' has a concern about charac }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [26, 27] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -872,7 +920,7 @@ describe("when 'useIssueLinks' with position 'anywhere' and Jira-style issue lin }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [10, 11] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -900,7 +948,7 @@ describe("when 'useIssueLinks' with position 'prefix' and custom-style issue lin }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [0, 1] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` @@ -928,7 +976,7 @@ describe("when 'useIssueLinks' with position 'suffix' and Jira-style issue links }) const concern = subjectLineConcern("useIssueLinks", commit.sha, { range: [41, 42] }) - it("describes the rule violation", () => { + it("describes the rule violation in the subject line", () => { const actualOutput = commitwiseReport([concern], [commit], configuration) expect(actualOutput).toBe( ` diff --git a/src/domains/rules/reports/CommitwiseReport.ts b/src/domains/rules/reports/CommitwiseReport.ts index c2f478d5..07c9529b 100644 --- a/src/domains/rules/reports/CommitwiseReport.ts +++ b/src/domains/rules/reports/CommitwiseReport.ts @@ -244,7 +244,7 @@ function getRuleMessage(rule: RuleKey, configuration: Configuration): RuleMessag throw new Error(`Not implemented yet: ${rule}`) } case "useSignedCommits": { - throw new Error(`Not implemented yet: ${rule}`) + return ruleMessage("Commits must be signed with a signing key.") } } } diff --git a/src/utilities/git/cli/GetGitLog.tests.ts b/src/utilities/git/cli/GetGitLog.tests.ts index 07e65a44..c190fc1f 100644 --- a/src/utilities/git/cli/GetGitLog.tests.ts +++ b/src/utilities/git/cli/GetGitLog.tests.ts @@ -50,6 +50,7 @@ committer Leonardo da Vinci <71091436+katanaturtle@users.noreply.github.com> 176 committer: [ "Leonardo da Vinci <71091436+katanaturtle@users.noreply.github.com> 1766222736 -0500", ], + gpgsig: [], message: ["init"], }, ]) @@ -80,6 +81,7 @@ committer Master Splinter 1769801867 -0500 parent: ["b34f54dae2045478f57846dfdd5762f4afb721"], author: ["Raph 1769800893 -0500"], committer: ["Master Splinter 1769801867 -0500"], + gpgsig: [], message: ["fixup! fighting the foot clan"], }, ]) @@ -114,6 +116,7 @@ committer Donatello <42069849+gogogadget@users.noreply.github.com> 1771411407 -0 ], author: ["April O'Neil 1771411236 -0500"], committer: ["Donatello <42069849+gogogadget@users.noreply.github.com> 1771411407 -0500"], + gpgsig: [], message: ["Deliver unlimited power with technological genius"], }, ]) @@ -154,6 +157,7 @@ committer Casey "Goongala" Jones 1784831778 -0500 "Michelangelo di Lodovico Buonarroti Simoni 1784831778 -0500", ], committer: ['Casey "Goongala" Jones 1784831778 -0500'], + gpgsig: [], message: [ "Let's gather all mutants for a massive pizza party!!\n\nUnder parental supervision, of course ;)", ], @@ -438,6 +442,7 @@ gpgsig -----BEGIN SSH SIGNATURE----- "Leonardo da Vinci <71091436+katanaturtle@users.noreply.github.com> 1809064863 -0500", ], committer: ["Donatello <42069849+gogogadget@users.noreply.github.com> 1809068484 -0500"], + gpgsig: [], message: ["stealth: be one with the shadows\n\nmutant chain reaction\nin the underground"], }, { @@ -446,6 +451,7 @@ gpgsig -----BEGIN SSH SIGNATURE----- parent: ["26ecb332bc4787332cc9bb5fff90c8100225a69ff"], author: ["Oroku Saki 1723364474 -0400"], committer: ["Karai <02968712+misssaki@users.noreply.github.com> 1723469209 -0400"], + gpgsig: [], message: ["squash! the defects"], }, { diff --git a/src/utilities/git/cli/GetGitLog.ts b/src/utilities/git/cli/GetGitLog.ts index a741e020..1aa62b7e 100644 --- a/src/utilities/git/cli/GetGitLog.ts +++ b/src/utilities/git/cli/GetGitLog.ts @@ -84,6 +84,7 @@ function gitLogCommitDto(commitSha: string): GitLogCommitDto { author: [], commit: [`${commitSha}\n`], committer: [], + gpgsig: [], message: [""], parent: [], } diff --git a/src/utilities/git/cli/dtos/GitLogCommitDto.fixtures.ts b/src/utilities/git/cli/dtos/GitLogCommitDto.fixtures.ts index d413e9f0..142b08c6 100644 --- a/src/utilities/git/cli/dtos/GitLogCommitDto.fixtures.ts +++ b/src/utilities/git/cli/dtos/GitLogCommitDto.fixtures.ts @@ -9,6 +9,7 @@ export function fakeGitLogCommitDto(overrides: GitLogCommitDtoTemplate = {}): Gi author: ["Master Splinter 1769801867 -0500"], commit: [fakeCommitSha()], committer: ["Master Splinter 1769801867 -0500"], + gpgsig: [], message: ["Introduce a cool feature\n\nIt is really awesome!"], parent: [fakeCommitSha()], ...overrides, diff --git a/src/utilities/git/cli/dtos/GitLogCommitDto.ts b/src/utilities/git/cli/dtos/GitLogCommitDto.ts index 6b58557d..483e85f6 100644 --- a/src/utilities/git/cli/dtos/GitLogCommitDto.ts +++ b/src/utilities/git/cli/dtos/GitLogCommitDto.ts @@ -2,6 +2,7 @@ export type GitLogCommitDto = { author: Array commit: [string] committer: Array + gpgsig: Array message: [string] parent: Array [key: string]: Array diff --git a/src/utilities/github/api/dtos/GithubCommitDto.ts b/src/utilities/github/api/dtos/GithubCommitDto.ts index 19e5fa39..46430278 100644 --- a/src/utilities/github/api/dtos/GithubCommitDto.ts +++ b/src/utilities/github/api/dtos/GithubCommitDto.ts @@ -1,4 +1,4 @@ -import { type InferOutput, array, nullable, object, string } from "valibot" +import { type InferOutput, array, exactOptional, nullable, object, string } from "valibot" import { commitSha } from "#types/CommitSha.ts" import { githubCommitUserDto } from "#utilities/github/api/dtos/GithubCommitUserDto.ts" import { githubParentCommitDto } from "#utilities/github/api/dtos/GithubParentCommitDto.ts" @@ -17,6 +17,11 @@ export function githubCommitDto() { author: nullable(githubCommitUserDto()), committer: nullable(githubCommitUserDto()), message: string(), + verification: exactOptional( + object({ + signature: nullable(string()), + }), + ), }), }) }