UNPKG

5.68 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path')
4const execa = require('execa')
5const gStatus = require('g-status')
6const del = require('del')
7const debug = require('debug')('lint-staged:git')
8const resolveGitDir = require('./resolveGitDir')
9
10let workingCopyTree = null
11let indexTree = null
12let formattedIndexTree = null
13
14function getAbsolutePath(dir) {
15 return path.isAbsolute(dir) ? dir : path.resolve(dir)
16}
17
18async function execGit(cmd, options) {
19 const cwd = options && options.cwd ? options.cwd : resolveGitDir()
20 debug('Running git command', cmd)
21 try {
22 const { stdout } = await execa('git', [].concat(cmd), {
23 ...options,
24 cwd: getAbsolutePath(cwd)
25 })
26 return stdout
27 } catch (err) {
28 throw new Error(err)
29 }
30}
31
32async function writeTree(options) {
33 return execGit(['write-tree'], options)
34}
35
36async function getDiffForTrees(tree1, tree2, options) {
37 debug(`Generating diff between trees ${tree1} and ${tree2}...`)
38 return execGit(
39 [
40 'diff-tree',
41 '--ignore-submodules',
42 '--binary',
43 '--no-color',
44 '--no-ext-diff',
45 '--unified=0',
46 tree1,
47 tree2
48 ],
49 options
50 )
51}
52
53async function hasPartiallyStagedFiles(options) {
54 const cwd = options && options.cwd ? options.cwd : resolveGitDir()
55 const files = await gStatus({ cwd })
56 const partiallyStaged = files.filter(
57 file =>
58 file.index !== ' ' &&
59 file.workingTree !== ' ' &&
60 file.index !== '?' &&
61 file.workingTree !== '?'
62 )
63 return partiallyStaged.length > 0
64}
65
66// eslint-disable-next-line
67async function gitStashSave(options) {
68 debug('Stashing files...')
69 // Save ref to the current index
70 indexTree = await writeTree(options)
71 // Add working copy changes to index
72 await execGit(['add', '.'], options)
73 // Save ref to the working copy index
74 workingCopyTree = await writeTree(options)
75 // Restore the current index
76 await execGit(['read-tree', indexTree], options)
77 // Remove all modifications
78 await execGit(['checkout-index', '-af'], options)
79 // await execGit(['clean', '-dfx'], options)
80 debug('Done stashing files!')
81 return [workingCopyTree, indexTree]
82}
83
84async function updateStash(options) {
85 formattedIndexTree = await writeTree(options)
86 return formattedIndexTree
87}
88
89async function applyPatchFor(tree1, tree2, options) {
90 const diff = await getDiffForTrees(tree1, tree2, options)
91 /**
92 * This is crucial for patch to work
93 * For some reason, git-apply requires that the patch ends with the newline symbol
94 * See http://git.661346.n2.nabble.com/Bug-in-Git-Gui-Creates-corrupt-patch-td2384251.html
95 * and https://stackoverflow.com/questions/13223868/how-to-stage-line-by-line-in-git-gui-although-no-newline-at-end-of-file-warnin
96 */
97 // TODO: Figure out how to test this. For some reason tests were working but in the real env it was failing
98 const patch = `${diff}\n` // TODO: This should also work on Windows but test would be good
99 if (patch) {
100 try {
101 /**
102 * Apply patch to index. We will apply it with --reject so it it will try apply hunk by hunk
103 * We're not interested in failied hunks since this mean that formatting conflicts with user changes
104 * and we prioritize user changes over formatter's
105 */
106 await execGit(
107 ['apply', '-v', '--whitespace=nowarn', '--reject', '--recount', '--unidiff-zero'],
108 {
109 ...options,
110 input: patch
111 }
112 )
113 } catch (err) {
114 debug('Could not apply patch to the stashed files cleanly')
115 debug(err)
116 debug('Patch content:')
117 debug(patch)
118 throw new Error('Could not apply patch to the stashed files cleanly.', err)
119 }
120 }
121}
122
123async function gitStashPop(options) {
124 if (workingCopyTree === null) {
125 throw new Error('Trying to restore from stash but could not find working copy stash.')
126 }
127
128 debug('Restoring working copy')
129 // Restore the stashed files in the index
130 await execGit(['read-tree', workingCopyTree], options)
131 // and sync it to the working copy (i.e. update files on fs)
132 await execGit(['checkout-index', '-af'], options)
133
134 // Then, restore the index after working copy is restored
135 if (indexTree !== null && formattedIndexTree === null) {
136 // Restore changes that were in index if there are no formatting changes
137 debug('Restoring index')
138 await execGit(['read-tree', indexTree], options)
139 } else {
140 /**
141 * There are formatting changes we want to restore in the index
142 * and in the working copy. So we start by restoring the index
143 * and after that we'll try to carry as many as possible changes
144 * to the working copy by applying the patch with --reject option.
145 */
146 debug('Restoring index with formatting changes')
147 await execGit(['read-tree', formattedIndexTree], options)
148 try {
149 await applyPatchFor(indexTree, formattedIndexTree, options)
150 } catch (err) {
151 debug(
152 'Found conflicts between formatters and local changes. Formatters changes will be ignored for conflicted hunks.'
153 )
154 /**
155 * Clean up working directory from *.rej files that contain conflicted hanks.
156 * These hunks are coming from formatters so we'll just delete them since they are irrelevant.
157 */
158 try {
159 const rejFiles = await del(['*.rej'], options)
160 debug('Deleted files and folders:\n', rejFiles.join('\n'))
161 } catch (delErr) {
162 debug('Error deleting *.rej files', delErr)
163 }
164 }
165 }
166 // Clean up references
167 workingCopyTree = null
168 indexTree = null
169 formattedIndexTree = null
170
171 return null
172}
173
174module.exports = {
175 execGit,
176 gitStashSave,
177 gitStashPop,
178 hasPartiallyStagedFiles,
179 updateStash
180}