Feature: add staged files multiple selection (#6)

* chore(package.json): add "@bdsqqq/try" dependency
* refactor(api.ts): remove unnecessary whitespace
* refactor(cli.ts): remove unused imports and variables

* refactor(commit.ts): rename getStagedGitDiff to getDif
* refactor(commit.ts): add getStagedFiles and getChangedFiles functions
* feat(commit.ts): add multiselect prompt to select files to stage
* feat(commit.ts): add gitAdd function to stage selected files
* feat(commit.ts): add trytm function to handle errors
* feat(commit.ts): add exitProgram function to exit the program with an error message if an error occurs during execution

* refactor(commit.ts): refactor commit function to handle unstaged files
* feat(commit.ts): add multiselect prompt to select files to add to commit when there are unstaged files

* feat(git.ts): add getStagedFiles function to get list of staged files
* feat(git.ts): add getChangedFiles function to get list of changed files
* feat(git.ts): add gitAdd function to add files to commit
* feat(git.ts): add getDif function to get diff of staged files

* refactor(commit.ts): replace exitProgram function with process.exit(1) to exit the program

* refactor(commit.ts): change message prompt to English in multiselect function

* chore(package.json): add prettier to format code
* refactor(api.ts): remove unnecessary whitespace and comments

* refactor(commit.ts): add missing semicolons and fix formatting
* feat(commit.ts): add support for selecting files to add to the commit when there are changed files but no staged files

* refactor(commit.ts): add isStageAllFlag parameter to commit function
* refactor(commit.ts): add whitespace to getDif function call
* refactor(commit.ts): add whitespace to generateCommitMessageFromGitDiff function call

* refactor(git.ts): reformat code for better readability
* chore(git.ts): add semicolons to the end of each statement

* chore(package.json): remove "@bdsqqq/try" dependency
* refactor(commit.ts): move trytm function to utils/trytm.ts
* refactor(commit.ts): add isStageAllFlag parameter to gitAdd function call in commit function
* refactor(commit.ts): remove getStagedGitDiff function call and use getStagedFiles function call instead
* refactor(commit.ts): add error handling to generateCommitMessageFromGitDiff function call in commit function

* refactor(prepare-commit-msg-hook.ts): rename getStagedGitDiff to getStagedFiles
* feat(prepare-commit-msg-hook.ts): add gitAdd function to stage changes before generating commit message
* refactor(prepare-commit-msg-hook.ts): use getDif function to get staged changes diff instead of staged.diff
* refactor(prepare-commit-msg-hook.ts): remove unnecessary if statement and return statement

* refactor(git.ts): remove StagedDiff interface and getStagedGitDiff function
* feat(git.ts): add support for untracked files in getChangedFiles function
* refactor(git.ts): rename stdout variable in getChangedFiles function
* refactor(git.ts): add excludeBigFilesFromDiff to getDif function
* feat(trytm.ts): add trytm utility function for handling promises with try-catch block

* fix(commit.ts): add missing function call parentheses in return statement

* refactor(commit.ts): remove unused variable generateCommitResponse
* refactor(commit.ts): exit process with code 0 after successful commit

* fix(commit.ts): add check for no changes detected before opening commit prompt

* fix(commit.ts): fix typo in function name from getDif to getDiff
* fix(prepare-commit-msg-hook.ts): fix typo in function name from getDif to getDiff
* refactor(git.ts): rename getDif function to getDiff for consistency and clarity

* chore(git.ts): add excludeBigFilesFromDiff option to getStagedFiles function

* chore(git.ts): import text function from @clack/prompts package

* refactor(git.ts): remove excludeBigFilesFromDiff constant and filter out .lock files from getStagedFiles and getChangedFiles functions
* feat(git.ts): add error message when all staged files are .lock files
* feat(git.ts): add error message when all changed files are .lock files
* feat(git.ts): add warning message when some files are .lock files and excluded from git add and git diff

* refactor(git.ts): add filter to remove empty strings from returned array in getStagedFiles and getChangedFiles functions

* refactor(commit.ts): pass isStageAllFlag to getChangedFiles function
* fix(commit.ts): handle errorStagedFiles and errorChangedFiles variables

* fix(git.ts): filter out empty strings from excludedFiles array
* feat(git.ts): add isStageAllFlag parameter to getChangedFiles function to handle git add --all command

* refactor(git.ts): remove console.log statement from getChangedFiles function and refactor code to improve readability

* refactor(commit.ts): remove unnecessary parameter from getChangedFiles function call
* refactor(git.ts): remove isStageAllFlag parameter from getChangedFiles function and add check for .lock files in the returned files list

* refactor(commit.ts): remove unnecessary line breaks and whitespace
* refactor(commit.ts): remove unnecessary parentheses in function calls

* feat(git.ts): add someFilesExcludedMessage function to display excluded files message
* refactor(git.ts): use someFilesExcludedMessage function instead of text function in getChangedFiles, gitAdd, and getDiff functions

* refactor(git.ts): extract someFilesExcludedMessage function to handle excluded files message
* fix(git.ts): use someFilesExcludedMessage function instead of throwing an error when all staged files are excluded files

* refactor(git.ts): pad excluded files list with 5 spaces

* refactor(git.ts): remove unnecessary padStart method call in someFilesExcludedMessage function
* chore(git.ts): update someFilesExcludedMessage function to improve readability

---------

Co-authored-by: Sukharev <57486732+di-sukharev@users.noreply.github.com>
This commit is contained in:
Vladyslav Kapkan
2023-03-16 15:41:21 +01:00
committed by GitHub
parent 5d0c69e849
commit 0d1f72bdec
6 changed files with 183 additions and 41 deletions

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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<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 [];
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<string[]> => {
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;
};

12
src/utils/trytm.ts Normal file
View 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;
}
};