Merge branch 'main' of https://github.com/Nutlope/aicommits
This commit is contained in:
98
src/cli.ts
98
src/cli.ts
@@ -3,82 +3,54 @@ import chalk from 'chalk';
|
|||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import {
|
import {
|
||||||
getConfig,
|
getConfig,
|
||||||
|
assertGitRepo,
|
||||||
|
getStagedDiff,
|
||||||
generateCommitMessage,
|
generateCommitMessage,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
console.log(chalk.white('▲ ') + chalk.green('Welcome to AICommits!'));
|
||||||
|
|
||||||
|
await assertGitRepo();
|
||||||
|
|
||||||
|
const staged = await getStagedDiff();
|
||||||
|
if (!staged) {
|
||||||
|
throw new Error('No staged changes found. Make sure to stage your changes with `git add`.');
|
||||||
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const OPENAI_KEY = process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY ?? config.OPENAI_KEY;
|
const OPENAI_KEY = process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY ?? config.OPENAI_KEY;
|
||||||
|
|
||||||
console.log(chalk.white('▲ ') + chalk.green('Welcome to AICommits!'));
|
|
||||||
|
|
||||||
if (!OPENAI_KEY) {
|
if (!OPENAI_KEY) {
|
||||||
console.error(
|
throw new Error('Please set your OpenAI API key in ~/.aicommits');
|
||||||
`${chalk.white('▲ ')
|
|
||||||
}Please save your OpenAI API key as an env variable by doing 'export OPENAI_KEY=YOUR_API_KEY'`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await execa('git', ['rev-parse', '--is-inside-work-tree']);
|
|
||||||
} catch {
|
|
||||||
console.error(`${chalk.white('▲ ')}This is not a git repository`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout: diff } = await execa(
|
|
||||||
'git',
|
|
||||||
['diff', '--cached', '.', ':(exclude)package-lock.json', ':(exclude)yarn.lock', ':(exclude)pnpm-lock.yaml'],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!diff) {
|
|
||||||
console.log(
|
|
||||||
`${chalk.white('▲ ')
|
|
||||||
}No staged changes found. Make sure there are changes and run \`git add .\``,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accounting for GPT-3's input req of 4k tokens (approx 8k chars)
|
|
||||||
if (diff.length > 8000) {
|
|
||||||
console.log(
|
|
||||||
`${chalk.white('▲ ')}The diff is too large to write a commit message.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = `I want you to act like a git commit message writer. I will input a git diff and your job is to convert it into a useful commit message. Do not preface the commit with anything, use the present tense, return a complete sentence, and do not repeat yourself: ${diff}`;
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.white('▲ ') + chalk.gray('Generating your AI commit message...\n'),
|
chalk.white('▲ ') + chalk.gray('Generating your AI commit message...\n'),
|
||||||
);
|
);
|
||||||
|
const aiCommitMessage = await generateCommitMessage(OPENAI_KEY, staged.diff);
|
||||||
|
console.log(
|
||||||
|
`${chalk.white('▲')} ${chalk.bold('Commit message:')} ${aiCommitMessage}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
const confirmationMessage = await inquirer.prompt([
|
||||||
const aiCommitMessage = await generateCommitMessage(OPENAI_KEY, prompt);
|
{
|
||||||
console.log(
|
name: 'useCommitMessage',
|
||||||
`${chalk.white('▲ ') + chalk.bold('Commit message: ') + aiCommitMessage
|
message: 'Would you like to use this commit message? (Y / n)',
|
||||||
}\n`,
|
choices: ['Y', 'y', 'n'],
|
||||||
);
|
default: 'y',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const confirmationMessage = await inquirer.prompt([
|
if (confirmationMessage.useCommitMessage === 'n') {
|
||||||
{
|
console.log(`${chalk.white('▲ ')}Commit message has not been commited.`);
|
||||||
name: 'useCommitMessage',
|
return;
|
||||||
message: 'Would you like to use this commit message? (Y / n)',
|
|
||||||
choices: ['Y', 'y', 'n'],
|
|
||||||
default: 'y',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (confirmationMessage.useCommitMessage === 'n') {
|
|
||||||
console.log(`${chalk.white('▲ ')}Commit message has not been commited.`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await execa('git', ['commit', '-m', aiCommitMessage], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(chalk.white('▲ ') + chalk.red((error as any).message));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
await execa('git', ['commit', '-m', aiCommitMessage], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
})().catch((error) => {
|
||||||
|
console.error(`${chalk.white('▲')} ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
48
src/utils.ts
48
src/utils.ts
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import ini from 'ini';
|
import ini from 'ini';
|
||||||
|
import { execa } from 'execa';
|
||||||
import { Configuration, OpenAIApi } from 'openai';
|
import { Configuration, OpenAIApi } from 'openai';
|
||||||
|
|
||||||
const fileExists = (filePath: string) => fs.access(filePath).then(() => true, () => false);
|
const fileExists = (filePath: string) => fs.access(filePath).then(() => true, () => false);
|
||||||
@@ -21,10 +22,27 @@ export const getConfig = async (): Promise<ConfigType> => {
|
|||||||
return ini.parse(configString);
|
return ini.parse(configString);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const assertGitRepo = async () => {
|
||||||
|
const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], { reject: false });
|
||||||
|
|
||||||
|
if (stdout !== 'true') {
|
||||||
|
throw new Error('The current directory must be a Git repository!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const promptTemplate = 'I want you to act like a git commit message writer. I will input a git diff and your job is to convert it into a useful commit message. Do not preface the commit with anything, use the present tense, return a complete sentence, and do not repeat yourself:';
|
||||||
|
|
||||||
export const generateCommitMessage = async (
|
export const generateCommitMessage = async (
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
prompt: string,
|
diff: string,
|
||||||
) => {
|
) => {
|
||||||
|
const prompt = `${promptTemplate}\n${diff}`;
|
||||||
|
|
||||||
|
// Accounting for GPT-3's input req of 4k tokens (approx 8k chars)
|
||||||
|
if (prompt.length > 8000) {
|
||||||
|
throw new Error('The diff is too large for the OpenAI API');
|
||||||
|
}
|
||||||
|
|
||||||
const openai = new OpenAIApi(new Configuration({ apiKey }));
|
const openai = new OpenAIApi(new Configuration({ apiKey }));
|
||||||
try {
|
try {
|
||||||
const completion = await openai.createCompletion({
|
const completion = await openai.createCompletion({
|
||||||
@@ -46,3 +64,31 @@ export const generateCommitMessage = async (
|
|||||||
throw errorAsAny;
|
throw errorAsAny;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const excludeFromDiff = [
|
||||||
|
'package-lock.json',
|
||||||
|
'yarn.lock',
|
||||||
|
'pnpm-lock.yaml',
|
||||||
|
].map(file => `:(exclude)${file}`);
|
||||||
|
|
||||||
|
export const getStagedDiff = async () => {
|
||||||
|
const diffCached = ['diff', '--cached'];
|
||||||
|
const { stdout: files } = await execa(
|
||||||
|
'git',
|
||||||
|
[...diffCached, '--name-only', ...excludeFromDiff],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout: diff } = await execa(
|
||||||
|
'git',
|
||||||
|
[...diffCached, ...excludeFromDiff],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: files.split('\n'),
|
||||||
|
diff,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user