Skip to content

Commit

Permalink
refactor: use native git stash/pop
Browse files Browse the repository at this point in the history
  • Loading branch information
Iiro Jäppinen committed Jun 18, 2019
1 parent d4a3c28 commit 594bdd1
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 200 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -31,7 +31,6 @@
"cosmiconfig": "^5.2.0",
"debug": "^3.1.0",
"dedent": "^0.7.0",
"del": "^3.0.0",
"execa": "^1.0.0",
"g-status": "^2.0.2",
"is-glob": "^4.0.0",
Expand Down
151 changes: 23 additions & 128 deletions src/gitWorkflow.js
@@ -1,36 +1,10 @@
'use strict'

const gStatus = require('g-status')
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)
}

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
const files = await gStatus({ cwd })
Expand All @@ -44,118 +18,39 @@ async function hasPartiallyStagedFiles(options) {
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 saveStagedFiles(options) {
debug('Saving modified files...')
// Stash changed changes
await execGit(['stash', 'push', '-m temporary lint-staged stash'], options)

async function updateStash(options) {
formattedIndexTree = await writeTree(options)
return formattedIndexTree
}
// Restore changed files back
await execGit(['stash', 'apply', '--index'], options)

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)
}
}
debug('Done saving modified files!')
}

async function gitStashPop(options) {
if (workingCopyTree === null) {
throw new Error('Trying to restore from stash but could not find working copy stash.')
}
async function restoreStagedFiles(options) {
debug('Restoring modified files...')

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)
// Reset everything to clear out modifications by linters
await execGit(['reset', '--hard'], 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 changed files back
await execGit(['stash', 'apply', '--index'], options)

debug('Done restoring modified files!')
}

return null
async function clearStagedFileStash(options) {
debug('Clearing saved modified files...')
await execGit(['stash', 'drop'], options)
debug('Done clearing saved modified files!')
}

module.exports = {
execGit,
gitStashSave,
gitStashPop,
hasPartiallyStagedFiles,
updateStash
saveStagedFiles,
restoreStagedFiles,
clearStagedFileStash
}
28 changes: 13 additions & 15 deletions src/runAll.js
Expand Up @@ -82,17 +82,16 @@ module.exports = async function runAll(config) {
[
{
title: 'Stashing changes...',
skip: async () => {
skip: async ctx => {
const hasPSF = await git.hasPartiallyStagedFiles({ cwd: gitDir })
if (!hasPSF) {
return 'No partially staged files found...'
if (hasPSF) {
ctx.hasPSF = true
return false
}
return false

return 'No partially staged files found...'
},
task: ctx => {
ctx.hasStash = true
return git.gitStashSave({ cwd: gitDir })
}
task: () => git.saveStagedFiles({ cwd: gitDir })
},
{
title: 'Running linters...',
Expand All @@ -104,15 +103,14 @@ module.exports = async function runAll(config) {
})
},
{
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: 'Restoring local changes...',
enabled: ctx => ctx.hasErrors,
task: () => git.restoreStagedFiles({ cwd: gitDir })
},
{
title: 'Restoring local changes...',
enabled: ctx => ctx.hasStash,
task: () => git.gitStashPop({ cwd: gitDir })
title: 'Clearing temporary stashed changed...',
skip: ctx => !ctx.hasPSF,
task: () => git.clearStagedFileStash({ cwd: gitDir })
}
],
listrBaseOptions
Expand Down
58 changes: 2 additions & 56 deletions yarn.lock
Expand Up @@ -227,18 +227,6 @@ array-includes@^3.0.3:
define-properties "^1.1.2"
es-abstract "^1.7.0"

array-union@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
dependencies:
array-uniq "^1.0.1"

array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=

array-unique@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
Expand Down Expand Up @@ -1465,18 +1453,6 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"

del@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=
dependencies:
globby "^6.1.0"
is-path-cwd "^1.0.0"
is-path-in-cwd "^1.0.0"
p-map "^1.1.1"
pify "^3.0.0"
rimraf "^2.2.8"

delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
Expand Down Expand Up @@ -2361,17 +2337,6 @@ globals@^9.18.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==

globby@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
dependencies:
array-union "^1.0.1"
glob "^7.0.3"
object-assign "^4.0.1"
pify "^2.0.0"
pinkie-promise "^2.0.0"

graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
Expand Down Expand Up @@ -2862,25 +2827,6 @@ is-observable@^1.1.0:
dependencies:
symbol-observable "^1.1.0"

is-path-cwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=

is-path-in-cwd@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==
dependencies:
is-path-inside "^1.0.0"

is-path-inside@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
dependencies:
path-is-inside "^1.0.1"

is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
Expand Down Expand Up @@ -4350,7 +4296,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=

path-is-inside@^1.0.1, path-is-inside@^1.0.2:
path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
Expand Down Expand Up @@ -4851,7 +4797,7 @@ right-pad@^1.0.1:
resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0"
integrity sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=

rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@~2.6.2:
rimraf@^2.5.4, rimraf@^2.6.1, rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
Expand Down

0 comments on commit 594bdd1

Please sign in to comment.