'use strict'
|
|
const path = require('path')
|
const execa = require('execa')
|
const gStatus = require('g-status')
|
const del = require('del')
|
const debug = require('debug')('lint-staged:git')
|
const resolveGitDir = require('./resolveGitDir')
|
|
let workingCopyTree = null
|
let indexTree = null
|
let formattedIndexTree = null
|
|
function getAbsolutePath(dir) {
|
return path.isAbsolute(dir) ? dir : path.resolve(dir)
|
}
|
|
async function execGit(cmd, options) {
|
const cwd = options && options.cwd ? options.cwd : resolveGitDir()
|
debug('Running git command', cmd)
|
try {
|
const { stdout } = await execa('git', [].concat(cmd), {
|
...options,
|
cwd: getAbsolutePath(cwd)
|
})
|
return stdout
|
} catch (err) {
|
throw new Error(err)
|
}
|
}
|
|
async function writeTree(options) {
|
return execGit(['write-tree'], options)
|
}
|
|
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
|
],
|
options
|
)
|
}
|
|
async function hasPartiallyStagedFiles(options) {
|
const cwd = options && options.cwd ? options.cwd : resolveGitDir()
|
const files = await gStatus({ cwd })
|
const partiallyStaged = files.filter(
|
file =>
|
file.index !== ' ' &&
|
file.workingTree !== ' ' &&
|
file.index !== '?' &&
|
file.workingTree !== '?'
|
)
|
return partiallyStaged.length > 0
|
}
|
|
// 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]
|
}
|
|
async function updateStash(options) {
|
formattedIndexTree = await writeTree(options)
|
return formattedIndexTree
|
}
|
|
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
|
const patch = `${diff}\n` // TODO: This should also work on Windows but test would be good
|
if (patch) {
|
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: patch
|
}
|
)
|
} catch (err) {
|
debug('Could not apply patch to the stashed files cleanly')
|
debug(err)
|
debug('Patch content:')
|
debug(patch)
|
throw new Error('Could not apply patch to the stashed files cleanly.', err)
|
}
|
}
|
}
|
|
async function gitStashPop(options) {
|
if (workingCopyTree === null) {
|
throw new Error('Trying to restore from stash but could not find working copy stash.')
|
}
|
|
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
|
|
return null
|
}
|
|
module.exports = {
|
execGit,
|
gitStashSave,
|
gitStashPop,
|
hasPartiallyStagedFiles,
|
updateStash
|
}
|