mirror of
https://github.com/di-sukharev/opencommit.git
synced 2026-04-20 03:02:51 -04:00
* feat: add staged files multiple selection (#6)
This commit is contained in:
@@ -20,7 +20,7 @@ All the commits in this repo are done with OpenCommit — look into [the commits
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install opencommit globally to use in any repository:
|
||||
1. Install OpenCommit globally to use in any repository:
|
||||
|
||||
```sh
|
||||
npm install -g opencommit
|
||||
@@ -28,7 +28,7 @@ All the commits in this repo are done with OpenCommit — look into [the commits
|
||||
|
||||
2. Get your API key from [OpenAI](https://platform.openai.com/account/api-keys). Make sure you add payment details, so API works.
|
||||
|
||||
3. Set the key to opencommit config:
|
||||
3. Set the key to OpenCommit config:
|
||||
|
||||
```sh
|
||||
opencommit config set OPENAI_API_KEY=<your_api_key>
|
||||
@@ -38,7 +38,7 @@ All the commits in this repo are done with OpenCommit — look into [the commits
|
||||
|
||||
## Usage
|
||||
|
||||
You can call `opencommit` directly to generate a commit message for your staged changes:
|
||||
You can call OpenCommit directly to generate a commit message for your staged changes:
|
||||
|
||||
```sh
|
||||
git add <files...>
|
||||
@@ -86,7 +86,7 @@ oc config set description=false
|
||||
|
||||
## Git hook
|
||||
|
||||
You can set opencommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with you IDE Source Control and allows you edit the message before commit.
|
||||
You can set OpenCommit as Git [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) hook. Hook integrates with you IDE Source Control and allows you edit the message before commit.
|
||||
|
||||
To set the hook:
|
||||
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.28.0",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
@@ -2497,6 +2498,21 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
|
||||
"integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"dev": "ts-node ./src/cli.ts",
|
||||
"build": "rimraf out && esbuild ./src/cli.ts --bundle --outfile=out/cli.cjs --format=cjs --platform=node",
|
||||
"deploy": "npm run build && npm version patch && npm publish --tag latest",
|
||||
"lint": "eslint src --ext ts && tsc --noEmit"
|
||||
"lint": "eslint src --ext ts && tsc --noEmit",
|
||||
"format": "prettier --write src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ini": "^1.3.31",
|
||||
@@ -53,6 +54,7 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.28.0",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
|
||||
@@ -3,9 +3,23 @@ import {
|
||||
GenerateCommitMessageErrorEnum,
|
||||
generateCommitMessageWithChatCompletion
|
||||
} from '../generateCommitMessageFromGitDiff';
|
||||
import { assertGitRepo, getStagedGitDiff } from '../utils/git';
|
||||
import { spinner, confirm, outro, isCancel, intro } from '@clack/prompts';
|
||||
import {
|
||||
assertGitRepo,
|
||||
getChangedFiles,
|
||||
getDiff,
|
||||
getStagedFiles,
|
||||
gitAdd
|
||||
} from '../utils/git';
|
||||
import {
|
||||
spinner,
|
||||
confirm,
|
||||
outro,
|
||||
isCancel,
|
||||
intro,
|
||||
multiselect
|
||||
} from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { trytm } from '../utils/trytm';
|
||||
|
||||
const generateCommitMessageFromGitDiff = async (
|
||||
diff: string
|
||||
@@ -46,8 +60,11 @@ ${chalk.grey('——————————————————')}`
|
||||
|
||||
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
||||
const { stdout } = await execa('git', ['commit', '-m', commitMessage]);
|
||||
|
||||
outro(`${chalk.green('✔')} successfully committed`);
|
||||
|
||||
outro(stdout);
|
||||
|
||||
const isPushConfirmedByUser = await confirm({
|
||||
message: 'Do you want to run `git push`?'
|
||||
});
|
||||
@@ -65,35 +82,33 @@ ${chalk.grey('——————————————————')}`
|
||||
};
|
||||
|
||||
export async function commit(isStageAllFlag = false) {
|
||||
intro('open-commit');
|
||||
if (isStageAllFlag) {
|
||||
const changedFiles = await getChangedFiles();
|
||||
if (changedFiles) await gitAdd({ files: changedFiles });
|
||||
else {
|
||||
outro('No changes detected, write some code and run `oc` again');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const stagedFilesSpinner = spinner();
|
||||
stagedFilesSpinner.start('Counting staged files');
|
||||
const staged = await getStagedGitDiff(isStageAllFlag);
|
||||
|
||||
if (!staged && isStageAllFlag) {
|
||||
outro(
|
||||
`${chalk.red(
|
||||
'No changes detected'
|
||||
)} — write some code, stage the files ${chalk
|
||||
.hex('0000FF')
|
||||
.bold('`git add .`')} and rerun ${chalk
|
||||
.hex('0000FF')
|
||||
.bold('`oc`')} command.`
|
||||
);
|
||||
const [stagedFiles, errorStagedFiles] = await trytm(getStagedFiles());
|
||||
const [changedFiles, errorChangedFiles] = await trytm(getChangedFiles());
|
||||
|
||||
if (!changedFiles?.length && !stagedFiles?.length) {
|
||||
outro(chalk.red('No changes detected'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!staged) {
|
||||
outro(
|
||||
`${chalk.red('Nothing to commit')} — stage the files ${chalk
|
||||
.hex('0000FF')
|
||||
.bold('`git add .`')} and rerun ${chalk
|
||||
.hex('0000FF')
|
||||
.bold('`oc`')} command.`
|
||||
);
|
||||
intro('open-commit');
|
||||
if (errorChangedFiles ?? errorStagedFiles) {
|
||||
outro(`${chalk.red('✖')} ${errorChangedFiles ?? errorStagedFiles}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const stagedFilesSpinner = spinner();
|
||||
stagedFilesSpinner.start('Counting staged files');
|
||||
|
||||
if (!stagedFiles.length) {
|
||||
stagedFilesSpinner.stop('No files are staged');
|
||||
const isStageAllAndCommitConfirmedByUser = await confirm({
|
||||
message: 'Do you want to stage all files and generate commit message?'
|
||||
@@ -104,16 +119,41 @@ export async function commit(isStageAllFlag = false) {
|
||||
!isCancel(isStageAllAndCommitConfirmedByUser)
|
||||
) {
|
||||
await commit(true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (stagedFiles.length === 0 && changedFiles.length > 0) {
|
||||
const files = (await multiselect({
|
||||
message: chalk.cyan('Select the files you want to add to the commit:'),
|
||||
options: changedFiles.map((file) => ({
|
||||
value: file,
|
||||
label: file
|
||||
}))
|
||||
})) as string[];
|
||||
|
||||
if (isCancel(files)) process.exit(1);
|
||||
|
||||
await gitAdd({ files });
|
||||
}
|
||||
|
||||
await commit(false);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
stagedFilesSpinner.stop(
|
||||
`${staged.files.length} staged files:\n${staged.files
|
||||
`${stagedFiles.length} staged files:\n${stagedFiles
|
||||
.map((file) => ` ${file}`)
|
||||
.join('\n')}`
|
||||
);
|
||||
|
||||
await generateCommitMessageFromGitDiff(staged.diff);
|
||||
const [, generateCommitError] = await trytm(
|
||||
generateCommitMessageFromGitDiff(await getDiff({ files: stagedFiles }))
|
||||
);
|
||||
|
||||
if (generateCommitError) {
|
||||
outro(`${chalk.red('✖')} ${generateCommitError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { intro, outro } from '@clack/prompts';
|
||||
import { getStagedGitDiff } from '../utils/git';
|
||||
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
||||
import { getConfig } from './config';
|
||||
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
|
||||
|
||||
@@ -17,7 +17,14 @@ export const prepareCommitMessageHook = async () => {
|
||||
|
||||
if (commitSource) return;
|
||||
|
||||
const staged = await getStagedGitDiff();
|
||||
const changedFiles = await getChangedFiles();
|
||||
if (changedFiles) await gitAdd({ files: changedFiles });
|
||||
else {
|
||||
outro("No changes detected, write some code and run `oc` again");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const staged = await getStagedFiles();
|
||||
|
||||
if (!staged) return;
|
||||
|
||||
@@ -32,7 +39,7 @@ export const prepareCommitMessageHook = async () => {
|
||||
}
|
||||
|
||||
const commitMessage = await generateCommitMessageWithChatCompletion(
|
||||
staged.diff
|
||||
await getDiff({ files: staged })
|
||||
);
|
||||
|
||||
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execa } from 'execa';
|
||||
import { spinner } from '@clack/prompts';
|
||||
import { outro, spinner } from '@clack/prompts';
|
||||
|
||||
export const assertGitRepo = async () => {
|
||||
try {
|
||||
@@ -9,41 +9,66 @@ export const assertGitRepo = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map(
|
||||
(file) => `:(exclude)${file}`
|
||||
);
|
||||
// const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map(
|
||||
// (file) => `:(exclude)${file}`
|
||||
// );
|
||||
|
||||
export interface StagedDiff {
|
||||
files: string[];
|
||||
diff: string;
|
||||
}
|
||||
export const getStagedFiles = async (): Promise<string[]> => {
|
||||
const { stdout: files } = await execa('git', [
|
||||
'diff',
|
||||
'--name-only',
|
||||
'--cached'
|
||||
]);
|
||||
|
||||
export const getStagedGitDiff = async (
|
||||
isStageAllFlag = false
|
||||
): Promise<StagedDiff | null> => {
|
||||
if (isStageAllFlag) {
|
||||
const stageAllSpinner = spinner();
|
||||
stageAllSpinner.start('Staging all changes');
|
||||
await execa('git', ['add', '.']);
|
||||
stageAllSpinner.stop('Done');
|
||||
if (!files) return [];
|
||||
|
||||
return files.split('\n').sort();
|
||||
};
|
||||
|
||||
export const getChangedFiles = async (): Promise<string[]> => {
|
||||
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
|
||||
const { stdout: others } = await execa('git', [
|
||||
'ls-files',
|
||||
'--others',
|
||||
'--exclude-standard'
|
||||
]);
|
||||
|
||||
const files = [...modified.split('\n'), ...others.split('\n')].filter(
|
||||
(file) => !!file
|
||||
);
|
||||
|
||||
return files.sort();
|
||||
};
|
||||
|
||||
export const gitAdd = async ({ files }: { files: string[] }) => {
|
||||
const gitAddSpinner = spinner();
|
||||
gitAddSpinner.start('Adding files to commit');
|
||||
await execa('git', ['add', ...files]);
|
||||
gitAddSpinner.stop('Done');
|
||||
};
|
||||
|
||||
export const getDiff = async ({ files }: { files: string[] }) => {
|
||||
const lockFiles = files.filter(
|
||||
(file) => file.includes('.lock') || file.includes('-lock.')
|
||||
);
|
||||
|
||||
if (lockFiles.length) {
|
||||
outro(
|
||||
`Some files are '.lock' files which are excluded by default from 'git diff'. No commit messages are generated for this files:\n${lockFiles.join(
|
||||
'\n'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const diffStaged = ['diff', '--staged'];
|
||||
const { stdout: files } = await execa('git', [
|
||||
...diffStaged,
|
||||
'--name-only',
|
||||
...excludeBigFilesFromDiff
|
||||
]);
|
||||
|
||||
if (!files) return null;
|
||||
const filesWithoutLocks = files.filter(
|
||||
(file) => !file.includes('.lock') && !file.includes('-lock.')
|
||||
);
|
||||
|
||||
const { stdout: diff } = await execa('git', [
|
||||
...diffStaged,
|
||||
...excludeBigFilesFromDiff
|
||||
'diff',
|
||||
'--staged',
|
||||
...filesWithoutLocks
|
||||
]);
|
||||
|
||||
return {
|
||||
files: files.split('\n').sort(),
|
||||
diff
|
||||
};
|
||||
return diff;
|
||||
};
|
||||
|
||||
12
src/utils/trytm.ts
Normal file
12
src/utils/trytm.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const trytm = async <T>(
|
||||
promise: Promise<T>
|
||||
): Promise<[T, null] | [null, Error]> => {
|
||||
try {
|
||||
const data = await promise;
|
||||
return [data, null];
|
||||
} catch (throwable) {
|
||||
if (throwable instanceof Error) return [null, throwable];
|
||||
|
||||
throw throwable;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user