/
stash-test.ts
338 lines (276 loc) · 11 KB
/
stash-test.ts
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import * as FSE from 'fs-extra'
import * as path from 'path'
import { Repository } from '../../../src/models/repository'
import { setupEmptyRepository } from '../../helpers/repositories'
import { GitProcess } from 'dugite'
import {
createDesktopStashMessage,
createDesktopStashEntry,
getLastDesktopStashEntryForBranch,
dropDesktopStashEntry,
popStashEntry,
getStashes,
} from '../../../src/lib/git/stash'
import { getStatusOrThrow } from '../../helpers/status'
import { AppFileStatusKind } from '../../../src/models/status'
import {
IStashEntry,
StashedChangesLoadStates,
} from '../../../src/models/stash-entry'
import { generateString } from '../../helpers/random-data'
describe('git/stash', () => {
describe('getStash', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
it('handles unborn repo by returning empty list', async () => {
const repo = await setupEmptyRepository()
const stash = await getStashes(repo)
expect(stash.desktopEntries).toHaveLength(0)
})
it('returns an empty list when no stash entries have been created', async () => {
const stash = await getStashes(repository)
expect(stash.desktopEntries).toHaveLength(0)
})
it('returns all stash entries created by Desktop', async () => {
await generateTestStashEntry(repository, 'master', false)
await generateTestStashEntry(repository, 'master', false)
await generateTestStashEntry(repository, 'master', true)
const stash = await getStashes(repository)
const entries = stash.desktopEntries
expect(entries).toHaveLength(1)
expect(entries[0].branchName).toBe('master')
})
})
describe('createDesktopStashEntry', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
it('creates a stash entry when repo is not unborn or in any kind of conflict or rebase state', async () => {
await FSE.appendFile(readme, 'just testing stuff')
await createDesktopStashEntry(repository, 'master', [])
const stash = await getStashes(repository)
const entries = stash.desktopEntries
expect(entries).toHaveLength(1)
expect(entries[0].branchName).toBe('master')
})
it('stashes untracked files and removes them from the working directory', async () => {
const untrackedFile = path.join(repository.path, 'not-tracked.txt')
FSE.writeFile(untrackedFile, 'some untracked file')
let status = await getStatusOrThrow(repository)
let files = status.workingDirectory.files
expect(files).toHaveLength(1)
expect(files[0].status.kind).toBe(AppFileStatusKind.Untracked)
await createDesktopStashEntry(repository, 'master', ['not-tracked.txt'])
status = await getStatusOrThrow(repository)
files = status.workingDirectory.files
expect(files).toHaveLength(0)
})
})
describe('getLastDesktopStashEntryForBranch', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
it('returns null when no stash entries exist for branch', async () => {
await generateTestStashEntry(repository, 'some-other-branch', true)
const entry = await getLastDesktopStashEntryForBranch(
repository,
'master'
)
expect(entry).toBeNull()
})
it('returns last entry made for branch', async () => {
const branchName = 'master'
await generateTestStashEntry(repository, branchName, true)
await generateTestStashEntry(repository, branchName, true)
const stash = await getStashes(repository)
// entries are returned in LIFO order
const lastEntry = stash.desktopEntries[0]
const actual = await getLastDesktopStashEntryForBranch(
repository,
branchName
)
expect(actual).not.toBeNull()
expect(actual!.stashSha).toBe(lastEntry.stashSha)
})
})
describe('createDesktopStashMessage', () => {
it('creates message that matches Desktop stash entry format', () => {
const branchName = 'master'
const message = createDesktopStashMessage(branchName)
expect(message).toBe('!!GitHub_Desktop<master>')
})
})
describe('dropDesktopStashEntry', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
it('removes the entry identified by `stashSha`', async () => {
await generateTestStashEntry(repository, 'master', true)
await generateTestStashEntry(repository, 'master', true)
let stash = await getStashes(repository)
let entries = stash.desktopEntries
expect(entries.length).toBe(2)
const stashToDelete = entries[1]
await dropDesktopStashEntry(repository, stashToDelete.stashSha)
// using this function to get stashSha since it parses
// the output from git into easy to use objects
stash = await getStashes(repository)
entries = stash.desktopEntries
expect(entries.length).toBe(1)
expect(entries[0].stashSha).not.toEqual(stashToDelete)
})
it('does not fail when attempting to delete when stash is empty', async () => {
let didFail = false
const doesNotExist: IStashEntry = {
name: 'stash@{0}',
branchName: 'master',
stashSha: 'xyz',
files: { kind: StashedChangesLoadStates.NotLoaded },
}
try {
await dropDesktopStashEntry(repository, doesNotExist.stashSha)
} catch {
didFail = true
}
expect(didFail).toBe(false)
})
it("does not fail when attempting to delete stash entry that doesn't exist", async () => {
let didFail = false
const doesNotExist: IStashEntry = {
name: 'stash@{4}',
branchName: 'master',
stashSha: 'xyz',
files: { kind: StashedChangesLoadStates.NotLoaded },
}
await generateTestStashEntry(repository, 'master', true)
await generateTestStashEntry(repository, 'master', true)
await generateTestStashEntry(repository, 'master', true)
try {
await dropDesktopStashEntry(repository, doesNotExist.stashSha)
} catch {
didFail = true
}
expect(didFail).toBe(false)
})
})
describe('popStashEntry', () => {
let repository: Repository
let readme: string
beforeEach(async () => {
repository = await setupEmptyRepository()
readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, '')
await GitProcess.exec(['add', 'README.md'], repository.path)
await GitProcess.exec(['commit', '-m', 'initial commit'], repository.path)
})
describe('without any conflicts', () => {
it('restores changes back to the working directory', async () => {
await generateTestStashEntry(repository, 'master', true)
const stash = await getStashes(repository)
const { desktopEntries } = stash
expect(desktopEntries.length).toBe(1)
let status = await getStatusOrThrow(repository)
let files = status.workingDirectory.files
expect(files).toHaveLength(0)
const entryToApply = desktopEntries[0]
await popStashEntry(repository, entryToApply.stashSha)
status = await getStatusOrThrow(repository)
files = status.workingDirectory.files
expect(files).toHaveLength(1)
})
})
describe('when there are (resolvable) conflicts', () => {
it('restores changes and drops stash', async () => {
await generateTestStashEntry(repository, 'master', true)
const stash = await getStashes(repository)
const { desktopEntries } = stash
expect(desktopEntries.length).toBe(1)
const readme = path.join(repository.path, 'README.md')
await FSE.appendFile(readme, generateString())
await GitProcess.exec(
['commit', '-am', 'later commit'],
repository.path
)
let status = await getStatusOrThrow(repository)
let files = status.workingDirectory.files
expect(files).toHaveLength(0)
const entryToApply = desktopEntries[0]
await popStashEntry(repository, entryToApply.stashSha)
status = await getStatusOrThrow(repository)
files = status.workingDirectory.files
expect(files).toHaveLength(1)
const stashAfter = await getStashes(repository)
expect(stashAfter.desktopEntries).not.toContain(entryToApply)
})
})
describe('when there are unresolvable conflicts', () => {
it('throws an error', async () => {
await generateTestStashEntry(repository, 'master', true)
const stash = await getStashes(repository)
const { desktopEntries } = stash
expect(desktopEntries.length).toBe(1)
const readme = path.join(repository.path, 'README.md')
await FSE.writeFile(readme, generateString())
const entryToApply = desktopEntries[0]
await expect(
popStashEntry(repository, entryToApply.stashSha)
).rejects.toThrowError()
})
})
})
})
/**
* Creates a stash entry using `git stash push` to allow for simulating
* entries created via the CLI and Desktop
*
* @param repository the repository to create the stash entry for
* @param message passing null will similate a Desktop created stash entry
*/
async function stash(
repository: Repository,
branchName: string,
message: string | null
): Promise<void> {
const result = await GitProcess.exec(
['stash', 'push', '-m', message || createDesktopStashMessage(branchName)],
repository.path
)
if (result.exitCode !== 0) {
throw new Error(result.stderr)
}
}
async function generateTestStashEntry(
repository: Repository,
branchName: string,
simulateDesktopEntry: boolean
): Promise<void> {
const message = simulateDesktopEntry ? null : 'Should get filtered'
const readme = path.join(repository.path, 'README.md')
await FSE.appendFile(readme, generateString())
await stash(repository, branchName, message)
}