From 40a5db1f6b1ad17b5a593974b6db93015f50824c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Sun, 21 Jul 2019 11:40:12 +0300 Subject: [PATCH] feat: use git stashes for gitWorkflow --- src/gitWorkflow.js | 249 ++++++------ src/runAll.js | 31 +- test/__mocks__/gitWorkflow.js | 16 +- test/__snapshots__/runAll.spec.js.snap | 132 +------ test/execGit.spec.js | 22 ++ test/gitStash.spec.js | 507 ------------------------- test/gitWorkflow.spec.js | 32 -- test/runAll.spec.js | 127 +------ 8 files changed, 175 insertions(+), 941 deletions(-) create mode 100644 test/execGit.spec.js delete mode 100644 test/gitStash.spec.js delete mode 100644 test/gitWorkflow.spec.js diff --git a/src/gitWorkflow.js b/src/gitWorkflow.js index 404d23cef..422712cb3 100644 --- a/src/gitWorkflow.js +++ b/src/gitWorkflow.js @@ -1,164 +1,133 @@ 'use strict' -const del = require('del') const debug = require('debug')('lint-staged:git') const execGit = require('./execGit') -let workingCopyTree = null -let indexTree = null -let formattedIndexTree = null - -async function writeTree(options) { - return execGit(['write-tree'], options) +const STASH_ORGINAL = 'lint-staged backup (original state)' +const STASH_MODIFICATIONS = 'lint-staged backup (modifications)' + +let unstagedDiff + +/** + * From `array` of strings find index number of string containing `test` string + * + * @param {Array} array - Array of strings + * @param {string} test - Test string + * @returns {number} - Index number + */ +const findIndex = (array, test) => array.findIndex(line => line.includes(test)) + +/** + * Get names of stashes + * + * @param {Object} [options] + * @returns {Promise} + */ +async function getStashes(options) { + const stashList = (await execGit(['stash', 'list'], options)).split('\n') + return { + original: `stash@{${findIndex(stashList, STASH_ORGINAL)}}`, + modifications: `stash@{${findIndex(stashList, STASH_MODIFICATIONS)}}` + } } -async function getDiffForTrees(tree1, tree2, options) { - debug(`Generating diff between trees ${tree1} and ${tree2}...`) - return execGit( - [ - 'diff-tree', - '--ignore-submodules', - '--binary', - '--no-color', - '--no-ext-diff', - '--unified=0', - tree1, - tree2 - ], +/** + * Create backup stashes, one of everything and one of only staged changes + * Leves stages files in index for running tasks + * + * @param {Object} [options] + * @returns {Promise} + */ +async function backupOriginalState(options) { + debug('Backing up original state...') + + // Get stash of entire original state, including unstaged changes + // Keep index so that tasks only work on those files + await execGit(['stash', 'save', '--include-untracked', '--keep-index', STASH_ORGINAL], options) + + // Since only staged files are now present, get a diff of unstaged changes + // by comparing current index against original stash, but in reverse + const { original } = await getStashes(options) + unstagedDiff = await execGit( + ['diff', '--unified=0', '--no-color', '--no-ext-diff', '-p', original, '-R'], options ) -} - -async function hasPartiallyStagedFiles(options) { - const stdout = await execGit(['status', '--porcelain'], options) - if (!stdout) return false - - const changedFiles = stdout.split('\n') - const partiallyStaged = changedFiles.filter(line => { - /** - * See https://git-scm.com/docs/git-status#_short_format - * The first letter of the line represents current index status, - * and second the working tree - */ - const [index, workingTree] = line - return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?' - }) - return partiallyStaged.length > 0 + debug('Done backing up original state!') } -// eslint-disable-next-line -async function gitStashSave(options) { - debug('Stashing files...') - // Save ref to the current index - indexTree = await writeTree(options) - // Add working copy changes to index - await execGit(['add', '.'], options) - // Save ref to the working copy index - workingCopyTree = await writeTree(options) - // Restore the current index - await execGit(['read-tree', indexTree], options) - // Remove all modifications - await execGit(['checkout-index', '-af'], options) - // await execGit(['clean', '-dfx'], options) - debug('Done stashing files!') - return [workingCopyTree, indexTree] -} +/** + * Resets everything and applies back unstaged and staged changes, + * possibly with modifications by tasks + * + * @param {Object} [options] + * @returns {Promise} + */ +async function applyModifications(options) { + debug('Applying modifications by tasks...') + + // Save index with possible modifications by tasks. + await execGit(['stash', 'save', STASH_MODIFICATIONS], options) + // Reset HEAD + await execGit(['reset'], options) + await execGit(['checkout', '.'], options) + + // Get diff of index against reseted HEAD, this includes all staged changes, + // with possible changes by tasks + const { modifications } = await getStashes(options) + const stagedDiff = await execGit( + ['diff', '--unified=0', '--no-color', '--no-ext-diff', 'HEAD', '-p', modifications], + options + ) -async function updateStash(options) { - formattedIndexTree = await writeTree(options) - return formattedIndexTree -} + await execGit(['apply', '-v', '--index', '--whitespace=nowarn', '--recount', '--unidiff-zero'], { + ...options, + input: `${stagedDiff}\n` + }) -async function applyPatchFor(tree1, tree2, options) { - const diff = await getDiffForTrees(tree1, tree2, options) - /** - * This is crucial for patch to work - * For some reason, git-apply requires that the patch ends with the newline symbol - * See http://git.661346.n2.nabble.com/Bug-in-Git-Gui-Creates-corrupt-patch-td2384251.html - * and https://stackoverflow.com/questions/13223868/how-to-stage-line-by-line-in-git-gui-although-no-newline-at-end-of-file-warnin - */ - // TODO: Figure out how to test this. For some reason tests were working but in the real env it was failing - if (diff) { - try { - /** - * Apply patch to index. We will apply it with --reject so it it will try apply hunk by hunk - * We're not interested in failied hunks since this mean that formatting conflicts with user changes - * and we prioritize user changes over formatter's - */ - await execGit( - ['apply', '-v', '--whitespace=nowarn', '--reject', '--recount', '--unidiff-zero'], - { - ...options, - input: `${diff}\n` // TODO: This should also work on Windows but test would be good - } - ) - } catch (err) { - debug('Could not apply patch to the stashed files cleanly') - debug(err) - debug('Patch content:') - debug(diff) - throw new Error('Could not apply patch to the stashed files cleanly.', err) - } + if (unstagedDiff) { + await execGit(['apply', '-v', '--whitespace=nowarn', '--recount', '--unidiff-zero'], { + ...options, + input: `${unstagedDiff}\n` + }) } -} -async function gitStashPop(options) { - if (workingCopyTree === null) { - throw new Error('Trying to restore from stash but could not find working copy stash.') - } + debug('Done applying modifications by tasks!') +} - debug('Restoring working copy') - // Restore the stashed files in the index - await execGit(['read-tree', workingCopyTree], options) - // and sync it to the working copy (i.e. update files on fs) - await execGit(['checkout-index', '-af'], options) - - // Then, restore the index after working copy is restored - if (indexTree !== null && formattedIndexTree === null) { - // Restore changes that were in index if there are no formatting changes - debug('Restoring index') - await execGit(['read-tree', indexTree], options) - } else { - /** - * There are formatting changes we want to restore in the index - * and in the working copy. So we start by restoring the index - * and after that we'll try to carry as many as possible changes - * to the working copy by applying the patch with --reject option. - */ - debug('Restoring index with formatting changes') - await execGit(['read-tree', formattedIndexTree], options) - try { - await applyPatchFor(indexTree, formattedIndexTree, options) - } catch (err) { - debug( - 'Found conflicts between formatters and local changes. Formatters changes will be ignored for conflicted hunks.' - ) - /** - * Clean up working directory from *.rej files that contain conflicted hanks. - * These hunks are coming from formatters so we'll just delete them since they are irrelevant. - */ - try { - const rejFiles = await del(['*.rej'], options) - debug('Deleted files and folders:\n', rejFiles.join('\n')) - } catch (delErr) { - debug('Error deleting *.rej files', delErr) - } - } - } - // Clean up references - workingCopyTree = null - indexTree = null - formattedIndexTree = null +/** + * Restore original HEAD state in case of errors + * + * @param {Object} [options] + * @returns {Promise} + */ +async function restoreOriginalState(options) { + debug('Restoring original state...') + const { original } = await getStashes(options) + await execGit(['reset', '--hard', 'HEAD'], options) + await execGit(['stash', 'apply', '--index', original], options) + debug('Done restoring original state!') +} - return null +/** + * Drop the created stashes after everything has run + * + * @param {Object} [options] + * @returns {Promise} + */ +async function dropBackupStashes(options) { + debug('Dropping backup stash...') + const { original, modifications } = await getStashes(options) + await execGit(['stash', 'drop', original], options) + await execGit(['stash', 'drop', modifications], options) + debug('Done dropping backup stash!') } module.exports = { execGit, - gitStashSave, - gitStashPop, - hasPartiallyStagedFiles, - updateStash + backupOriginalState, + applyModifications, + restoreOriginalState, + dropBackupStashes } diff --git a/src/runAll.js b/src/runAll.js index 5f4d652f0..82c85b1b9 100644 --- a/src/runAll.js +++ b/src/runAll.js @@ -105,33 +105,26 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com return new Listr( [ { - title: 'Stashing changes...', - skip: async () => { - const hasPSF = await git.hasPartiallyStagedFiles({ cwd: gitDir }) - if (!hasPSF) { - return 'No partially staged files found...' - } - return false - }, - task: ctx => { - ctx.hasStash = true - return git.gitStashSave({ cwd: gitDir }) - } + title: 'Backing up...', + task: () => git.backupOriginalState({ cwd: gitDir }) }, { title: 'Running tasks...', task: () => new Listr(tasks, { ...listrOptions, concurrent: true, exitOnError: false }) }, { - title: 'Updating stash...', - enabled: ctx => ctx.hasStash, - skip: ctx => ctx.hasErrors && 'Skipping stash update since some tasks exited with errors', - task: () => git.updateStash({ cwd: gitDir }) + title: 'Applying modifications by tasks...', + skip: ctx => ctx.hasErrors, + task: () => git.applyModifications({ cwd: gitDir }) + }, + { + title: 'Restoring original state due to errors...', + enabled: ctx => ctx.hasErrors, + task: () => git.restoreOriginalState({ cwd: gitDir }) }, { - title: 'Restoring local changes...', - enabled: ctx => ctx.hasStash, - task: () => git.gitStashPop({ cwd: gitDir }) + title: 'Clearing backup...', + task: () => git.dropBackupStashes({ cwd: gitDir }) } ], listrOptions diff --git a/test/__mocks__/gitWorkflow.js b/test/__mocks__/gitWorkflow.js index dca093f73..6692f2577 100644 --- a/test/__mocks__/gitWorkflow.js +++ b/test/__mocks__/gitWorkflow.js @@ -1,11 +1,11 @@ -const hasPartiallyStagedFiles = jest.fn().mockImplementation(() => Promise.resolve(false)) -const gitStashSave = jest.fn().mockImplementation(() => Promise.resolve(null)) -const gitStashPop = jest.fn().mockImplementation(() => Promise.resolve(null)) -const updateStash = jest.fn().mockImplementation(() => Promise.resolve(null)) +const backupOriginalState = jest.fn().mockImplementation(() => Promise.resolve(null)) +const applyModifications = jest.fn().mockImplementation(() => Promise.resolve(null)) +const restoreOriginalState = jest.fn().mockImplementation(() => Promise.resolve(null)) +const dropBackupStashes = jest.fn().mockImplementation(() => Promise.resolve(null)) module.exports = { - gitStashSave, - gitStashPop, - updateStash, - hasPartiallyStagedFiles + backupOriginalState, + applyModifications, + restoreOriginalState, + dropBackupStashes } diff --git a/test/__snapshots__/runAll.spec.js.snap b/test/__snapshots__/runAll.spec.js.snap index 85a9f214e..4351a59f2 100644 --- a/test/__snapshots__/runAll.spec.js.snap +++ b/test/__snapshots__/runAll.spec.js.snap @@ -1,117 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`runAll should not skip stashing and restoring if there are partially staged files 1`] = ` -" -LOG Stashing changes... [started] -LOG Stashing changes... [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 Updating stash... [started] -LOG Updating stash... [completed] -LOG Restoring local changes... [started] -LOG Restoring local changes... [completed]" -`; - exports[`runAll should not skip tasks if there are files 1`] = ` " -LOG Stashing changes... [started] -LOG Stashing changes... [skipped] -LOG → No partially staged files found... +LOG Backing up... [started] +LOG Backing up... [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 Running tasks... [completed] +LOG Applying modifications by tasks... [started] +LOG Applying modifications by tasks... [completed] +LOG Clearing backup... [started] +LOG Clearing backup... [completed]" `; -exports[`runAll should reject promise when error during getStagedFiles 1`] = `"Unable to get staged files!"`; - exports[`runAll should resolve the promise with no files 1`] = ` " LOG No staged files match any of provided globs." `; -exports[`runAll should skip linters and stash update but perform working copy restore if terminated 1`] = ` -" -LOG Stashing changes... [started] -LOG Stashing changes... [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 Updating stash... [started] -LOG Updating stash... [skipped] -LOG → Skipping stash update since some tasks exited with errors -LOG Restoring local changes... [started] -LOG Restoring local changes... [completed] -LOG { - name: 'ListrError', - errors: [ - { - privateMsg: '\\\\n\\\\n\\\\n‼ echo \\"sample\\" was terminated with SIGINT', - context: {hasStash: true, hasErrors: true} - } - ], - context: {hasStash: true, hasErrors: true} -}" -`; - -exports[`runAll should skip stashing and restoring if there are no partially staged files 1`] = ` -" -LOG Stashing changes... [started] -LOG Stashing changes... [skipped] -LOG → No partially staged files found... -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]" -`; - -exports[`runAll should skip stashing changes if no lint-staged files are changed 1`] = ` -" -LOG No staged files match any of provided globs." -`; - -exports[`runAll should skip updating stash if there are errors during linting 1`] = ` -" -LOG Stashing changes... [started] -LOG Stashing changes... [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 Updating stash... [started] -LOG Updating stash... [skipped] -LOG → Skipping stash update since some tasks exited with errors -LOG Restoring local changes... [started] -LOG Restoring local changes... [completed] -LOG { - name: 'ListrError', - errors: [ - { - privateMsg: '\\\\n\\\\n\\\\n× echo \\"sample\\" found some errors. Please fix them and try committing again.\\\\n\\\\nLinter finished with error', - context: {hasStash: true, hasErrors: true} - } - ], - context: {hasStash: true, hasErrors: true} -}" -`; - exports[`runAll should use an injected logger 1`] = ` " LOG No staged files match any of provided globs." @@ -122,25 +31,16 @@ exports[`runAll should warn if the argument length is longer than what the platf WARN ‼ lint-staged generated an argument string of 999999 characters, and commands might not run correctly on your platform. It is recommended to use functions as linters and split your command based on the number of staged files. For more info, please visit: https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-commands -LOG Stashing changes... [started] -LOG Stashing changes... [skipped] -LOG → No partially staged files found... +LOG Backing up... [started] +LOG Backing up... [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 { - name: 'ListrError', - errors: [ - { - privateMsg: '\\\\n\\\\n\\\\n× echo \\"sample\\" found some errors. Please fix them and try committing again.\\\\n\\\\nLinter finished with error', - context: {hasErrors: true} - } - ], - context: {hasErrors: true} -}" +LOG echo \\"sample\\" [completed] +LOG Running tasks for *.js [completed] +LOG Running tasks... [completed] +LOG Applying modifications by tasks... [started] +LOG Applying modifications by tasks... [completed] +LOG Clearing backup... [started] +LOG Clearing backup... [completed]" `; diff --git a/test/execGit.spec.js b/test/execGit.spec.js new file mode 100644 index 000000000..5499ceb19 --- /dev/null +++ b/test/execGit.spec.js @@ -0,0 +1,22 @@ +/* eslint no-underscore-dangle: 0 */ + +import path from 'path' +import tmp from 'tmp' +import execa from 'execa' +import execGit from '../src/execGit' + +tmp.setGracefulCleanup() + +describe('execGit', () => { + it('should execute git in process.cwd if working copy is not specified', async () => { + const cwd = process.cwd() + await execGit(['init', 'param']) + expect(execa).toHaveBeenCalledWith('git', ['init', 'param'], { cwd }) + }) + + it('should execute git in a given working copy', async () => { + const cwd = path.join(process.cwd(), 'test', '__fixtures__') + await execGit(['init', 'param'], { cwd }) + expect(execa).toHaveBeenCalledWith('git', ['init', 'param'], { cwd }) + }) +}) diff --git a/test/gitStash.spec.js b/test/gitStash.spec.js deleted file mode 100644 index 27c95f295..000000000 --- a/test/gitStash.spec.js +++ /dev/null @@ -1,507 +0,0 @@ -const execa = require('execa') -const path = require('path') -const tmp = require('tmp') -const gitflow = require('../src/gitWorkflow') -const fs = require('fs-extra') - -tmp.setGracefulCleanup() -jest.unmock('execa') - -let wcDir -let wcDirPath -let gitOpts -const initialContent = `module.exports = { - test: 'test2' -} -` - -async function gitStatus(opts = gitOpts) { - return gitflow.execGit(['status', '--porcelain'], opts) -} - -async function readFile(filepath, dir = wcDirPath) { - const content = await fs.readFile(path.join(dir, filepath), { encoding: 'utf-8' }) - return content.replace(/\r\n/g, '\n') -} - -describe('git', () => { - beforeEach(async () => { - wcDir = tmp.dirSync({ unsafeCleanup: true }) - wcDirPath = wcDir.name - gitOpts = { - cwd: wcDirPath - } - - // Init repository - await gitflow.execGit('init', gitOpts) - // Create JS file - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: 'test', - - foo: 'bar' -} -` - ) - await fs.writeFile( - path.join(wcDirPath, 'test.css'), - `.test { - border: 1px solid green; -} -` - ) - await gitflow.execGit(['config', 'user.name', '"test"'], gitOpts) - await gitflow.execGit(['config', 'user.email', '"test@test.com"'], gitOpts) - // Track all files - await gitflow.execGit(['add', '.'], gitOpts) - // Create inital commit - await gitflow.execGit(['commit', '-m', '"Initial commit"'], gitOpts) - // Update one of the files - await fs.writeFile(path.join(wcDirPath, 'test.css'), '.test { border: red; }') - // Update one of the files - await fs.writeFile(path.join(wcDirPath, 'test.js'), initialContent) - }) - - afterEach(() => { - wcDir.removeCallback() - }) - - describe('hasPartiallyStagedFiles', () => { - it('should return false if files are not staged', async () => { - const res = await gitflow.hasPartiallyStagedFiles(gitOpts) - expect(res).toEqual(false) - }) - - it('should return false if there are no modified files exist', async () => { - await gitflow.execGit(['checkout', '.'], gitOpts) - const res = await gitflow.hasPartiallyStagedFiles(gitOpts) - expect(res).toEqual(false) - }) - - it('should return false if changes are already in the index', async () => { - await gitflow.execGit(['checkout', 'test.css'], gitOpts) - await gitflow.execGit(['add', 'test.js'], gitOpts) - const res = await gitflow.hasPartiallyStagedFiles(gitOpts) - expect(res).toEqual(false) - }) - - it('should return false if there are untracked files', async () => { - const touch = process.platform === 'win32' ? 'echo.>' : 'touch' - await execa(touch, ['untracked.file'], gitOpts) - const res = await gitflow.hasPartiallyStagedFiles(gitOpts) - expect(res).toEqual(false) - }) - - it('should return true if files are modified and in the index', async () => { - await gitflow.execGit(['checkout', 'test.css'], gitOpts) - await gitflow.execGit(['add', 'test.js'], gitOpts) - await fs.writeFile(path.join(wcDirPath, 'test.js'), '') - const res = await gitflow.hasPartiallyStagedFiles(gitOpts) - expect(res).toEqual(true) - }) - }) - - describe('gitStashSave/gitStashPop', () => { - it('should stash and restore WC state without a commit', async () => { - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - M test.js" -`) - - // Add test.js to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - - // Stashing files - await gitflow.gitStashSave(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - }) - - it('should not re-create deleted files as untracked files', async () => { - // Delete test.js - await gitflow.execGit(['checkout', 'test.js'], gitOpts) - await gitflow.execGit(['rm', 'test.js'], gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -D test.js" -`) - - // Stashing files - await gitflow.gitStashSave(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(`"D test.js"`) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -D test.js" -`) - }) - - it('should handle renamed files', async () => { - // Delete test.js - await gitflow.execGit(['checkout', 'test.js'], gitOpts) - await gitflow.execGit(['mv', 'test.js', 'test-renamed.js'], gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -"R test.js -> test-renamed.js - M test.css" -`) - - // Stashing files - await gitflow.gitStashSave(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(`"R test.js -> test-renamed.js"`) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -"R test.js -> test-renamed.js - M test.css" -`) - }) - - it('should handle rename and reset (both deleted and untracked) files', async () => { - // Delete test.js - await gitflow.execGit(['checkout', 'test.js'], gitOpts) - await gitflow.execGit(['mv', 'test.js', 'test-renamed.js'], gitOpts) - await gitflow.execGit(['reset', '.'], gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - D test.js -?? test-renamed.js" -`) - - // Stashing files - await gitflow.gitStashSave(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(`"?? test-renamed.js"`) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -?? test-renamed.js" -`) - }) - - it('should drop hooks fixes when aborted', async () => { - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - M test.js" -`) - - // Add test.js to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - // Save diff for the reference - const initialIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // Expect test.js is in index - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Stashing state - await gitflow.gitStashSave(gitOpts) - - // Only index should remain - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Do additional edits (imitate eslint --fix) - const eslintContent = `module.exports = { - test: 'test2', - test: 'test2', - test: 'test2', - test: 'test2', -};` - await fs.writeFile(path.join(wcDirPath, 'test.js'), eslintContent) - - // Expect both indexed and modified state on one file - expect(await gitStatus()).toMatchInlineSnapshot(`"MM test.js"`) - // and index isn't modified - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - // Expect stashed files to be back - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - // and modification are gone - expect(await readFile('test.js')).toEqual(initialContent) - // Expect no modifications in index - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - }) - - it('should drop hooks fixes and revert to user modifications when aborted', async () => { - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - M test.js" -`) - - // Add test.js to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - // Save diff for the reference - const initialIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // User does additional edits - const userContent = `module.exports = { - test: 'test2', - test: 'test3', -}` - await fs.writeFile(path.join(wcDirPath, 'test.js'), userContent) - - // Expect test.js is in both index and modified - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -MM test.js" -`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Stashing state - await gitflow.gitStashSave(gitOpts) - - // Only index should remain - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - - // Do additional edits (imitate eslint --fix) - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: 'test2', -};` - ) - - // Expect both indexed and modified state on one file - expect(await gitStatus()).toMatchInlineSnapshot(`"MM test.js"`) - // and index isn't modified - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - - // Expect stashed files to be back - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -MM test.js" -`) - // and content is back to user modifications - expect(await readFile('test.js')).toEqual(userContent) - // Expect no modifications in index - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - }) - - it('should add hooks fixes to index when not aborted', async () => { - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - M test.js" -`) - - // Add test.js to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - // Save diff for the reference - const initialIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Stashing state - await gitflow.gitStashSave(gitOpts) - - // Only index should remain - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - - // Do additional edits (imitate eslint --fix) - const newContent = `module.exports = { - test: "test2", -};` - await fs.writeFile(path.join(wcDirPath, 'test.js'), newContent) - // and add to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - await gitflow.updateStash(gitOpts) - const newIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // Expect only index changes - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - // and index is modified - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).not.toEqual(initialIndex) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(newIndex) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - - // Expect stashed files to be back - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -M test.js" -`) - // and content keeps linter modifications - expect(await readFile('test.js')).toEqual(newContent) - // Expect modifications in index - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(newIndex) - }) - - it('should add hooks fixes to index and keep user modifications when not aborted', async () => { - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css - M test.js" -`) - - // Add test.js to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - // Save diff for the reference - const initialIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // User does additional edits - const userContent = `module.exports = { - test: 'test2', - test: 'test3' -}` - await fs.writeFile(path.join(wcDirPath, 'test.js'), userContent) - - // Expect test.js is in both index and modified - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -MM test.js" -`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Stashing state - await gitflow.gitStashSave(gitOpts) - - // Only index should remain - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Do additional edits (imitate eslint --fix) - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: "test2" -};` - ) - // and add to index - await gitflow.execGit(['add', 'test.js'], gitOpts) - await gitflow.updateStash(gitOpts) - const newIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // Expect index is modified - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).not.toEqual(initialIndex) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(newIndex) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - - // Expect stashed files to be back - expect(await gitStatus()).toMatchInlineSnapshot(` -" M test.css -MM test.js" -`) - // and content is back to user modifications - expect(await readFile('test.js')).toEqual(userContent) - // Expect formatting changes in the index - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(newIndex) - }) - - it('should add hooks fixes to index and working copy on partially staged files', async () => { - // Start with a clean state - await gitflow.execGit(['checkout', '--', '.'], gitOpts) - - // Do additional edits and stage them - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: 'test', - - - - - - - foo: ' - baz - ' -}` - ) - await gitflow.execGit(['add', 'test.js'], gitOpts) - - // Do additional edits without adding to index - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: 'edited', - - - - - - - foo: ' - baz - ' -}` - ) - - expect(await gitStatus()).toMatchInlineSnapshot(`"MM test.js"`) - // Save diff for the reference - const initialIndex = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // Stashing state - await gitflow.gitStashSave(gitOpts) - - // Only index should remain - expect(await gitStatus()).toMatchInlineSnapshot(`"M test.js"`) - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(initialIndex) - - // Imitate running prettier on the version from the index - await fs.writeFile( - path.join(wcDirPath, 'test.js'), - `module.exports = { - test: "test", - - - - - - - foo: "baz" -};` - ) - await gitflow.execGit(['add', 'test.js'], gitOpts) - await gitflow.updateStash(gitOpts) - const indexAfterEslint = await gitflow.execGit(['diff', '--cached'], gitOpts) - - // Restoring state - await gitflow.gitStashPop(gitOpts) - - // Expect stashed files to be back - expect(await gitStatus()).toMatchInlineSnapshot(`"MM test.js"`) - // and all lint-staged modifications to be gone - expect(await gitflow.execGit(['diff', '--cached'], gitOpts)).toEqual(indexAfterEslint) - expect(await readFile('test.js')).toEqual(`module.exports = { - test: 'edited', - - - - - - - foo: "baz" -};`) - }) - }) -}) diff --git a/test/gitWorkflow.spec.js b/test/gitWorkflow.spec.js deleted file mode 100644 index 2ba98bcb6..000000000 --- a/test/gitWorkflow.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ - -import path from 'path' -import tmp from 'tmp' -import execa from 'execa' -import { execGit, gitStashPop } from '../src/gitWorkflow' - -tmp.setGracefulCleanup() - -describe('gitWorkflow', () => { - describe('execGit', () => { - it('should execute git in process.cwd if working copy is not specified', async () => { - const cwd = process.cwd() - await execGit(['init', 'param']) - expect(execa).toHaveBeenCalledWith('git', ['init', 'param'], { cwd }) - }) - - it('should execute git in a given working copy', async () => { - const cwd = path.join(process.cwd(), 'test', '__fixtures__') - await execGit(['init', 'param'], { cwd }) - expect(execa).toHaveBeenCalledWith('git', ['init', 'param'], { cwd }) - }) - }) - - describe('gitStashPop', () => { - it('should throw when workingTree is null', () => { - expect(gitStashPop()).rejects.toThrowErrorMatchingInlineSnapshot( - `"Trying to restore from stash but could not find working copy stash."` - ) - }) - }) -}) diff --git a/test/runAll.spec.js b/test/runAll.spec.js index 696e6c7fd..34e137be9 100644 --- a/test/runAll.spec.js +++ b/test/runAll.spec.js @@ -1,11 +1,9 @@ import makeConsoleMock from 'consolemock' -import execa from 'execa' import normalize from 'normalize-path' import resolveGitDir from '../src/resolveGitDir' import getStagedFiles from '../src/getStagedFiles' import runAll from '../src/runAll' -import { hasPartiallyStagedFiles, gitStashSave, gitStashPop, updateStash } from '../src/gitWorkflow' jest.mock('../src/resolveGitDir') jest.mock('../src/getStagedFiles') @@ -23,9 +21,6 @@ describe('runAll', () => { afterEach(() => { console.clearHistory() - gitStashSave.mockClear() - gitStashPop.mockClear() - updateStash.mockClear() }) afterAll(() => { @@ -53,69 +48,7 @@ describe('runAll', () => { expect(console.printHistory()).toMatchSnapshot() }) - 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() - }) - - 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() - }) - - it('should not skip stashing and restoring if there are partially staged files', async () => { - expect.assertions(4) - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(true)) - getStagedFiles.mockImplementationOnce(async () => ['sample.js']) - await runAll({ config: { '*.js': ['echo "sample"'] } }) - expect(gitStashSave).toHaveBeenCalledTimes(1) - expect(updateStash).toHaveBeenCalledTimes(1) - expect(gitStashPop).toHaveBeenCalledTimes(1) - expect(console.printHistory()).toMatchSnapshot() - }) - - it('should skip stashing and restoring if there are no partially staged files', async () => { - expect.assertions(4) - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(false)) - getStagedFiles.mockImplementationOnce(async () => ['sample.js']) - await runAll({ config: { '*.js': ['echo "sample"'] } }) - expect(gitStashSave).toHaveBeenCalledTimes(0) - expect(updateStash).toHaveBeenCalledTimes(0) - expect(gitStashPop).toHaveBeenCalledTimes(0) - expect(console.printHistory()).toMatchSnapshot() - }) - - it('should skip updating stash if there are errors during linting', async () => { - expect.assertions(4) - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(true)) - getStagedFiles.mockImplementationOnce(async () => ['sample.js']) - execa.mockImplementation(() => - Promise.resolve({ - stdout: '', - stderr: 'Linter finished with error', - code: 1, - failed: true, - cmd: 'mock cmd' - }) - ) - - try { - await runAll({ config: { '*.js': ['echo "sample"'] } }) - } catch (err) { - console.log(err) - } - expect(console.printHistory()).toMatchSnapshot() - expect(gitStashSave).toHaveBeenCalledTimes(1) - expect(updateStash).toHaveBeenCalledTimes(0) - expect(gitStashPop).toHaveBeenCalledTimes(1) - }) - it('should warn if the argument length is longer than what the platform can handle', async () => { - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(false)) getStagedFiles.mockImplementationOnce(async () => new Array(100000).fill('sample.js')) try { @@ -126,61 +59,17 @@ describe('runAll', () => { expect(console.printHistory()).toMatchSnapshot() }) - it('should skip linters and stash update but perform working copy restore if terminated', async () => { - expect.assertions(4) - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(true)) - getStagedFiles.mockImplementationOnce(async () => ['sample.js']) - execa.mockImplementation(() => - Promise.resolve({ - stdout: '', - stderr: '', - code: 0, - failed: false, - killed: true, - signal: 'SIGINT', - cmd: 'mock cmd' - }) - ) - - try { - await runAll({ config: { '*.js': ['echo "sample"'] } }) - } catch (err) { - console.log(err) - } - expect(console.printHistory()).toMatchSnapshot() - expect(gitStashSave).toHaveBeenCalledTimes(1) - expect(updateStash).toHaveBeenCalledTimes(0) - expect(gitStashPop).toHaveBeenCalledTimes(1) - }) - - it('should reject promise when error during getStagedFiles', async () => { + it('should use an injected logger', async () => { expect.assertions(1) - getStagedFiles.mockImplementationOnce(async () => null) - await expect(runAll({})).rejects.toThrowErrorMatchingSnapshot() + const logger = makeConsoleMock() + await runAll({ config: { '*.js': ['echo "sample"'] }, debug: true }, logger) + expect(logger.printHistory()).toMatchSnapshot() }) - it('should skip stashing changes if no lint-staged files are changed', async () => { - expect.assertions(4) - hasPartiallyStagedFiles.mockImplementationOnce(() => Promise.resolve(true)) - getStagedFiles.mockImplementationOnce(async () => ['sample.java']) - execa.mockImplementationOnce(() => - Promise.resolve({ - stdout: '', - stderr: 'Linter finished with error', - code: 1, - failed: true, - cmd: 'mock cmd' - }) - ) - - try { - await runAll({ config: { '*.js': ['echo "sample"'] } }) - } catch (err) { - console.log(err) - } + 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(gitStashSave).toHaveBeenCalledTimes(0) - expect(updateStash).toHaveBeenCalledTimes(0) - expect(gitStashPop).toHaveBeenCalledTimes(0) }) })