ts-repo-utils 6.1.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/cmd/assert-repo-is-clean.mjs +1 -1
- package/dist/cmd/check-should-run-type-checks.mjs +1 -1
- package/dist/cmd/format-diff-from.mjs +29 -8
- package/dist/cmd/format-diff-from.mjs.map +1 -1
- package/dist/cmd/format-uncommitted.d.mts +3 -0
- package/dist/cmd/format-uncommitted.d.mts.map +1 -0
- package/dist/cmd/format-uncommitted.mjs +59 -0
- package/dist/cmd/format-uncommitted.mjs.map +1 -0
- package/dist/cmd/gen-index-ts.mjs +1 -1
- package/dist/functions/diff.d.mts +32 -2
- package/dist/functions/diff.d.mts.map +1 -1
- package/dist/functions/diff.mjs +47 -29
- package/dist/functions/diff.mjs.map +1 -1
- package/dist/functions/exec-async.d.mts +2 -2
- package/dist/functions/exec-async.d.mts.map +1 -1
- package/dist/functions/format.d.mts +20 -11
- package/dist/functions/format.d.mts.map +1 -1
- package/dist/functions/format.mjs +134 -95
- package/dist/functions/format.mjs.map +1 -1
- package/dist/functions/index.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
- package/src/cmd/assert-repo-is-clean.mts +1 -1
- package/src/cmd/check-should-run-type-checks.mts +1 -1
- package/src/cmd/format-diff-from.mts +35 -9
- package/src/cmd/format-uncommitted.mts +67 -0
- package/src/cmd/gen-index-ts.mts +1 -1
- package/src/functions/diff.mts +85 -32
- package/src/functions/diff.test.mts +569 -102
- package/src/functions/exec-async.mts +2 -2
- package/src/functions/exec-async.test.mts +77 -47
- package/src/functions/format.mts +224 -141
- package/src/functions/format.test.mts +625 -20
- package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +266 -0
- package/dist/cmd/format-untracked.d.mts +0 -3
- package/dist/cmd/format-untracked.d.mts.map +0 -1
- package/dist/cmd/format-untracked.mjs +0 -34
- package/dist/cmd/format-untracked.mjs.map +0 -1
- package/src/cmd/format-untracked.mts +0 -31
|
@@ -1,176 +1,643 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
1
2
|
import { Result } from 'ts-data-forge';
|
|
2
3
|
import '../node-global.mjs';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getDiffFrom,
|
|
6
|
+
getModifiedFiles,
|
|
7
|
+
getStagedFiles,
|
|
8
|
+
getUntrackedFiles,
|
|
9
|
+
} from './diff.mjs';
|
|
10
|
+
import { type ExecResult } from './exec-async.mjs';
|
|
11
|
+
|
|
12
|
+
describe('diff', () => {
|
|
13
|
+
// Helper function to create a temporary git repository
|
|
14
|
+
const createTempRepo = async (): Promise<{
|
|
15
|
+
repoPath: string;
|
|
16
|
+
cleanup: () => Promise<void>;
|
|
17
|
+
execInRepo: (
|
|
18
|
+
cmd: string,
|
|
19
|
+
options?: Readonly<{ silent?: boolean }>,
|
|
20
|
+
) => Promise<Result<{ stdout: string; stderr: string }, unknown>>;
|
|
21
|
+
}> => {
|
|
22
|
+
const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'temp-repo-'));
|
|
23
|
+
const repoPath = tempDir;
|
|
24
|
+
|
|
25
|
+
// Initialize git repository
|
|
26
|
+
const execInRepo = async (
|
|
27
|
+
cmd: string,
|
|
28
|
+
options?: Readonly<{ silent?: boolean }>,
|
|
29
|
+
): Promise<ExecResult<string>> => {
|
|
30
|
+
const originalCwd = process.cwd();
|
|
31
|
+
process.chdir(repoPath);
|
|
32
|
+
try {
|
|
33
|
+
const result = await $(cmd, { silent: options?.silent ?? false });
|
|
34
|
+
return result;
|
|
35
|
+
} finally {
|
|
36
|
+
process.chdir(originalCwd);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
4
39
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
40
|
+
await execInRepo('git init', { silent: true });
|
|
41
|
+
await execInRepo('git config user.name "Test User"', { silent: true });
|
|
42
|
+
await execInRepo('git config user.email "test@example.com"', {
|
|
43
|
+
silent: true,
|
|
44
|
+
});
|
|
8
45
|
|
|
9
|
-
|
|
10
|
-
// Helper function to clean up test files
|
|
11
|
-
const cleanupTestFiles = async (files: Set<string>): Promise<void> => {
|
|
12
|
-
for (const file of files) {
|
|
46
|
+
const cleanup = async (): Promise<void> => {
|
|
13
47
|
try {
|
|
14
|
-
|
|
15
|
-
await fs.rm(file, { force: true });
|
|
48
|
+
await fs.rm(repoPath, { recursive: true, force: true });
|
|
16
49
|
} catch {
|
|
17
50
|
// Ignore cleanup errors
|
|
18
51
|
}
|
|
19
|
-
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { repoPath, cleanup, execInRepo };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Wrapper functions to execute diff functions in the temporary repository
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
59
|
+
const createRepoFunctions = (repoPath: string) => {
|
|
60
|
+
const executeInRepo = async <T,>(fn: () => Promise<T>): Promise<T> => {
|
|
61
|
+
const originalCwd = process.cwd();
|
|
62
|
+
process.chdir(repoPath);
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
} finally {
|
|
66
|
+
process.chdir(originalCwd);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
getUntrackedFiles: (options?: Parameters<typeof getUntrackedFiles>[0]) =>
|
|
72
|
+
executeInRepo(() => getUntrackedFiles(options)),
|
|
73
|
+
getStagedFiles: (options?: Parameters<typeof getStagedFiles>[0]) =>
|
|
74
|
+
executeInRepo(() => getStagedFiles(options)),
|
|
75
|
+
getModifiedFiles: (options?: Parameters<typeof getModifiedFiles>[0]) =>
|
|
76
|
+
executeInRepo(() => getModifiedFiles(options)),
|
|
77
|
+
getDiffFrom: (
|
|
78
|
+
base: string,
|
|
79
|
+
options?: Parameters<typeof getDiffFrom>[1],
|
|
80
|
+
) => executeInRepo(() => getDiffFrom(base, options)),
|
|
81
|
+
};
|
|
20
82
|
};
|
|
21
83
|
|
|
22
84
|
describe('getUntrackedFiles', () => {
|
|
23
85
|
test('should return empty array when no files are changed', async () => {
|
|
24
|
-
const
|
|
86
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
87
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
25
88
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
89
|
+
try {
|
|
90
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
91
|
+
|
|
92
|
+
expect(Result.isOk(result)).toBe(true);
|
|
93
|
+
if (Result.isOk(result)) {
|
|
94
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
await cleanup();
|
|
29
98
|
}
|
|
30
99
|
});
|
|
31
100
|
|
|
32
101
|
test('should detect newly created files', async () => {
|
|
33
|
-
const
|
|
102
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
103
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
34
104
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
105
|
+
try {
|
|
106
|
+
// Create a new file in the temp repository
|
|
107
|
+
const testFileName = `test-new-file-${crypto.randomUUID()}.tmp`;
|
|
108
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
39
109
|
|
|
40
|
-
|
|
110
|
+
await fs.writeFile(testFilePath, 'test content');
|
|
41
111
|
|
|
42
|
-
|
|
112
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
43
113
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
expect(Result.isOk(result)).toBe(true);
|
|
115
|
+
if (Result.isOk(result)) {
|
|
116
|
+
const files = result.value;
|
|
117
|
+
expect(files).toContain(testFileName);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
await cleanup();
|
|
48
121
|
}
|
|
49
|
-
|
|
50
|
-
await cleanupTestFiles(mut_testFiles);
|
|
51
122
|
});
|
|
52
123
|
|
|
53
124
|
test('should detect modified existing files', async () => {
|
|
54
|
-
const
|
|
125
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
126
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
55
127
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
128
|
+
try {
|
|
129
|
+
// Create and track a file first
|
|
130
|
+
const testFileName = `test-modify-file-${crypto.randomUUID()}.tmp`;
|
|
131
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
60
132
|
|
|
61
|
-
|
|
62
|
-
await fs.writeFile(mut_testFilePath, 'initial content');
|
|
133
|
+
await fs.writeFile(testFilePath, 'initial content');
|
|
63
134
|
|
|
64
|
-
|
|
65
|
-
|
|
135
|
+
// Add to git to track it
|
|
136
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
66
137
|
|
|
67
|
-
|
|
68
|
-
|
|
138
|
+
// Modify the file
|
|
139
|
+
await fs.writeFile(testFilePath, 'modified content');
|
|
69
140
|
|
|
70
|
-
|
|
141
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
71
142
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
143
|
+
expect(Result.isOk(result)).toBe(true);
|
|
144
|
+
if (Result.isOk(result)) {
|
|
145
|
+
const files = result.value;
|
|
146
|
+
expect(files).not.toContain(testFileName);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await cleanup();
|
|
76
150
|
}
|
|
151
|
+
});
|
|
77
152
|
|
|
78
|
-
|
|
79
|
-
|
|
153
|
+
test('should detect multiple types of changes', async () => {
|
|
154
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
155
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
80
156
|
|
|
81
|
-
|
|
157
|
+
try {
|
|
158
|
+
// Create multiple test files
|
|
159
|
+
const uuid = crypto.randomUUID();
|
|
160
|
+
const newFile = `test-new-file-${uuid}.tmp`;
|
|
161
|
+
const modifyFile = `test-modify-file-${uuid}.tmp`;
|
|
162
|
+
const newFilePath = path.join(repoPath, newFile);
|
|
163
|
+
const modifyFilePath = path.join(repoPath, modifyFile);
|
|
164
|
+
|
|
165
|
+
// Create new file
|
|
166
|
+
await fs.writeFile(newFilePath, 'new file content');
|
|
167
|
+
|
|
168
|
+
// Create and track another file
|
|
169
|
+
await fs.writeFile(modifyFilePath, 'initial content');
|
|
170
|
+
await execInRepo(`git add ${modifyFile}`, { silent: true });
|
|
171
|
+
|
|
172
|
+
// Modify the tracked file
|
|
173
|
+
await fs.writeFile(modifyFilePath, 'modified content');
|
|
174
|
+
|
|
175
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
176
|
+
|
|
177
|
+
expect(Result.isOk(result)).toBe(true);
|
|
178
|
+
if (Result.isOk(result)) {
|
|
179
|
+
const files = result.value;
|
|
180
|
+
expect(files).toContain(newFile);
|
|
181
|
+
expect(files).not.toContain(modifyFile);
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
await cleanup();
|
|
185
|
+
}
|
|
82
186
|
});
|
|
83
187
|
|
|
84
|
-
test('should
|
|
85
|
-
const
|
|
188
|
+
test('should exclude deleted files from results', async () => {
|
|
189
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
190
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
194
|
+
|
|
195
|
+
expect(Result.isOk(result)).toBe(true);
|
|
196
|
+
if (Result.isOk(result)) {
|
|
197
|
+
const files = result.value;
|
|
198
|
+
// Verify no deleted files are included (status 'D')
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
expect(typeof file).toBe('string');
|
|
201
|
+
expect(file.length).toBeGreaterThan(0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
await cleanup();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('should handle git command errors gracefully', async () => {
|
|
210
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
211
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
86
212
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const modifyFile = path.join(process.cwd(), 'test-modify-file.tmp');
|
|
90
|
-
mut_testFiles.add(newFile);
|
|
91
|
-
mut_testFiles.add(modifyFile);
|
|
213
|
+
try {
|
|
214
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
92
215
|
|
|
93
|
-
|
|
94
|
-
|
|
216
|
+
// Should always return a Result, either Ok or Err
|
|
217
|
+
expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
|
|
218
|
+
} finally {
|
|
219
|
+
await cleanup();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
95
222
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
223
|
+
test('should parse git status output correctly', async () => {
|
|
224
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
225
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
99
226
|
|
|
100
|
-
|
|
101
|
-
|
|
227
|
+
try {
|
|
228
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
229
|
+
|
|
230
|
+
expect(Result.isOk(result)).toBe(true);
|
|
231
|
+
if (Result.isOk(result)) {
|
|
232
|
+
const files = result.value;
|
|
233
|
+
|
|
234
|
+
// Each file should be a non-empty string
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
expect(typeof file).toBe('string');
|
|
237
|
+
expect(file.trim()).toBe(file); // No leading/trailing whitespace
|
|
238
|
+
expect(file.length).toBeGreaterThan(0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
await cleanup();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('should work with silent option', async () => {
|
|
247
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
248
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
102
249
|
|
|
103
|
-
|
|
250
|
+
try {
|
|
251
|
+
const result = await repoFunctions.getUntrackedFiles({ silent: true });
|
|
104
252
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
253
|
+
expect(Result.isOk(result)).toBe(true);
|
|
254
|
+
if (Result.isOk(result)) {
|
|
255
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
256
|
+
}
|
|
257
|
+
} finally {
|
|
258
|
+
await cleanup();
|
|
110
259
|
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
111
262
|
|
|
112
|
-
|
|
113
|
-
|
|
263
|
+
describe('getStagedFiles', () => {
|
|
264
|
+
test('should return empty array when no files are staged', async () => {
|
|
265
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
266
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
114
267
|
|
|
115
|
-
|
|
268
|
+
try {
|
|
269
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
270
|
+
expect(Result.isOk(result)).toBe(true);
|
|
271
|
+
if (Result.isOk(result)) {
|
|
272
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
await cleanup();
|
|
276
|
+
}
|
|
116
277
|
});
|
|
117
278
|
|
|
118
|
-
test('should
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
279
|
+
test('should detect staged files', async () => {
|
|
280
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
281
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Create a new file
|
|
285
|
+
const testFileName = `test-staged-file-${crypto.randomUUID()}.tmp`;
|
|
286
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
287
|
+
await fs.writeFile(testFilePath, 'staged file content');
|
|
288
|
+
// Stage the file
|
|
289
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
290
|
+
|
|
291
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
292
|
+
expect(Result.isOk(result)).toBe(true);
|
|
293
|
+
if (Result.isOk(result)) {
|
|
294
|
+
const files = result.value;
|
|
295
|
+
expect(files).toContain(testFileName);
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
await cleanup();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('should detect multiple staged files', async () => {
|
|
303
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
304
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Create multiple test files
|
|
308
|
+
const uuid = crypto.randomUUID();
|
|
309
|
+
const file1 = `test-staged-file1-${uuid}.tmp`;
|
|
310
|
+
const file2 = `test-staged-file2-${uuid}.tmp`;
|
|
311
|
+
const filePath1 = path.join(repoPath, file1);
|
|
312
|
+
const filePath2 = path.join(repoPath, file2);
|
|
313
|
+
|
|
314
|
+
await fs.writeFile(filePath1, 'staged file 1 content');
|
|
315
|
+
await fs.writeFile(filePath2, 'staged file 2 content');
|
|
316
|
+
// Stage both files
|
|
317
|
+
await execInRepo(`git add ${file1} ${file2}`, { silent: true });
|
|
318
|
+
|
|
319
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
320
|
+
expect(Result.isOk(result)).toBe(true);
|
|
321
|
+
if (Result.isOk(result)) {
|
|
322
|
+
const files = result.value;
|
|
323
|
+
expect(files).toContain(file1);
|
|
324
|
+
expect(files).toContain(file2);
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
await cleanup();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('should exclude deleted files by default', async () => {
|
|
332
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
333
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const testFileName = `test-deleted-file-${crypto.randomUUID()}.tmp`;
|
|
337
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
338
|
+
|
|
339
|
+
// Create a file and commit it
|
|
340
|
+
await fs.writeFile(testFilePath, 'file to be deleted');
|
|
341
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
342
|
+
await execInRepo(
|
|
343
|
+
`git commit -m "Add test file for deletion" --no-verify`,
|
|
344
|
+
{
|
|
345
|
+
silent: true,
|
|
346
|
+
},
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Delete the file and stage the deletion
|
|
350
|
+
await fs.rm(testFilePath, { force: true });
|
|
351
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
352
|
+
|
|
353
|
+
// Test with excludeDeleted = true (default)
|
|
354
|
+
const resultExclude = await repoFunctions.getStagedFiles({
|
|
355
|
+
silent: true,
|
|
356
|
+
});
|
|
357
|
+
expect(Result.isOk(resultExclude)).toBe(true);
|
|
358
|
+
if (Result.isOk(resultExclude)) {
|
|
359
|
+
const files = resultExclude.value;
|
|
360
|
+
expect(files).not.toContain(testFileName);
|
|
361
|
+
}
|
|
122
362
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
363
|
+
// Test with excludeDeleted = false
|
|
364
|
+
// First verify the file is actually staged for deletion by checking git status
|
|
365
|
+
const gitStatusResult = await execInRepo(`git status --porcelain`, {
|
|
366
|
+
silent: true,
|
|
367
|
+
});
|
|
368
|
+
const hasDeletion =
|
|
369
|
+
Result.isOk(gitStatusResult) &&
|
|
370
|
+
gitStatusResult.value.stdout.includes(`D ${testFileName}`);
|
|
371
|
+
|
|
372
|
+
if (hasDeletion) {
|
|
373
|
+
const resultInclude = await repoFunctions.getStagedFiles({
|
|
374
|
+
excludeDeleted: false,
|
|
375
|
+
silent: true,
|
|
376
|
+
});
|
|
377
|
+
expect(Result.isOk(resultInclude)).toBe(true);
|
|
378
|
+
if (Result.isOk(resultInclude)) {
|
|
379
|
+
const files = resultInclude.value;
|
|
380
|
+
expect(files).toContain(testFileName);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// If the file is not properly staged for deletion, just log and skip the assertion
|
|
384
|
+
console.warn(
|
|
385
|
+
`Test file ${testFileName} was not properly staged for deletion, skipping inclusion test`,
|
|
386
|
+
);
|
|
130
387
|
}
|
|
388
|
+
} finally {
|
|
389
|
+
await cleanup();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('should parse staged files output correctly', async () => {
|
|
394
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
395
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
399
|
+
expect(Result.isOk(result)).toBe(true);
|
|
400
|
+
if (Result.isOk(result)) {
|
|
401
|
+
const files = result.value;
|
|
402
|
+
// Each file should be a non-empty string
|
|
403
|
+
for (const file of files) {
|
|
404
|
+
expect(typeof file).toBe('string');
|
|
405
|
+
expect(file.trim()).toBe(file); // No leading/trailing whitespace
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
await cleanup();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('should work with silent option', async () => {
|
|
414
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
415
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
419
|
+
expect(Result.isOk(result)).toBe(true);
|
|
420
|
+
if (Result.isOk(result)) {
|
|
421
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
422
|
+
}
|
|
423
|
+
} finally {
|
|
424
|
+
await cleanup();
|
|
131
425
|
}
|
|
132
426
|
});
|
|
133
427
|
|
|
134
428
|
test('should handle git command errors gracefully', async () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const result = await getUntrackedFiles({ silent: true });
|
|
429
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
430
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
138
431
|
|
|
139
|
-
|
|
140
|
-
|
|
432
|
+
try {
|
|
433
|
+
const result = await repoFunctions.getStagedFiles({ silent: true });
|
|
434
|
+
// Should always return a Result, either Ok or Err
|
|
435
|
+
expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
|
|
436
|
+
} finally {
|
|
437
|
+
await cleanup();
|
|
438
|
+
}
|
|
141
439
|
});
|
|
440
|
+
});
|
|
142
441
|
|
|
143
|
-
|
|
144
|
-
|
|
442
|
+
describe('getModifiedFiles', () => {
|
|
443
|
+
test('should return empty array when no files are modified', async () => {
|
|
444
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
445
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
449
|
+
expect(Result.isOk(result)).toBe(true);
|
|
450
|
+
if (Result.isOk(result)) {
|
|
451
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
452
|
+
}
|
|
453
|
+
} finally {
|
|
454
|
+
await cleanup();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('should detect modified files', async () => {
|
|
459
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
460
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
// Create a new file and commit it first
|
|
464
|
+
const testFileName = `test-modified-file-${crypto.randomUUID()}.tmp`;
|
|
465
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
466
|
+
|
|
467
|
+
await fs.writeFile(testFilePath, 'initial content');
|
|
468
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
469
|
+
await execInRepo(
|
|
470
|
+
`git commit -m "Add file for modification test" --no-verify`,
|
|
471
|
+
{ silent: true },
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Now modify the file (without staging)
|
|
475
|
+
await fs.writeFile(testFilePath, 'modified content');
|
|
476
|
+
|
|
477
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
478
|
+
expect(Result.isOk(result)).toBe(true);
|
|
479
|
+
if (Result.isOk(result)) {
|
|
480
|
+
const files = result.value;
|
|
481
|
+
expect(files).toContain(testFileName);
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
await cleanup();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
145
487
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
488
|
+
test('should detect multiple modified files', async () => {
|
|
489
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
490
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
149
491
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
492
|
+
try {
|
|
493
|
+
const uuid = crypto.randomUUID();
|
|
494
|
+
const file1 = `test-modified-file1-${uuid}.tmp`;
|
|
495
|
+
const file2 = `test-modified-file2-${uuid}.tmp`;
|
|
496
|
+
const filePath1 = path.join(repoPath, file1);
|
|
497
|
+
const filePath2 = path.join(repoPath, file2);
|
|
498
|
+
|
|
499
|
+
// Create and commit both files
|
|
500
|
+
await fs.writeFile(filePath1, 'initial content 1');
|
|
501
|
+
await fs.writeFile(filePath2, 'initial content 2');
|
|
502
|
+
await execInRepo(`git add ${file1} ${file2}`, { silent: true });
|
|
503
|
+
await execInRepo(
|
|
504
|
+
`git commit -m "Add files for modification test" --no-verify`,
|
|
505
|
+
{ silent: true },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Modify both files
|
|
509
|
+
await fs.writeFile(filePath1, 'modified content 1');
|
|
510
|
+
await fs.writeFile(filePath2, 'modified content 2');
|
|
511
|
+
|
|
512
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
513
|
+
expect(Result.isOk(result)).toBe(true);
|
|
514
|
+
if (Result.isOk(result)) {
|
|
515
|
+
const files = result.value;
|
|
516
|
+
expect(files).toContain(file1);
|
|
517
|
+
expect(files).toContain(file2);
|
|
155
518
|
}
|
|
519
|
+
} finally {
|
|
520
|
+
await cleanup();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('should exclude deleted files by default', async () => {
|
|
525
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
526
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const testFileName = `test-deleted-modified-file-${crypto.randomUUID()}.tmp`;
|
|
530
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
531
|
+
|
|
532
|
+
// Create a file and commit it
|
|
533
|
+
await fs.writeFile(testFilePath, 'file to be deleted');
|
|
534
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
535
|
+
await execInRepo(
|
|
536
|
+
`git commit -m "Add test file for deletion" --no-verify`,
|
|
537
|
+
{ silent: true },
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
// Delete the file (this makes it show up in git diff as deleted)
|
|
541
|
+
await fs.rm(testFilePath, { force: true });
|
|
542
|
+
|
|
543
|
+
// Test with excludeDeleted = true (default)
|
|
544
|
+
const resultExclude = await repoFunctions.getModifiedFiles({
|
|
545
|
+
silent: true,
|
|
546
|
+
});
|
|
547
|
+
expect(Result.isOk(resultExclude)).toBe(true);
|
|
548
|
+
if (Result.isOk(resultExclude)) {
|
|
549
|
+
const files = resultExclude.value;
|
|
550
|
+
expect(files).not.toContain(testFileName);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Test with excludeDeleted = false
|
|
554
|
+
const resultInclude = await repoFunctions.getModifiedFiles({
|
|
555
|
+
excludeDeleted: false,
|
|
556
|
+
silent: true,
|
|
557
|
+
});
|
|
558
|
+
expect(Result.isOk(resultInclude)).toBe(true);
|
|
559
|
+
if (Result.isOk(resultInclude)) {
|
|
560
|
+
const files = resultInclude.value;
|
|
561
|
+
expect(files).toContain(testFileName);
|
|
562
|
+
}
|
|
563
|
+
} finally {
|
|
564
|
+
await cleanup();
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('should parse modified files output correctly', async () => {
|
|
569
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
570
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
574
|
+
expect(Result.isOk(result)).toBe(true);
|
|
575
|
+
if (Result.isOk(result)) {
|
|
576
|
+
const files = result.value;
|
|
577
|
+
// Each file should be a non-empty string
|
|
578
|
+
for (const file of files) {
|
|
579
|
+
expect(typeof file).toBe('string');
|
|
580
|
+
expect(file.trim()).toBe(file); // No leading/trailing whitespace
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} finally {
|
|
584
|
+
await cleanup();
|
|
156
585
|
}
|
|
157
586
|
});
|
|
158
587
|
|
|
159
588
|
test('should work with silent option', async () => {
|
|
160
|
-
const
|
|
589
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
590
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
594
|
+
expect(Result.isOk(result)).toBe(true);
|
|
595
|
+
if (Result.isOk(result)) {
|
|
596
|
+
expect(Array.isArray(result.value)).toBe(true);
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
await cleanup();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('should handle git command errors gracefully', async () => {
|
|
604
|
+
const { repoPath, cleanup } = await createTempRepo();
|
|
605
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
161
606
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
607
|
+
try {
|
|
608
|
+
const result = await repoFunctions.getModifiedFiles({ silent: true });
|
|
609
|
+
// Should always return a Result, either Ok or Err
|
|
610
|
+
expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
|
|
611
|
+
} finally {
|
|
612
|
+
await cleanup();
|
|
165
613
|
}
|
|
166
614
|
});
|
|
167
615
|
});
|
|
168
616
|
|
|
169
617
|
describe('getDiffFrom', () => {
|
|
170
618
|
test('should work with silent option', async () => {
|
|
171
|
-
const
|
|
619
|
+
const { repoPath, cleanup, execInRepo } = await createTempRepo();
|
|
620
|
+
const repoFunctions = createRepoFunctions(repoPath);
|
|
172
621
|
|
|
173
|
-
|
|
622
|
+
try {
|
|
623
|
+
// Create an initial commit to have something to diff against
|
|
624
|
+
const testFileName = `test-initial-file-${crypto.randomUUID()}.tmp`;
|
|
625
|
+
const testFilePath = path.join(repoPath, testFileName);
|
|
626
|
+
|
|
627
|
+
await fs.writeFile(testFilePath, 'initial content');
|
|
628
|
+
await execInRepo(`git add ${testFileName}`, { silent: true });
|
|
629
|
+
await execInRepo(`git commit -m "Initial commit" --no-verify`, {
|
|
630
|
+
silent: true,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const result = await repoFunctions.getDiffFrom('HEAD~1', {
|
|
634
|
+
silent: true,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
|
|
638
|
+
} finally {
|
|
639
|
+
await cleanup();
|
|
640
|
+
}
|
|
174
641
|
});
|
|
175
642
|
});
|
|
176
643
|
});
|