Navigation Menu

Skip to content

Commit

Permalink
fix: allow lint-staged to run on empty git repo by disabling backup
Browse files Browse the repository at this point in the history
Since v10, lint-staged has not been able to run on empty git repos, because the backup stash requires at least the initial commit.

In v10.1.0 an option `--no-stash` was added to make lint-staged skip creating the stash. We can use this same logic to enable running on empty repos, and print a descriptive warning message.
  • Loading branch information
iiroj committed Apr 17, 2020
1 parent 1ac6863 commit 0bf1fb0
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 139 deletions.
7 changes: 2 additions & 5 deletions lib/gitWorkflow.js
Expand Up @@ -166,7 +166,7 @@ class GitWorkflow {
/**
* Create a diff of partially staged files and backup stash if enabled.
*/
async prepare(ctx, stash) {
async prepare(ctx, shouldBackup) {
try {
debug('Backing up original state...')

Expand All @@ -184,7 +184,7 @@ class GitWorkflow {
/**
* If backup stash should be skipped, no need to continue
*/
if (!stash) return
if (!shouldBackup) return

// Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
// - in git versions =< 2.13.0 the `--keep-index` flag resurrects deleted files
Expand All @@ -207,9 +207,6 @@ class GitWorkflow {

debug('Done backing up original state!')
} catch (error) {
if (error.message && error.message.includes('You do not have the initial commit yet')) {
ctx.emptyGitRepo = true
}
handleError(error, ctx)
}
}
Expand Down
35 changes: 19 additions & 16 deletions lib/runAll.js
Expand Up @@ -7,6 +7,7 @@ const Listr = require('listr')
const symbols = require('log-symbols')

const chunkFiles = require('./chunkFiles')
const execGit = require('./execGit')
const generateTasks = require('./generateTasks')
const getStagedFiles = require('./getStagedFiles')
const GitWorkflow = require('./gitWorkflow')
Expand Down Expand Up @@ -91,15 +92,22 @@ const runAll = async (
) => {
debugLog('Running all linter scripts')

if (!stash) {
logger.warn(
`${symbols.warning} ${chalk.yellow('Skipping backup because `--no-stash` was used.')}`
)
}

const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
if (!gitDir) throw new Error('Current directory is not a git directory!')

// Test whether we have any commits or not.
// Stashing must be disabled with no initial commit.
const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir })
.then(() => true)
.catch(() => false)

// Lint-staged should create a backup stash only when there's an initial commit
const shouldBackup = hasInitialCommit && stash
if (!shouldBackup) {
const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet'
logger.warn(`${symbols.warning} ${chalk.yellow(`Skipping backup because ${reason}.\n`)}`)
}

const files = await getStagedFiles({ cwd: gitDir })
if (!files) throw new Error('Unable to get staged files!')
debugLog('Loaded list of staged files in git:\n%O', files)
Expand Down Expand Up @@ -202,7 +210,7 @@ const runAll = async (
[
{
title: 'Preparing...',
task: ctx => git.prepare(ctx, stash)
task: ctx => git.prepare(ctx, shouldBackup)
},
{
title: 'Hiding unstaged changes to partially staged files...',
Expand All @@ -214,7 +222,7 @@ const runAll = async (
title: 'Applying modifications...',
task: ctx => git.applyModifications(ctx),
// Always apply back unstaged modifications when skipping backup
skip: ctx => stash && shouldSkipApplyModifications(ctx)
skip: ctx => shouldBackup && shouldSkipApplyModifications(ctx)
},
{
title: 'Restoring unstaged changes to partially staged files...',
Expand All @@ -226,14 +234,14 @@ const runAll = async (
title: 'Reverting to original state because of errors...',
task: ctx => git.restoreOriginalState(ctx),
enabled: ctx =>
stash &&
shouldBackup &&
(ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError),
skip: shouldSkipRevert
},
{
title: 'Cleaning up...',
task: ctx => git.cleanup(ctx),
enabled: () => stash,
enabled: () => shouldBackup,
skip: shouldSkipCleanup
}
],
Expand All @@ -251,12 +259,7 @@ const runAll = async (
} else if (error.context.gitError && !error.context.gitGetBackupStashError) {
logger.error(`\n ${symbols.error} ${chalk.red(`lint-staged failed due to a git error.`)}`)

if (error.context.emptyGitRepo) {
logger.error(
`\n The initial commit is needed for lint-staged to work.
Please use the --no-verify flag to skip running lint-staged.`
)
} else if (stash) {
if (shouldBackup) {
// No sense to show this if the backup stash itself is missing.
logger.error(` Any lost modifications can be restored from a git stash:
Expand Down
89 changes: 0 additions & 89 deletions test/__snapshots__/runAll.spec.js.snap

This file was deleted.

98 changes: 84 additions & 14 deletions test/runAll.spec.js
Expand Up @@ -17,9 +17,9 @@ resolveGitRepo.mockImplementation(async () => {
})
getStagedFiles.mockImplementation(async () => [])

const globalConsoleTemp = console

describe('runAll', () => {
const globalConsoleTemp = console

beforeAll(() => {
console = makeConsoleMock()
})
Expand All @@ -33,30 +33,52 @@ describe('runAll', () => {
})

it('should resolve the promise with no tasks', async () => {
await expect(runAll({ config: {} })).resolves
expect.assertions(1)
await expect(runAll({ config: {} })).resolves.toEqual(undefined)
})

it('should resolve the promise with no files', async () => {
expect.assertions(1)
await runAll({ config: { '*.js': ['echo "sample"'] } })
expect(console.printHistory()).toMatchSnapshot()
expect(console.printHistory()).toMatchInlineSnapshot(`
"
LOG i No staged files found."
`)
})

it('should use an injected logger', async () => {
expect.assertions(1)
const logger = makeConsoleMock()
await runAll({ config: { '*.js': ['echo "sample"'] }, debug: true }, logger)
expect(logger.printHistory()).toMatchSnapshot()
expect(logger.printHistory()).toMatchInlineSnapshot(`
"
LOG i No staged files found."
`)
})

it('should not skip tasks if there are files', async () => {
expect.assertions(1)
getStagedFiles.mockImplementationOnce(async () => ['sample.js'])
await runAll({ config: { '*.js': ['echo "sample"'] } })
expect(console.printHistory()).toMatchSnapshot()
expect(console.printHistory()).toMatchInlineSnapshot(`
"
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
LOG echo \\"sample\\" [completed]
LOG Running tasks for *.js [completed]
LOG Running tasks... [completed]
LOG Applying modifications... [started]
LOG Applying modifications... [completed]
LOG Cleaning up... [started]
LOG Cleaning up... [completed]"
`)
})

it('should skip applying unstaged modifications if there are errors during linting', async () => {
expect.assertions(1)
expect.assertions(2)
getStagedFiles.mockImplementationOnce(async () => ['sample.js'])
execa.mockImplementation(() =>
Promise.resolve({
Expand All @@ -68,12 +90,30 @@ describe('runAll', () => {
})
)

try {
await runAll({ config: { '*.js': ['echo "sample"'] } })
} catch (err) {
console.log(err)
}
expect(console.printHistory()).toMatchSnapshot()
await expect(
runAll({ config: { '*.js': ['echo "sample"'] } })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`)

expect(console.printHistory()).toMatchInlineSnapshot(`
"
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
LOG echo \\"sample\\" [failed]
LOG →
LOG Running tasks for *.js [failed]
LOG →
LOG Running tasks... [failed]
LOG Applying modifications... [started]
LOG Applying modifications... [skipped]
LOG → Skipped because of errors from tasks.
LOG Reverting to original state because of errors... [started]
LOG Reverting to original state because of errors... [completed]
LOG Cleaning up... [started]
LOG Cleaning up... [completed]"
`)
})

it('should skip tasks and restore state if terminated', async () => {
Expand All @@ -96,7 +136,37 @@ describe('runAll', () => {
} catch (err) {
console.log(err)
}
expect(console.printHistory()).toMatchSnapshot()

expect(console.printHistory()).toMatchInlineSnapshot(`
"
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
LOG echo \\"sample\\" [failed]
LOG →
LOG Running tasks for *.js [failed]
LOG →
LOG Running tasks... [failed]
LOG Applying modifications... [started]
LOG Applying modifications... [skipped]
LOG → Skipped because of errors from tasks.
LOG Reverting to original state because of errors... [started]
LOG Reverting to original state because of errors... [completed]
LOG Cleaning up... [started]
LOG Cleaning up... [completed]
LOG {
name: 'ListrError',
errors: [
{
privateMsg: '\\\\n\\\\n\\\\n‼ echo was terminated with SIGINT',
context: {taskError: true}
}
],
context: {taskError: true}
}"
`)
})
})

Expand Down

0 comments on commit 0bf1fb0

Please sign in to comment.