@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.
- package/LICENSE +21 -0
- package/MIGRATION.md +72 -0
- package/README.md +1120 -0
- package/bin/lint-staged.js +175 -0
- package/lib/chunkFiles.js +65 -0
- package/lib/colors.js +106 -0
- package/lib/configFiles.js +30 -0
- package/lib/debug.js +27 -0
- package/lib/execGit.js +37 -0
- package/lib/figures.js +9 -0
- package/lib/file.js +53 -0
- package/lib/generateTasks.js +69 -0
- package/lib/getAbortController.js +18 -0
- package/lib/getDiffCommand.js +19 -0
- package/lib/getFunctionTask.js +37 -0
- package/lib/getRenderer.js +63 -0
- package/lib/getSpawnedTask.js +160 -0
- package/lib/getSpawnedTasks.js +76 -0
- package/lib/getStagedFiles.js +86 -0
- package/lib/gitWorkflow.js +434 -0
- package/lib/groupFilesByConfig.js +62 -0
- package/lib/index.d.ts +125 -0
- package/lib/index.js +184 -0
- package/lib/killSubprocesses.js +42 -0
- package/lib/loadConfig.js +102 -0
- package/lib/messages.js +91 -0
- package/lib/normalizePath.js +50 -0
- package/lib/parseGitZOutput.js +11 -0
- package/lib/printTaskOutput.js +12 -0
- package/lib/readStdin.js +19 -0
- package/lib/resolveConfig.js +15 -0
- package/lib/resolveGitRepo.js +65 -0
- package/lib/runAll.js +372 -0
- package/lib/searchConfigs.js +186 -0
- package/lib/state.js +102 -0
- package/lib/symbols.js +29 -0
- package/lib/validateBraces.js +95 -0
- package/lib/validateConfig.js +108 -0
- package/lib/validateOptions.js +33 -0
- package/lib/version.js +6 -0
- package/package.json +93 -0
package/lib/runAll.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/** @typedef {import('./index').Logger} Logger */
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { Listr } from 'listr2'
|
|
6
|
+
|
|
7
|
+
import { chunkFiles } from './chunkFiles.js'
|
|
8
|
+
import { blackBright } from './colors.js'
|
|
9
|
+
import { createDebug } from './debug.js'
|
|
10
|
+
import { execGit } from './execGit.js'
|
|
11
|
+
import { generateTasks } from './generateTasks.js'
|
|
12
|
+
import { getAbortController } from './getAbortController.js'
|
|
13
|
+
import { getFunctionTask, isFunctionTask } from './getFunctionTask.js'
|
|
14
|
+
import { getRenderer } from './getRenderer.js'
|
|
15
|
+
import { getSpawnedTasks } from './getSpawnedTasks.js'
|
|
16
|
+
import { getStagedFiles } from './getStagedFiles.js'
|
|
17
|
+
import { GitWorkflow } from './gitWorkflow.js'
|
|
18
|
+
import { groupFilesByConfig } from './groupFilesByConfig.js'
|
|
19
|
+
import {
|
|
20
|
+
DEPRECATED_GIT_ADD,
|
|
21
|
+
FAILED_GET_STAGED_FILES,
|
|
22
|
+
NO_STAGED_FILES,
|
|
23
|
+
NO_TASKS,
|
|
24
|
+
NOT_GIT_REPO,
|
|
25
|
+
SKIPPED_GIT_ERROR,
|
|
26
|
+
SKIPPING_HIDE_PARTIALLY_CHANGED,
|
|
27
|
+
skippingBackup,
|
|
28
|
+
} from './messages.js'
|
|
29
|
+
import { normalizePath } from './normalizePath.js'
|
|
30
|
+
import { resolveGitRepo } from './resolveGitRepo.js'
|
|
31
|
+
import { searchConfigs } from './searchConfigs.js'
|
|
32
|
+
import {
|
|
33
|
+
applyModificationsSkipped,
|
|
34
|
+
cleanupEnabled,
|
|
35
|
+
cleanupSkipped,
|
|
36
|
+
getInitialState,
|
|
37
|
+
restoreOriginalStateEnabled,
|
|
38
|
+
restoreOriginalStateSkipped,
|
|
39
|
+
restoreUnstagedChangesSkipped,
|
|
40
|
+
shouldHidePartiallyStagedFiles,
|
|
41
|
+
shouldRestoreUnstagedChanges,
|
|
42
|
+
} from './state.js'
|
|
43
|
+
import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from './symbols.js'
|
|
44
|
+
|
|
45
|
+
const debugLog = createDebug('lint-staged:runAll')
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {ReturnType<typeof getInitialState>} ctx context
|
|
49
|
+
* @param {unknown} cause error cause
|
|
50
|
+
*/
|
|
51
|
+
const createError = (ctx, cause) =>
|
|
52
|
+
Object.assign(new Error('lint-staged failed', { cause }), { ctx })
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Executes all tasks and either resolves or rejects the promise
|
|
56
|
+
*
|
|
57
|
+
* @param {object} options
|
|
58
|
+
* @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
|
|
59
|
+
* @param {boolean} [options.color] - Enable or disable ANSI color codes in output.
|
|
60
|
+
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
|
|
61
|
+
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
62
|
+
* @param {string} [options.configPath] - Explicit path to a config file
|
|
63
|
+
* @param {boolean} [options.continueOnError] - Run all tasks to completion even if one fails
|
|
64
|
+
* @param {string} [options.cwd] - Current working directory
|
|
65
|
+
* @param {boolean} [options.debug] - Enable debug mode
|
|
66
|
+
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
|
|
67
|
+
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
|
|
68
|
+
* @param {boolean} [options.failOnChanges] - Fail with exit code 1 when tasks modify tracked files
|
|
69
|
+
* @param {boolean} [options.hidePartiallyStaged] - Whether to hide unstaged changes from partially staged files before running tasks
|
|
70
|
+
* @param {boolean} [options.hideUnstaged] - Whether to hide all unstaged changes before running tasks
|
|
71
|
+
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
72
|
+
* @param {boolean} [options.quiet] - Disable lint-staged's own console output
|
|
73
|
+
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
74
|
+
* @param {boolean} [options.revert] - revert to original state in case of errors
|
|
75
|
+
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
|
|
76
|
+
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
|
|
77
|
+
* @param {Logger} logger
|
|
78
|
+
* @returns {Promise}
|
|
79
|
+
*/
|
|
80
|
+
export const runAll = async (
|
|
81
|
+
{
|
|
82
|
+
allowEmpty = false,
|
|
83
|
+
color = false,
|
|
84
|
+
concurrent = true,
|
|
85
|
+
configObject,
|
|
86
|
+
configPath,
|
|
87
|
+
continueOnError = false,
|
|
88
|
+
cwd,
|
|
89
|
+
debug = false,
|
|
90
|
+
diff,
|
|
91
|
+
diffFilter,
|
|
92
|
+
failOnChanges = false,
|
|
93
|
+
hideUnstaged = false,
|
|
94
|
+
hidePartiallyStaged = !hideUnstaged,
|
|
95
|
+
maxArgLength,
|
|
96
|
+
quiet = false,
|
|
97
|
+
relative = false,
|
|
98
|
+
// Stashing should be disabled by default when the `diff` option is used
|
|
99
|
+
stash = diff === undefined,
|
|
100
|
+
// Cannot revert to original state without stash
|
|
101
|
+
revert = stash,
|
|
102
|
+
verbose = false,
|
|
103
|
+
},
|
|
104
|
+
logger = console
|
|
105
|
+
) => {
|
|
106
|
+
debugLog('Running all linter scripts...')
|
|
107
|
+
|
|
108
|
+
// Resolve relative CWD option
|
|
109
|
+
const hasExplicitCwd = !!cwd
|
|
110
|
+
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
|
|
111
|
+
debugLog('Using working directory `%s`', cwd)
|
|
112
|
+
|
|
113
|
+
const ctx = getInitialState({
|
|
114
|
+
failOnChanges,
|
|
115
|
+
hidePartiallyStaged,
|
|
116
|
+
hideUnstaged,
|
|
117
|
+
quiet,
|
|
118
|
+
revert,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const { topLevelDir, gitConfigDir } = await resolveGitRepo(cwd)
|
|
122
|
+
if (!topLevelDir) {
|
|
123
|
+
if (!quiet) ctx.output.push(NOT_GIT_REPO)
|
|
124
|
+
ctx.errors.add(GitRepoError)
|
|
125
|
+
throw createError(ctx, GitRepoError)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Test whether we have any commits or not.
|
|
129
|
+
// Stashing must be disabled with no initial commit.
|
|
130
|
+
const hasInitialCommit = await execGit(['log', '-1'], { cwd: topLevelDir })
|
|
131
|
+
.then(() => true)
|
|
132
|
+
.catch(() => false)
|
|
133
|
+
|
|
134
|
+
// Lint-staged will create a backup stash only when there's an initial commit,
|
|
135
|
+
// and when using the default list of staged files by default
|
|
136
|
+
ctx.shouldBackup = hasInitialCommit && stash
|
|
137
|
+
if (!ctx.shouldBackup && !quiet) {
|
|
138
|
+
logger.warn(skippingBackup(hasInitialCommit, diff))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!ctx.shouldHidePartiallyStaged && !ctx.shouldHideUnstaged && !quiet) {
|
|
142
|
+
logger.warn(SKIPPING_HIDE_PARTIALLY_CHANGED)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Run staged files retrieval and config search in parallel since they're independent
|
|
146
|
+
const [stagedFiles, foundConfigs] = await Promise.all([
|
|
147
|
+
getStagedFiles({ cwd: topLevelDir, diff, diffFilter }),
|
|
148
|
+
searchConfigs({ configObject, configPath, cwd, topLevelDir }, logger),
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
if (!stagedFiles) {
|
|
152
|
+
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
|
153
|
+
ctx.errors.add(GetStagedFilesError)
|
|
154
|
+
throw createError(ctx, GetStagedFilesError)
|
|
155
|
+
}
|
|
156
|
+
debugLog('Loaded list of staged files in git:\n%O', stagedFiles)
|
|
157
|
+
|
|
158
|
+
// If there are no files avoid executing any lint-staged logic
|
|
159
|
+
if (stagedFiles.length === 0) {
|
|
160
|
+
if (!quiet) ctx.output.push(NO_STAGED_FILES)
|
|
161
|
+
return ctx
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const numberOfConfigs = Object.keys(foundConfigs).length
|
|
165
|
+
|
|
166
|
+
// Throw if no configurations were found
|
|
167
|
+
if (numberOfConfigs === 0) {
|
|
168
|
+
ctx.errors.add(ConfigNotFoundError)
|
|
169
|
+
throw createError(ctx, ConfigNotFoundError)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const filesByConfig = await groupFilesByConfig({
|
|
173
|
+
configs: foundConfigs,
|
|
174
|
+
files: stagedFiles,
|
|
175
|
+
singleConfigMode: configObject || configPath !== undefined,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const hasMultipleConfigs = numberOfConfigs > 1
|
|
179
|
+
|
|
180
|
+
// lint-staged 10 will automatically add modifications to index
|
|
181
|
+
// Warn user when their command includes `git add`
|
|
182
|
+
let hasDeprecatedGitAdd = false
|
|
183
|
+
|
|
184
|
+
const listrOptions = {
|
|
185
|
+
ctx,
|
|
186
|
+
exitOnError: false,
|
|
187
|
+
registerSignalListeners: false,
|
|
188
|
+
...getRenderer({ color, debug, quiet }, logger),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const listrTasks = []
|
|
192
|
+
|
|
193
|
+
// Set of all staged files that matched a task glob. Values in a set are unique.
|
|
194
|
+
/** @type {Set<import('./getStagedFiles.js').StagedFile>} */
|
|
195
|
+
const matchedFiles = new Set()
|
|
196
|
+
|
|
197
|
+
const abortController = getAbortController()
|
|
198
|
+
|
|
199
|
+
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
|
200
|
+
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
|
|
201
|
+
|
|
202
|
+
const stagedFileChunks = chunkFiles({ baseDir: topLevelDir, files, maxArgLength, relative })
|
|
203
|
+
|
|
204
|
+
// Use actual cwd if it's specified, or there's only a single config file.
|
|
205
|
+
// Otherwise use the directory of the config file for each config group,
|
|
206
|
+
// to make sure tasks are separated from each other.
|
|
207
|
+
const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
|
|
208
|
+
|
|
209
|
+
const chunkCount = stagedFileChunks.length
|
|
210
|
+
if (chunkCount > 1) {
|
|
211
|
+
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const [index, files] of stagedFileChunks.entries()) {
|
|
215
|
+
const chunkListrTasks = await Promise.all(
|
|
216
|
+
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
|
|
217
|
+
(isFunctionTask(task.commands)
|
|
218
|
+
? getFunctionTask(task.commands, task.fileList)
|
|
219
|
+
: getSpawnedTasks({
|
|
220
|
+
abortController,
|
|
221
|
+
color,
|
|
222
|
+
commands: task.commands,
|
|
223
|
+
continueOnError,
|
|
224
|
+
cwd: groupCwd,
|
|
225
|
+
files: task.fileList,
|
|
226
|
+
topLevelDir,
|
|
227
|
+
verbose,
|
|
228
|
+
})
|
|
229
|
+
).then((subTasks) => {
|
|
230
|
+
// Add files from task to match set
|
|
231
|
+
task.fileList.forEach((file) => {
|
|
232
|
+
// Make sure relative files are normalized to the
|
|
233
|
+
// group cwd, because other there might be identical
|
|
234
|
+
// relative filenames in the entire set.
|
|
235
|
+
const normalizedFile = path.isAbsolute(file.filepath)
|
|
236
|
+
? file
|
|
237
|
+
: {
|
|
238
|
+
filepath: normalizePath(path.join(groupCwd, file.filepath)),
|
|
239
|
+
status: file.status,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
matchedFiles.add(normalizedFile)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
hasDeprecatedGitAdd =
|
|
246
|
+
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
|
|
247
|
+
|
|
248
|
+
const fileCount = task.fileList.length
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
title: `${task.pattern}${blackBright(
|
|
252
|
+
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
|
|
253
|
+
)}`,
|
|
254
|
+
task: async (ctx, task) =>
|
|
255
|
+
task.newListr(
|
|
256
|
+
subTasks,
|
|
257
|
+
// Subtasks should not run in parallel, and should exit on error
|
|
258
|
+
{ concurrent: false, exitOnError: !continueOnError }
|
|
259
|
+
),
|
|
260
|
+
skip: () => {
|
|
261
|
+
// Skip task when no files matched
|
|
262
|
+
if (fileCount === 0) {
|
|
263
|
+
return `${task.pattern}${blackBright(' — no files')}`
|
|
264
|
+
}
|
|
265
|
+
return false
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
listrTasks.push({
|
|
273
|
+
title:
|
|
274
|
+
`${configName}${blackBright(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
275
|
+
(chunkCount > 1 ? blackBright(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
276
|
+
task: (ctx, task) =>
|
|
277
|
+
task.newListr(chunkListrTasks, { concurrent, exitOnError: !continueOnError }),
|
|
278
|
+
skip: () => {
|
|
279
|
+
// Skip if the first step (backup) failed
|
|
280
|
+
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
281
|
+
// Skip chunk when no every task is skipped (due to no matches)
|
|
282
|
+
if (chunkListrTasks.every((task) => task.skip())) {
|
|
283
|
+
return `${configName}${blackBright(' — no tasks to run')}`
|
|
284
|
+
}
|
|
285
|
+
return false
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (hasDeprecatedGitAdd && !quiet) {
|
|
292
|
+
logger.warn(DEPRECATED_GIT_ADD)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If all of the configured tasks should be skipped
|
|
296
|
+
// avoid executing any lint-staged logic
|
|
297
|
+
if (listrTasks.every((task) => task.skip())) {
|
|
298
|
+
if (!quiet) ctx.output.push(NO_TASKS)
|
|
299
|
+
return ctx
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Chunk matched files for better Windows compatibility
|
|
303
|
+
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
304
|
+
const matchedFileChunks = chunkFiles({
|
|
305
|
+
// matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
|
|
306
|
+
baseDir: cwd,
|
|
307
|
+
files: Array.from(matchedFiles),
|
|
308
|
+
maxArgLength,
|
|
309
|
+
relative: false,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const git = new GitWorkflow({
|
|
313
|
+
allowEmpty,
|
|
314
|
+
diff,
|
|
315
|
+
diffFilter,
|
|
316
|
+
failOnChanges,
|
|
317
|
+
gitConfigDir,
|
|
318
|
+
matchedFileChunks,
|
|
319
|
+
topLevelDir,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const runner = new Listr(
|
|
323
|
+
[
|
|
324
|
+
{
|
|
325
|
+
title: ctx.shouldBackup ? 'Backing up original state...' : 'Preparing lint-staged...',
|
|
326
|
+
task: (ctx, task) => git.prepare(ctx, task),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
title: 'Hiding unstaged changes to partially staged files...',
|
|
330
|
+
task: (ctx) => git.hidePartiallyStagedChanges(ctx),
|
|
331
|
+
enabled: shouldHidePartiallyStagedFiles,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
title: `Running tasks for ${diff ? 'changed' : 'staged'} files...`,
|
|
335
|
+
task: (ctx, task) => git.runTasks(ctx, task, { listrTasks, concurrent }),
|
|
336
|
+
skip: () => listrTasks.every((task) => task.skip()),
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
title: 'Applying modifications from tasks...',
|
|
340
|
+
task: (ctx) => git.applyModifications(ctx),
|
|
341
|
+
skip: applyModificationsSkipped,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
title: 'Restoring unstaged changes...',
|
|
345
|
+
task: (ctx) => git.restoreUnstagedChanges(ctx),
|
|
346
|
+
enabled: shouldRestoreUnstagedChanges,
|
|
347
|
+
skip: restoreUnstagedChangesSkipped,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
title: 'Reverting to original state because of errors...',
|
|
351
|
+
task: (ctx) => git.restoreOriginalState(ctx),
|
|
352
|
+
enabled: restoreOriginalStateEnabled,
|
|
353
|
+
skip: restoreOriginalStateSkipped,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
title: 'Cleaning up temporary files...',
|
|
357
|
+
task: (ctx) => git.cleanup(ctx),
|
|
358
|
+
enabled: cleanupEnabled,
|
|
359
|
+
skip: cleanupSkipped,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
listrOptions
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
await runner.run()
|
|
366
|
+
|
|
367
|
+
if (ctx.errors.size > 0) {
|
|
368
|
+
throw createError(ctx)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return ctx
|
|
372
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** @typedef {import('./index').Logger} Logger */
|
|
2
|
+
|
|
3
|
+
import fs, { constants } from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { CONFIG_FILE_NAMES } from './configFiles.js'
|
|
7
|
+
import { createDebug } from './debug.js'
|
|
8
|
+
import { execGit } from './execGit.js'
|
|
9
|
+
import { loadConfig } from './loadConfig.js'
|
|
10
|
+
import { normalizePath } from './normalizePath.js'
|
|
11
|
+
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
12
|
+
import { validateConfig } from './validateConfig.js'
|
|
13
|
+
|
|
14
|
+
const debugLog = createDebug('lint-staged:searchConfigs')
|
|
15
|
+
|
|
16
|
+
const EXEC_GIT = ['ls-files', '-z', '--full-name', '-t']
|
|
17
|
+
|
|
18
|
+
const CONFIG_PATHSPEC = CONFIG_FILE_NAMES.map((f) => `:(glob)**/${f}`)
|
|
19
|
+
|
|
20
|
+
const numberOfLevels = (file) => file.split('/').length
|
|
21
|
+
|
|
22
|
+
const sortAlphabetically = (a, b) => a.localeCompare(b)
|
|
23
|
+
|
|
24
|
+
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all possible config files from git
|
|
28
|
+
*
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {string} options.cwd
|
|
31
|
+
* @param {string} options.topLevelDir
|
|
32
|
+
* @returns {Promise<string[]>}
|
|
33
|
+
*/
|
|
34
|
+
const listConfigFilesFromGit = async ({ cwd, topLevelDir }) =>
|
|
35
|
+
execGit(
|
|
36
|
+
[
|
|
37
|
+
...EXEC_GIT,
|
|
38
|
+
'--cached', // show all tracked files
|
|
39
|
+
'--others', // show untracked files
|
|
40
|
+
'--exclude-standard', // apply standard git exclusions (.gitignore, etc.)
|
|
41
|
+
'--',
|
|
42
|
+
...CONFIG_PATHSPEC,
|
|
43
|
+
],
|
|
44
|
+
{ cwd }
|
|
45
|
+
)
|
|
46
|
+
.then(parseGitZOutput)
|
|
47
|
+
.then((lines) => {
|
|
48
|
+
const possibleConfigFiles = lines.flatMap((line) => {
|
|
49
|
+
/**
|
|
50
|
+
* Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
|
|
51
|
+
* The "S" status means a tracked file that is "skip-worktree"
|
|
52
|
+
* @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
|
|
53
|
+
*/
|
|
54
|
+
if (line.startsWith('S ')) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const relativePath = line.replace(/^[HSMRCK?U] /, '')
|
|
59
|
+
const absolutePath = normalizePath(path.join(topLevelDir, relativePath))
|
|
60
|
+
return [absolutePath]
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
debugLog('Found possible config files from git:', possibleConfigFiles)
|
|
64
|
+
|
|
65
|
+
return possibleConfigFiles
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all possible config files from filesystem, starting from `cwd` and
|
|
70
|
+
* moving upwards if nothing is found.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} options
|
|
73
|
+
* @param {string} options.cwd
|
|
74
|
+
* @param {string[]} [possibleConfigFiles]
|
|
75
|
+
* @returns {Promise<string[]>}
|
|
76
|
+
*/
|
|
77
|
+
export const listConfigFilesFromFs = async ({ cwd }) => {
|
|
78
|
+
debugLog('Listing possible configs from filesystem starting from "%s"...', cwd)
|
|
79
|
+
|
|
80
|
+
const results = await Promise.allSettled(
|
|
81
|
+
CONFIG_FILE_NAMES.map(async (f) => {
|
|
82
|
+
const filepath = path.join(cwd, f)
|
|
83
|
+
await fs.access(filepath, constants.F_OK)
|
|
84
|
+
return filepath
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const possibleConfigFiles = results.flatMap((r) =>
|
|
89
|
+
r.status === 'fulfilled' ? [normalizePath(r.value)] : []
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (possibleConfigFiles.length > 0) {
|
|
93
|
+
debugLog('Found possible config files from filesystem:', possibleConfigFiles)
|
|
94
|
+
return possibleConfigFiles
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parentDir = path.dirname(cwd)
|
|
98
|
+
if (parentDir === cwd) {
|
|
99
|
+
return [] /** Root-level / */
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return listConfigFilesFromFs({ cwd: parentDir })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Search all config files from the git repository, preferring those inside `cwd`.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} options
|
|
109
|
+
* @param {Object} [options.configObject] - Explicit config object from the js API
|
|
110
|
+
* @param {string} [options.configPath] - Explicit path to a config file
|
|
111
|
+
* @param {string} [options.cwd] - Current working directory
|
|
112
|
+
* @param {string} [options.topLevelDir] - Top-level directory of the git repo
|
|
113
|
+
* @param {Logger} logger
|
|
114
|
+
*
|
|
115
|
+
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
|
|
116
|
+
*/
|
|
117
|
+
export const searchConfigs = async (
|
|
118
|
+
{ configObject, configPath, cwd = process.cwd(), topLevelDir = cwd },
|
|
119
|
+
logger
|
|
120
|
+
) => {
|
|
121
|
+
debugLog('Searching for configuration files...')
|
|
122
|
+
|
|
123
|
+
// Return explicit config object from js API
|
|
124
|
+
if (configObject) {
|
|
125
|
+
debugLog('Using single direct configuration object...')
|
|
126
|
+
|
|
127
|
+
return { '': validateConfig(configObject, 'config object', logger) }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Use only explicit config path instead of discovering multiple
|
|
131
|
+
if (configPath) {
|
|
132
|
+
debugLog('Using single configuration path...')
|
|
133
|
+
|
|
134
|
+
const { config, filepath } = await loadConfig(configPath, logger)
|
|
135
|
+
|
|
136
|
+
if (!config) return {}
|
|
137
|
+
return { [configPath]: validateConfig(config, filepath, logger) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const possibleConfigFiles = new Set()
|
|
141
|
+
|
|
142
|
+
const addToSet = (files) => {
|
|
143
|
+
files.forEach((f) => {
|
|
144
|
+
possibleConfigFiles.add(f)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await Promise.all([
|
|
149
|
+
listConfigFilesFromGit({ cwd, topLevelDir }).then(addToSet),
|
|
150
|
+
listConfigFilesFromFs({ cwd }).then(addToSet),
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
/** Create object with key as config file, and value as null */
|
|
154
|
+
const configs = Array.from(possibleConfigFiles)
|
|
155
|
+
.sort(sortAlphabetically)
|
|
156
|
+
.sort(sortDeepestParth)
|
|
157
|
+
.reduce((acc, configPath) => Object.assign(acc, { [configPath]: null }), {})
|
|
158
|
+
|
|
159
|
+
/** Load and validate all configs to the above object */
|
|
160
|
+
await Promise.all(
|
|
161
|
+
Object.keys(configs).map((configPath) =>
|
|
162
|
+
loadConfig(configPath, logger).then(({ config, filepath }) => {
|
|
163
|
+
if (config) {
|
|
164
|
+
if (configPath !== filepath) {
|
|
165
|
+
debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
configs[configPath] = validateConfig(config, filepath, logger)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
/** Get validated configs from the above object, without any `null` values (not found) */
|
|
175
|
+
const foundConfigs = Object.entries(configs).reduce((acc, [key, value]) => {
|
|
176
|
+
if (value) {
|
|
177
|
+
Object.assign(acc, { [key]: value })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return acc
|
|
181
|
+
}, {})
|
|
182
|
+
|
|
183
|
+
debugLog('Found %d config files', Object.keys(foundConfigs).length)
|
|
184
|
+
|
|
185
|
+
return foundConfigs
|
|
186
|
+
}
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { GIT_ERROR, TASK_ERROR } from './messages.js'
|
|
2
|
+
import {
|
|
3
|
+
FailOnChangesError,
|
|
4
|
+
GitError,
|
|
5
|
+
RestoreOriginalStateError,
|
|
6
|
+
RestoreUnstagedChangesError,
|
|
7
|
+
TaskError,
|
|
8
|
+
} from './symbols.js'
|
|
9
|
+
|
|
10
|
+
export const getInitialState = ({
|
|
11
|
+
failOnChanges = false,
|
|
12
|
+
hideUnstaged = false,
|
|
13
|
+
hidePartiallyStaged = !hideUnstaged,
|
|
14
|
+
quiet = false,
|
|
15
|
+
revert = true,
|
|
16
|
+
} = {}) => ({
|
|
17
|
+
backupHash: null,
|
|
18
|
+
errors: new Set([]),
|
|
19
|
+
shouldFailOnChanges: failOnChanges,
|
|
20
|
+
hasFilesToHide: null,
|
|
21
|
+
output: [],
|
|
22
|
+
quiet,
|
|
23
|
+
shouldBackup: null,
|
|
24
|
+
shouldHidePartiallyStaged: hidePartiallyStaged,
|
|
25
|
+
shouldHideUnstaged: hideUnstaged,
|
|
26
|
+
shouldRevert: revert,
|
|
27
|
+
unstagedDiffSha256: null,
|
|
28
|
+
unstagedPatch: null,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export const shouldHidePartiallyStagedFiles = (ctx) =>
|
|
32
|
+
ctx.shouldHidePartiallyStaged && ctx.hasFilesToHide
|
|
33
|
+
|
|
34
|
+
export const shouldRestoreUnstagedChanges = (ctx) =>
|
|
35
|
+
(ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) && ctx.hasFilesToHide
|
|
36
|
+
|
|
37
|
+
export const applyModificationsSkipped = (ctx) => {
|
|
38
|
+
// Always apply back unstaged modifications when skipping revert or backup
|
|
39
|
+
if (!ctx.shouldRevert || !ctx.shouldBackup) return false
|
|
40
|
+
|
|
41
|
+
// Should be skipped in case of git errors
|
|
42
|
+
if (ctx.errors.has(GitError)) {
|
|
43
|
+
return GIT_ERROR
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Should be skipped when tasks fail
|
|
47
|
+
if (ctx.errors.has(TaskError)) {
|
|
48
|
+
return TASK_ERROR
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const restoreUnstagedChangesSkipped = (ctx) => {
|
|
53
|
+
// Should be skipped in case of git errors
|
|
54
|
+
if (ctx.errors.has(GitError)) {
|
|
55
|
+
return GIT_ERROR
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// When complete reverting to original state is skipped,
|
|
59
|
+
// we can still restore unstaged changes to make it easier
|
|
60
|
+
// to do manually.
|
|
61
|
+
if (!ctx.shouldRevert) {
|
|
62
|
+
false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Should be skipped when tasks fail
|
|
66
|
+
if (ctx.errors.has(TaskError)) {
|
|
67
|
+
return TASK_ERROR
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const restoreOriginalStateEnabled = (ctx) =>
|
|
72
|
+
!!ctx.shouldRevert &&
|
|
73
|
+
!!ctx.shouldBackup &&
|
|
74
|
+
(ctx.errors.has(FailOnChangesError) ||
|
|
75
|
+
ctx.errors.has(TaskError) ||
|
|
76
|
+
ctx.errors.has(RestoreUnstagedChangesError))
|
|
77
|
+
|
|
78
|
+
export const restoreOriginalStateSkipped = (ctx) => {
|
|
79
|
+
// Should be skipped in case of unknown git errors
|
|
80
|
+
if (ctx.errors.has(GitError) && !ctx.errors.has(RestoreUnstagedChangesError)) {
|
|
81
|
+
return GIT_ERROR
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const cleanupEnabled = (ctx) => ctx.shouldBackup
|
|
86
|
+
|
|
87
|
+
export const cleanupSkipped = (ctx) => {
|
|
88
|
+
// "--fail-on-changes" was used, so we shouldn't drop the backup stash
|
|
89
|
+
if (ctx.errors.has(FailOnChangesError) && !ctx.shouldRevert) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Should be skipped in case of unknown git errors
|
|
94
|
+
if (restoreOriginalStateSkipped(ctx)) {
|
|
95
|
+
return GIT_ERROR
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Should be skipped when reverting to original state fails
|
|
99
|
+
if (ctx.errors.has(RestoreOriginalStateError)) {
|
|
100
|
+
return GIT_ERROR
|
|
101
|
+
}
|
|
102
|
+
}
|
package/lib/symbols.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
|
|
2
|
+
|
|
3
|
+
export const ConfigNotFoundError = new Error('Configuration could not be found')
|
|
4
|
+
|
|
5
|
+
export const ConfigFormatError = new Error('Configuration should be an object or a function')
|
|
6
|
+
|
|
7
|
+
export const ConfigEmptyError = new Error('Configuration should not be empty')
|
|
8
|
+
|
|
9
|
+
export const GetBackupStashError = Symbol('GetBackupStashError')
|
|
10
|
+
|
|
11
|
+
export const GetStagedFilesError = Symbol('GetStagedFilesError')
|
|
12
|
+
|
|
13
|
+
export const GitError = Symbol('GitError')
|
|
14
|
+
|
|
15
|
+
export const GitRepoError = Symbol('GitRepoError')
|
|
16
|
+
|
|
17
|
+
export const HideUnstagedChangesError = Symbol('HideUnstagedChangesError')
|
|
18
|
+
|
|
19
|
+
export const InvalidOptionsError = new Error('Invalid Options')
|
|
20
|
+
|
|
21
|
+
export const RestoreMergeStatusError = Symbol('RestoreMergeStatusError')
|
|
22
|
+
|
|
23
|
+
export const RestoreOriginalStateError = Symbol('RestoreOriginalStateError')
|
|
24
|
+
|
|
25
|
+
export const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
|
|
26
|
+
|
|
27
|
+
export const TaskError = Symbol('TaskError')
|
|
28
|
+
|
|
29
|
+
export const FailOnChangesError = Symbol('FailOnChangesError')
|