diff --git a/package-lock.json b/package-lock.json index 4a24ce2..729f0f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } @@ -60,6 +61,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2497,6 +2499,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", diff --git a/package.json b/package.json index 52c3beb..7c238ac 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/commands/commit.ts b/src/commands/commit.ts index e791b32..a48ff6e 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -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 @@ -65,13 +79,33 @@ ${chalk.grey('——————————————————')}` }; export async function commit(isStageAllFlag = false) { + 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 [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); + } + intro('open-commit'); + if (errorChangedFiles ?? errorStagedFiles) { + outro(`${chalk.red('✖')} ${errorChangedFiles ?? errorStagedFiles}`); + process.exit(1); + } const stagedFilesSpinner = spinner(); stagedFilesSpinner.start('Counting staged files'); - const staged = await getStagedGitDiff(isStageAllFlag); - if (!staged && isStageAllFlag) { + if (!stagedFiles.length && isStageAllFlag) { outro( `${chalk.red( 'No changes detected' @@ -85,7 +119,7 @@ export async function commit(isStageAllFlag = false) { process.exit(1); } - if (!staged) { + if (!stagedFiles.length) { outro( `${chalk.red('Nothing to commit')} — stage the files ${chalk .hex('0000FF') @@ -103,17 +137,40 @@ export async function commit(isStageAllFlag = false) { isStageAllAndCommitConfirmedByUser && !isCancel(isStageAllAndCommitConfirmedByUser) ) { - await commit(true); + return 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 }); + } + + commit(false); } 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); } diff --git a/src/commands/prepare-commit-msg-hook.ts b/src/commands/prepare-commit-msg-hook.ts index 512cd52..68547dc 100644 --- a/src/commands/prepare-commit-msg-hook.ts +++ b/src/commands/prepare-commit-msg-hook.ts @@ -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); diff --git a/src/utils/git.ts b/src/utils/git.ts index 97af5fa..5315224 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,5 +1,5 @@ import { execa } from 'execa'; -import { spinner } from '@clack/prompts'; +import { spinner, text } from '@clack/prompts'; export const assertGitRepo = async () => { try { @@ -9,41 +9,88 @@ export const assertGitRepo = async () => { } }; -const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map( - (file) => `:(exclude)${file}` -); +export const someFilesExcludedMessage = (files: string[]) => { + return text({ + message: `Some files are .lock files which are excluded by default as it's too big, commit it yourself, don't waste your api tokens. \n${files + .filter((file) => file.includes('.lock') || file.includes('-lock.')) + .join('\n') + }` + }); +}; -export interface StagedDiff { - files: string[]; - diff: string; -} +export const getStagedFiles = async (): Promise => { + const { stdout: files } = await execa('git', [ + 'diff', + '--name-only', + '--cached' + ]); -export const getStagedGitDiff = async ( - isStageAllFlag = false -): Promise => { - if (isStageAllFlag) { - const stageAllSpinner = spinner(); - stageAllSpinner.start('Staging all changes'); - await execa('git', ['add', '.']); - stageAllSpinner.stop('Done'); + if (!files) return []; + + const excludedFiles = files + .split('\n') + .filter(Boolean) + .filter((file) => file.includes('.lock') || file.includes('-lock.')); + + if (excludedFiles.length === files.split('\n').length) { + someFilesExcludedMessage(files.split('\n')); } - const diffStaged = ['diff', '--staged']; - const { stdout: files } = await execa('git', [ - ...diffStaged, - '--name-only', - ...excludeBigFilesFromDiff + return files.split('\n').sort(); +}; + +export const getChangedFiles = async (): Promise => { + const { stdout: modified } = await execa('git', ['ls-files', '--modified']); + const { stdout: others } = await execa('git', [ + 'ls-files', + '--others', + '--exclude-standard' ]); - if (!files) return null; + const files = [...modified.split('\n'), ...others.split('\n')].filter( + (file) => !!file + ); + + const filesWithoutLocks = files.filter( + (file) => !file.includes('.lock') && !file.includes('-lock.') + ); + + if (files.length !== filesWithoutLocks.length) { + someFilesExcludedMessage(files); + } + + return filesWithoutLocks.sort(); +}; + +export const gitAdd = async ({ files }: { files: string[] }) => { + const filteredFiles = files.filter( + (file) => !file.includes('.lock') && !file.includes('-lock.') + ); + + const gitAddSpinner = spinner(); + gitAddSpinner.start('Adding files to commit'); + await execa('git', ['add', ...filteredFiles]); + gitAddSpinner.stop('Done'); + + if (filteredFiles.length !== files.length) { + someFilesExcludedMessage(files); + } +}; + +export const getDiff = async ({ files }: { files: string[] }) => { + const filesWithoutLocks = files.filter( + (file) => !file.includes('.lock') && !file.includes('-lock.') + ); + + if (filesWithoutLocks.length !== files.length) { + someFilesExcludedMessage(files); + } const { stdout: diff } = await execa('git', [ - ...diffStaged, - ...excludeBigFilesFromDiff + 'diff', + '--staged', + ...filesWithoutLocks ]); - return { - files: files.split('\n').sort(), - diff - }; + return diff; }; diff --git a/src/utils/trytm.ts b/src/utils/trytm.ts new file mode 100644 index 0000000..310ede3 --- /dev/null +++ b/src/utils/trytm.ts @@ -0,0 +1,12 @@ +export const trytm = async ( + promise: Promise +): Promise<[T, null] | [null, Error]> => { + try { + const data = await promise; + return [data, null]; + } catch (throwable) { + if (throwable instanceof Error) return [null, throwable]; + + throw throwable; + } +};