* 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
|
## Setup
|
||||||
|
|
||||||
1. Install opencommit globally to use in any repository:
|
1. Install OpenCommit globally to use in any repository:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install -g opencommit
|
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.
|
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
|
```sh
|
||||||
opencommit config set OPENAI_API_KEY=<your_api_key>
|
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
|
## 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
|
```sh
|
||||||
git add <files...>
|
git add <files...>
|
||||||
@@ -86,7 +86,7 @@ oc config set description=false
|
|||||||
|
|
||||||
## Git hook
|
## 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:
|
To set the hook:
|
||||||
|
|
||||||
|
|||||||
Generated
+16
@@ -31,6 +31,7 @@
|
|||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"esbuild": "^0.15.18",
|
"esbuild": "^0.15.18",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
@@ -2497,6 +2498,21 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
|||||||
+3
-1
@@ -42,7 +42,8 @@
|
|||||||
"dev": "ts-node ./src/cli.ts",
|
"dev": "ts-node ./src/cli.ts",
|
||||||
"build": "rimraf out && esbuild ./src/cli.ts --bundle --outfile=out/cli.cjs --format=cjs --platform=node",
|
"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",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/ini": "^1.3.31",
|
"@types/ini": "^1.3.31",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"esbuild": "^0.15.18",
|
"esbuild": "^0.15.18",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
},
|
},
|
||||||
|
|||||||
+67
-27
@@ -3,9 +3,23 @@ import {
|
|||||||
GenerateCommitMessageErrorEnum,
|
GenerateCommitMessageErrorEnum,
|
||||||
generateCommitMessageWithChatCompletion
|
generateCommitMessageWithChatCompletion
|
||||||
} from '../generateCommitMessageFromGitDiff';
|
} from '../generateCommitMessageFromGitDiff';
|
||||||
import { assertGitRepo, getStagedGitDiff } from '../utils/git';
|
import {
|
||||||
import { spinner, confirm, outro, isCancel, intro } from '@clack/prompts';
|
assertGitRepo,
|
||||||
|
getChangedFiles,
|
||||||
|
getDiff,
|
||||||
|
getStagedFiles,
|
||||||
|
gitAdd
|
||||||
|
} from '../utils/git';
|
||||||
|
import {
|
||||||
|
spinner,
|
||||||
|
confirm,
|
||||||
|
outro,
|
||||||
|
isCancel,
|
||||||
|
intro,
|
||||||
|
multiselect
|
||||||
|
} from '@clack/prompts';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { trytm } from '../utils/trytm';
|
||||||
|
|
||||||
const generateCommitMessageFromGitDiff = async (
|
const generateCommitMessageFromGitDiff = async (
|
||||||
diff: string
|
diff: string
|
||||||
@@ -46,8 +60,11 @@ ${chalk.grey('——————————————————')}`
|
|||||||
|
|
||||||
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
if (isCommitConfirmedByUser && !isCancel(isCommitConfirmedByUser)) {
|
||||||
const { stdout } = await execa('git', ['commit', '-m', commitMessage]);
|
const { stdout } = await execa('git', ['commit', '-m', commitMessage]);
|
||||||
|
|
||||||
outro(`${chalk.green('✔')} successfully committed`);
|
outro(`${chalk.green('✔')} successfully committed`);
|
||||||
|
|
||||||
outro(stdout);
|
outro(stdout);
|
||||||
|
|
||||||
const isPushConfirmedByUser = await confirm({
|
const isPushConfirmedByUser = await confirm({
|
||||||
message: 'Do you want to run `git push`?'
|
message: 'Do you want to run `git push`?'
|
||||||
});
|
});
|
||||||
@@ -65,35 +82,33 @@ ${chalk.grey('——————————————————')}`
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function commit(isStageAllFlag = false) {
|
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();
|
const [stagedFiles, errorStagedFiles] = await trytm(getStagedFiles());
|
||||||
stagedFilesSpinner.start('Counting staged files');
|
const [changedFiles, errorChangedFiles] = await trytm(getChangedFiles());
|
||||||
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.`
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (!changedFiles?.length && !stagedFiles?.length) {
|
||||||
|
outro(chalk.red('No changes detected'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!staged) {
|
intro('open-commit');
|
||||||
outro(
|
if (errorChangedFiles ?? errorStagedFiles) {
|
||||||
`${chalk.red('Nothing to commit')} — stage the files ${chalk
|
outro(`${chalk.red('✖')} ${errorChangedFiles ?? errorStagedFiles}`);
|
||||||
.hex('0000FF')
|
process.exit(1);
|
||||||
.bold('`git add .`')} and rerun ${chalk
|
}
|
||||||
.hex('0000FF')
|
|
||||||
.bold('`oc`')} command.`
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const stagedFilesSpinner = spinner();
|
||||||
|
stagedFilesSpinner.start('Counting staged files');
|
||||||
|
|
||||||
|
if (!stagedFiles.length) {
|
||||||
stagedFilesSpinner.stop('No files are staged');
|
stagedFilesSpinner.stop('No files are staged');
|
||||||
const isStageAllAndCommitConfirmedByUser = await confirm({
|
const isStageAllAndCommitConfirmedByUser = await confirm({
|
||||||
message: 'Do you want to stage all files and generate commit message?'
|
message: 'Do you want to stage all files and generate commit message?'
|
||||||
@@ -104,16 +119,41 @@ export async function commit(isStageAllFlag = false) {
|
|||||||
!isCancel(isStageAllAndCommitConfirmedByUser)
|
!isCancel(isStageAllAndCommitConfirmedByUser)
|
||||||
) {
|
) {
|
||||||
await commit(true);
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
stagedFilesSpinner.stop(
|
stagedFilesSpinner.stop(
|
||||||
`${staged.files.length} staged files:\n${staged.files
|
`${stagedFiles.length} staged files:\n${stagedFiles
|
||||||
.map((file) => ` ${file}`)
|
.map((file) => ` ${file}`)
|
||||||
.join('\n')}`
|
.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 fs from 'fs/promises';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { intro, outro } from '@clack/prompts';
|
import { intro, outro } from '@clack/prompts';
|
||||||
import { getStagedGitDiff } from '../utils/git';
|
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
|
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
|
||||||
|
|
||||||
@@ -17,7 +17,14 @@ export const prepareCommitMessageHook = async () => {
|
|||||||
|
|
||||||
if (commitSource) return;
|
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;
|
if (!staged) return;
|
||||||
|
|
||||||
@@ -32,7 +39,7 @@ export const prepareCommitMessageHook = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const commitMessage = await generateCommitMessageWithChatCompletion(
|
const commitMessage = await generateCommitMessageWithChatCompletion(
|
||||||
staged.diff
|
await getDiff({ files: staged })
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
|
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);
|
||||||
|
|||||||
+55
-30
@@ -1,5 +1,5 @@
|
|||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { spinner } from '@clack/prompts';
|
import { outro, spinner } from '@clack/prompts';
|
||||||
|
|
||||||
export const assertGitRepo = async () => {
|
export const assertGitRepo = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -9,41 +9,66 @@ export const assertGitRepo = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map(
|
// const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map(
|
||||||
(file) => `:(exclude)${file}`
|
// (file) => `:(exclude)${file}`
|
||||||
);
|
// );
|
||||||
|
|
||||||
export interface StagedDiff {
|
export const getStagedFiles = async (): Promise<string[]> => {
|
||||||
files: string[];
|
const { stdout: files } = await execa('git', [
|
||||||
diff: string;
|
'diff',
|
||||||
}
|
'--name-only',
|
||||||
|
'--cached'
|
||||||
|
]);
|
||||||
|
|
||||||
export const getStagedGitDiff = async (
|
if (!files) return [];
|
||||||
isStageAllFlag = false
|
|
||||||
): Promise<StagedDiff | null> => {
|
return files.split('\n').sort();
|
||||||
if (isStageAllFlag) {
|
};
|
||||||
const stageAllSpinner = spinner();
|
|
||||||
stageAllSpinner.start('Staging all changes');
|
export const getChangedFiles = async (): Promise<string[]> => {
|
||||||
await execa('git', ['add', '.']);
|
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
|
||||||
stageAllSpinner.stop('Done');
|
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 filesWithoutLocks = files.filter(
|
||||||
const { stdout: files } = await execa('git', [
|
(file) => !file.includes('.lock') && !file.includes('-lock.')
|
||||||
...diffStaged,
|
);
|
||||||
'--name-only',
|
|
||||||
...excludeBigFilesFromDiff
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!files) return null;
|
|
||||||
|
|
||||||
const { stdout: diff } = await execa('git', [
|
const { stdout: diff } = await execa('git', [
|
||||||
...diffStaged,
|
'diff',
|
||||||
...excludeBigFilesFromDiff
|
'--staged',
|
||||||
|
...filesWithoutLocks
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return diff;
|
||||||
files: files.split('\n').sort(),
|
|
||||||
diff
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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