feat: support Conventional Commits via --type flag (#177)
Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
This commit is contained in:
@@ -35,6 +35,11 @@ cli(
|
|||||||
alias: 'a',
|
alias: 'a',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
description: 'Type of commit message to generate',
|
||||||
|
alias: 't',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
commands: [
|
commands: [
|
||||||
@@ -56,6 +61,7 @@ cli(
|
|||||||
argv.flags.generate,
|
argv.flags.generate,
|
||||||
argv.flags.exclude,
|
argv.flags.exclude,
|
||||||
argv.flags.all,
|
argv.flags.all,
|
||||||
|
argv.flags.type,
|
||||||
rawArgv,
|
rawArgv,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default async (
|
|||||||
generate: number | undefined,
|
generate: number | undefined,
|
||||||
excludeFiles: string[],
|
excludeFiles: string[],
|
||||||
stageAll: boolean,
|
stageAll: boolean,
|
||||||
|
commitType: string | undefined,
|
||||||
rawArgv: string[],
|
rawArgv: string[],
|
||||||
) => (async () => {
|
) => (async () => {
|
||||||
intro(bgCyan(black(' aicommits ')));
|
intro(bgCyan(black(' aicommits ')));
|
||||||
@@ -45,6 +46,7 @@ export default async (
|
|||||||
OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY,
|
OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY,
|
||||||
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
|
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
|
||||||
generate: generate?.toString(),
|
generate: generate?.toString(),
|
||||||
|
type: commitType?.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
@@ -58,6 +60,7 @@ export default async (
|
|||||||
staged.diff,
|
staged.diff,
|
||||||
config.generate,
|
config.generate,
|
||||||
config['max-length'],
|
config['max-length'],
|
||||||
|
config.type,
|
||||||
config.timeout,
|
config.timeout,
|
||||||
config.proxy,
|
config.proxy,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default () => (async () => {
|
|||||||
staged!.diff,
|
staged!.diff,
|
||||||
config.generate,
|
config.generate,
|
||||||
config['max-length'],
|
config['max-length'],
|
||||||
|
config.type,
|
||||||
config.timeout,
|
config.timeout,
|
||||||
config.proxy,
|
config.proxy,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import type { TiktokenModel } from '@dqbd/tiktoken';
|
|||||||
import { fileExists } from './fs.js';
|
import { fileExists } from './fs.js';
|
||||||
import { KnownError } from './error.js';
|
import { KnownError } from './error.js';
|
||||||
|
|
||||||
|
const commitTypes = ['', 'conventional'] as const;
|
||||||
|
|
||||||
|
export type CommitType = typeof commitTypes[number];
|
||||||
|
|
||||||
const { hasOwnProperty } = Object.prototype;
|
const { hasOwnProperty } = Object.prototype;
|
||||||
export const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key);
|
export const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key);
|
||||||
|
|
||||||
@@ -51,6 +55,15 @@ const configParsers = {
|
|||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
},
|
},
|
||||||
|
type(type?: string) {
|
||||||
|
if (!type) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAssert('type', commitTypes.includes(type as CommitType), 'Invalid commit type');
|
||||||
|
|
||||||
|
return type as CommitType;
|
||||||
|
},
|
||||||
proxy(url?: string) {
|
proxy(url?: string) {
|
||||||
if (!url || url.length === 0) {
|
if (!url || url.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -99,7 +112,7 @@ type RawConfig = {
|
|||||||
[key in ConfigKeys]?: string;
|
[key in ConfigKeys]?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ValidConfig = {
|
export type ValidConfig = {
|
||||||
[Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>;
|
[Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import https from 'https';
|
import https from 'https';
|
||||||
import type { ClientRequest, IncomingMessage } from 'http';
|
import type { ClientRequest, IncomingMessage } from 'http';
|
||||||
import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
|
import type { ChatCompletionRequestMessage, CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
|
||||||
import {
|
import {
|
||||||
TiktokenModel,
|
TiktokenModel,
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@dqbd/tiktoken';
|
} from '@dqbd/tiktoken';
|
||||||
import createHttpsProxyAgent from 'https-proxy-agent';
|
import createHttpsProxyAgent from 'https-proxy-agent';
|
||||||
import { KnownError } from './error.js';
|
import { KnownError } from './error.js';
|
||||||
|
import type { CommitType } from './config.js';
|
||||||
|
|
||||||
const httpsPost = async (
|
const httpsPost = async (
|
||||||
hostname: string,
|
hostname: string,
|
||||||
@@ -104,16 +105,51 @@ const sanitizeMessage = (message: string) => message.trim().replace(/[\n\r]/g, '
|
|||||||
|
|
||||||
const deduplicateMessages = (array: string[]) => Array.from(new Set(array));
|
const deduplicateMessages = (array: string[]) => Array.from(new Set(array));
|
||||||
|
|
||||||
const getPrompt = (
|
const getBasePrompt = (
|
||||||
locale: string,
|
locale: string,
|
||||||
diff: string,
|
|
||||||
maxLength: number,
|
maxLength: number,
|
||||||
) => `${[
|
) => `${[
|
||||||
'Generate a concise git commit message written in present tense for the following code diff with the given specifications below:',
|
'Generate a concise git commit message written in present tense for the following code diff with the given specifications below:',
|
||||||
`Message language: ${locale}`,
|
`Message language: ${locale}`,
|
||||||
`Commit message must be a maximum of ${maxLength} characters.`,
|
`Commit message must be a maximum of ${maxLength} characters.`,
|
||||||
'Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
|
'Exclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.',
|
||||||
].join('\n')}\n\n${diff}`;
|
].join('\n')}`;
|
||||||
|
|
||||||
|
const getCommitMessageFormatOutputExample = (type: CommitType) => `The output response must be in format:\n${getCommitMessageFormat(type)}`;
|
||||||
|
|
||||||
|
const getCommitMessageFormat = (type: CommitType) => {
|
||||||
|
if (type === 'conventional') {
|
||||||
|
return '<type>(<optional scope>): <commit message>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<commit message>';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* References:
|
||||||
|
* Commitlint:
|
||||||
|
* https://github.com/conventional-changelog/commitlint/blob/18fbed7ea86ac0ec9d5449b4979b762ec4305a92/%40commitlint/config-conventional/index.js#L40-L100
|
||||||
|
*
|
||||||
|
* Conventional Changelog:
|
||||||
|
* https://github.com/conventional-changelog/conventional-changelog/blob/d0e5d5926c8addba74bc962553dd8bcfba90e228/packages/conventional-changelog-conventionalcommits/writer-opts.js#L182-L193
|
||||||
|
*/
|
||||||
|
const getExtraContextForConventionalCommits = () => (
|
||||||
|
`Choose a type from the type-to-description JSON below that best describes the git diff:\n${
|
||||||
|
JSON.stringify({
|
||||||
|
docs: 'Documentation only changes',
|
||||||
|
style: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
|
||||||
|
refactor: 'A code change that neither fixes a bug nor adds a feature',
|
||||||
|
perf: 'A code change that improves performance',
|
||||||
|
test: 'Adding missing tests or correcting existing tests',
|
||||||
|
build: 'Changes that affect the build system or external dependencies',
|
||||||
|
ci: 'Changes to our CI configuration files and scripts',
|
||||||
|
chore: "Other changes that don't modify src or test files",
|
||||||
|
revert: 'Reverts a previous commit',
|
||||||
|
feat: 'A new feature',
|
||||||
|
fix: 'A bug fix',
|
||||||
|
}, null, 2)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
const generateStringFromLength = (length: number) => {
|
const generateStringFromLength = (length: number) => {
|
||||||
let result = '';
|
let result = '';
|
||||||
@@ -139,10 +175,28 @@ export const generateCommitMessage = async (
|
|||||||
diff: string,
|
diff: string,
|
||||||
completions: number,
|
completions: number,
|
||||||
maxLength: number,
|
maxLength: number,
|
||||||
|
type: CommitType,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
proxy?: string,
|
proxy?: string,
|
||||||
) => {
|
) => {
|
||||||
const prompt = getPrompt(locale, diff, maxLength);
|
const prompt = getBasePrompt(locale, maxLength);
|
||||||
|
|
||||||
|
const conventionalCommitsExtraContext = type === 'conventional'
|
||||||
|
? getExtraContextForConventionalCommits()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const commitMessageFormatOutputExample = getCommitMessageFormatOutputExample(type);
|
||||||
|
|
||||||
|
const messages: ChatCompletionRequestMessage[] = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `${prompt}\n${conventionalCommitsExtraContext}\n${commitMessageFormatOutputExample}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: diff,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Padded by 5 for more room for the completion.
|
// Padded by 5 for more room for the completion.
|
||||||
const stringFromLength = generateStringFromLength(maxLength + 5);
|
const stringFromLength = generateStringFromLength(maxLength + 5);
|
||||||
@@ -155,10 +209,7 @@ export const generateCommitMessage = async (
|
|||||||
apiKey,
|
apiKey,
|
||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
messages: [{
|
messages,
|
||||||
role: 'user',
|
|
||||||
content: prompt,
|
|
||||||
}],
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
top_p: 1,
|
top_p: 1,
|
||||||
frequency_penalty: 0,
|
frequency_penalty: 0,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe } from 'manten';
|
|||||||
|
|
||||||
describe('aicommits', ({ runTestSuite }) => {
|
describe('aicommits', ({ runTestSuite }) => {
|
||||||
runTestSuite(import('./specs/cli/index.js'));
|
runTestSuite(import('./specs/cli/index.js'));
|
||||||
|
runTestSuite(import('./specs/openai/index.js'));
|
||||||
runTestSuite(import('./specs/config.js'));
|
runTestSuite(import('./specs/config.js'));
|
||||||
runTestSuite(import('./specs/git-hook.js'));
|
runTestSuite(import('./specs/git-hook.js'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default testSuite(({ describe }) => {
|
|||||||
commitMessage,
|
commitMessage,
|
||||||
length: commitMessage.length,
|
length: commitMessage.length,
|
||||||
});
|
});
|
||||||
expect(commitMessage.length <= 20).toBe(true);
|
expect(commitMessage.length).toBeLessThanOrEqual(20);
|
||||||
|
|
||||||
await fixture.rm();
|
await fixture.rm();
|
||||||
});
|
});
|
||||||
@@ -208,6 +208,140 @@ export default testSuite(({ describe }) => {
|
|||||||
await fixture.rm();
|
await fixture.rm();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('commit types', ({ test }) => {
|
||||||
|
test('Should not use conventional commits by default', async () => {
|
||||||
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
||||||
|
const { fixture, aicommits } = await createFixture({
|
||||||
|
...files,
|
||||||
|
});
|
||||||
|
const git = await createGit(fixture.path);
|
||||||
|
|
||||||
|
await git('add', ['data.json']);
|
||||||
|
|
||||||
|
const committing = aicommits();
|
||||||
|
|
||||||
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
||||||
|
const stdout = buffer.toString();
|
||||||
|
if (stdout.match('└')) {
|
||||||
|
committing.stdin!.write('y');
|
||||||
|
committing.stdin!.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await committing;
|
||||||
|
|
||||||
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
||||||
|
expect(statusAfter.stdout).toBe('');
|
||||||
|
|
||||||
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
expect(commitMessage).not.toMatch(conventionalCommitPattern);
|
||||||
|
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Conventional commits', async () => {
|
||||||
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
||||||
|
const { fixture, aicommits } = await createFixture({
|
||||||
|
...files,
|
||||||
|
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
|
||||||
|
});
|
||||||
|
const git = await createGit(fixture.path);
|
||||||
|
|
||||||
|
await git('add', ['data.json']);
|
||||||
|
|
||||||
|
const committing = aicommits();
|
||||||
|
|
||||||
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
||||||
|
const stdout = buffer.toString();
|
||||||
|
if (stdout.match('└')) {
|
||||||
|
committing.stdin!.write('y');
|
||||||
|
committing.stdin!.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await committing;
|
||||||
|
|
||||||
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
||||||
|
expect(statusAfter.stdout).toBe('');
|
||||||
|
|
||||||
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
expect(commitMessage).toMatch(conventionalCommitPattern);
|
||||||
|
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Accepts --type flag, overriding config', async () => {
|
||||||
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
||||||
|
const { fixture, aicommits } = await createFixture({
|
||||||
|
...files,
|
||||||
|
'.aicommits': `${files['.aicommits']}\ntype=other`,
|
||||||
|
});
|
||||||
|
const git = await createGit(fixture.path);
|
||||||
|
|
||||||
|
await git('add', ['data.json']);
|
||||||
|
|
||||||
|
// Generate flag should override generate config
|
||||||
|
const committing = aicommits([
|
||||||
|
'--type', 'conventional',
|
||||||
|
]);
|
||||||
|
|
||||||
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
||||||
|
const stdout = buffer.toString();
|
||||||
|
if (stdout.match('└')) {
|
||||||
|
committing.stdin!.write('y');
|
||||||
|
committing.stdin!.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await committing;
|
||||||
|
|
||||||
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
||||||
|
expect(statusAfter.stdout).toBe('');
|
||||||
|
|
||||||
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
expect(commitMessage).toMatch(conventionalCommitPattern);
|
||||||
|
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Accepts empty --type flag', async () => {
|
||||||
|
const conventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test):\s/;
|
||||||
|
const { fixture, aicommits } = await createFixture({
|
||||||
|
...files,
|
||||||
|
'.aicommits': `${files['.aicommits']}\ntype=conventional`,
|
||||||
|
});
|
||||||
|
const git = await createGit(fixture.path);
|
||||||
|
|
||||||
|
await git('add', ['data.json']);
|
||||||
|
|
||||||
|
const committing = aicommits([
|
||||||
|
'--type', '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
committing.stdout!.on('data', (buffer: Buffer) => {
|
||||||
|
const stdout = buffer.toString();
|
||||||
|
if (stdout.match('└')) {
|
||||||
|
committing.stdin!.write('y');
|
||||||
|
committing.stdin!.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await committing;
|
||||||
|
|
||||||
|
const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']);
|
||||||
|
expect(statusAfter.stdout).toBe('');
|
||||||
|
|
||||||
|
const { stdout: commitMessage } = await git('log', ['--oneline']);
|
||||||
|
console.log('Committed with:', commitMessage);
|
||||||
|
expect(commitMessage).not.toMatch(conventionalCommitPattern);
|
||||||
|
|
||||||
|
await fixture.rm();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('proxy', ({ test }) => {
|
describe('proxy', ({ test }) => {
|
||||||
test('Fails on invalid proxy', async () => {
|
test('Fails on invalid proxy', async () => {
|
||||||
const { fixture, aicommits } = await createFixture({
|
const { fixture, aicommits } = await createFixture({
|
||||||
|
|||||||
161
tests/specs/openai/conventional-commits.ts
Normal file
161
tests/specs/openai/conventional-commits.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { expect, testSuite } from 'manten';
|
||||||
|
import {
|
||||||
|
generateCommitMessage,
|
||||||
|
} from '../../../src/utils/openai.js';
|
||||||
|
import type { ValidConfig } from '../../../src/utils/config.js';
|
||||||
|
|
||||||
|
const { OPENAI_KEY } = process.env;
|
||||||
|
|
||||||
|
export default testSuite(({ describe }) => {
|
||||||
|
if (!OPENAI_KEY) {
|
||||||
|
console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Conventional Commits', async ({ test }) => {
|
||||||
|
await test('Should not translate conventional commit type to Japanase when locale config is set to japanese', async () => {
|
||||||
|
const japaneseConventionalCommitPattern = /(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?: [\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/;
|
||||||
|
|
||||||
|
const gitDiff = await readDiffFromFile('new-feature.txt');
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff, {
|
||||||
|
locale: 'ja',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(commitMessage).toMatch(japaneseConventionalCommitPattern);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "feat:" conventional commit when change relate to adding a new feature', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('new-feature.txt');
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "feat:" or "feat(<scope>):"
|
||||||
|
expect(commitMessage).toMatch(/(feat(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "refactor:" conventional commit when change relate to code refactoring', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('code-refactoring.txt');
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "refactor:" or "refactor(<scope>):"
|
||||||
|
expect(commitMessage).toMatch(/(refactor(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "test:" conventional commit when change relate to testing a React application', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('testing-react-application.txt');
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "test:" or "test(<scope>):"
|
||||||
|
expect(commitMessage).toMatch(/(test(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "build:" conventional commit when change relate to github action build pipeline', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile(
|
||||||
|
'github-action-build-pipeline.txt',
|
||||||
|
);
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "build:" or "build(<scope>):"
|
||||||
|
expect(commitMessage).toMatch(/((build|ci)(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "(ci|build):" conventional commit when change relate to continious integration', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('continous-integration.txt');
|
||||||
|
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "ci:" or "ci(<scope>):
|
||||||
|
// It also sometimes generates build and feat
|
||||||
|
expect(commitMessage).toMatch(/((ci|build|feat)(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "docs:" conventional commit when change relate to documentation changes', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('documentation-changes.txt');
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "docs:" or "docs(<scope>):"
|
||||||
|
expect(commitMessage).toMatch(/(docs(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "fix:" conventional commit when change relate to fixing code', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('fix-nullpointer-exception.txt');
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "fix:" or "fix(<scope>):"
|
||||||
|
// Sometimes it generates refactor
|
||||||
|
expect(commitMessage).toMatch(/((fix|refactor)(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "style:" conventional commit when change relate to code style improvements', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('code-style.txt');
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "style:" or "style(<style>):"
|
||||||
|
expect(commitMessage).toMatch(/(style|refactor|fix)(\(.*\))?:/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "chore:" conventional commit when change relate to a chore or maintenance', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('chore.txt');
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "chore:" or "chore(<style>):"
|
||||||
|
// Sometimes it generates build|feat
|
||||||
|
expect(commitMessage).toMatch(/((chore|build|feat)(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test('Should use "perf:" conventional commit when change relate to a performance improvement', async () => {
|
||||||
|
const gitDiff = await readDiffFromFile('performance-improvement.txt');
|
||||||
|
const commitMessage = await runGenerateCommitMessage(gitDiff);
|
||||||
|
|
||||||
|
// should match "perf:" or "perf(<style>):"
|
||||||
|
// It also sometimes generates refactor:
|
||||||
|
expect(commitMessage).toMatch(/((perf|refactor)(\(.*\))?):/);
|
||||||
|
console.log('Generated message:', commitMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function runGenerateCommitMessage(gitDiff: string,
|
||||||
|
configOverrides: Partial<ValidConfig> = {}): Promise<string> {
|
||||||
|
const config = {
|
||||||
|
locale: 'en',
|
||||||
|
type: 'conventional',
|
||||||
|
generate: 1,
|
||||||
|
'max-length': 50,
|
||||||
|
...configOverrides,
|
||||||
|
} as ValidConfig;
|
||||||
|
const commitMessages = await generateCommitMessage(OPENAI_KEY!, 'gpt-3.5-turbo', config.locale, gitDiff, config.generate, config['max-length'], config.type, 7000);
|
||||||
|
|
||||||
|
return commitMessages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* See ./diffs/README.md in order to generate diff files
|
||||||
|
*/
|
||||||
|
async function readDiffFromFile(filename: string): Promise<string> {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const gitDiff = await readFile(
|
||||||
|
path.resolve(__dirname, `./diff-fixtures/${filename}`),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
return gitDiff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
11
tests/specs/openai/diff-fixtures/README.md
Normal file
11
tests/specs/openai/diff-fixtures/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Generating diffs
|
||||||
|
|
||||||
|
1. Instruct ChatGPT with the following command:
|
||||||
|
```
|
||||||
|
I want you to act as a git cli
|
||||||
|
I will give you the type of content and you will generate a random git diff based on that
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Insert the type of change
|
||||||
|
|
||||||
|
ChatGPT will generate a fictional git diff based on the type of change you inserted.
|
||||||
18
tests/specs/openai/diff-fixtures/chore.txt
Normal file
18
tests/specs/openai/diff-fixtures/chore.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
diff --git a/package.json b/package.json
|
||||||
|
index 2a7398e..6b2a3f0 100644
|
||||||
|
--- a/package.json
|
||||||
|
+++ b/package.json
|
||||||
|
@@ -3,7 +3,7 @@
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A sample project",
|
||||||
|
"main": "index.js",
|
||||||
|
- "scripts": {
|
||||||
|
+ "scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"test": "mocha",
|
||||||
|
- "lint": "eslint ."
|
||||||
|
+ "lint": "eslint .",
|
||||||
|
+ "clean": "rm -rf node_modules && npm install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1",
|
||||||
34
tests/specs/openai/diff-fixtures/code-refactoring.txt
Normal file
34
tests/specs/openai/diff-fixtures/code-refactoring.txt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
diff --git a/old_example.ts b/new_example.ts
|
||||||
|
index 1234567..abcdefg 100644
|
||||||
|
--- a/old_example.ts
|
||||||
|
+++ b/new_example.ts
|
||||||
|
|
||||||
|
@@ -1,15 +1,16 @@
|
||||||
|
-import { Component, OnInit } from '@angular/core';
|
||||||
|
+import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
-@Component({
|
||||||
|
- selector: 'app-example',
|
||||||
|
- templateUrl: './example.component.html',
|
||||||
|
- styleUrls: ['./example.component.css']
|
||||||
|
-})
|
||||||
|
-export class ExampleComponent implements OnInit {
|
||||||
|
- message: string;
|
||||||
|
+@Component({
|
||||||
|
+ selector: 'app-improved-example',
|
||||||
|
+ templateUrl: './improved-example.component.html',
|
||||||
|
+ styleUrls: ['./improved-example.component.css']
|
||||||
|
+})
|
||||||
|
+export class ImprovedExampleComponent {
|
||||||
|
+ private _message: string;
|
||||||
|
|
||||||
|
- ngOnInit() {
|
||||||
|
- this.message = 'Hello, world!';
|
||||||
|
+ constructor() {
|
||||||
|
+ this._message = 'Hello, world!';
|
||||||
|
}
|
||||||
|
|
||||||
|
+ get message(): string {
|
||||||
|
+ return this._message;
|
||||||
|
+ }
|
||||||
|
}
|
||||||
28
tests/specs/openai/diff-fixtures/code-style.txt
Normal file
28
tests/specs/openai/diff-fixtures/code-style.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
diff --git a/src/app.js b/src/app.js
|
||||||
|
index 8741c37..91b2e74 100644
|
||||||
|
--- a/src/app.js
|
||||||
|
+++ b/src/app.js
|
||||||
|
@@ -10,12 +10,12 @@ app.use(express.json());
|
||||||
|
// Routes
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
- res.send('Welcome to the API!');
|
||||||
|
+ res.send('Welcome to the API!');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/users', (req, res) => {
|
||||||
|
- const user = createUser(req.body);
|
||||||
|
- res.status(201).send(user);
|
||||||
|
+ const user = createUser(req.body);
|
||||||
|
+ res.status(201).send(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/users/:id', (req, res) => {
|
||||||
|
@@ -27,7 +27,7 @@ app.get('/users/:id', (req, res) => {
|
||||||
|
if (user) {
|
||||||
|
res.send(user);
|
||||||
|
} else {
|
||||||
|
- res.status(404).send('User not found');
|
||||||
|
+ res.status(404).send('User not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
30
tests/specs/openai/diff-fixtures/continous-integration.txt
Normal file
30
tests/specs/openai/diff-fixtures/continous-integration.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..b6e5789
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/.github/workflows/ci.yml
|
||||||
|
@@ -0,0 +1,16 @@
|
||||||
|
+name: Continuous Integration
|
||||||
|
+
|
||||||
|
+on:
|
||||||
|
+ push:
|
||||||
|
+ branches:
|
||||||
|
+ - main
|
||||||
|
+ pull_request:
|
||||||
|
+ branches:
|
||||||
|
+ - main
|
||||||
|
+
|
||||||
|
+jobs:
|
||||||
|
+ build-and-test:
|
||||||
|
+ runs-on: ubuntu-latest
|
||||||
|
+ steps:
|
||||||
|
+ - name: Checkout repository
|
||||||
|
+ uses: actions/checkout@v2
|
||||||
|
+ - name: Set up Node.js
|
||||||
|
+ uses: actions/setup-node@v2
|
||||||
|
+ with:
|
||||||
|
+ node-version: '16'
|
||||||
|
+ - name: Install dependencies
|
||||||
|
+ run: npm ci
|
||||||
|
+ - name: Run tests
|
||||||
|
+ run: npm test
|
||||||
27
tests/specs/openai/diff-fixtures/deprecate-feature.txt
Normal file
27
tests/specs/openai/diff-fixtures/deprecate-feature.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
diff --git a/old_feature.py b/old_feature.py
|
||||||
|
index 1234567..abcdefg 100644
|
||||||
|
--- a/old_feature.py
|
||||||
|
+++ b/old_feature.py
|
||||||
|
@@ -1,7 +1,9 @@
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class OldFeature:
|
||||||
|
+ def __init__(self):
|
||||||
|
+ warnings.warn("OldFeature is deprecated and will be removed in the next release. Please use NewFeature instead.", DeprecationWarning)
|
||||||
|
|
||||||
|
def do_something(self):
|
||||||
|
print("Doing something with the old feature...")
|
||||||
|
diff --git a/new_feature.py b/new_feature.py
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..1111111
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/new_feature.py
|
||||||
|
@@ -0,0 +1,7 @@
|
||||||
|
+class NewFeature:
|
||||||
|
+ def __init__(self):
|
||||||
|
+ print("Initializing the new feature...")
|
||||||
|
+
|
||||||
|
+ def do_something(self):
|
||||||
|
+ print("Doing something with the new feature...")
|
||||||
|
+
|
||||||
30
tests/specs/openai/diff-fixtures/documentation-changes.txt
Normal file
30
tests/specs/openai/diff-fixtures/documentation-changes.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
diff --git a/README.md b/README.md
|
||||||
|
index a0c3e1b..9d1b6f8 100644
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -1,6 +1,11 @@
|
||||||
|
# My Awesome Project
|
||||||
|
|
||||||
|
+## Overview
|
||||||
|
+
|
||||||
|
+My Awesome Project is a web application that allows users to manage their tasks and projects in a simple and intuitive way. The project is built with React and Node.js and uses MongoDB for data storage.
|
||||||
|
+
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
+To install and run My Awesome Project, follow these steps:
|
||||||
|
+
|
||||||
|
1. Clone the repository: `git clone https://github.com/username/my-awesome-project.git`
|
||||||
|
2. Install dependencies: `npm install`
|
||||||
|
3. Start the development server: `npm start`
|
||||||
|
@@ -13,6 +18,11 @@ To install and run My Awesome Project, follow these steps:
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use My Awesome Project, follow these steps:
|
||||||
|
+
|
||||||
|
+1. Open your web browser and navigate to `http://localhost:3000`
|
||||||
|
+2. Sign up for a new account or log in to an existing one
|
||||||
|
+3. Create a new task or project and start managing your work!
|
||||||
|
+
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions from anyone and everyone. To contribute to My Awesome Project, follow these steps:
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
diff --git a/src/main/java/com/example/MyClass.java b/src/main/java/com/example/MyClass.java
|
||||||
|
index e7d8f38..caab7f1 100644
|
||||||
|
--- a/src/main/java/com/example/MyClass.java
|
||||||
|
+++ b/src/main/java/com/example/MyClass.java
|
||||||
|
@@ -23,7 +23,10 @@ public class MyClass {
|
||||||
|
public void processItems(List<Item> items) {
|
||||||
|
for (Item item : items) {
|
||||||
|
- if (item.getValue().equalsIgnoreCase("example")) {
|
||||||
|
+ // Fixing NullPointerException by adding a null check
|
||||||
|
+ String itemValue = item.getValue();
|
||||||
|
+ if (itemValue != null && itemValue.equalsIgnoreCase("example")) {
|
||||||
|
processExampleItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
|
||||||
|
index 1d07d31..085eb64 100644
|
||||||
|
--- a/.github/workflows/build.yml
|
||||||
|
+++ b/.github/workflows/build.yml
|
||||||
|
@@ -10,6 +10,8 @@ jobs:
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
+ - name: Install dependencies
|
||||||
|
+ run: npm install
|
||||||
|
- name: Build and test
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
@@ -22,3 +24,7 @@ jobs:
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
+ name: Build artifact
|
||||||
|
+ path: build
|
||||||
|
+ - name: Deploy to production
|
||||||
|
+ uses: some-third-party/deploy-action@v1
|
||||||
47
tests/specs/openai/diff-fixtures/new-feature.txt
Normal file
47
tests/specs/openai/diff-fixtures/new-feature.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
diff --git a/src/features/newFeature.js b/src/features/newFeature.js
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..b6e5789
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/features/newFeature.js
|
||||||
|
@@ -0,0 +1,18 @@
|
||||||
|
+/**
|
||||||
|
+ * New feature: Calculates the factorial of a given number.
|
||||||
|
+ * @param {number} n - The input number.
|
||||||
|
+ * @returns {number} - The factorial of the input number.
|
||||||
|
+ */
|
||||||
|
+function factorial(n) {
|
||||||
|
+ if (n === 0 || n === 1) {
|
||||||
|
+ return 1;
|
||||||
|
+ }
|
||||||
|
+ return n * factorial(n - 1);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+module.exports = {
|
||||||
|
+ factorial,
|
||||||
|
+};
|
||||||
|
+
|
||||||
|
diff --git a/src/app.js b/src/app.js
|
||||||
|
index 8741c37..91b2e74 100644
|
||||||
|
--- a/src/app.js
|
||||||
|
+++ b/src/app.js
|
||||||
|
@@ -2,6 +2,7 @@
|
||||||
|
const express = require('express');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const userRoutes = require('./routes/userRoutes');
|
||||||
|
+const { factorial } = require('./features/newFeature');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
@@ -21,6 +22,12 @@
|
||||||
|
res.send('Welcome to the API!');
|
||||||
|
});
|
||||||
|
|
||||||
|
+app.get('/factorial/:number', (req, res) => {
|
||||||
|
+ const number = parseInt(req.params.number, 10);
|
||||||
|
+ const result = factorial(number);
|
||||||
|
+ res.send(`Factorial of ${number} is ${result}`);
|
||||||
|
+});
|
||||||
|
+
|
||||||
|
// Other routes...
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
26
tests/specs/openai/diff-fixtures/performance-improvement.txt
Normal file
26
tests/specs/openai/diff-fixtures/performance-improvement.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
diff --git a/src/loop.js b/src/loop.js
|
||||||
|
index 1d45a2b..8c52e81 100644
|
||||||
|
--- a/src/loop.js
|
||||||
|
+++ b/src/loop.js
|
||||||
|
@@ -5,14 +5,14 @@ const items = generateItems(100000);
|
||||||
|
function processData(items) {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
- for (let i = 0; i < items.length; i++) {
|
||||||
|
- const item = items[i];
|
||||||
|
- if (item.isValid()) {
|
||||||
|
- sum += item.value;
|
||||||
|
- }
|
||||||
|
+ for (const item of items) {
|
||||||
|
+ if (item.isValid()) sum += item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
-const result = processData(items);
|
||||||
|
+const result = processData(items); // Improved loop iteration
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
console.log(`Result: ${result}, Time: ${endTime - startTime} ms`);
|
||||||
27
tests/specs/openai/diff-fixtures/remove-feature.txt
Normal file
27
tests/specs/openai/diff-fixtures/remove-feature.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
diff --git a/Controllers/FeatureController.cs b/Controllers/FeatureController.cs
|
||||||
|
index 8a3b7c1..3e29f9a 100644
|
||||||
|
--- a/Controllers/FeatureController.cs
|
||||||
|
+++ b/Controllers/FeatureController.cs
|
||||||
|
@@ -1,16 +1,7 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MyWebApi.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class FeatureController : ControllerBase
|
||||||
|
{
|
||||||
|
- [HttpGet("old-feature")]
|
||||||
|
- public ActionResult<string> GetOldFeature()
|
||||||
|
- {
|
||||||
|
- return "This is the removed old feature.";
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
[HttpGet("new-feature")]
|
||||||
|
public ActionResult<string> GetNewFeature()
|
||||||
|
{
|
||||||
|
return "This is the new feature.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
diff --git a/src/components/MyComponent.test.js b/src/components/MyComponent.test.js
|
||||||
|
index 37eabf2..976c6bf 100644
|
||||||
|
--- a/src/components/MyComponent.test.js
|
||||||
|
+++ b/src/components/MyComponent.test.js
|
||||||
|
@@ -10,6 +10,7 @@ describe("MyComponent", () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the component correctly", () => {
|
||||||
|
+ const props = { name: "John Doe", age: 25 };
|
||||||
|
const tree = renderer.create(<MyComponent {...props} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
@@ -25,6 +26,11 @@ describe("MyComponent", () => {
|
||||||
|
expect(wrapper.find("h1").text()).toEqual("Hello, John Doe!");
|
||||||
|
});
|
||||||
|
|
||||||
|
+ it("displays the correct age", () => {
|
||||||
|
+ const props = { name: "Jane Doe", age: 30 };
|
||||||
|
+ const wrapper = shallow(<MyComponent {...props} />);
|
||||||
|
+ expect(wrapper.find("p").text()).toEqual("Age: 30");
|
||||||
|
+ });
|
||||||
|
});
|
||||||
7
tests/specs/openai/index.ts
Normal file
7
tests/specs/openai/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from 'manten';
|
||||||
|
|
||||||
|
export default testSuite(({ describe }) => {
|
||||||
|
describe('OpenAI', ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import('./conventional-commits.js'));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user