From 46cc8cea50247dc82dd971935cd9f86eae126e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Sat, 25 Jan 2020 20:02:16 +0200 Subject: [PATCH 1/7] fix: make sure deleted files aren't restored due to git bugs --- lib/gitWorkflow.js | 44 ++++++++++++++++++++---------------- test/gitWorkflow.spec.js | 12 +--------- test/runAll.unmocked.spec.js | 19 +++++++++++++--- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index e650ca2fd..ad72172a9 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -18,23 +18,6 @@ const PATCH_UNTRACKED = 'lint-staged_untracked.patch' const GIT_APPLY_ARGS = ['apply', '-v', '--whitespace=nowarn', '--recount', '--unidiff-zero'] const GIT_DIFF_ARGS = ['--binary', '--unified=0', '--no-color', '--no-ext-diff', '--patch'] -/** - * Delete untracked files using `git clean` - * @param {Function} execGit function for executing git commands using execa - * @returns {Promise} - */ -const cleanUntrackedFiles = async execGit => { - const untrackedFiles = await execGit(['ls-files', '--others', '--exclude-standard']) - if (untrackedFiles) { - debug('Detected unstaged, untracked files: ', untrackedFiles) - debug( - 'This is probably due to a bug in git =< 2.13.0 where `git stash --keep-index` resurrects deleted files.' - ) - debug('Deleting the files using `git clean`...') - await execGit(['clean', '--force', ...untrackedFiles.split('\n')]) - } -} - const handleError = (error, ctx) => { ctx.gitError = true throw error @@ -44,6 +27,7 @@ class GitWorkflow { constructor({ allowEmpty, gitConfigDir, gitDir, stagedFileChunks }) { this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir }) this.gitConfigDir = gitConfigDir + this.gitDir = gitDir this.unstagedDiff = null this.allowEmpty = allowEmpty this.stagedFileChunks = stagedFileChunks @@ -135,6 +119,13 @@ class GitWorkflow { // Manually check and backup if necessary await this.backupMergeStatus() + // 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 + // - git stash can't infer RD or MD states correctly, and will lose the deletion + this.deletedFiles = (await this.execGit(['ls-files', '--deleted'])) + .split('\n') + .map(file => path.resolve(this.gitDir, file)) + // Save stash of entire original state, including unstaged and untracked changes. // `--keep-index leaves only staged files on disk, for tasks.` await this.execGit(['stash', 'save', '--include-untracked', '--keep-index', STASH]) @@ -144,7 +135,13 @@ class GitWorkflow { // There is a bug in git =< 2.13.0 where `--keep-index` resurrects deleted files. // These files should be listed and deleted before proceeding. - await cleanUntrackedFiles(this.execGit) + const untrackedFiles = (await execGit(['ls-files', '--others', '--exclude-standard'])) + .split('\n') + .map(file => path.resolve(this.gitDir, file)) + + for (const file of untrackedFiles) { + await unlink(file) + } // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. await this.execGit([ @@ -238,6 +235,11 @@ class GitWorkflow { ctx.gitRestoreUntrackedError = true handleError(error, ctx) } + + // If stashing resurrected deleted files, clean them out + for (const file of this.deletedFiles) { + await unlink(file) + } } /** @@ -251,6 +253,11 @@ class GitWorkflow { await this.execGit(['stash', 'apply', '--quiet', '--index', backupStash]) debug('Done restoring original state!') + // If stashing resurrected deleted files, clean them out + for (const file of this.deletedFiles) { + await unlink(file) + } + // Restore meta information about ongoing git merge await this.restoreMergeStatus() } catch (error) { @@ -278,4 +285,3 @@ class GitWorkflow { } module.exports = GitWorkflow -module.exports.cleanUntrackedFiles = cleanUntrackedFiles diff --git a/test/gitWorkflow.spec.js b/test/gitWorkflow.spec.js index c49e2aef2..972ee79ef 100644 --- a/test/gitWorkflow.spec.js +++ b/test/gitWorkflow.spec.js @@ -5,7 +5,7 @@ import path from 'path' import nanoid from 'nanoid' import execGitBase from '../lib/execGit' -import GitWorkflow, { cleanUntrackedFiles } from '../lib/gitWorkflow' +import GitWorkflow from '../lib/gitWorkflow' jest.unmock('execa') @@ -65,16 +65,6 @@ describe('gitWorkflow', () => { } }) - describe('cleanUntrackedFiles', () => { - it('should delete untracked, unstaged files', async () => { - const testFile = path.resolve(cwd, 'test.js') - await appendFile(testFile, 'test') - expect(await fs.exists(testFile)).toEqual(true) - await cleanUntrackedFiles(execGit) - expect(await fs.exists(testFile)).toEqual(false) - }) - }) - describe('hasPatch', () => { it('should return false when patch file not found', async () => { const gitWorkflow = new GitWorkflow({ diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index fd96f6502..9488b8c86 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -604,11 +604,24 @@ describe('runAll', () => { expect(status).toMatch('no changes added to commit') }) - it('should not resurrect removed files due to git bug', async () => { + it('should not resurrect removed files due to git bug when tasks pass', async () => { const readmeFile = path.resolve(cwd, 'README.md') await fs.remove(readmeFile) // Remove file from previous commit - await execGit(['add', '.']) - await gitCommit({ config: { '*.{js,md}': 'prettier --list-different' } }) + await appendFile('test.js', testJsFilePretty) + await execGit(['add', 'test.js']) + await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) + const exists = await fs.exists(readmeFile) + expect(exists).toEqual(false) + }) + + it('should not resurrect removed files due to git bug when tasks fail', async () => { + const readmeFile = path.resolve(cwd, 'README.md') + await fs.remove(readmeFile) // Remove file from previous commit + await appendFile('test.js', testJsFileUgly) + await execGit(['add', 'test.js']) + await expect( + runAll({ allowEmpty: true, cwd, config: { '*.{js,md}': 'prettier --list-different' } }) + ).rejects.toThrowError() const exists = await fs.exists(readmeFile) expect(exists).toEqual(false) }) From b5e4b30bc05fda0106168a4edd8b586cde524c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Sat, 25 Jan 2020 21:00:24 +0200 Subject: [PATCH 2/7] refactor(file): use fs.promises --- lib/file.js | 46 ++++++++++++++++------------------ lib/getStagedFiles.js | 2 +- lib/gitWorkflow.js | 16 ++++++------ lib/index.js | 2 +- lib/resolveGitRepo.js | 4 +-- test/runAll.unmocked.2.spec.js | 6 ++--- 6 files changed, 37 insertions(+), 39 deletions(-) diff --git a/lib/file.js b/lib/file.js index 462b2c2fa..95a810d7f 100644 --- a/lib/file.js +++ b/lib/file.js @@ -7,7 +7,7 @@ const fsPromises = fs.promises /** * Check if a file exists. Returns the filepath if exists. - * @param {string} filepath + * @param {String} filepath */ const exists = async filepath => { try { @@ -19,21 +19,23 @@ const exists = async filepath => { } /** + * Read contents of a file to buffer * @param {String} filename + * @param {Boolean} [rejectENOENT=false] — Whether to throw if the file doesn't exist * @returns {Promise} */ -const readBufferFromFile = (filename, rejectENOENT = false) => - new Promise(resolve => { - debug('Reading buffer from file `%s`', filename) - fs.readFile(filename, (error, buffer) => { - if (!rejectENOENT && error && error.code === 'ENOENT') { - debug("File `%s` doesn't exist, ignoring...", filename) - return resolve(null) // no-op file doesn't exist - } - debug('Done reading buffer from file `%s`!', filename) - resolve(buffer) - }) - }) +const readFile = async (filename, rejectENOENT = false) => { + debug('Reading file `%s`', filename) + try { + return await fsPromises.readFile(filename) + } catch (error) { + if (!rejectENOENT && error.code === 'ENOENT') { + debug("File `%s` doesn't exist, ignoring...", filename) + return null // no-op file doesn't exist + } + throw error + } +} /** * Unlink a file if it exists @@ -47,22 +49,18 @@ const unlink = async filepath => { } /** + * Write buffer to file * @param {String} filename * @param {Buffer} buffer - * @returns {Promise} */ -const writeBufferToFile = (filename, buffer) => - new Promise(resolve => { - debug('Writing buffer to file `%s`', filename) - fs.writeFile(filename, buffer, () => { - debug('Done writing buffer to file `%s`!', filename) - resolve() - }) - }) +const writeFile = async (filename, buffer) => { + debug('Writing file `%s`', filename) + await fsPromises.writeFile(filename, buffer) +} module.exports = { exists, - readBufferFromFile, + readFile, unlink, - writeBufferToFile + writeFile } diff --git a/lib/getStagedFiles.js b/lib/getStagedFiles.js index 1a7b81e1b..61f2dd6c2 100644 --- a/lib/getStagedFiles.js +++ b/lib/getStagedFiles.js @@ -6,7 +6,7 @@ module.exports = async function getStagedFiles(options) { try { const lines = await execGit(['diff', '--staged', '--diff-filter=ACMR', '--name-only'], options) return lines ? lines.split('\n') : [] - } catch (error) { + } catch { return null } } diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index ad72172a9..f9213325e 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -4,7 +4,7 @@ const debug = require('debug')('lint-staged:git') const path = require('path') const execGit = require('./execGit') -const { exists, readBufferFromFile, unlink, writeBufferToFile } = require('./file') +const { exists, readFile, unlink, writeFile } = require('./file') const MERGE_HEAD = 'MERGE_HEAD' const MERGE_MODE = 'MERGE_MODE' @@ -57,7 +57,7 @@ class GitWorkflow { const resolved = this.getHiddenFilepath(filename) const pathIfExists = await exists(resolved) if (!pathIfExists) return false - const buffer = await readBufferFromFile(pathIfExists) + const buffer = await readFile(pathIfExists) const patch = buffer.toString().trim() return patch.length ? filename : false } @@ -81,9 +81,9 @@ class GitWorkflow { async backupMergeStatus() { debug('Backing up merge state...') await Promise.all([ - readBufferFromFile(this.mergeHeadFilename).then(buffer => (this.mergeHeadBuffer = buffer)), - readBufferFromFile(this.mergeModeFilename).then(buffer => (this.mergeModeBuffer = buffer)), - readBufferFromFile(this.mergeMsgFilename).then(buffer => (this.mergeMsgBuffer = buffer)) + readFile(this.mergeHeadFilename).then(buffer => (this.mergeHeadBuffer = buffer)), + readFile(this.mergeModeFilename).then(buffer => (this.mergeModeBuffer = buffer)), + readFile(this.mergeMsgFilename).then(buffer => (this.mergeMsgBuffer = buffer)) ]) debug('Done backing up merge state!') } @@ -95,9 +95,9 @@ class GitWorkflow { debug('Restoring merge state...') try { await Promise.all([ - this.mergeHeadBuffer && writeBufferToFile(this.mergeHeadFilename, this.mergeHeadBuffer), - this.mergeModeBuffer && writeBufferToFile(this.mergeModeFilename, this.mergeModeBuffer), - this.mergeMsgBuffer && writeBufferToFile(this.mergeMsgFilename, this.mergeMsgBuffer) + this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer), + this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer), + this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer) ]) debug('Done restoring merge state!') } catch (error) { diff --git a/lib/index.js b/lib/index.js index b33b1c7af..e5ca47b00 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,7 +14,7 @@ const errConfigNotFound = new Error('Config could not be found') function resolveConfig(configPath) { try { return require.resolve(configPath) - } catch (ignore) { + } catch { return configPath } } diff --git a/lib/resolveGitRepo.js b/lib/resolveGitRepo.js index fa064986a..b65013ead 100644 --- a/lib/resolveGitRepo.js +++ b/lib/resolveGitRepo.js @@ -7,7 +7,7 @@ const path = require('path') const debugLog = require('debug')('lint-staged:resolveGitRepo') const execGit = require('./execGit') -const { readBufferFromFile } = require('./file') +const { readFile } = require('./file') /** * Resolve path to the .git directory, with special handling for @@ -19,7 +19,7 @@ const resolveGitConfigDir = async gitDir => { // If .git is a directory, use it if (stats.isDirectory()) return defaultDir // Otherwise .git is a file containing path to real location - const file = (await readBufferFromFile(defaultDir)).toString() + const file = (await readFile(defaultDir)).toString() return path.resolve(gitDir, file.replace(/^gitdir: /, '')).trim() } diff --git a/test/runAll.unmocked.2.spec.js b/test/runAll.unmocked.2.spec.js index 202449e33..86aea6588 100644 --- a/test/runAll.unmocked.2.spec.js +++ b/test/runAll.unmocked.2.spec.js @@ -8,7 +8,7 @@ import nanoid from 'nanoid' jest.mock('../lib/file') import execGitBase from '../lib/execGit' -import { readBufferFromFile, writeBufferToFile } from '../lib/file' +import { readFile, writeFile } from '../lib/file' import runAll from '../lib/runAll' jest.unmock('execa') @@ -89,8 +89,8 @@ describe('runAll', () => { }) it.only('Should throw when restoring untracked files fails', async () => { - readBufferFromFile.mockImplementation(async () => Buffer.from('test')) - writeBufferToFile.mockImplementation(async () => Promise.reject('test')) + readFile.mockImplementation(async () => Buffer.from('test')) + writeFile.mockImplementation(async () => Promise.reject('test')) // Stage pretty file await appendFile('test.js', testJsFilePretty) From 14d5b0eeb2aafbd473f5facd3e6136ba9fcaa8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Sun, 26 Jan 2020 20:32:12 +0200 Subject: [PATCH 3/7] perf: run file removals in parallel --- lib/file.js | 31 +++++++++++++++---------------- lib/gitWorkflow.js | 13 +++---------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/file.js b/lib/file.js index 95a810d7f..a81084366 100644 --- a/lib/file.js +++ b/lib/file.js @@ -1,18 +1,17 @@ 'use strict' const debug = require('debug')('lint-staged:file') -const fs = require('fs') - -const fsPromises = fs.promises +const fs = require('fs').promises /** - * Check if a file exists. Returns the filepath if exists. - * @param {String} filepath + * Check if a file exists. Returns the filename if exists. + * @param {String} filename + * @returns {String|Boolean} */ -const exists = async filepath => { +const exists = async filename => { try { - await fsPromises.access(filepath) - return filepath + await fs.access(filename) + return filename } catch { return false } @@ -22,12 +21,12 @@ const exists = async filepath => { * Read contents of a file to buffer * @param {String} filename * @param {Boolean} [rejectENOENT=false] — Whether to throw if the file doesn't exist - * @returns {Promise} + * @returns {Promise} */ const readFile = async (filename, rejectENOENT = false) => { debug('Reading file `%s`', filename) try { - return await fsPromises.readFile(filename) + return await fs.readFile(filename) } catch (error) { if (!rejectENOENT && error.code === 'ENOENT') { debug("File `%s` doesn't exist, ignoring...", filename) @@ -39,12 +38,12 @@ const readFile = async (filename, rejectENOENT = false) => { /** * Unlink a file if it exists - * @param {*} filepath + * @param {String} filename */ -const unlink = async filepath => { - if (filepath) { - await fsPromises.access(filepath) - await fsPromises.unlink(filepath) +const unlink = async filename => { + if (filename) { + await fs.access(filename) + await fs.unlink(filename) } } @@ -55,7 +54,7 @@ const unlink = async filepath => { */ const writeFile = async (filename, buffer) => { debug('Writing file `%s`', filename) - await fsPromises.writeFile(filename, buffer) + await fs.writeFile(filename, buffer) } module.exports = { diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index f9213325e..a55c1f624 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -138,10 +138,7 @@ class GitWorkflow { const untrackedFiles = (await execGit(['ls-files', '--others', '--exclude-standard'])) .split('\n') .map(file => path.resolve(this.gitDir, file)) - - for (const file of untrackedFiles) { - await unlink(file) - } + await Promise.all(untrackedFiles.map(unlink)) // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. await this.execGit([ @@ -237,9 +234,7 @@ class GitWorkflow { } // If stashing resurrected deleted files, clean them out - for (const file of this.deletedFiles) { - await unlink(file) - } + await Promise.all(this.deletedFiles.map(unlink)) } /** @@ -254,9 +249,7 @@ class GitWorkflow { debug('Done restoring original state!') // If stashing resurrected deleted files, clean them out - for (const file of this.deletedFiles) { - await unlink(file) - } + await Promise.all(this.deletedFiles.map(unlink)) // Restore meta information about ongoing git merge await this.restoreMergeStatus() From cfdf4812496d798a41b6681858421385b0abc943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Tue, 28 Jan 2020 15:59:17 +0200 Subject: [PATCH 4/7] fix: filter out empty string for when there is no output --- lib/gitWorkflow.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index a55c1f624..749764cb0 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -124,6 +124,7 @@ class GitWorkflow { // - git stash can't infer RD or MD states correctly, and will lose the deletion this.deletedFiles = (await this.execGit(['ls-files', '--deleted'])) .split('\n') + .filter(Boolean) .map(file => path.resolve(this.gitDir, file)) // Save stash of entire original state, including unstaged and untracked changes. @@ -137,6 +138,7 @@ class GitWorkflow { // These files should be listed and deleted before proceeding. const untrackedFiles = (await execGit(['ls-files', '--others', '--exclude-standard'])) .split('\n') + .filter(Boolean) .map(file => path.resolve(this.gitDir, file)) await Promise.all(untrackedFiles.map(unlink)) From 11403be1a240b7c084682587a2d037d499e25205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Tue, 28 Jan 2020 16:26:51 +0200 Subject: [PATCH 5/7] fix: correct file handling --- lib/file.js | 24 +++++++++++++++++------- lib/gitWorkflow.js | 6 +++--- test/file.spec.js | 17 +++++++++++++++++ test/runAll.unmocked.spec.js | 9 ++++++++- 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 test/file.spec.js diff --git a/lib/file.js b/lib/file.js index a81084366..68339b3c7 100644 --- a/lib/file.js +++ b/lib/file.js @@ -20,30 +20,40 @@ const exists = async filename => { /** * Read contents of a file to buffer * @param {String} filename - * @param {Boolean} [rejectENOENT=false] — Whether to throw if the file doesn't exist + * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist * @returns {Promise} */ -const readFile = async (filename, rejectENOENT = false) => { +const readFile = async (filename, ignoreENOENT = true) => { debug('Reading file `%s`', filename) try { return await fs.readFile(filename) } catch (error) { - if (!rejectENOENT && error.code === 'ENOENT') { + if (ignoreENOENT && error.code === 'ENOENT') { debug("File `%s` doesn't exist, ignoring...", filename) return null // no-op file doesn't exist + } else { + throw error } - throw error } } /** * Unlink a file if it exists * @param {String} filename + * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist */ -const unlink = async filename => { +const unlink = async (filename, ignoreENOENT = true) => { if (filename) { - await fs.access(filename) - await fs.unlink(filename) + debug('Unlinking file `%s`', filename) + try { + await fs.unlink(filename) + } catch (error) { + if (ignoreENOENT && error.code === 'ENOENT') { + debug("File `%s` doesn't exist, ignoring...", filename) + } else { + throw error + } + } } } diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index 749764cb0..df4a6f813 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -140,7 +140,7 @@ class GitWorkflow { .split('\n') .filter(Boolean) .map(file => path.resolve(this.gitDir, file)) - await Promise.all(untrackedFiles.map(unlink)) + await Promise.all(untrackedFiles.map(file => unlink(file))) // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. await this.execGit([ @@ -236,7 +236,7 @@ class GitWorkflow { } // If stashing resurrected deleted files, clean them out - await Promise.all(this.deletedFiles.map(unlink)) + await Promise.all(this.deletedFiles.map(file => unlink(file))) } /** @@ -251,7 +251,7 @@ class GitWorkflow { debug('Done restoring original state!') // If stashing resurrected deleted files, clean them out - await Promise.all(this.deletedFiles.map(unlink)) + await Promise.all(this.deletedFiles.map(file => unlink(file))) // Restore meta information about ongoing git merge await this.restoreMergeStatus() diff --git a/test/file.spec.js b/test/file.spec.js new file mode 100644 index 000000000..48badfaab --- /dev/null +++ b/test/file.spec.js @@ -0,0 +1,17 @@ +import { unlink, readFile } from '../lib/file' + +describe('unlink', () => { + it('should throw when second argument is false and file is not found', async () => { + await expect(unlink('example', false)).rejects.toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, unlink 'example'"` + ) + }) +}) + +describe('readFile', () => { + it('should throw when second argument is false and file is not found', async () => { + await expect(readFile('example', false)).rejects.toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open 'example'"` + ) + }) +}) diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index 9488b8c86..660ca38ee 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -609,7 +609,14 @@ describe('runAll', () => { await fs.remove(readmeFile) // Remove file from previous commit await appendFile('test.js', testJsFilePretty) await execGit(['add', 'test.js']) - await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) + + try { + await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) + } catch (error) { + globalConsoleTemp.warn(error) + globalConsoleTemp.error(console.printHistory()) + } + const exists = await fs.exists(readmeFile) expect(exists).toEqual(false) }) From c27fe5eebfb0ea9a2610d407335f2c2a990da227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Tue, 28 Jan 2020 16:51:41 +0200 Subject: [PATCH 6/7] test: add test for complex additions and deletions --- test/file.spec.js | 8 ++------ test/runAll.unmocked.spec.js | 38 ++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/test/file.spec.js b/test/file.spec.js index 48badfaab..c49a7dbde 100644 --- a/test/file.spec.js +++ b/test/file.spec.js @@ -2,16 +2,12 @@ import { unlink, readFile } from '../lib/file' describe('unlink', () => { it('should throw when second argument is false and file is not found', async () => { - await expect(unlink('example', false)).rejects.toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, unlink 'example'"` - ) + await expect(unlink('example', false)).rejects.toThrowError() }) }) describe('readFile', () => { it('should throw when second argument is false and file is not found', async () => { - await expect(readFile('example', false)).rejects.toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open 'example'"` - ) + await expect(readFile('example', false)).rejects.toThrowError() }) }) diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index 660ca38ee..afff84736 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -609,18 +609,40 @@ describe('runAll', () => { await fs.remove(readmeFile) // Remove file from previous commit await appendFile('test.js', testJsFilePretty) await execGit(['add', 'test.js']) - - try { - await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) - } catch (error) { - globalConsoleTemp.warn(error) - globalConsoleTemp.error(console.printHistory()) - } - + await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) const exists = await fs.exists(readmeFile) expect(exists).toEqual(false) }) + it('should not resurrect removed files in complex case', async () => { + // Add file to index, and remove it from disk + await appendFile('test.js', testJsFilePretty) + await execGit(['add', 'test.js']) + const testFile = path.resolve(cwd, 'test.js') + await fs.remove(testFile) + + // Rename file in index, and remove it from disk + const readmeFile = path.resolve(cwd, 'README.md') + const readme = await readFile(readmeFile) + await fs.remove(readmeFile) + await execGit(['add', readmeFile]) + const newReadmeFile = path.resolve(cwd, 'README_NEW.md') + await appendFile(newReadmeFile, readme) + await execGit(['add', newReadmeFile]) + await fs.remove(newReadmeFile) + + const status = await execGit(['status', '--porcelain']) + expect(status).toMatchInlineSnapshot(` + "RD README.md -> README_NEW.md + AD test.js" + `) + + await runAll({ cwd, config: { '*.{js,md}': 'prettier --list-different' } }) + expect(await fs.exists(testFile)).toEqual(false) + expect(await fs.exists(newReadmeFile)).toEqual(false) + expect(await execGit(['status', '--porcelain'])).toEqual(status) + }) + it('should not resurrect removed files due to git bug when tasks fail', async () => { const readmeFile = path.resolve(cwd, 'README.md') await fs.remove(readmeFile) // Remove file from previous commit From 0121e8b81a82ef5b2d55f1533d761ca556b93542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Tue, 28 Jan 2020 17:49:02 +0200 Subject: [PATCH 7/7] fix: make cleaning untracked files resurrected due to git bug testable --- lib/gitWorkflow.js | 18 +++++++++++++----- test/file.spec.js | 4 ++-- test/gitWorkflow.spec.js | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index df4a6f813..c8570f29a 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -107,6 +107,18 @@ class GitWorkflow { } } + /** + * List and delete untracked files + */ + async cleanUntrackedFiles() { + const lsFiles = await this.execGit(['ls-files', '--others', '--exclude-standard']) + const untrackedFiles = lsFiles + .split('\n') + .filter(Boolean) + .map(file => path.resolve(this.gitDir, file)) + await Promise.all(untrackedFiles.map(file => unlink(file))) + } + /** * Create backup stashes, one of everything and one of only staged changes * Staged files are left in the index for running tasks @@ -136,11 +148,7 @@ class GitWorkflow { // There is a bug in git =< 2.13.0 where `--keep-index` resurrects deleted files. // These files should be listed and deleted before proceeding. - const untrackedFiles = (await execGit(['ls-files', '--others', '--exclude-standard'])) - .split('\n') - .filter(Boolean) - .map(file => path.resolve(this.gitDir, file)) - await Promise.all(untrackedFiles.map(file => unlink(file))) + await this.cleanUntrackedFiles() // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. await this.execGit([ diff --git a/test/file.spec.js b/test/file.spec.js index c49a7dbde..93f3430c9 100644 --- a/test/file.spec.js +++ b/test/file.spec.js @@ -2,12 +2,12 @@ import { unlink, readFile } from '../lib/file' describe('unlink', () => { it('should throw when second argument is false and file is not found', async () => { - await expect(unlink('example', false)).rejects.toThrowError() + await expect(unlink('example', false)).rejects.toThrow('ENOENT') }) }) describe('readFile', () => { it('should throw when second argument is false and file is not found', async () => { - await expect(readFile('example', false)).rejects.toThrowError() + await expect(readFile('example', false)).rejects.toThrow('ENOENT') }) }) diff --git a/test/gitWorkflow.spec.js b/test/gitWorkflow.spec.js index 972ee79ef..97d7cb5e9 100644 --- a/test/gitWorkflow.spec.js +++ b/test/gitWorkflow.spec.js @@ -91,4 +91,19 @@ describe('gitWorkflow', () => { }) }) }) + + describe('cleanUntrackedFiles', () => { + it('should remove untracked files', async () => { + const tempFile = path.resolve(cwd, 'tempFile') + await fs.writeFile(tempFile, 'Hello') + + const gitWorkflow = new GitWorkflow({ + gitDir: cwd, + gitConfigDir: path.resolve(cwd, './.git') + }) + + await gitWorkflow.cleanUntrackedFiles() + await expect(fs.access(tempFile)).rejects.toThrow('ENOENT') + }) + }) })