@depup/lint-staged 16.3.2-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,434 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ import { createDebug } from './debug.js'
6
+ import { execGit } from './execGit.js'
7
+ import { readFile, unlink, writeFile } from './file.js'
8
+ import { getDiffCommand } from './getDiffCommand.js'
9
+ import { parseGitZOutput } from './parseGitZOutput.js'
10
+ import {
11
+ ApplyEmptyCommitError,
12
+ FailOnChangesError,
13
+ GetBackupStashError,
14
+ GitError,
15
+ HideUnstagedChangesError,
16
+ RestoreMergeStatusError,
17
+ RestoreOriginalStateError,
18
+ RestoreUnstagedChangesError,
19
+ } from './symbols.js'
20
+
21
+ const debugLog = createDebug('lint-staged:GitWorkflow')
22
+
23
+ const MERGE_HEAD = 'MERGE_HEAD'
24
+ const MERGE_MODE = 'MERGE_MODE'
25
+ const MERGE_MSG = 'MERGE_MSG'
26
+
27
+ // In git status machine output, renames are presented as `to`NUL`from`
28
+ // When diffing, both need to be taken into account, but in some cases on the `to`.
29
+ // eslint-disable-next-line no-control-regex
30
+ const RENAME = /\x00/
31
+
32
+ /**
33
+ * From list of files, split renames and flatten into two files `to`NUL`from`.
34
+ * @param {string[]} files
35
+ * @param {Boolean} [includeRenameFrom=true] Whether or not to include the `from` renamed file, which is no longer on disk
36
+ */
37
+ const processRenames = (files, includeRenameFrom = true) =>
38
+ files.reduce((flattened, file) => {
39
+ if (RENAME.test(file)) {
40
+ const [to, from] = file.split(RENAME)
41
+ if (includeRenameFrom) flattened.push(from)
42
+ flattened.push(to)
43
+ } else {
44
+ flattened.push(file)
45
+ }
46
+ return flattened
47
+ }, [])
48
+
49
+ export const STASH = 'lint-staged automatic backup'
50
+
51
+ const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'
52
+
53
+ const GIT_DIFF_ARGS = [
54
+ '--binary', // support binary files
55
+ '--unified=0', // do not add lines around diff for consistent behaviour
56
+ '--no-color', // disable colors for consistent behaviour
57
+ '--no-ext-diff', // disable external diff tools for consistent behaviour
58
+ '--src-prefix=a/', // force prefix for consistent behaviour
59
+ '--dst-prefix=b/', // force prefix for consistent behaviour
60
+ '--patch', // output a patch that can be applied
61
+ '--submodule=short', // always use the default short format for submodules
62
+ ]
63
+ const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']
64
+
65
+ const handleError = (error, ctx, symbol) => {
66
+ ctx.errors.add(GitError)
67
+ if (symbol) ctx.errors.add(symbol)
68
+ throw error
69
+ }
70
+
71
+ const calculateSha256 = (input) => crypto.createHash('sha256').update(input, 'utf-8').digest('hex')
72
+
73
+ /**
74
+ * The lines are wrapped in double quotes
75
+ * @returns {string[]}
76
+ */
77
+ const cleanGitStashOutput = (lines) => lines.map((line) => line.replace(/^"(.*)"$/, '$1'))
78
+
79
+ export class GitWorkflow {
80
+ /**
81
+ * @param {Object} opts
82
+ * @param {import('./getStagedFiles.js').StagedFile[][]} opts.matchedFileChunks
83
+ */
84
+ constructor({
85
+ allowEmpty,
86
+ diff,
87
+ diffFilter,
88
+ failOnChanges,
89
+ gitConfigDir,
90
+ matchedFileChunks,
91
+ topLevelDir,
92
+ }) {
93
+ this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
94
+ this.allowEmpty = allowEmpty
95
+ this.deletedFiles = []
96
+ this.diff = diff
97
+ this.diffFilter = diffFilter
98
+ this.gitConfigDir = gitConfigDir
99
+ this.failOnChanges = !!failOnChanges
100
+ /** @type {import('./getStagedFiles.js').StagedFile[][]} */
101
+ this.matchedFileChunks = matchedFileChunks
102
+ this.topLevelDir = topLevelDir
103
+
104
+ /**
105
+ * These three files hold state about an ongoing git merge
106
+ * Resolve paths during constructor
107
+ */
108
+ this.mergeHeadFilename = path.resolve(gitConfigDir, MERGE_HEAD)
109
+ this.mergeModeFilename = path.resolve(gitConfigDir, MERGE_MODE)
110
+ this.mergeMsgFilename = path.resolve(gitConfigDir, MERGE_MSG)
111
+ }
112
+
113
+ /**
114
+ * Get absolute path to file hidden inside .git
115
+ * @param {string} filename
116
+ */
117
+ getHiddenFilepath(filename) {
118
+ return path.resolve(this.gitConfigDir, `./${filename}`)
119
+ }
120
+
121
+ /**
122
+ * Get name of backup stash
123
+ */
124
+ async getBackupStash(ctx) {
125
+ /** Print stash list with short hash and subject */
126
+ const stashes = await this.execGit(['stash', 'list', '--format="%h %s"', '-z'])
127
+ .then(parseGitZOutput)
128
+ .then(cleanGitStashOutput)
129
+
130
+ const index = stashes.findIndex((line) => line.startsWith(ctx.backupHash))
131
+
132
+ if (index === -1) {
133
+ ctx.errors.add(GetBackupStashError)
134
+ throw new Error('lint-staged automatic backup is missing!')
135
+ }
136
+
137
+ return String(index)
138
+ }
139
+
140
+ /**
141
+ * Get a list of unstaged deleted files
142
+ */
143
+ async getDeletedFiles() {
144
+ debugLog('Getting deleted files...')
145
+ const lsFiles = await this.execGit(['ls-files', '--deleted'])
146
+ const deletedFiles = lsFiles
147
+ .split('\n')
148
+ .filter(Boolean)
149
+ .map((file) => path.resolve(this.topLevelDir, file))
150
+ debugLog('Found deleted files:', deletedFiles)
151
+ return deletedFiles
152
+ }
153
+
154
+ /**
155
+ * Save meta information about ongoing git merge
156
+ */
157
+ async backupMergeStatus() {
158
+ debugLog('Backing up merge state...')
159
+ await Promise.all([
160
+ readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)),
161
+ readFile(this.mergeModeFilename).then((buffer) => (this.mergeModeBuffer = buffer)),
162
+ readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer)),
163
+ ])
164
+ debugLog('Done backing up merge state!')
165
+ }
166
+
167
+ /**
168
+ * Restore meta information about ongoing git merge
169
+ */
170
+ async restoreMergeStatus(ctx) {
171
+ debugLog('Restoring merge state...')
172
+ try {
173
+ await Promise.all([
174
+ this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer),
175
+ this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer),
176
+ this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer),
177
+ ])
178
+ debugLog('Done restoring merge state!')
179
+ } catch (error) {
180
+ debugLog('Failed restoring merge state with error:')
181
+ debugLog(error)
182
+ handleError(
183
+ new Error('Merge state could not be restored due to an error!'),
184
+ ctx,
185
+ RestoreMergeStatusError
186
+ )
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get a list of all files with both staged and unstaged modifications.
192
+ * Renames have special treatment, since the single status line includes
193
+ * both the "from" and "to" filenames, where "from" is no longer on disk.
194
+ */
195
+ async getUnstagedFiles({ onlyPartial = false } = {}) {
196
+ debugLog('Getting partially staged files...')
197
+ const status = await this.execGit(['status', '-z'])
198
+ /**
199
+ * See https://git-scm.com/docs/git-status#_short_format
200
+ * Entries returned in machine format are separated by a NUL character.
201
+ * The first letter of each entry represents current index status,
202
+ * and second the working tree. Index and working tree status codes are
203
+ * separated from the file name by a space. If an entry includes a
204
+ * renamed file, the file names are separated by a NUL character
205
+ * (e.g. `to`\0`from`)
206
+ */
207
+ const unstagedFiles = status
208
+ // eslint-disable-next-line no-control-regex
209
+ .split(/\x00(?=[ AMDRCU?!]{2} |$)/)
210
+ .filter((line) => {
211
+ const [index, workingTree] = line
212
+ const updatedInIndex = index !== ' ' && index !== '?'
213
+ const updatedInWorkingTree = workingTree !== ' ' && workingTree !== '?'
214
+
215
+ if (onlyPartial) {
216
+ return updatedInIndex && updatedInWorkingTree
217
+ }
218
+
219
+ return updatedInWorkingTree
220
+ })
221
+ .map((line) => line.slice(3)) // Remove first three letters (index, workingTree, and a whitespace)
222
+ .filter(Boolean) // Filter empty strings
223
+ debugLog(`Found ${onlyPartial ? 'partially staged' : 'unstaged'} files:`, unstagedFiles)
224
+ return unstagedFiles.length ? unstagedFiles : null
225
+ }
226
+
227
+ /**
228
+ * Create a diff of unstaged or partially staged files and backup stash if enabled.
229
+ */
230
+ async prepare(ctx, task) {
231
+ try {
232
+ debugLog(task.title)
233
+
234
+ if (ctx.shouldBackup) {
235
+ // When backup is enabled, the revert will clear ongoing merge status.
236
+ await this.backupMergeStatus()
237
+
238
+ // Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
239
+ // - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
240
+ // - git stash can't infer RD or MD states correctly, and will lose the deletion
241
+ this.deletedFiles = await this.getDeletedFiles()
242
+ }
243
+
244
+ if (ctx.shouldHideUnstaged) {
245
+ this.unstagedFiles = await this.getUnstagedFiles({ onlyPartial: false })
246
+ ctx.hasFilesToHide = !!this.unstagedFiles
247
+ } else if (ctx.shouldHidePartiallyStaged) {
248
+ this.unstagedFiles = await this.getUnstagedFiles({ onlyPartial: true })
249
+ ctx.hasFilesToHide = !!this.unstagedFiles
250
+ }
251
+
252
+ if (this.unstagedFiles) {
253
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
254
+ ctx.unstagedPatch = unstagedPatch
255
+ const files = processRenames(this.unstagedFiles)
256
+ await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
257
+ }
258
+
259
+ if (ctx.shouldBackup) {
260
+ if (ctx.shouldHideUnstaged) {
261
+ /** Save stash of all changes, clearing the working tree but keeping staged files as-is */
262
+ await this.execGit(['stash', 'push', '--keep-index', '--message', STASH])
263
+ /** Print stash list with short hash and subject */
264
+ const stashes = await this.execGit(['stash', 'list', '--format="%h %s"', '-z'])
265
+ .then(parseGitZOutput)
266
+ .then(cleanGitStashOutput)
267
+
268
+ /** The stash line starts with the short hash, so we split from space and choose the first part */
269
+ ctx.backupHash = stashes.find((line) => line.includes(STASH))?.split(' ')[0]
270
+ } else {
271
+ /** Save stash of all changes, keeping all files as-is */
272
+ const stashHash = await this.execGit(['stash', 'create'])
273
+ ctx.backupHash = await this.execGit(['rev-parse', '--short', stashHash])
274
+ await this.execGit(['stash', 'store', '--quiet', '--message', STASH, ctx.backupHash])
275
+ }
276
+
277
+ task.title = `Backed up original state in git stash (${ctx.backupHash})`
278
+ debugLog(task.title)
279
+ }
280
+ } catch (error) {
281
+ handleError(error, ctx)
282
+ }
283
+ }
284
+
285
+ async hidePartiallyStagedChanges(ctx) {
286
+ try {
287
+ const files = processRenames(this.unstagedFiles, false)
288
+ await this.execGit(['checkout', '--force', '--', ...files])
289
+ } catch (error) {
290
+ /**
291
+ * `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
292
+ * If this does fail, the handleError method will set ctx.gitError and lint-staged will fail.
293
+ */
294
+ handleError(error, ctx, HideUnstagedChangesError)
295
+ }
296
+ }
297
+
298
+ async runTasks(ctx, task, { listrTasks, concurrent }) {
299
+ if (ctx.shouldFailOnChanges) {
300
+ debugLog(
301
+ 'Calculating SHA-256 hash of unstaged changes because "--fail-on-changes" was used...'
302
+ )
303
+ const diff = await this.execGit(['diff', '--patch', '--unified=0'])
304
+ ctx.unstagedDiffSha256 = calculateSha256(diff)
305
+ debugLog('SHA-256 hash of unstaged changes is %s', ctx.unstagedDiffSha256)
306
+ }
307
+
308
+ return task.newListr(listrTasks, { concurrent })
309
+ }
310
+
311
+ /**
312
+ * Applies back task modifications, and unstaged changes hidden in the stash.
313
+ * In case of a merge-conflict retry with 3-way merge.
314
+ */
315
+ async applyModifications(ctx) {
316
+ if (ctx.shouldFailOnChanges) {
317
+ debugLog(
318
+ 'Calculating SHA-256 hash of changes after tasks because "--fail-on-changes" was used...'
319
+ )
320
+ const diff = await this.execGit(['diff', '--patch', '--unified=0'])
321
+ const diffSha256 = calculateSha256(diff)
322
+ debugLog('SHA-256 hash of changes after tasks is %s', diffSha256)
323
+ if (ctx.unstagedDiffSha256 !== diffSha256) {
324
+ ctx.errors.add(FailOnChangesError)
325
+ throw new Error('Tasks modified files and --fail-on-changes was used!')
326
+ }
327
+ }
328
+
329
+ debugLog('Adding task modifications to index...')
330
+
331
+ // `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
332
+ // Add only these files so any 3rd-party edits to other files won't be included in the commit.
333
+ // These additions per chunk are run "serially" to prevent race conditions.
334
+ // Git add creates a lockfile in the repo causing concurrent operations to fail.
335
+ for (const files of this.matchedFileChunks) {
336
+ const accessCheckedFiles = await Promise.allSettled(
337
+ files.map(async (f) => {
338
+ if (f.status === 'D') {
339
+ await fs.access(f.filepath)
340
+ return f.filepath // File is no longer deleted and can be added
341
+ } else {
342
+ return f.filepath
343
+ }
344
+ })
345
+ )
346
+
347
+ const addableFiles = accessCheckedFiles.flatMap((r) =>
348
+ r.status === 'fulfilled' ? [r.value] : []
349
+ )
350
+
351
+ await this.execGit(['add', '--', ...addableFiles])
352
+ }
353
+
354
+ debugLog('Done adding task modifications to index!')
355
+
356
+ const stagedFilesAfterAdd = await this.execGit([
357
+ ...getDiffCommand(this.diff, this.diffFilter),
358
+ '--name-only',
359
+ '-z',
360
+ ])
361
+
362
+ if (!stagedFilesAfterAdd && !this.allowEmpty) {
363
+ // Tasks reverted all staged changes and the commit would be empty
364
+ // Throw error to stop commit unless `--allow-empty` was used
365
+ handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Restore unstaged changes to partially changed files. If it at first fails,
371
+ * this is probably because of conflicts between new task modifications.
372
+ * 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
373
+ */
374
+ async restoreUnstagedChanges(ctx) {
375
+ debugLog('Restoring unstaged changes...')
376
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
377
+ try {
378
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
379
+ } catch (applyError) {
380
+ debugLog('Error while restoring changes:')
381
+ debugLog(applyError)
382
+ debugLog('Retrying with 3-way merge')
383
+ // Retry with a 3-way merge if normal apply fails
384
+ try {
385
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
386
+ } catch (threeWayApplyError) {
387
+ debugLog('Error while restoring unstaged changes using 3-way merge:')
388
+ debugLog(threeWayApplyError)
389
+ handleError(
390
+ new Error('Unstaged changes could not be restored due to a merge conflict!'),
391
+ ctx,
392
+ RestoreUnstagedChangesError
393
+ )
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Restore original HEAD state in case of errors
400
+ */
401
+ async restoreOriginalState(ctx) {
402
+ try {
403
+ debugLog('Restoring original state...')
404
+ await this.execGit(['reset', '--hard', 'HEAD'])
405
+ await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])
406
+
407
+ // Restore meta information about ongoing git merge
408
+ await this.restoreMergeStatus(ctx)
409
+
410
+ // If stashing resurrected deleted files, clean them out
411
+ await Promise.all(this.deletedFiles.map((file) => unlink(file)))
412
+
413
+ // Clean out patch
414
+ await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))
415
+
416
+ debugLog('Done restoring original state!')
417
+ } catch (error) {
418
+ handleError(error, ctx, RestoreOriginalStateError)
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Drop the created stashes after everything has run
424
+ */
425
+ async cleanup(ctx) {
426
+ try {
427
+ debugLog('Dropping backup stash...')
428
+ await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
429
+ debugLog('Done dropping backup stash!')
430
+ } catch (error) {
431
+ handleError(error, ctx)
432
+ }
433
+ }
434
+ }
@@ -0,0 +1,62 @@
1
+ import path from 'node:path'
2
+
3
+ import { createDebug } from './debug.js'
4
+
5
+ const debugLog = createDebug('lint-staged:groupFilesByConfig')
6
+
7
+ /**
8
+ * @typedef {import('./getStagedFiles.js').StagedFile} StagedFile
9
+ * @type {(args: { config: {[key: string]: { config: any; files: string[] }}; files: StagedFile[]; singleConfigMode?: boolean }) => Promise<{[key: string]: { config: any; files: StagedFile[] } }>
10
+ */
11
+ export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) => {
12
+ debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)
13
+
14
+ /** @type {Set<StagedFile>} */
15
+ const filesSet = new Set(files)
16
+
17
+ /** @type {{[key: string]: { config: any; files: StagedFile[] } }} */
18
+ const filesByConfig = {}
19
+
20
+ /** Configs are sorted deepest first by `searchConfigs` */
21
+ for (const [filepath, config] of Object.entries(configs)) {
22
+ /** When passed an explicit config object via the Node.js API‚ or an explicit path, skip logic */
23
+ if (singleConfigMode) {
24
+ filesByConfig[filepath] = { config, files }
25
+ break
26
+ }
27
+
28
+ const dir = path.normalize(path.dirname(filepath))
29
+
30
+ /** Check if file is inside directory of the configuration file */
31
+ const isInsideDir = (file) => {
32
+ const relative = path.relative(dir, file.filepath)
33
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
34
+ }
35
+
36
+ /** This config should match all files since it has a parent glob */
37
+ const includeAllFiles = Object.keys(config).some((glob) => glob.startsWith('..'))
38
+
39
+ const scopedFiles = new Set(includeAllFiles ? filesSet : undefined)
40
+
41
+ /**
42
+ * Without a parent glob, if file is inside the config file's directory,
43
+ * assign it to that configuration.
44
+ */
45
+ if (!includeAllFiles) {
46
+ filesSet.forEach((file) => {
47
+ if (isInsideDir(file)) {
48
+ scopedFiles.add(file)
49
+ }
50
+ })
51
+ }
52
+
53
+ /** Files should only match a single config */
54
+ scopedFiles.forEach((file) => {
55
+ filesSet.delete(file)
56
+ })
57
+
58
+ filesByConfig[filepath] = { config, files: Array.from(scopedFiles) }
59
+ }
60
+
61
+ return filesByConfig
62
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ type SyncGenerateTask = (stagedFileNames: readonly string[]) => string | string[]
2
+
3
+ type AsyncGenerateTask = (stagedFileNames: readonly string[]) => Promise<string | string[]>
4
+
5
+ type GenerateTask = SyncGenerateTask | AsyncGenerateTask
6
+
7
+ type TaskFunction = {
8
+ title: string
9
+ task: (stagedFileNames: readonly string[]) => void | Promise<void>
10
+ }
11
+
12
+ export type Configuration =
13
+ | Record<string, string | TaskFunction | GenerateTask | (string | GenerateTask)[]>
14
+ | GenerateTask
15
+
16
+ export type Options = {
17
+ /**
18
+ * Allow empty commits when tasks revert all staged changes
19
+ * @default false
20
+ */
21
+ allowEmpty?: boolean
22
+ /**
23
+ * Enable or disable ANSI color codes in output. By default value is auto-detected
24
+ * and controlled by `FORCE_COLOR` or `NO_COLOR` env variables.
25
+ */
26
+ color?: boolean
27
+ /**
28
+ * The number of tasks to run concurrently, or `false` to run tasks serially
29
+ * @default true
30
+ */
31
+ concurrent?: boolean | number
32
+ /**
33
+ * Manual task configuration; disables automatic config file discovery when used
34
+ */
35
+ config?: Configuration
36
+ /**
37
+ * Path to single configuration file; disables automatic config file discovery when used
38
+ */
39
+ configPath?: string
40
+ /**
41
+ * Run all tasks to completion even if one fails
42
+ * @default false
43
+ */
44
+ continueOnError?: boolean
45
+ /**
46
+ * Working directory to run all tasks in, defaults to current working directory
47
+ */
48
+ cwd?: string
49
+ /**
50
+ * Whether or not to enable debug output
51
+ * @default false
52
+ */
53
+ debug?: boolean
54
+ /**
55
+ * Override the default `--staged` flag of `git diff` to get list of files.
56
+ * @warn changing this also implies `stash: false`.
57
+ * @example HEAD...origin/main
58
+ */
59
+ diff?: string
60
+ /**
61
+ * Override the default `--diff-filter=ACMR` flag of `git diff` to get list of files
62
+ * @default "ACMR"
63
+ */
64
+ diffFilter?: string
65
+ /**
66
+ * Fail with exit code 1 when tasks modify tracked files
67
+ * @default false
68
+ */
69
+ failOnChanges?: boolean
70
+ /**
71
+ * Maximum argument string length, by default automatically detected
72
+ */
73
+ maxArgLength?: number
74
+ /**
75
+ * Whether to hide unstaged changes from partially staged files before running tasks
76
+ * @default true
77
+ */
78
+ hidePartiallyStaged?: boolean
79
+ /**
80
+ * Whether to hide all unstaged changes before running tasks
81
+ * @default false
82
+ */
83
+ hideUnstaged?: boolean
84
+ /**
85
+ * Disable lint-staged’s own console output
86
+ * @default false
87
+ */
88
+ quiet?: boolean
89
+ /**
90
+ * Pass filepaths relative to `CWD` to tasks, instead of absolute
91
+ * @default false
92
+ */
93
+ relative?: boolean
94
+ /**
95
+ * Revert to original state in case of errors
96
+ * @default true
97
+ */
98
+ revert?: boolean
99
+ /**
100
+ * Enable the backup stash, and revert in case of errors.
101
+ * @warn Disabling this also implies `hidePartiallyStaged: false`.
102
+ * @default true
103
+ */
104
+ stash?: boolean
105
+ /**
106
+ * Show task output even when tasks succeed; by default only failed output is shown
107
+ * @default false
108
+ */
109
+ verbose?: boolean
110
+ }
111
+
112
+ type LogFunction = typeof console.log
113
+
114
+ type Logger = {
115
+ log: LogFunction
116
+ warn: LogFunction
117
+ error: LogFunction
118
+ debug: LogFunction
119
+ }
120
+
121
+ /**
122
+ * @returns {boolean} `true` when all tasks were successful, `false` when some tasks failed with errors
123
+ * @throws {Error} when failed to some other errors
124
+ */
125
+ export default function lintStaged(options: Options, logger?: Logger): Promise<boolean>