feat: support Conventional Commits via --type flag (#177)

Co-authored-by: Hiroki Osame <hiroki.osame@gmail.com>
This commit is contained in:
Thijs Limmen
2023-05-03 15:17:09 +02:00
committed by GitHub
parent f466f0527e
commit 0562761dc2
22 changed files with 723 additions and 11 deletions

View File

@@ -35,6 +35,11 @@ cli(
alias: 'a',
default: false,
},
type: {
type: String,
description: 'Type of commit message to generate',
alias: 't',
},
},
commands: [
@@ -56,6 +61,7 @@ cli(
argv.flags.generate,
argv.flags.exclude,
argv.flags.all,
argv.flags.type,
rawArgv,
);
}

View File

@@ -18,6 +18,7 @@ export default async (
generate: number | undefined,
excludeFiles: string[],
stageAll: boolean,
commitType: string | undefined,
rawArgv: string[],
) => (async () => {
intro(bgCyan(black(' aicommits ')));
@@ -45,6 +46,7 @@ export default async (
OPENAI_KEY: env.OPENAI_KEY || env.OPENAI_API_KEY,
proxy: env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY,
generate: generate?.toString(),
type: commitType?.toString(),
});
const s = spinner();
@@ -58,6 +60,7 @@ export default async (
staged.diff,
config.generate,
config['max-length'],
config.type,
config.timeout,
config.proxy,
);

View File

@@ -46,6 +46,7 @@ export default () => (async () => {
staged!.diff,
config.generate,
config['max-length'],
config.type,
config.timeout,
config.proxy,
);

View File

@@ -6,6 +6,10 @@ import type { TiktokenModel } from '@dqbd/tiktoken';
import { fileExists } from './fs.js';
import { KnownError } from './error.js';
const commitTypes = ['', 'conventional'] as const;
export type CommitType = typeof commitTypes[number];
const { hasOwnProperty } = Object.prototype;
export const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key);
@@ -51,6 +55,15 @@ const configParsers = {
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) {
if (!url || url.length === 0) {
return undefined;
@@ -99,7 +112,7 @@ type RawConfig = {
[key in ConfigKeys]?: string;
};
type ValidConfig = {
export type ValidConfig = {
[Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>;
};

View File

@@ -1,6 +1,6 @@
import https from 'https';
import type { ClientRequest, IncomingMessage } from 'http';
import type { CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
import type { ChatCompletionRequestMessage, CreateChatCompletionRequest, CreateChatCompletionResponse } from 'openai';
import {
TiktokenModel,
// eslint-disable-next-line camelcase
@@ -8,6 +8,7 @@ import {
} from '@dqbd/tiktoken';
import createHttpsProxyAgent from 'https-proxy-agent';
import { KnownError } from './error.js';
import type { CommitType } from './config.js';
const httpsPost = async (
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 getPrompt = (
const getBasePrompt = (
locale: string,
diff: string,
maxLength: number,
) => `${[
'Generate a concise git commit message written in present tense for the following code diff with the given specifications below:',
`Message language: ${locale}`,
`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.',
].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) => {
let result = '';
@@ -139,10 +175,28 @@ export const generateCommitMessage = async (
diff: string,
completions: number,
maxLength: number,
type: CommitType,
timeout: number,
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.
const stringFromLength = generateStringFromLength(maxLength + 5);
@@ -155,10 +209,7 @@ export const generateCommitMessage = async (
apiKey,
{
model,
messages: [{
role: 'user',
content: prompt,
}],
messages,
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,