1 | 'use strict'
|
2 |
|
3 | const path = require('path')
|
4 | const execa = require('execa')
|
5 | const gStatus = require('g-status')
|
6 | const del = require('del')
|
7 | const debug = require('debug')('lint-staged:git')
|
8 | const resolveGitDir = require('./resolveGitDir')
|
9 |
|
10 | let workingCopyTree = null
|
11 | let indexTree = null
|
12 | let formattedIndexTree = null
|
13 |
|
14 | function getAbsolutePath(dir) {
|
15 | return path.isAbsolute(dir) ? dir : path.resolve(dir)
|
16 | }
|
17 |
|
18 | async 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 |
|
32 | async function writeTree(options) {
|
33 | return execGit(['write-tree'], options)
|
34 | }
|
35 |
|
36 | async 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 |
|
53 | async 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 |
|
67 | async function gitStashSave(options) {
|
68 | debug('Stashing files...')
|
69 |
|
70 | indexTree = await writeTree(options)
|
71 |
|
72 | await execGit(['add', '.'], options)
|
73 |
|
74 | workingCopyTree = await writeTree(options)
|
75 |
|
76 | await execGit(['read-tree', indexTree], options)
|
77 |
|
78 | await execGit(['checkout-index', '-af'], options)
|
79 |
|
80 | debug('Done stashing files!')
|
81 | return [workingCopyTree, indexTree]
|
82 | }
|
83 |
|
84 | async function updateStash(options) {
|
85 | formattedIndexTree = await writeTree(options)
|
86 | return formattedIndexTree
|
87 | }
|
88 |
|
89 | async function applyPatchFor(tree1, tree2, options) {
|
90 | const diff = await getDiffForTrees(tree1, tree2, options)
|
91 | |
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | const patch = `${diff}\n`
|
99 | if (patch) {
|
100 | try {
|
101 | |
102 |
|
103 |
|
104 |
|
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 |
|
123 | async 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 |
|
130 | await execGit(['read-tree', workingCopyTree], options)
|
131 |
|
132 | await execGit(['checkout-index', '-af'], options)
|
133 |
|
134 |
|
135 | if (indexTree !== null && formattedIndexTree === null) {
|
136 |
|
137 | debug('Restoring index')
|
138 | await execGit(['read-tree', indexTree], options)
|
139 | } else {
|
140 | |
141 |
|
142 |
|
143 |
|
144 |
|
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 |
|
156 |
|
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 |
|
167 | workingCopyTree = null
|
168 | indexTree = null
|
169 | formattedIndexTree = null
|
170 |
|
171 | return null
|
172 | }
|
173 |
|
174 | module.exports = {
|
175 | execGit,
|
176 | gitStashSave,
|
177 | gitStashPop,
|
178 | hasPartiallyStagedFiles,
|
179 | updateStash
|
180 | }
|