+73
-78
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user