GitHub action (#173)

* add option to run OpenCommit as a Github Action
This commit is contained in:
Sukharev
2023-05-21 16:15:45 +08:00
committed by GitHub
parent 54006826f8
commit 3eb319a919
17 changed files with 50424 additions and 217 deletions
+73 -78
View File
@@ -1,7 +1,7 @@
import { execa } from 'execa';
import {
GenerateCommitMessageErrorEnum,
generateCommitMessageWithChatCompletion
generateCommitMessageByDiff
} from '../generateCommitMessageFromGitDiff';
import {
assertGitRepo,
@@ -35,103 +35,98 @@ const generateCommitMessageFromGitDiff = async (
const commitSpinner = spinner();
commitSpinner.start('Generating the commit message');
const commitMessage = await generateCommitMessageWithChatCompletion(diff);
try {
const commitMessage = await generateCommitMessageByDiff(diff);
// TODO: show proper error messages
if (typeof commitMessage !== 'string') {
const errorMessages = {
[GenerateCommitMessageErrorEnum.emptyMessage]:
'empty openAI response, weird, try again',
[GenerateCommitMessageErrorEnum.internalError]:
'internal error, try again',
[GenerateCommitMessageErrorEnum.tooMuchTokens]:
'too much tokens in git diff, stage and commit files in parts'
};
commitSpinner.stop('📝 Commit message generated');
outro(`${chalk.red('✖')} ${errorMessages[commitMessage.error]}`);
process.exit(1);
}
commitSpinner.stop('📝 Commit message generated');
outro(
`Commit message:
outro(
`Commit message:
${chalk.grey('——————————————————')}
${commitMessage}
${chalk.grey('——————————————————')}`
);
);
const isCommitConfirmedByUser = await confirm({
message: 'Confirm the commit message?'
});
const isCommitConfirmedByUser = await confirm({
message: 'Confirm the commit message?'
});
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const { stdout } = await execa('git', [
'commit',
'-m',
commitMessage,
...extraArgs
]);
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
const { stdout } = await execa('git', [
'commit',
'-m',
commitMessage,
...extraArgs
]);
outro(`${chalk.green('✔')} Successfully committed`);
outro(`${chalk.green('✔')} Successfully committed`);
outro(stdout);
outro(stdout);
const remotes = await getGitRemotes();
if (!remotes.length) {
const { stdout } = await execa('git', ['push']);
if (stdout) outro(stdout);
process.exit(0);
}
if (remotes.length === 1) {
const isPushConfirmedByUser = await confirm({
message: 'Do you want to run `git push`?'
});
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
const pushSpinner = spinner();
pushSpinner.start(`Running \`git push ${remotes[0]}\``);
const { stdout } = await execa('git', [
'push',
'--verbose',
remotes[0]
]);
pushSpinner.stop(
`${chalk.green('✔')} Successfully pushed all commits to ${remotes[0]}`
);
const remotes = await getGitRemotes();
if (!remotes.length) {
const { stdout } = await execa('git', ['push']);
if (stdout) outro(stdout);
} else {
outro('`git push` aborted');
process.exit(0);
}
} else {
const selectedRemote = (await select({
message: 'Choose a remote to push to',
options: remotes.map((remote) => ({ value: remote, label: remote }))
})) as string;
if (!isCancel(selectedRemote)) {
const pushSpinner = spinner();
if (remotes.length === 1) {
const isPushConfirmedByUser = await confirm({
message: 'Do you want to run `git push`?'
});
pushSpinner.start(`Running \`git push ${selectedRemote}\``);
if (isPushConfirmedByUser && !isCancel(isPushConfirmedByUser)) {
const pushSpinner = spinner();
const { stdout } = await execa('git', ['push', selectedRemote]);
pushSpinner.start(`Running \`git push ${remotes[0]}\``);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
const { stdout } = await execa('git', [
'push',
'--verbose',
remotes[0]
]);
if (stdout) outro(stdout);
} else outro(`${chalk.gray('')} process cancelled`);
pushSpinner.stop(
`${chalk.green('')} Successfully pushed all commits to ${
remotes[0]
}`
);
if (stdout) outro(stdout);
} else {
outro('`git push` aborted');
process.exit(0);
}
} else {
const selectedRemote = (await select({
message: 'Choose a remote to push to',
options: remotes.map((remote) => ({ value: remote, label: remote }))
})) as string;
if (!isCancel(selectedRemote)) {
const pushSpinner = spinner();
pushSpinner.start(`Running \`git push ${selectedRemote}\``);
const { stdout } = await execa('git', ['push', selectedRemote]);
pushSpinner.stop(
`${chalk.green(
'✔'
)} Successfully pushed all commits to ${selectedRemote}`
);
if (stdout) outro(stdout);
} else outro(`${chalk.gray('✖')} process cancelled`);
}
}
} catch (error) {
commitSpinner.stop('📝 Commit message generated');
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
process.exit(1);
}
};
+54 -29
View File
@@ -8,14 +8,18 @@ import chalk from 'chalk';
import { COMMANDS } from '../CommandsEnum';
import { getI18nLocal } from '../i18n';
import * as dotenv from 'dotenv';
dotenv.config();
export enum CONFIG_KEYS {
OPENAI_API_KEY = 'OPENAI_API_KEY',
OPENAI_MAX_TOKENS = 'OPENAI_MAX_TOKENS',
OPENAI_BASE_PATH = 'OPENAI_BASE_PATH',
description = 'description',
emoji = 'emoji',
model = 'model',
language = 'language'
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
OCO_OPENAI_MAX_TOKENS = 'OCO_OPENAI_MAX_TOKENS',
OCO_OPENAI_BASE_PATH = 'OCO_OPENAI_BASE_PATH',
OCO_DESCRIPTION = 'OCO_DESCRIPTION',
OCO_EMOJI = 'OCO_EMOJI',
OCO_MODEL = 'OCO_MODEL',
OCO_LANGUAGE = 'OCO_LANGUAGE'
}
export enum CONFIG_MODES {
@@ -38,15 +42,15 @@ const validateConfig = (
};
export const configValidators = {
[CONFIG_KEYS.OPENAI_API_KEY](value: any) {
validateConfig(CONFIG_KEYS.OPENAI_API_KEY, value, 'Cannot be empty');
[CONFIG_KEYS.OCO_OPENAI_API_KEY](value: any) {
validateConfig(CONFIG_KEYS.OCO_OPENAI_API_KEY, value, 'Cannot be empty');
validateConfig(
CONFIG_KEYS.OPENAI_API_KEY,
CONFIG_KEYS.OCO_OPENAI_API_KEY,
value.startsWith('sk-'),
'Must start with "sk-"'
);
validateConfig(
CONFIG_KEYS.OPENAI_API_KEY,
CONFIG_KEYS.OCO_OPENAI_API_KEY,
value.length === 51,
'Must be 51 characters long'
);
@@ -54,9 +58,9 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.description](value: any) {
[CONFIG_KEYS.OCO_DESCRIPTION](value: any) {
validateConfig(
CONFIG_KEYS.description,
CONFIG_KEYS.OCO_DESCRIPTION,
typeof value === 'boolean',
'Must be true or false'
);
@@ -64,18 +68,18 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.OPENAI_MAX_TOKENS](value: any) {
[CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS](value: any) {
// If the value is a string, convert it to a number.
if (typeof value === 'string') {
value = parseInt(value);
validateConfig(
CONFIG_KEYS.OPENAI_MAX_TOKENS,
CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS,
!isNaN(value),
'Must be a number'
);
}
validateConfig(
CONFIG_KEYS.OPENAI_MAX_TOKENS,
CONFIG_KEYS.OCO_OPENAI_MAX_TOKENS,
typeof value === 'number',
'Must be a number'
);
@@ -83,9 +87,9 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.emoji](value: any) {
[CONFIG_KEYS.OCO_EMOJI](value: any) {
validateConfig(
CONFIG_KEYS.emoji,
CONFIG_KEYS.OCO_EMOJI,
typeof value === 'boolean',
'Must be true or false'
);
@@ -93,27 +97,27 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.language](value: any) {
[CONFIG_KEYS.OCO_LANGUAGE](value: any) {
validateConfig(
CONFIG_KEYS.language,
CONFIG_KEYS.OCO_LANGUAGE,
getI18nLocal(value),
`${value} is not supported yet`
);
return getI18nLocal(value);
},
[CONFIG_KEYS.OPENAI_BASE_PATH](value: any) {
[CONFIG_KEYS.OCO_OPENAI_BASE_PATH](value: any) {
validateConfig(
CONFIG_KEYS.OPENAI_BASE_PATH,
CONFIG_KEYS.OCO_OPENAI_BASE_PATH,
typeof value === 'string',
'Must be string'
);
return value;
},
[CONFIG_KEYS.model](value: any) {
[CONFIG_KEYS.OCO_MODEL](value: any) {
validateConfig(
CONFIG_KEYS.OPENAI_BASE_PATH,
CONFIG_KEYS.OCO_OPENAI_BASE_PATH,
value === 'gpt-3.5-turbo' || value === 'gpt-4',
`${value} is not supported yet, use 'gpt-4' or 'gpt-3.5-turbo' (default)`
);
@@ -128,18 +132,39 @@ export type ConfigType = {
const configPath = pathJoin(homedir(), '.opencommit');
export const getConfig = (): ConfigType | null => {
const configFromEnv = {
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
OCO_OPENAI_MAX_TOKENS: Number(process.env.OCO_OPENAI_MAX_TOKENS),
OCO_OPENAI_BASE_PATH: process.env.OCO_OPENAI_BASE_PATH,
OCO_DESCRIPTION: process.env.OCO_DESCRIPTION === 'true' ? true : false,
OCO_EMOJI: process.env.OCO_EMOJI === 'true' ? true : false,
OCO_MODEL: process.env.OCO_MODEL,
OCO_LANGUAGE: process.env.OCO_LANGUAGE
};
const configExists = existsSync(configPath);
if (!configExists) return null;
if (!configExists) return configFromEnv;
const configFile = readFileSync(configPath, 'utf8');
const config = iniParse(configFile);
for (const configKey of Object.keys(config)) {
const validValue = configValidators[configKey as CONFIG_KEYS](
config[configKey]
);
try {
const validator = configValidators[configKey as CONFIG_KEYS];
const validValue = validator(
config[configKey] ?? configFromEnv[configKey as CONFIG_KEYS]
);
config[configKey] = validValue;
config[configKey] = validValue;
} catch (error) {
outro(
`'${configKey}' name is invalid, it should be either 'OCO_${configKey.toUpperCase()}' or it doesn't exist.`
);
outro(
`Manually fix the '.env' file or global '~/.opencommit' config file.`
);
process.exit(1);
}
}
return config;
+5 -7
View File
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import { intro, outro, spinner } from '@clack/prompts';
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
import { getConfig } from './config';
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
import { generateCommitMessageByDiff } from '../generateCommitMessageFromGitDiff';
const [messageFilePath, commitSource] = process.argv.slice(2);
@@ -37,7 +37,7 @@ export const prepareCommitMessageHook = async (
const config = getConfig();
if (!config?.OPENAI_API_KEY) {
if (!config?.OCO_OPENAI_API_KEY) {
throw new Error(
'No OPEN_AI_API exists. Set your OPEN_AI_API=<key> in ~/.opencommit'
);
@@ -45,13 +45,11 @@ export const prepareCommitMessageHook = async (
const spin = spinner();
spin.start('Generating commit message');
const commitMessage = await generateCommitMessageWithChatCompletion(
const commitMessage = await generateCommitMessageByDiff(
await getDiff({ files: staged })
);
if (typeof commitMessage !== 'string') {
spin.stop('Error');
throw new Error(commitMessage.error);
} else spin.stop('Done');
spin.stop('Done');
const fileContent = await fs.readFile(messageFilePath);