/
gitWorkflow.js
179 lines (164 loc) Β· 5.48 KB
/
gitWorkflow.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
'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 files = await gStatus(options)
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
}