diff --git a/lib/file.js b/lib/file.js index 9a26a44a5..462b2c2fa 100644 --- a/lib/file.js +++ b/lib/file.js @@ -3,11 +3,26 @@ const debug = require('debug')('lint-staged:file') const fs = require('fs') +const fsPromises = fs.promises + +/** + * Check if a file exists. Returns the filepath if exists. + * @param {string} filepath + */ +const exists = async filepath => { + try { + await fsPromises.access(filepath) + return filepath + } catch { + return false + } +} + /** * @param {String} filename * @returns {Promise} */ -module.exports.readBufferFromFile = (filename, rejectENOENT = false) => +const readBufferFromFile = (filename, rejectENOENT = false) => new Promise(resolve => { debug('Reading buffer from file `%s`', filename) fs.readFile(filename, (error, buffer) => { @@ -20,12 +35,23 @@ module.exports.readBufferFromFile = (filename, rejectENOENT = false) => }) }) +/** + * Unlink a file if it exists + * @param {*} filepath + */ +const unlink = async filepath => { + if (filepath) { + await fsPromises.access(filepath) + await fsPromises.unlink(filepath) + } +} + /** * @param {String} filename * @param {Buffer} buffer * @returns {Promise} */ -module.exports.writeBufferToFile = (filename, buffer) => +const writeBufferToFile = (filename, buffer) => new Promise(resolve => { debug('Writing buffer to file `%s`', filename) fs.writeFile(filename, buffer, () => { @@ -33,3 +59,10 @@ module.exports.writeBufferToFile = (filename, buffer) => resolve() }) }) + +module.exports = { + exists, + readBufferFromFile, + unlink, + writeBufferToFile +} diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index d43216af7..e650ca2fd 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 { readBufferFromFile, writeBufferToFile } = require('./file') +const { exists, readBufferFromFile, unlink, writeBufferToFile } = require('./file') const MERGE_HEAD = 'MERGE_HEAD' const MERGE_MODE = 'MERGE_MODE' @@ -12,6 +12,9 @@ const MERGE_MSG = 'MERGE_MSG' const STASH = 'lint-staged automatic backup' +const PATCH_UNSTAGED = 'lint-staged_unstaged.patch' +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'] @@ -40,6 +43,7 @@ const handleError = (error, ctx) => { class GitWorkflow { constructor({ allowEmpty, gitConfigDir, gitDir, stagedFileChunks }) { this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir }) + this.gitConfigDir = gitConfigDir this.unstagedDiff = null this.allowEmpty = allowEmpty this.stagedFileChunks = stagedFileChunks @@ -53,6 +57,27 @@ class GitWorkflow { this.mergeMsgFilename = path.resolve(gitConfigDir, MERGE_MSG) } + /** + * Get absolute path to file hidden inside .git + * @param {string} filename + */ + getHiddenFilepath(filename) { + return path.resolve(this.gitConfigDir, `./${filename}`) + } + + /** + * Check if patch file exists and has content. + * @param {string} filename + */ + async hasPatch(filename) { + const resolved = this.getHiddenFilepath(filename) + const pathIfExists = await exists(resolved) + if (!pathIfExists) return false + const buffer = await readBufferFromFile(pathIfExists) + const patch = buffer.toString().trim() + return patch.length ? filename : false + } + /** * Get name of backup stash */ @@ -122,9 +147,10 @@ class GitWorkflow { await cleanUntrackedFiles(this.execGit) // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. - this.unstagedDiff = await this.execGit([ + await this.execGit([ 'diff', ...GIT_DIFF_ARGS, + `--output=${this.getHiddenFilepath(PATCH_UNSTAGED)}`, await this.getBackupStash(ctx), '-R' // Show diff in reverse ]) @@ -167,10 +193,11 @@ class GitWorkflow { // Restore unstaged changes by applying the diff back. If it at first fails, // this is probably because of conflicts between task modifications. // 3-way merge usually fixes this, and in case it doesn't we should just give up and throw. - if (this.unstagedDiff) { + if (await this.hasPatch(PATCH_UNSTAGED)) { debug('Restoring unstaged changes...') + const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED) try { - await this.execGit(GIT_APPLY_ARGS, { input: `${this.unstagedDiff}\n` }) + await this.execGit([...GIT_APPLY_ARGS, unstagedPatch]) } catch (error) { debug('Error while restoring changes:') debug(error) @@ -178,7 +205,7 @@ class GitWorkflow { try { // Retry with `--3way` if normal apply fails - await this.execGit([...GIT_APPLY_ARGS, '--3way'], { input: `${this.unstagedDiff}\n` }) + await this.execGit([...GIT_APPLY_ARGS, '--3way', unstagedPatch]) } catch (error2) { debug('Error while restoring unstaged changes using 3-way merge:') debug(error2) @@ -196,14 +223,17 @@ class GitWorkflow { // See https://stackoverflow.com/a/52357762 try { const backupStash = await this.getBackupStash(ctx) - const output = await this.execGit([ + const untrackedPatch = this.getHiddenFilepath(PATCH_UNTRACKED) + await this.execGit([ 'show', ...GIT_DIFF_ARGS, '--format=%b', + `--output=${untrackedPatch}`, `${backupStash}^3` ]) - const untrackedDiff = output.trim() - if (untrackedDiff) await this.execGit([...GIT_APPLY_ARGS], { input: `${untrackedDiff}\n` }) + if (await this.hasPatch(PATCH_UNTRACKED)) { + await this.execGit([...GIT_APPLY_ARGS, untrackedPatch]) + } } catch (error) { ctx.gitRestoreUntrackedError = true handleError(error, ctx) @@ -234,6 +264,10 @@ class GitWorkflow { async dropBackup(ctx) { try { debug('Dropping backup stash...') + await Promise.all([ + exists(this.getHiddenFilepath(PATCH_UNSTAGED)).then(unlink), + exists(this.getHiddenFilepath(PATCH_UNTRACKED)).then(unlink) + ]) const backupStash = await this.getBackupStash(ctx) await this.execGit(['stash', 'drop', '--quiet', backupStash]) debug('Done dropping backup stash!') diff --git a/test/gitWorkflow.spec.js b/test/gitWorkflow.spec.js index 73cab1452..c49e2aef2 100644 --- a/test/gitWorkflow.spec.js +++ b/test/gitWorkflow.spec.js @@ -75,6 +75,16 @@ describe('gitWorkflow', () => { }) }) + describe('hasPatch', () => { + it('should return false when patch file not found', async () => { + const gitWorkflow = new GitWorkflow({ + gitDir: cwd, + gitConfigDir: path.resolve(cwd, './.git') + }) + expect(await gitWorkflow.hasPatch('foo')).toEqual(false) + }) + }) + describe('dropBackup', () => { it('should handle errors', async () => { const gitWorkflow = new GitWorkflow({