Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow a path to be supplied to the --shell option (#993) #994

Merged
merged 2 commits into from Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -71,7 +71,7 @@ Options:
tasks serially (default: true)
-q, --quiet disable lint-staged’s own console output (default: false)
-r, --relative pass relative filepaths to tasks (default: false)
-x, --shell skip parsing of tasks for better shell support (default:
-x, --shell <path> skip parsing of tasks for better shell support (default:
false)
-v, --verbose show task output even when tasks succeed; by default only
failed output is shown (default: false)
Expand All @@ -90,7 +90,7 @@ Options:
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
- **`--quiet`**: Supress all CLI output, except from tasks.
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option.
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. To use a specific shell, use a path like `--shell "/bin/bash"`.
- **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.

## Configuration
Expand Down
4 changes: 2 additions & 2 deletions bin/lint-staged.js
Expand Up @@ -42,7 +42,7 @@ cmdline
)
.option('-q, --quiet', 'disable lint-staged’s own console output', false)
.option('-r, --relative', 'pass relative filepaths to tasks', false)
.option('-x, --shell', 'skip parsing of tasks for better shell support', false)
.option('-x, --shell <path>', 'skip parsing of tasks for better shell support', false)
.option(
'-v, --verbose',
'show task output even when tasks succeed; by default only failed output is shown',
Expand Down Expand Up @@ -85,7 +85,7 @@ const options = {
stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
quiet: !!cmdlineOptions.quiet,
relative: !!cmdlineOptions.relative,
shell: !!cmdlineOptions.shell,
shell: cmdlineOptions.shell /* Either a boolean or a string pointing to the shell */,
verbose: !!cmdlineOptions.verbose,
}

Expand Down
40 changes: 30 additions & 10 deletions lib/index.js
Expand Up @@ -8,21 +8,26 @@ const stringifyObject = require('stringify-object')
const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
const printTaskOutput = require('./printTaskOutput')
const runAll = require('./runAll')
const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols')
const {
ApplyEmptyCommitError,
ConfigNotFoundError,
GetBackupStashError,
GitError,
InvalidOptionsError,
} = require('./symbols')
const formatConfig = require('./formatConfig')
const validateConfig = require('./validateConfig')
const validateOptions = require('./validateOptions')

const errConfigNotFound = new Error('Config could not be found')

function resolveConfig(configPath) {
const resolveConfig = (configPath) => {
try {
return require.resolve(configPath)
} catch {
return configPath
}
}

function loadConfig(configPath) {
const loadConfig = (configPath) => {
const explorer = cosmiconfig('lint-staged', {
searchPlaces: [
'package.json',
Expand Down Expand Up @@ -56,14 +61,14 @@ function loadConfig(configPath) {
* @param {number} [options.maxArgLength] - Maximum argument string length
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
* @param {Logger} [logger]
*
* @returns {Promise<boolean>} Promise of whether the linting passed or failed
*/
module.exports = async function lintStaged(
const lintStaged = async (
{
allowEmpty = false,
concurrent = true,
Expand All @@ -79,20 +84,27 @@ module.exports = async function lintStaged(
verbose = false,
} = {},
logger = console
) {
) => {
try {
await validateOptions({ shell }, logger)

debugLog('Loading config using `cosmiconfig`')

const resolved = configObject
? { config: configObject, filepath: '(input)' }
: await loadConfig(configPath)
if (resolved == null) throw errConfigNotFound

if (resolved == null) {
throw ConfigNotFoundError
}

debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)

// resolved.config is the parsed configuration object
// resolved.filepath is the path to the config file that was found
const formattedConfig = formatConfig(resolved.config)
const config = validateConfig(formattedConfig)

if (debug) {
// Log using logger to be able to test through `consolemock`.
logger.log('Running lint-staged with the following config:')
Expand Down Expand Up @@ -148,7 +160,13 @@ module.exports = async function lintStaged(
throw runAllError
}
} catch (lintStagedError) {
if (lintStagedError === errConfigNotFound) {
/** throw early because `validateOptions` options contains own logging */
if (lintStagedError === InvalidOptionsError) {
throw InvalidOptionsError
}

/** @todo move logging to `validateConfig` and remove this try/catch block */
if (lintStagedError === ConfigNotFoundError) {
logger.error(`${lintStagedError.message}.`)
} else {
// It was probably a parsing error
Expand All @@ -168,3 +186,5 @@ module.exports = async function lintStaged(
throw lintStagedError
}
}

module.exports = lintStaged
23 changes: 16 additions & 7 deletions lib/messages.js
Expand Up @@ -27,6 +27,14 @@ const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'

const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git error.`)}`

const invalidOption = (name, value, message) => `${chalk.redBright(`${error} Validation Error:`)}

Invalid value for option ${chalk.bold(name)}: ${chalk.bold(value)}

${message}

See https://github.com/okonet/lint-staged#command-line-flags`

const PREVENTED_EMPTY_COMMIT = `
${warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
Use the --allow-empty option to continue, or check your task configuration`)}
Expand All @@ -42,16 +50,17 @@ const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a g
const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'

module.exports = {
NOT_GIT_REPO,
CONFIG_STDIN_ERROR,
DEPRECATED_GIT_ADD,
FAILED_GET_STAGED_FILES,
GIT_ERROR,
invalidOption,
NO_STAGED_FILES,
NO_TASKS,
skippingBackup,
DEPRECATED_GIT_ADD,
TASK_ERROR,
SKIPPED_GIT_ERROR,
GIT_ERROR,
NOT_GIT_REPO,
PREVENTED_EMPTY_COMMIT,
RESTORE_STASH_EXAMPLE,
CONFIG_STDIN_ERROR,
SKIPPED_GIT_ERROR,
skippingBackup,
TASK_ERROR,
}
4 changes: 4 additions & 0 deletions lib/symbols.js
@@ -1,22 +1,26 @@
'use strict'

const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
const ConfigNotFoundError = new Error('Config could not be found')
const GetBackupStashError = Symbol('GetBackupStashError')
const GetStagedFilesError = Symbol('GetStagedFilesError')
const GitError = Symbol('GitError')
const GitRepoError = Symbol('GitRepoError')
const HideUnstagedChangesError = Symbol('HideUnstagedChangesError')
const InvalidOptionsError = new Error('Invalid Options')
const RestoreMergeStatusError = Symbol('RestoreMergeStatusError')
const RestoreOriginalStateError = Symbol('RestoreOriginalStateError')
const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
const TaskError = Symbol('TaskError')

module.exports = {
ApplyEmptyCommitError,
ConfigNotFoundError,
GetBackupStashError,
GetStagedFilesError,
GitError,
GitRepoError,
InvalidOptionsError,
HideUnstagedChangesError,
RestoreMergeStatusError,
RestoreOriginalStateError,
Expand Down
31 changes: 31 additions & 0 deletions lib/validateOptions.js
@@ -0,0 +1,31 @@
const { promises: fs, constants } = require('fs')

const { invalidOption } = require('./messages')
const { InvalidOptionsError } = require('./symbols')

const debug = require('debug')('lint-staged:options')

/**
* Validate lint-staged options, either from the Node.js API or the command line flags.
* @param {*} options
* @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
*
* @throws {InvalidOptionsError}
*/
const validateOptions = async (options = {}, logger) => {
debug('Validating options...')

/** Ensure the passed shell option is executable */
if (typeof options.shell === 'string') {
try {
await fs.access(options.shell, constants.X_OK)
} catch (error) {
logger.error(invalidOption('shell', options.shell, error.message))
throw InvalidOptionsError
}
}

debug('Validated options!')
}

module.exports = validateOptions
81 changes: 0 additions & 81 deletions test/__snapshots__/index.spec.js.snap

This file was deleted.