Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Nutlope committed Feb 16, 2023
2 parents bb30a6c + 0486e5a commit 44e01ee
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 64 deletions.
98 changes: 35 additions & 63 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,54 @@ import chalk from 'chalk';
import inquirer from 'inquirer';
import {
getConfig,
assertGitRepo,
getStagedDiff,
generateCommitMessage,
} from './utils.js';

(async () => {
const config = await getConfig();
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) {
console.error(
`${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'],
);
await assertGitRepo();

if (!diff) {
console.log(
`${chalk.white('▲ ')
}No staged changes found. Make sure there are changes and run \`git add .\``,
);
process.exit(1);
const staged = await getStagedDiff();
if (!staged) {
throw new Error('No staged changes found. Make sure to stage your changes with `git add`.');
}

// 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 config = await getConfig();
const OPENAI_KEY = process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY ?? config.OPENAI_KEY;

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}`;
if (!OPENAI_KEY) {
throw new Error('Please set your OpenAI API key in ~/.aicommits');
}

console.log(
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 aiCommitMessage = await generateCommitMessage(OPENAI_KEY, prompt);
console.log(
`${chalk.white('▲ ') + chalk.bold('Commit message: ') + aiCommitMessage
}\n`,
);

const confirmationMessage = await inquirer.prompt([
{
name: 'useCommitMessage',
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);
const confirmationMessage = await inquirer.prompt([
{
name: 'useCommitMessage',
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.`);
return;
}
})();

await execa('git', ['commit', '-m', aiCommitMessage], {
stdio: 'inherit',
});
})().catch((error) => {
console.error(`${chalk.white('▲')} ${error.message}`);
process.exit(1);
});
48 changes: 47 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import ini from 'ini';
import { execa } from 'execa';
import { Configuration, OpenAIApi } from 'openai';

const fileExists = (filePath: string) => fs.access(filePath).then(() => true, () => false);
Expand All @@ -21,10 +22,27 @@ export const getConfig = async (): Promise<ConfigType> => {
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 (
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 }));
try {
const completion = await openai.createCompletion({
Expand All @@ -46,3 +64,31 @@ export const generateCommitMessage = async (
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,
};
};

0 comments on commit 44e01ee

Please sign in to comment.