Skip to content

Commit 0d1f72b

Browse files
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>
1 parent 5d0c69e commit 0d1f72b

File tree

6 files changed

+184
-42
lines changed

6 files changed

+184
-42
lines changed

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"dev": "ts-node ./src/cli.ts",
4343
"build": "rimraf out && esbuild ./src/cli.ts --bundle --outfile=out/cli.cjs --format=cjs --platform=node",
4444
"deploy": "npm run build && npm version patch && npm publish --tag latest",
45-
"lint": "eslint src --ext ts && tsc --noEmit"
45+
"lint": "eslint src --ext ts && tsc --noEmit",
46+
"format": "prettier --write src"
4647
},
4748
"devDependencies": {
4849
"@types/ini": "^1.3.31",
@@ -53,6 +54,7 @@
5354
"dotenv": "^16.0.3",
5455
"esbuild": "^0.15.18",
5556
"eslint": "^8.28.0",
57+
"prettier": "^2.8.4",
5658
"ts-node": "^10.9.1",
5759
"typescript": "^4.9.3"
5860
},

src/commands/commit.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,23 @@ import {
33
GenerateCommitMessageErrorEnum,
44
generateCommitMessageWithChatCompletion
55
} from '../generateCommitMessageFromGitDiff';
6-
import { assertGitRepo, getStagedGitDiff } from '../utils/git';
7-
import { spinner, confirm, outro, isCancel, intro } from '@clack/prompts';
6+
import {
7+
assertGitRepo,
8+
getChangedFiles,
9+
getDiff,
10+
getStagedFiles,
11+
gitAdd
12+
} from '../utils/git';
13+
import {
14+
spinner,
15+
confirm,
16+
outro,
17+
isCancel,
18+
intro,
19+
multiselect
20+
} from '@clack/prompts';
821
import chalk from 'chalk';
22+
import { trytm } from '../utils/trytm';
923

1024
const generateCommitMessageFromGitDiff = async (
1125
diff: string
@@ -65,13 +79,33 @@ ${chalk.grey('——————————————————')}`
6579
};
6680

6781
export async function commit(isStageAllFlag = false) {
82+
if (isStageAllFlag) {
83+
const changedFiles = await getChangedFiles();
84+
if (changedFiles) await gitAdd({ files: changedFiles });
85+
else {
86+
outro("No changes detected, write some code and run `oc` again");
87+
process.exit(1);
88+
}
89+
}
90+
91+
const [stagedFiles, errorStagedFiles] = await trytm(getStagedFiles());
92+
const [changedFiles, errorChangedFiles] = await trytm(getChangedFiles());
93+
94+
if (!changedFiles?.length && !stagedFiles?.length) {
95+
outro(chalk.red('No changes detected'));
96+
process.exit(1);
97+
}
98+
6899
intro('open-commit');
100+
if (errorChangedFiles ?? errorStagedFiles) {
101+
outro(`${chalk.red('✖')} ${errorChangedFiles ?? errorStagedFiles}`);
102+
process.exit(1);
103+
}
69104

70105
const stagedFilesSpinner = spinner();
71106
stagedFilesSpinner.start('Counting staged files');
72-
const staged = await getStagedGitDiff(isStageAllFlag);
73107

74-
if (!staged && isStageAllFlag) {
108+
if (!stagedFiles.length && isStageAllFlag) {
75109
outro(
76110
`${chalk.red(
77111
'No changes detected'
@@ -85,7 +119,7 @@ export async function commit(isStageAllFlag = false) {
85119
process.exit(1);
86120
}
87121

88-
if (!staged) {
122+
if (!stagedFiles.length) {
89123
outro(
90124
`${chalk.red('Nothing to commit')} — stage the files ${chalk
91125
.hex('0000FF')
@@ -103,17 +137,40 @@ export async function commit(isStageAllFlag = false) {
103137
isStageAllAndCommitConfirmedByUser &&
104138
!isCancel(isStageAllAndCommitConfirmedByUser)
105139
) {
106-
await commit(true);
140+
return await commit(true);
107141
}
108142

109-
process.exit(1);
143+
if (stagedFiles.length === 0 && changedFiles.length > 0) {
144+
const files = (await multiselect({
145+
message: chalk.cyan('Select the files you want to add to the commit:'),
146+
options: changedFiles.map((file) => ({
147+
value: file,
148+
label: file
149+
}))
150+
})) as string[];
151+
152+
if (isCancel(files)) process.exit(1);
153+
154+
await gitAdd({ files });
155+
}
156+
157+
commit(false);
110158
}
111159

112160
stagedFilesSpinner.stop(
113-
`${staged.files.length} staged files:\n${staged.files
161+
`${stagedFiles.length} staged files:\n${stagedFiles
114162
.map((file) => ` ${file}`)
115163
.join('\n')}`
116164
);
117165

118-
await generateCommitMessageFromGitDiff(staged.diff);
166+
const [, generateCommitError] = await trytm(
167+
generateCommitMessageFromGitDiff(await getDiff({ files: stagedFiles }))
168+
);
169+
170+
if (generateCommitError) {
171+
outro(`${chalk.red('✖')} ${generateCommitError}`);
172+
process.exit(1);
173+
}
174+
175+
process.exit(0);
119176
}

src/commands/prepare-commit-msg-hook.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs/promises';
22
import chalk from 'chalk';
33
import { intro, outro } from '@clack/prompts';
4-
import { getStagedGitDiff } from '../utils/git';
4+
import { getChangedFiles, getDiff, getStagedFiles, gitAdd } from '../utils/git';
55
import { getConfig } from './config';
66
import { generateCommitMessageWithChatCompletion } from '../generateCommitMessageFromGitDiff';
77

@@ -17,7 +17,14 @@ export const prepareCommitMessageHook = async () => {
1717

1818
if (commitSource) return;
1919

20-
const staged = await getStagedGitDiff();
20+
const changedFiles = await getChangedFiles();
21+
if (changedFiles) await gitAdd({ files: changedFiles });
22+
else {
23+
outro("No changes detected, write some code and run `oc` again");
24+
process.exit(1);
25+
}
26+
27+
const staged = await getStagedFiles();
2128

2229
if (!staged) return;
2330

@@ -32,7 +39,7 @@ export const prepareCommitMessageHook = async () => {
3239
}
3340

3441
const commitMessage = await generateCommitMessageWithChatCompletion(
35-
staged.diff
42+
await getDiff({ files: staged })
3643
);
3744

3845
if (typeof commitMessage !== 'string') throw new Error(commitMessage.error);

src/utils/git.ts

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { execa } from 'execa';
2-
import { spinner } from '@clack/prompts';
2+
import { spinner, text } from '@clack/prompts';
33

44
export const assertGitRepo = async () => {
55
try {
@@ -9,41 +9,88 @@ export const assertGitRepo = async () => {
99
}
1010
};
1111

12-
const excludeBigFilesFromDiff = ['*-lock.*', '*.lock'].map(
13-
(file) => `:(exclude)${file}`
14-
);
15-
16-
export interface StagedDiff {
17-
files: string[];
18-
diff: string;
19-
}
20-
21-
export const getStagedGitDiff = async (
22-
isStageAllFlag = false
23-
): Promise<StagedDiff | null> => {
24-
if (isStageAllFlag) {
25-
const stageAllSpinner = spinner();
26-
stageAllSpinner.start('Staging all changes');
27-
await execa('git', ['add', '.']);
28-
stageAllSpinner.stop('Done');
29-
}
12+
export const someFilesExcludedMessage = (files: string[]) => {
13+
return text({
14+
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
15+
.filter((file) => file.includes('.lock') || file.includes('-lock.'))
16+
.join('\n')
17+
}`
18+
});
19+
};
3020

31-
const diffStaged = ['diff', '--staged'];
21+
export const getStagedFiles = async (): Promise<string[]> => {
3222
const { stdout: files } = await execa('git', [
33-
...diffStaged,
23+
'diff',
3424
'--name-only',
35-
...excludeBigFilesFromDiff
25+
'--cached'
26+
]);
27+
28+
if (!files) return [];
29+
30+
const excludedFiles = files
31+
.split('\n')
32+
.filter(Boolean)
33+
.filter((file) => file.includes('.lock') || file.includes('-lock.'));
34+
35+
if (excludedFiles.length === files.split('\n').length) {
36+
someFilesExcludedMessage(files.split('\n'));
37+
}
38+
39+
return files.split('\n').sort();
40+
};
41+
42+
export const getChangedFiles = async (): Promise<string[]> => {
43+
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
44+
const { stdout: others } = await execa('git', [
45+
'ls-files',
46+
'--others',
47+
'--exclude-standard'
3648
]);
3749

38-
if (!files) return null;
50+
const files = [...modified.split('\n'), ...others.split('\n')].filter(
51+
(file) => !!file
52+
);
53+
54+
const filesWithoutLocks = files.filter(
55+
(file) => !file.includes('.lock') && !file.includes('-lock.')
56+
);
57+
58+
if (files.length !== filesWithoutLocks.length) {
59+
someFilesExcludedMessage(files);
60+
}
61+
62+
return filesWithoutLocks.sort();
63+
};
64+
65+
export const gitAdd = async ({ files }: { files: string[] }) => {
66+
const filteredFiles = files.filter(
67+
(file) => !file.includes('.lock') && !file.includes('-lock.')
68+
);
69+
70+
const gitAddSpinner = spinner();
71+
gitAddSpinner.start('Adding files to commit');
72+
await execa('git', ['add', ...filteredFiles]);
73+
gitAddSpinner.stop('Done');
74+
75+
if (filteredFiles.length !== files.length) {
76+
someFilesExcludedMessage(files);
77+
}
78+
};
79+
80+
export const getDiff = async ({ files }: { files: string[] }) => {
81+
const filesWithoutLocks = files.filter(
82+
(file) => !file.includes('.lock') && !file.includes('-lock.')
83+
);
84+
85+
if (filesWithoutLocks.length !== files.length) {
86+
someFilesExcludedMessage(files);
87+
}
3988

4089
const { stdout: diff } = await execa('git', [
41-
...diffStaged,
42-
...excludeBigFilesFromDiff
90+
'diff',
91+
'--staged',
92+
...filesWithoutLocks
4393
]);
4494

45-
return {
46-
files: files.split('\n').sort(),
47-
diff
48-
};
95+
return diff;
4996
};

src/utils/trytm.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const trytm = async <T>(
2+
promise: Promise<T>
3+
): Promise<[T, null] | [null, Error]> => {
4+
try {
5+
const data = await promise;
6+
return [data, null];
7+
} catch (throwable) {
8+
if (throwable instanceof Error) return [null, throwable];
9+
10+
throw throwable;
11+
}
12+
};

0 commit comments

Comments
 (0)