-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Use classes to validate commits.
Closes #17
- Loading branch information
1 parent
ad3abeb
commit 2399349
Showing
14 changed files
with
645 additions
and
240 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"clearMocks": true, | ||
"testPathIgnorePatterns": [ "/dist/", "/node_modules/" ] | ||
"testPathIgnorePatterns": [ "/dist/", "/node_modules/" ], | ||
"modulePaths": ["<rootDir>/src/"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { execSync } from "child_process"; | ||
import * as actions from "@actions/core"; | ||
import TestUtils from "../test/util"; | ||
import preinstall from "../preinstall"; | ||
import * as commitlintExec from "../commitlint"; | ||
import GitEvent from "./gitEvent"; | ||
|
||
describe("src/events/gitEvent", () => { | ||
const commitlint = jest.spyOn(commitlintExec, 'default'); | ||
const actionsInfo = jest.spyOn(actions, "info"); | ||
|
||
const { | ||
createTempDirectory, | ||
intializeGitRepo, | ||
getNthCommitBack, | ||
teardownGitRepo, | ||
teardownTestDirectory, | ||
addInvalidCommit, | ||
addValidCommit, | ||
setupTestDirectory, | ||
options, | ||
} = new TestUtils(); | ||
|
||
beforeAll(() => { | ||
options.cwd = createTempDirectory(); | ||
setupTestDirectory(options.cwd); | ||
preinstall('@joberstein12/commitlint-config', options); | ||
}); | ||
|
||
beforeEach(() => { | ||
intializeGitRepo(); | ||
}); | ||
|
||
afterEach(() => { | ||
teardownGitRepo(); | ||
|
||
expect(actionsInfo).toHaveBeenCalled(); | ||
expect(actionsInfo).not.toHaveBeenCalledWith( | ||
expect.stringContaining('Skipping commit validation') | ||
); | ||
|
||
expect(commitlint).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
afterAll(() => { | ||
teardownTestDirectory(options.cwd as string); | ||
}); | ||
|
||
it("Validates a git event with no target ref", async () => { | ||
const event = new GitEvent({ target: '' }, options); | ||
console.log(execSync('git log', options).toString()); | ||
|
||
await expect(event.validateCommits()).resolves.not.toThrow(); | ||
|
||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: undefined }) | ||
); | ||
}); | ||
|
||
describe("Validating a git event with a target ref", () => { | ||
it("Successfully validates the target commit", async () => { | ||
const target = getNthCommitBack(1); | ||
const event = new GitEvent({ target }, options); | ||
|
||
await expect(event.validateCommits()).resolves.not.toThrow(); | ||
|
||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${target}^` }) | ||
); | ||
}); | ||
|
||
it("Successfully validates commits a merge commit", async () => { | ||
execSync("git checkout -qb other", options); | ||
addValidCommit(); | ||
|
||
execSync([ | ||
'git checkout master', | ||
'git merge --no-ff other', | ||
].join(" && "), options); | ||
|
||
const event = new GitEvent({ | ||
target: getNthCommitBack(1), | ||
}, options); | ||
|
||
await expect(event.validateCommits()).resolves.not.toThrow(); | ||
|
||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${event.target}^` }) | ||
); | ||
}); | ||
|
||
it("Fails when the target commit is invalid", async () => { | ||
addInvalidCommit(); | ||
|
||
const event = new GitEvent({ | ||
target: getNthCommitBack(1) | ||
}, options); | ||
|
||
await expect(event.validateCommits()).rejects.toThrow(); | ||
|
||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${event.target}^` }) | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { ExecSyncOptions, execSync } from "child_process"; | ||
import commitlint from "commitlint"; | ||
import { info } from "@actions/core"; | ||
|
||
/** | ||
* Represents a base Git event. | ||
*/ | ||
class GitEvent { | ||
#args: GitEventArgs; | ||
options?: ExecSyncOptions; | ||
|
||
constructor(args: GitEventArgs, options?: ExecSyncOptions) { | ||
this.options = options; | ||
this.#args = args; | ||
} | ||
|
||
get target() { | ||
return this.#args.target; | ||
} | ||
|
||
/** | ||
* Provide a list of refs to checkout for this Git event. | ||
* @returns A list of refs | ||
*/ | ||
getRefsToCheckout(): string[] { | ||
return [this.#args.target]; | ||
} | ||
|
||
/** | ||
* Perform a checkout on all truthy refs returned from {@link GitEvent#getRefsToCheckout} | ||
*/ | ||
performCheckouts(): void { | ||
this.getRefsToCheckout() | ||
.filter(ref => ref) | ||
.forEach(ref => execSync(`git checkout ${ref}`, this.options).toString()); | ||
} | ||
|
||
/** | ||
* Provide a list of commits for commitlint to start validation from. | ||
* @returns A list of commit hashes | ||
*/ | ||
getFromCommits(): string[] { | ||
return [this.#args.target]; | ||
} | ||
|
||
/** | ||
* Provide a reason to skip commit validation. | ||
* @returns A truthy value explaining why validation is skipped, or a falsey value to proceed with commit validation. | ||
*/ | ||
getSkipValidationReason(): string | undefined { | ||
return; | ||
} | ||
|
||
/** | ||
* Executes commit validation for this Git event. | ||
*/ | ||
async validateCommits(): Promise<void> { | ||
const skipReason = this.getSkipValidationReason(); | ||
|
||
if (skipReason) { | ||
info(`Skipping commit validation: ${skipReason}`); | ||
return; | ||
} | ||
|
||
await Promise.all( | ||
this.getFromCommits() | ||
.map(from => commitlint({ | ||
from: from ? `${from}^` : undefined, | ||
cwd: this.options?.cwd?.toString() || undefined, | ||
})) | ||
); | ||
} | ||
} | ||
|
||
export default GitEvent; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { execSync } from "child_process"; | ||
import * as actions from "@actions/core"; | ||
import TestUtils from "../test/util"; | ||
import preinstall from "../preinstall"; | ||
import * as commitlintExec from "../commitlint"; | ||
import PullRequest from "./pullRequest"; | ||
|
||
describe("src/events/pullRequest", () => { | ||
const commitlint = jest.spyOn(commitlintExec, 'default'); | ||
const actionsInfo = jest.spyOn(actions, "info"); | ||
|
||
const { | ||
createTempDirectory, | ||
intializeGitRepo, | ||
getNthCommitBack, | ||
teardownGitRepo, | ||
teardownTestDirectory, | ||
addInvalidCommit, | ||
addValidCommit, | ||
setupTestDirectory, | ||
options, | ||
} = new TestUtils(); | ||
|
||
beforeAll(() => { | ||
options.cwd = createTempDirectory(); | ||
setupTestDirectory(options.cwd); | ||
preinstall('@joberstein12/commitlint-config', options); | ||
}); | ||
|
||
beforeEach(() => { | ||
intializeGitRepo(); | ||
|
||
execSync("git checkout -qb other", options); | ||
[ ...Array(3).keys() ].forEach(addValidCommit); | ||
}); | ||
|
||
afterEach(() => { | ||
teardownGitRepo(); | ||
|
||
expect(actionsInfo).toHaveBeenCalled(); | ||
expect(actionsInfo).not.toHaveBeenCalledWith( | ||
expect.stringContaining('Skipping commit validation') | ||
); | ||
|
||
expect(commitlint).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
afterAll(() => { | ||
teardownTestDirectory(options.cwd as string); | ||
}); | ||
|
||
it("Successfully completes commit validation with a detached head", async () => { | ||
execSync(`git checkout --detach`, options); | ||
addValidCommit(); | ||
|
||
const event = new PullRequest({ | ||
base_ref: 'master', | ||
head_ref: 'other', | ||
target: getNthCommitBack(1) | ||
}, options); | ||
|
||
await expect(event.validateCommits()).resolves.not.toThrow(); | ||
|
||
[getNthCommitBack(4), event.target].forEach(from => { | ||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${from}^` })) | ||
}); | ||
}); | ||
|
||
it("Successfully completes commit validation between two branches", async () => { | ||
const event = new PullRequest({ | ||
base_ref: 'master', | ||
head_ref: 'other', | ||
target: getNthCommitBack(1) | ||
}, options); | ||
|
||
await expect(event.validateCommits()).resolves.not.toThrow(); | ||
|
||
[getNthCommitBack(3), event.target].forEach(from => { | ||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${from}^` })) | ||
}); | ||
}); | ||
|
||
it("Fails when there's an invalid commit between two branches", async () => { | ||
addInvalidCommit(); | ||
addValidCommit(); | ||
|
||
const event = new PullRequest({ | ||
base_ref: 'master', | ||
head_ref: 'other', | ||
target: getNthCommitBack(1), | ||
}, options); | ||
|
||
await expect(event.validateCommits()).rejects.toThrow(); | ||
|
||
expect(commitlint).toHaveBeenCalledWith( | ||
expect.objectContaining({ from: `${getNthCommitBack(5)}^` }) | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { ExecSyncOptions, execSync } from "child_process"; | ||
import GitEvent from "./gitEvent"; | ||
|
||
/** | ||
* Represents a Git Pull Request event. | ||
*/ | ||
export default class PullRequest extends GitEvent { | ||
#args: PullRequestEventArgs; | ||
|
||
constructor(args: PullRequestEventArgs, options?: ExecSyncOptions) { | ||
const { target } = args; | ||
super({ target }, options); | ||
this.#args = args; | ||
} | ||
|
||
/** | ||
* Provide a list containing the base, head, and target refs for the pull request. | ||
* @returns A list of refs | ||
*/ | ||
getRefsToCheckout(): string[] { | ||
const { base_ref, head_ref, target } = this.#args; | ||
return [ base_ref, head_ref, target ]; | ||
} | ||
|
||
/** | ||
* Provide a list containing the initial commit of the pull request, and the target ref, which may be detached from the pull request head. | ||
* @returns A list of commit hashes | ||
*/ | ||
getFromCommits(): string[] { | ||
const { head_ref, base_ref } = this.#args; | ||
const { options, target } = this; | ||
|
||
const [commit, ] = execSync(`git rev-list --no-merges --first-parent ${base_ref}..'${head_ref}'`, options) | ||
.toString() | ||
.trim() | ||
.split('\n') | ||
.reverse(); | ||
|
||
if (!commit) { | ||
throw new Error('Failed to get initial commit in the given range.'); | ||
} | ||
|
||
return [commit, target]; | ||
} | ||
} |
Oops, something went wrong.