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
2 changes: 1 addition & 1 deletion src/domains/configurations/Configuration.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function fakeConfiguration(overrides: ConfigurationTemplate = {}): Config
},
rules: {
noBlankSubjectLines: {},
noExcessiveCommitsPerBranch: {},
noExcessiveCommitsPerBranch: { maxCommits: 10 },
noMergeCommits: {},
noRepeatedSubjectLines: {},
noRestrictedFooterLines: {},
Expand Down
4 changes: 2 additions & 2 deletions src/domains/configurations/GetConfiguration.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("the default configuration in the command-line", () => {
it.each`
enabledRuleKey | expectedRuleOptions
${"noBlankSubjectLines"} | ${{}}
${"noExcessiveCommitsPerBranch"} | ${{}}
${"noExcessiveCommitsPerBranch"} | ${{ maxCommits: 10 }}
${"noMergeCommits"} | ${{}}
${"noSingleWordSubjectLines"} | ${{}}
${"noRestrictedFooterLines"} | ${{}}
Expand Down Expand Up @@ -70,7 +70,7 @@ describe("the default configuration in GitHub Actions", () => {
it.each`
enabledRuleKey | expectedRuleOptions
${"noBlankSubjectLines"} | ${{}}
${"noExcessiveCommitsPerBranch"} | ${{}}
${"noExcessiveCommitsPerBranch"} | ${{ maxCommits: 10 }}
${"noMergeCommits"} | ${{}}
${"noRepeatedSubjectLines"} | ${{}}
${"noRestrictedFooterLines"} | ${{}}
Expand Down
4 changes: 2 additions & 2 deletions src/domains/configurations/GetDefaultConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function getDefaultCliConfiguration(): Configuration {
tokens: getDefaultTokenConfiguration(),
rules: {
noBlankSubjectLines: {},
noExcessiveCommitsPerBranch: {},
noExcessiveCommitsPerBranch: { maxCommits: 10 },
noMergeCommits: {},
noRepeatedSubjectLines: null,
noRestrictedFooterLines: {},
Expand Down Expand Up @@ -51,7 +51,7 @@ function getDefaultGhaConfiguration(): Configuration {
tokens: getDefaultTokenConfiguration(),
rules: {
noBlankSubjectLines: {},
noExcessiveCommitsPerBranch: {},
noExcessiveCommitsPerBranch: { maxCommits: 10 },
noMergeCommits: {},
noRepeatedSubjectLines: {},
noRestrictedFooterLines: {},
Expand Down
249 changes: 249 additions & 0 deletions src/domains/rules/NoExcessiveCommitsPerBranch.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
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 { noExcessiveCommitsPerBranch } from "#rules/NoExcessiveCommitsPerBranch.ts"
import type { RuleKey, RuleOptions } from "#rules/Rule.ts"
import { fakeCommitSha } from "#types/CommitSha.fixtures.ts"
import type { Vector } from "#types/Vector.ts"

const rule = "noExcessiveCommitsPerBranch" satisfies RuleKey
const enabled3: RuleOptions<typeof rule> = { maxCommits: 3 }
const enabled10: RuleOptions<typeof rule> = { maxCommits: 10 }

const fakeCommit = fakeCommitFactory(fakeConfiguration())

describe("when verifying a set of 6 commits when up to 3 commits are allowed", () => {
const commits: Vector<Commit, 6> = [
fakeCommit({ message: "Open the bakery dashboard" }),
fakeCommit({ message: "Add the cinnamon telemetry" }),
fakeCommit({ message: "Wire the oat milk alert" }),
fakeCommit({ message: "fix the suspicious croissant counter" }),
fakeCommit({ message: "refactor the jam queue" }),
fakeCommit({ message: "Test the emergency toaster" }),
]

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

it("raises concerns about commits 4-6", () => {
expect(actualConcerns).toEqual<Concerns>([
commitConcern(rule, commits[3].sha),
commitConcern(rule, commits[4].sha),
commitConcern(rule, commits[5].sha),
])
})
})

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

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

describe("when verifying a set of 3 commits when up to 3 commits are allowed", () => {
const commits: Vector<Commit, 3> = [
fakeCommit({ message: "Replace guesswork with a tiny chart" }),
fakeCommit({ message: "Teach the kettle to apologise" }),
fakeCommit({ message: "Document the spooky button" }),
]

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

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

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

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

describe("when verifying a set of 1 commit when up to 3 commits are allowed", () => {
const commits: Vector<Commit, 1> = [fakeCommit({ message: "label the mystery switch" })]

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

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

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

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

describe("when verifying a set of 12 commits when up to 10 commits are allowed", () => {
const commits: Vector<Commit, 12> = [
fakeCommit({ message: "Open the emergency snack drawer" }),
fakeCommit({ message: "Close the emergency snack drawer" }),
fakeCommit({ message: "inventory the secret biscuit shelf" }),
fakeCommit({ message: "Name the suspicious toggle" }),
fakeCommit({ message: "Install the typo detector" }),
fakeCommit({ message: "Train the queue to wait politely" }),
fakeCommit({ message: "sort the dashboard mittens" }),
fakeCommit({ message: "Dust the release checklist" }),
fakeCommit({ message: "Label the pocket calculator" }),
fakeCommit({ message: "Patch the waffle endpoint" }),
fakeCommit({ message: "rebalance the pastry index" }),
fakeCommit({ message: "test the emergency toaster again" }),
]

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

it("raises concerns about commits 11-12", () => {
expect(actualConcerns).toEqual<Concerns>([
commitConcern(rule, commits[10].sha),
commitConcern(rule, commits[11].sha),
])
})
})

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

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

describe("when verifying a set of 10 commits when up to 10 commits are allowed", () => {
const commits: Vector<Commit, 10> = [
fakeCommit({ message: "Replace guesswork with a tiny chart" }),
fakeCommit({ message: "Teach the kettle to apologise" }),
fakeCommit({ message: "Document the spooky button" }),
fakeCommit({ message: "shrink the noisy diff" }),
fakeCommit({ message: "Invite the parser to brunch" }),
fakeCommit({ message: "mend the build badge" }),
fakeCommit({ message: "Adjust the polite timeout" }),
fakeCommit({ message: "Archive the sleepy migration" }),
fakeCommit({ message: "Untangle the ribbon config" }),
fakeCommit({ message: "tidy the peanut gallery" }),
]

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

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

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

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

describe("when verifying a set of 4 commits when up to 10 commits are allowed", () => {
const commits: Vector<Commit, 4> = [
fakeCommit({ message: "label the mystery switch" }),
fakeCommit({ message: "make the release notes less dramatic" }),
fakeCommit({ message: "Invite the parser to brunch" }),
fakeCommit({ message: "mend the build badge" }),
]

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

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

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

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

describe("when verifying a set of 6 commits when up to 3 commits are allowed and some commits are merge commits", () => {
const commits: Vector<Commit, 6> = [
fakeCommit({ message: "Map the odd socks drawer" }),
fakeCommit({
parents: [fakeCommitSha(), fakeCommitSha()],
message: "Merge branch 'main' into feature/odd-socks",
}),
fakeCommit({ message: "Add the cardigan forecast" }),
fakeCommit({
parents: [fakeCommitSha(), fakeCommitSha(), fakeCommitSha()],
message: "Keep the branch vaguely current",
}),
fakeCommit({ message: "tune the lint whistle" }),
fakeCommit({ message: "Polish the biscuit dashboard" }),
]

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

it("ignores merge commits when determining excessive commits", () => {
expect(actualConcerns).toEqual<Concerns>([commitConcern(rule, commits[5].sha)])
})
})

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

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

describe("when verifying a set of 8 commits when up to 3 commits are allowed and some commits have squash markers", () => {
const commits: Vector<Commit, 8> = [
fakeCommit({ message: "Teach the calendar to blink" }),
fakeCommit({ message: "fixup! Teach the calendar to blink" }),
fakeCommit({ message: "Replace the mysterious toggle" }),
fakeCommit({ message: "this commit is a lie" }),
fakeCommit({ message: "Document the noodle switch" }),
fakeCommit({ message: " squash! fixup! make the tests less theatrical" }),
fakeCommit({ message: "Calm down the invoice parser" }),
fakeCommit({ message: "I must have been drunk" }),
]

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

it("ignores commits with squash markers when determining excessive commits", () => {
expect(actualConcerns).toEqual<Concerns>([
commitConcern(rule, commits[4].sha),
commitConcern(rule, commits[6].sha),
commitConcern(rule, commits[7].sha),
])
})
})

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

it("does not raise any concerns", () => {
expect(actualConcerns).toEqual<Concerns>([])
})
})
})
37 changes: 28 additions & 9 deletions src/domains/rules/NoExcessiveCommitsPerBranch.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
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, RuleOptions } from "#rules/Rule.ts"
import type { EmptyObject } from "#types/EmptyObject.ts"
import { notNullish } from "#utilities/Arrays.ts"

const _rule = "noExcessiveCommitsPerBranch" satisfies RuleKey
const rule = "noExcessiveCommitsPerBranch" satisfies RuleKey

/**
* Verifies that the branch does not contain more than a given number of commits.
*
* Keeping pull requests small makes them easier to review and easier to revert if needed.
* It may also help to catch accidental rebases onto wrong branches or stale commits.
*
* It ignores merge commits and commits with squash markers.
*/
export function noExcessiveCommitsPerBranch(
commits: Commits,
options: EmptyObject | null,
options: { maxCommits: number } | null,
): Concerns {
return options !== null
? commits.map((commit) => verifyCommit(commit, options)).filter(notNullish)
: []
return options !== null ? [...verifyCommits(commits, options)] : []
}

function verifyCommit(_commit: Commit, _options: RuleOptions<typeof _rule>): Concern | null {
throw new Error("The `noExcessiveCommitsPerBranch` rule has not been implemented yet") // TODO: To be implemented.
function* verifyCommits(commits: Commits, options: RuleOptions<typeof rule>): Generator<Concern> {
let commitCount = 0

for (const commit of commits) {
if (!commit.isMergeCommit && !hasSquashMarker(commit)) {
commitCount += 1

if (commitCount > options.maxCommits) {
yield commitConcern(rule, commit.sha)
}
}
}
}

function hasSquashMarker(commit: Commit): boolean {
return commit.subjectLine[0]?.type === "squash-marker"
}
Loading
Loading