diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index d72ad28..3909f54 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -1,17 +1,30 @@ +import { parseArgs } from 'node:util'; import { loadRstackConfig } from './config.js'; const stagedHelpMessage = `Rstack v${RSTACK_VERSION} Usage: - $ rs staged + $ rs staged [options] Runs lint-staged with tasks from define.staged in rstack.config. Options: - -h, --help Display this help message`; + --allow-empty Allow empty commits when tasks revert all staged changes + -h, --help Display this help message`; export async function runStagedCLI(args: string[]): Promise { - if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) { + const { values } = parseArgs({ + args, + options: { + 'allow-empty': { type: 'boolean' }, + allowEmpty: { type: 'boolean' }, + help: { type: 'boolean', short: 'h' }, + }, + allowPositionals: false, + strict: true, + }); + + if (values.help) { console.log(stagedHelpMessage); return; } @@ -29,6 +42,7 @@ export async function runStagedCLI(args: string[]): Promise { 'lint-staged' ); const success = await lintStaged({ + allowEmpty: values['allow-empty'] ?? values.allowEmpty ?? false, config: stagedConfig, }); if (!success) { diff --git a/packages/rstack/test/cli/staged/index.test.ts b/packages/rstack/test/cli/staged/index.test.ts new file mode 100644 index 0000000..d394362 --- /dev/null +++ b/packages/rstack/test/cli/staged/index.test.ts @@ -0,0 +1,86 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { test } from '#test-helpers'; + +const git = (cwd: string, args: string[]): void => { + execFileSync('git', args, { cwd, stdio: 'ignore' }); +}; + +const createGitFixture = async (): Promise => { + const cwd = await mkdtemp(path.join(import.meta.dirname, 'fixture-')); + + await Promise.all([ + writeFile( + path.join(cwd, 'rstack.config.ts'), + `import { define } from 'rstack'; + +define.staged({ '*.txt': 'node revert.mjs' }); +`, + ), + writeFile( + path.join(cwd, 'revert.mjs'), + `import { writeFileSync } from 'node:fs'; + +for (const file of process.argv.slice(2)) { + writeFileSync(file, 'initial\\n'); +} +`, + ), + writeFile(path.join(cwd, 'file.txt'), 'initial\n'), + ]); + + git(cwd, ['init', '--quiet']); + git(cwd, ['add', '.']); + git(cwd, [ + '-c', + 'user.name=Rstack Test', + '-c', + 'user.email=rstack@example.com', + 'commit', + '--quiet', + '-m', + 'initial', + ]); + + await writeFile(path.join(cwd, 'file.txt'), 'changed\n'); + git(cwd, ['add', 'file.txt']); + + return cwd; +}; + +const withGitFixture = async (callback: (cwd: string) => Promise | void): Promise => { + const cwd = await createGitFixture(); + + try { + await callback(cwd); + } finally { + await rm(cwd, { recursive: true, force: true }); + } +}; + +test('should display the staged help message', ({ execCli, expect }) => { + const output = execCli('staged --help'); + + expect(output).toContain('Rstack v'); + expect(output).toContain('Usage:\n $ rs staged [options]'); + expect(output).toContain('Runs lint-staged with tasks from define.staged in rstack.config.'); + expect(output).toContain('--allow-empty'); + expect(output).toContain('-h, --help'); +}); + +test('should reject unknown staged options', ({ execCli, expect }) => { + expect(() => execCli('staged --unknown')).toThrow(); +}); + +test('should prevent an empty commit by default', async ({ execCli, expect }) => { + await withGitFixture((cwd) => { + expect(() => execCli('staged', { cwd })).toThrow(); + }); +}); + +test(`should allow an empty commit with --allow-empty`, async ({ execCli }) => { + await withGitFixture((cwd) => { + execCli(`staged --allow-empty`, { cwd }); + }); +});