git0 0.2.99 → 0.3.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/benchmark/README.md +104 -0
- package/benchmark/report.ts +142 -0
- package/benchmark/tarball-vs-clone.bench.ts +173 -0
- package/dist/cli.js +264 -25
- package/dist/cli.js.map +9 -7
- package/package.json +3 -2
- package/readme.md +120 -1
- package/src/args.ts +58 -0
- package/src/cli.ts +104 -20
- package/src/download.ts +214 -12
- package/src/github-api.ts +108 -2
- package/src/history.ts +225 -0
- package/src/utils.ts +29 -9
- package/test/args.test.ts +44 -0
- package/test/benchmark-report.test.ts +102 -0
- package/test/download-repo.test.ts +178 -0
- package/test/history.test.ts +157 -0
- package/test/parse-target.test.ts +73 -0
- package/test/subpath.test.ts +178 -0
- package/test/utils.test.ts +37 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import {
|
|
6
|
+
normalizeSubPath,
|
|
7
|
+
subPathFilter,
|
|
8
|
+
subPathDirName,
|
|
9
|
+
hoistSubPath,
|
|
10
|
+
} from '../src/download.ts';
|
|
11
|
+
|
|
12
|
+
describe('normalizeSubPath', () => {
|
|
13
|
+
test('returns empty string for nothing', () => {
|
|
14
|
+
expect(normalizeSubPath(null)).toBe('');
|
|
15
|
+
expect(normalizeSubPath(undefined)).toBe('');
|
|
16
|
+
expect(normalizeSubPath('')).toBe('');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('strips leading, trailing and doubled separators', () => {
|
|
20
|
+
expect(normalizeSubPath('/packages//ui/')).toBe('packages/ui');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('drops "." segments and normalizes backslashes', () => {
|
|
24
|
+
expect(normalizeSubPath('./packages/./ui')).toBe('packages/ui');
|
|
25
|
+
expect(normalizeSubPath('packages\\ui')).toBe('packages/ui');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('keeps dotfile directories, which are not "." segments', () => {
|
|
29
|
+
expect(normalizeSubPath('.continue/agents')).toBe('.continue/agents');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('rejects traversal rather than normalizing it away', () => {
|
|
33
|
+
expect(() => normalizeSubPath('../../etc')).toThrow(/\.\./);
|
|
34
|
+
expect(() => normalizeSubPath('packages/../../etc')).toThrow(/\.\./);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('subPathFilter', () => {
|
|
39
|
+
test('keeps every entry when there is no sub-path', () => {
|
|
40
|
+
const keep = subPathFilter('');
|
|
41
|
+
expect(keep('react-abc123/package.json')).toBe(true);
|
|
42
|
+
expect(keep('react-abc123/src/index.ts')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('keeps the sub-path itself and everything under it', () => {
|
|
46
|
+
const keep = subPathFilter('packages/ui');
|
|
47
|
+
expect(keep('react-abc123/packages/ui')).toBe(true);
|
|
48
|
+
expect(keep('react-abc123/packages/ui/')).toBe(true);
|
|
49
|
+
expect(keep('react-abc123/packages/ui/src/index.ts')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('drops everything outside it', () => {
|
|
53
|
+
const keep = subPathFilter('packages/ui');
|
|
54
|
+
expect(keep('react-abc123/package.json')).toBe(false);
|
|
55
|
+
expect(keep('react-abc123/packages/core/index.ts')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('matches whole segments, not string prefixes', () => {
|
|
59
|
+
const keep = subPathFilter('src/lib');
|
|
60
|
+
expect(keep('repo-abc123/src/library.ts')).toBe(false);
|
|
61
|
+
expect(keep('repo-abc123/src/lib/index.ts')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('matches a single file', () => {
|
|
65
|
+
const keep = subPathFilter('.continue/agents/new-config.yaml');
|
|
66
|
+
expect(keep('debate-ai.com-abc123/.continue/agents/new-config.yaml')).toBe(true);
|
|
67
|
+
expect(keep('debate-ai.com-abc123/.continue/agents/other.yaml')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('subPathDirName', () => {
|
|
72
|
+
test('falls back to the repo name when there is no sub-path', () => {
|
|
73
|
+
expect(subPathDirName('')).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('uses the last segment of a folder path', () => {
|
|
77
|
+
expect(subPathDirName('packages/ui', 'tree')).toBe('ui');
|
|
78
|
+
expect(subPathDirName('.continue/agents', 'tree')).toBe('agents');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('uses the enclosing folder for a file path', () => {
|
|
82
|
+
expect(subPathDirName('.continue/agents/new-config.yaml', 'blob')).toBe('agents');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('falls back to the repo name for a file at the repo root', () => {
|
|
86
|
+
expect(subPathDirName('readme.md', 'blob')).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('treats an untyped shorthand path as a folder', () => {
|
|
90
|
+
expect(subPathDirName('packages/react-dom')).toBe('react-dom');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('hoistSubPath', () => {
|
|
95
|
+
let tmp: string;
|
|
96
|
+
let extractPath: string;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'git0-hoist-test-'));
|
|
100
|
+
extractPath = path.join(tmp, 'target');
|
|
101
|
+
fs.mkdirSync(extractPath);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('is a no-op without a sub-path', () => {
|
|
109
|
+
fs.writeFileSync(path.join(extractPath, 'a.txt'), 'a');
|
|
110
|
+
hoistSubPath(extractPath, '');
|
|
111
|
+
expect(fs.readdirSync(extractPath)).toEqual(['a.txt']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('lifts a nested folder to the extraction root', () => {
|
|
115
|
+
fs.mkdirSync(path.join(extractPath, 'packages', 'ui', 'src'), { recursive: true });
|
|
116
|
+
fs.writeFileSync(path.join(extractPath, 'packages', 'ui', 'package.json'), '{}');
|
|
117
|
+
fs.writeFileSync(path.join(extractPath, 'packages', 'ui', 'src', 'index.ts'), 'export {}');
|
|
118
|
+
|
|
119
|
+
hoistSubPath(extractPath, 'packages/ui');
|
|
120
|
+
|
|
121
|
+
expect(fs.readdirSync(extractPath).sort()).toEqual(['package.json', 'src']);
|
|
122
|
+
expect(fs.readFileSync(path.join(extractPath, 'src', 'index.ts'), 'utf8')).toBe('export {}');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('removes the wrapper directories it hoisted out of', () => {
|
|
126
|
+
fs.mkdirSync(path.join(extractPath, 'packages', 'ui'), { recursive: true });
|
|
127
|
+
fs.writeFileSync(path.join(extractPath, 'packages', 'ui', 'a.txt'), 'a');
|
|
128
|
+
|
|
129
|
+
hoistSubPath(extractPath, 'packages/ui');
|
|
130
|
+
|
|
131
|
+
expect(fs.existsSync(path.join(extractPath, 'packages'))).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('lifts a single file and keeps its own name', () => {
|
|
135
|
+
fs.mkdirSync(path.join(extractPath, '.continue', 'agents'), { recursive: true });
|
|
136
|
+
fs.writeFileSync(path.join(extractPath, '.continue', 'agents', 'new-config.yaml'), 'name: x');
|
|
137
|
+
|
|
138
|
+
hoistSubPath(extractPath, '.continue/agents/new-config.yaml');
|
|
139
|
+
|
|
140
|
+
expect(fs.readdirSync(extractPath)).toEqual(['new-config.yaml']);
|
|
141
|
+
expect(fs.readFileSync(path.join(extractPath, 'new-config.yaml'), 'utf8')).toBe('name: x');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('handles a single-segment sub-path', () => {
|
|
145
|
+
fs.mkdirSync(path.join(extractPath, 'src'));
|
|
146
|
+
fs.writeFileSync(path.join(extractPath, 'src', 'index.ts'), 'x');
|
|
147
|
+
|
|
148
|
+
hoistSubPath(extractPath, 'src');
|
|
149
|
+
|
|
150
|
+
expect(fs.readdirSync(extractPath)).toEqual(['index.ts']);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('only clears the wrapper it hoisted out of, not unrelated siblings', () => {
|
|
154
|
+
// In the real flow the tar filter means nothing outside the sub-path is
|
|
155
|
+
// ever written. Hoisting still scopes its cleanup to the sub-path's own
|
|
156
|
+
// first segment, so it can never delete files it was not asked about.
|
|
157
|
+
fs.mkdirSync(path.join(extractPath, 'packages', 'ui'), { recursive: true });
|
|
158
|
+
fs.writeFileSync(path.join(extractPath, 'packages', 'ui', 'a.txt'), 'a');
|
|
159
|
+
fs.writeFileSync(path.join(extractPath, 'README.md'), 'readme');
|
|
160
|
+
|
|
161
|
+
hoistSubPath(extractPath, 'packages/ui');
|
|
162
|
+
|
|
163
|
+
expect(fs.readdirSync(extractPath).sort()).toEqual(['README.md', 'a.txt']);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('explains itself when the path is not in the repository', () => {
|
|
167
|
+
expect(() => hoistSubPath(extractPath, 'packages/nope')).toThrow(/not found/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('leaves no staging directory behind', () => {
|
|
171
|
+
fs.mkdirSync(path.join(extractPath, 'packages', 'ui'), { recursive: true });
|
|
172
|
+
fs.writeFileSync(path.join(extractPath, 'packages', 'ui', 'a.txt'), 'a');
|
|
173
|
+
|
|
174
|
+
hoistSubPath(extractPath, 'packages/ui');
|
|
175
|
+
|
|
176
|
+
expect(fs.readdirSync(tmp)).toEqual(['target']);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { printLogo, resetLogo } from '../src/utils.ts';
|
|
3
|
+
|
|
4
|
+
describe('printLogo', () => {
|
|
5
|
+
let log: ReturnType<typeof vi.spyOn>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resetLogo();
|
|
9
|
+
log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
log.mockRestore();
|
|
14
|
+
resetLogo();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('prints the logo', () => {
|
|
18
|
+
printLogo();
|
|
19
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
20
|
+
expect(log.mock.calls[0][0]).toContain('__ _(_)');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('prints it once however many times it is called', () => {
|
|
24
|
+
// main() prints the banner, then so does the download path it hands off to.
|
|
25
|
+
printLogo();
|
|
26
|
+
printLogo();
|
|
27
|
+
printLogo();
|
|
28
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resetLogo lets the next call print again', () => {
|
|
32
|
+
printLogo();
|
|
33
|
+
resetLogo();
|
|
34
|
+
printLogo();
|
|
35
|
+
expect(log).toHaveBeenCalledTimes(2);
|
|
36
|
+
});
|
|
37
|
+
});
|