diff --git a/.changeset/great-panthers-shout.md b/.changeset/great-panthers-shout.md new file mode 100644 index 0000000..2bb33d5 --- /dev/null +++ b/.changeset/great-panthers-shout.md @@ -0,0 +1,5 @@ +--- +"pretty-quick": major +--- + +feat!: use async and batch stage commands diff --git a/src/cli.mts b/src/cli.mts index 4a32c60..9aedcf6 100755 --- a/src/cli.mts +++ b/src/cli.mts @@ -50,6 +50,10 @@ const main = async () => { onExamineFile: file => { console.log(`🔍 Examining ${picocolors.bold(file)}.`) }, + + onStageFiles(files) { + console.log(`🏗️ Staging changed ${files.length} files.`) + }, }) if (prettyQuickResult.success) { @@ -71,10 +75,14 @@ const main = async () => { '✗ Code style issues found in the above file(s). Forgot to run Prettier?', ) } + if (prettyQuickResult.errors.includes('STAGE_FAILED')) { + console.log( + '✗ Failed to stage some or all of the above file(s). Please stage changes made by Prettier before committing.', + ) + } // eslint-disable-next-line n/no-process-exit process.exit(1) // ensure git hooks abort } } -// eslint-disable-next-line @typescript-eslint/no-floating-promises -main() +void main() diff --git a/src/createIgnorer.ts b/src/createIgnorer.ts index 9e90ba8..792a949 100644 --- a/src/createIgnorer.ts +++ b/src/createIgnorer.ts @@ -1,18 +1,19 @@ /* eslint-disable unicorn-x/filename-case */ -import fs from 'fs' +import fs from 'fs/promises' import path from 'path' +import { tryFile } from '@pkgr/core' import ignore from 'ignore' -export default function createIgnorer( +export default async function createIgnorer( directory: string, filename = '.prettierignore', ) { const file = path.join(directory, filename) - if (fs.existsSync(file)) { - const text = fs.readFileSync(file, 'utf8') + if (tryFile(file)) { + const text = await fs.readFile(file, 'utf8') const filter = ignore().add(text).createFilter() return (filepath: string) => filter(path.join(filepath)) } diff --git a/src/createMatcher.ts b/src/createMatcher.ts index a085df1..edfb8d9 100644 --- a/src/createMatcher.ts +++ b/src/createMatcher.ts @@ -10,7 +10,6 @@ export default function createMatcher(pattern: string[] | string | undefined) { return () => true } const patterns = Array.isArray(pattern) ? pattern : [pattern] - const isMatch = picomatch(patterns, { dot: true }) return (file: string) => isMatch(path.normalize(file)) } diff --git a/src/index.ts b/src/index.ts index 504f51c..62219a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export = async function prettyQuick( onExamineFile, onCheckFile, onWriteFile, + onStageFiles, resolveConfig = true, }: Partial = {}, ) { @@ -39,11 +40,11 @@ export = async function prettyQuick( onFoundSinceRevision?.(scm.name, revision) - const rootIgnorer = createIgnorer(directory, ignorePath) + const rootIgnorer = await createIgnorer(directory, ignorePath) const cwdIgnorer = currentDirectory === directory ? () => true - : createIgnorer(currentDirectory, ignorePath) + : await createIgnorer(currentDirectory, ignorePath) const patternMatcher = createMatcher(pattern) @@ -79,6 +80,7 @@ export = async function prettyQuick( const failReasons = new Set() + const filesToStage: string[] = [] await processFiles(directory, changedFiles, { check, config, @@ -89,7 +91,7 @@ export = async function prettyQuick( } if (staged && restage) { if (wasFullyStaged(file)) { - await scm.stageFile(directory, file) + filesToStage.push(file) } else { onPartiallyStagedFile?.(file) failReasons.add('PARTIALLY_STAGED_FILE') @@ -105,6 +107,15 @@ export = async function prettyQuick( onExamineFile: verbose ? onExamineFile : undefined, }) + if (filesToStage.length > 0) { + try { + await onStageFiles?.(filesToStage) + await scm.stageFiles(directory, filesToStage) + } catch { + failReasons.add('STAGE_FAILED') + } + } + return { success: failReasons.size === 0, errors: [...failReasons], diff --git a/src/processFiles.ts b/src/processFiles.ts index 6077041..44a09e9 100644 --- a/src/processFiles.ts +++ b/src/processFiles.ts @@ -1,6 +1,6 @@ /* eslint-disable unicorn-x/filename-case */ -import fs from 'fs' +import fs from 'fs/promises' import path from 'path' import { format, check as prettierCheck, resolveConfig } from 'prettier' @@ -18,29 +18,31 @@ export default async function processFiles( onWriteFile, }: Partial = {}, ) { - for (const relative of files) { - onExamineFile?.(relative) - const file = path.join(directory, relative) - const options = { - ...(await resolveConfig(file, { - config, - editorconfig: true, - })), - filepath: file, - } - const input = fs.readFileSync(file, 'utf8') + return Promise.all( + files.map(async relative => { + onExamineFile?.(relative) + const file = path.join(directory, relative) + const options = { + ...(await resolveConfig(file, { + config, + editorconfig: true, + })), + filepath: file, + } + const input = await fs.readFile(file, 'utf8') - if (check) { - const isFormatted = await prettierCheck(input, options) - onCheckFile?.(relative, isFormatted) - continue - } + if (check) { + const isFormatted = await prettierCheck(input, options) + onCheckFile?.(relative, isFormatted) + return + } - const output = await format(input, options) + const output = await format(input, options) - if (output !== input) { - fs.writeFileSync(file, output) - await onWriteFile?.(relative) - } - } + if (output !== input) { + await fs.writeFile(file, output) + await onWriteFile?.(relative) + } + }), + ) } diff --git a/src/scms/git.ts b/src/scms/git.ts index 774e37a..c31acad 100644 --- a/src/scms/git.ts +++ b/src/scms/git.ts @@ -81,9 +81,24 @@ export const getChangedFiles = async ( )), ].filter(Boolean) -export const getUnstagedChangedFiles = (directory: string) => { - return getChangedFiles(directory, null, false) -} +export const getUnstagedChangedFiles = (directory: string) => + getChangedFiles(directory, null, false) + +export const stageFiles = async (directory: string, files: string[]) => { + const maxArguments = 100 + const result = files.reduce((resultArray, file, index) => { + const chunkIndex = Math.floor(index / maxArguments) + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = [] // start a new chunk + } -export const stageFile = (directory: string, file: string) => - runGit(directory, ['add', file]) + resultArray[chunkIndex].push(file) + + return resultArray + }, []) + + for (const batchedFiles of result) { + await runGit(directory, ['add', ...batchedFiles]) + } +} diff --git a/src/scms/hg.ts b/src/scms/hg.ts index 670c9ad..e483e10 100644 --- a/src/scms/hg.ts +++ b/src/scms/hg.ts @@ -32,7 +32,6 @@ export const getSinceRevision = async ( branch || 'default', ]) const revision = revisionOutput.stdout.trim() - const hgOutput = await runHg(directory, ['id', '-i', '-r', revision]) return hgOutput.stdout.trim() } @@ -56,5 +55,21 @@ export const getChangedFiles = async ( export const getUnstagedChangedFiles = () => [] -export const stageFile = (directory: string, file: string) => - runHg(directory, ['add', file]) +export const stageFiles = async (directory: string, files: string[]) => { + const maxArguments = 100 + const result = files.reduce((resultArray, file, index) => { + const chunkIndex = Math.floor(index / maxArguments) + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = [] // start a new chunk + } + + resultArray[chunkIndex].push(file) + + return resultArray + }, []) + + for (const batchedFiles of result) { + await runHg(directory, ['add', ...batchedFiles]) + } +} diff --git a/src/types.ts b/src/types.ts index 9ff7fe9..021b506 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,4 +16,5 @@ export interface PrettyQuickOptions { onExamineFile(relative: string): void onCheckFile(relative: string, isFormatted: boolean): void onWriteFile(relative: string): Promise | void + onStageFiles(files: string[]): Promise | void }