Skip to content

Commit

Permalink
fix: save diffs to temporary files instead of keeping in memory
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj committed Jan 27, 2020
1 parent 151f148 commit b9d5939
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 10 deletions.
37 changes: 35 additions & 2 deletions lib/file.js
Expand Up @@ -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<Buffer|Null>}
*/
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) => {
Expand All @@ -20,16 +35,34 @@ 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<Void>}
*/
module.exports.writeBufferToFile = (filename, buffer) =>
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()
})
})

module.exports = {
exists,
readBufferFromFile,
unlink,
writeBufferToFile
}
50 changes: 42 additions & 8 deletions lib/gitWorkflow.js
Expand Up @@ -4,14 +4,17 @@ 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'
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']

Expand Down Expand Up @@ -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
Expand All @@ -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
*/
Expand Down Expand Up @@ -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
])
Expand Down Expand Up @@ -167,18 +193,19 @@ 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)
debug('Retrying with 3-way merge')

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)
Expand All @@ -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)
Expand Down Expand Up @@ -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!')
Expand Down
10 changes: 10 additions & 0 deletions test/gitWorkflow.spec.js
Expand Up @@ -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({
Expand Down

0 comments on commit b9d5939

Please sign in to comment.