@kubohiroya/turbowarp-plus-editor-git 0.11.0 → 0.12.1
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.ja.md +2 -0
- package/README.md +2 -0
- package/dist/index.js +37 -11
- package/dist/project-source-diff.d.ts +2 -0
- package/dist/project-source-diff.js +2 -2
- package/dist/project-source-review-model.d.ts +35 -0
- package/dist/review-words.d.ts +6 -0
- package/dist/{scratch-block-diff-renderer-CgAGfmAc.js → scratch-block-diff-renderer-e_drVlHE.js} +533 -254
- package/dist/style.css +14 -0
- package/package.json +2 -1
- package/src/index.tsx +40 -5
- package/src/project-source-diff.css +14 -0
- package/src/project-source-diff.tsx +2 -0
- package/src/project-source-review-model.ts +154 -8
- package/src/project-source-review.tsx +79 -13
- package/src/review-words.ts +66 -0
- package/test/smart-http-fixture.js +178 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {execFileSync} from 'node:child_process';
|
|
2
|
+
import {createServer} from 'node:http';
|
|
3
|
+
import {once} from 'node:events';
|
|
4
|
+
import {randomUUID} from 'node:crypto';
|
|
5
|
+
import {mkdtemp, mkdir, rm, writeFile} from 'node:fs/promises';
|
|
6
|
+
import {tmpdir} from 'node:os';
|
|
7
|
+
import {join} from 'node:path';
|
|
8
|
+
|
|
9
|
+
// Git's detached auto maintenance can repack loose objects while the code under test is reading them.
|
|
10
|
+
const gitEnv = {...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_COUNT: '2',
|
|
11
|
+
GIT_CONFIG_KEY_0: 'maintenance.auto', GIT_CONFIG_VALUE_0: 'false', GIT_CONFIG_KEY_1: 'gc.auto', GIT_CONFIG_VALUE_1: '0'};
|
|
12
|
+
const backend = join(execFileSync('git', ['--exec-path'], {encoding: 'utf8'}).trim(), 'git-http-backend');
|
|
13
|
+
export const remote = 'https://github.com/fixture/work.git';
|
|
14
|
+
export const readDiscovery = '/info/refs?service=git-upload-pack';
|
|
15
|
+
export const writeDiscovery = '/info/refs?service=git-receive-pack';
|
|
16
|
+
const routePrefix = '/v1/git/fixture/work.git';
|
|
17
|
+
const maxBody = 1024 * 1024;
|
|
18
|
+
const pushIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
19
|
+
|
|
20
|
+
export function git(dir, ...args) {
|
|
21
|
+
return execFileSync('git', args, {cwd: dir, encoding: 'utf8', env: gitEnv, stdio: ['ignore', 'pipe', 'pipe']}).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function commit(dir, message) {
|
|
25
|
+
git(dir, 'add', 'project.txt');
|
|
26
|
+
git(dir, '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', message);
|
|
27
|
+
return git(dir, 'rev-parse', 'HEAD');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function startFixture(t) {
|
|
31
|
+
const root = await mkdtemp(join(tmpdir(), 'tw-git-http-'));
|
|
32
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
33
|
+
await mkdir(join(root, 'fixture'));
|
|
34
|
+
const bare = join(root, 'fixture/work.git');
|
|
35
|
+
const seed = join(root, 'seed');
|
|
36
|
+
const client = join(root, 'client');
|
|
37
|
+
git(root, 'init', '-q', '--bare', '-b', 'main', bare);
|
|
38
|
+
git(bare, 'config', 'http.receivepack', 'true');
|
|
39
|
+
git(root, 'init', '-q', '-b', 'main', seed);
|
|
40
|
+
await writeFile(join(seed, 'project.txt'), 'initial\n');
|
|
41
|
+
commit(seed, 'initial');
|
|
42
|
+
git(seed, 'push', bare, 'main');
|
|
43
|
+
git(root, 'clone', '-q', bare, client);
|
|
44
|
+
git(client, 'remote', 'set-url', 'origin', remote);
|
|
45
|
+
git(client, 'config', 'http.corsProxy', 'https://rejected.example.invalid');
|
|
46
|
+
|
|
47
|
+
const origins = new Set(['https://editor.example.invalid']);
|
|
48
|
+
const sessions = new Map([
|
|
49
|
+
['fixture-read', {write: false, mode: 'desktop'}],
|
|
50
|
+
['fixture-write', {write: true, mode: 'desktop'}],
|
|
51
|
+
['fixture-expired', {expires: true}],
|
|
52
|
+
['fixture-cloudflare', {write: true, mode: 'cloudflare'}]
|
|
53
|
+
]);
|
|
54
|
+
const requests = [];
|
|
55
|
+
const upstream = [];
|
|
56
|
+
const pushIds = new WeakMap();
|
|
57
|
+
const fixture = {root, bare, seed, client, origins, sessions, requests, upstream, redirect: false, dropPush: false};
|
|
58
|
+
const reply = (res, status, code) => {
|
|
59
|
+
res.writeHead(status, {'Content-Type': 'application/json'});
|
|
60
|
+
res.end(JSON.stringify({code, requestId: randomUUID()}));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async function handle(req, res) {
|
|
64
|
+
const {url, method, headers} = req;
|
|
65
|
+
requests.push({url, method, headers});
|
|
66
|
+
if (headers.host !== new URL(fixture.origin).host || req.socket.remoteAddress !== '127.0.0.1' ||
|
|
67
|
+
!origins.has(headers.origin)) return reply(res, 403, 'FORBIDDEN');
|
|
68
|
+
|
|
69
|
+
res.setHeader('Access-Control-Allow-Origin', headers.origin);
|
|
70
|
+
res.setHeader('Vary', 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers');
|
|
71
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
72
|
+
const effectiveMethod = method === 'OPTIONS' ? headers['access-control-request-method'] : method;
|
|
73
|
+
const path = url.startsWith(routePrefix + '/') ? url.slice(routePrefix.length) : '';
|
|
74
|
+
const service = ['git-upload-pack', 'git-receive-pack'].find(service =>
|
|
75
|
+
(effectiveMethod === 'GET' && path === '/info/refs?service=' + service) ||
|
|
76
|
+
(effectiveMethod === 'POST' && path === '/' + service));
|
|
77
|
+
if (!service) return reply(res, 400, 'BAD_REQUEST');
|
|
78
|
+
const push = effectiveMethod === 'POST' && service === 'git-receive-pack';
|
|
79
|
+
const requestType = 'application/x-' + service + '-request';
|
|
80
|
+
if (method === 'OPTIONS') {
|
|
81
|
+
const allowed = effectiveMethod === 'GET' ? ['authorization'] : ['authorization', 'content-type'];
|
|
82
|
+
if (push) allowed.push('git-push-id');
|
|
83
|
+
const asked = (headers['access-control-request-headers'] ?? '').toLowerCase().split(',').map(name => name.trim());
|
|
84
|
+
if (asked.some(name => !allowed.includes(name))) return reply(res, 403, 'FORBIDDEN');
|
|
85
|
+
res.writeHead(204, {'Access-Control-Allow-Methods': effectiveMethod, 'Access-Control-Allow-Headers': allowed.join(', ')});
|
|
86
|
+
return res.end();
|
|
87
|
+
}
|
|
88
|
+
const session = headers.authorization?.startsWith('Bearer ') && sessions.get(headers.authorization.slice(7));
|
|
89
|
+
if (!session) return reply(res, 401, 'SESSION_REQUIRED');
|
|
90
|
+
if (session.expires) return reply(res, 401, 'SESSION_EXPIRED');
|
|
91
|
+
if (session.mode !== 'desktop' || (service === 'git-receive-pack' && !session.write)) return reply(res, 403, 'FORBIDDEN');
|
|
92
|
+
if (fixture.redirect) {
|
|
93
|
+
if (fixture.redirect === 'upstream') return reply(res, 409, 'REDIRECT_REJECTED');
|
|
94
|
+
res.writeHead(307, {Location: fixture.origin + '/redirect-target'});
|
|
95
|
+
return res.end();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let input;
|
|
99
|
+
if (method === 'POST') {
|
|
100
|
+
if (headers['content-type'] !== requestType) return reply(res, 400, 'BAD_REQUEST');
|
|
101
|
+
if (!/^\d+$/u.test(headers['content-length'] ?? '')) return reply(res, 411, 'LENGTH_REQUIRED');
|
|
102
|
+
const expected = Number(headers['content-length']);
|
|
103
|
+
if (expected > maxBody) return reply(res, 413, 'PAYLOAD_TOO_LARGE');
|
|
104
|
+
let size = 0;
|
|
105
|
+
const chunks = [];
|
|
106
|
+
for await (const chunk of req) {
|
|
107
|
+
size += chunk.length;
|
|
108
|
+
if (size > expected) return reply(res, 400, 'BAD_REQUEST');
|
|
109
|
+
chunks.push(chunk);
|
|
110
|
+
}
|
|
111
|
+
if (size !== expected) return reply(res, 400, 'BAD_REQUEST');
|
|
112
|
+
input = Buffer.concat(chunks);
|
|
113
|
+
}
|
|
114
|
+
const pushId = headers['git-push-id'];
|
|
115
|
+
if (push ? !pushIdPattern.test(pushId ?? '') || req.headersDistinct['git-push-id']?.length !== 1 :
|
|
116
|
+
pushId !== undefined) return reply(res, 400, 'BAD_REQUEST');
|
|
117
|
+
if (push) {
|
|
118
|
+
let claimed = pushIds.get(session);
|
|
119
|
+
if (!claimed) {
|
|
120
|
+
claimed = new Set();
|
|
121
|
+
pushIds.set(session, claimed);
|
|
122
|
+
}
|
|
123
|
+
if (claimed.has(pushId)) return reply(res, 409, 'PUSH_DUPLICATE');
|
|
124
|
+
claimed.add(pushId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [pathInfo, query = ''] = ('/fixture/work.git' + path).split('?');
|
|
128
|
+
// Model the credential boundary and invoke Git's CGI backend locally.
|
|
129
|
+
const authorization = 'Basic ' + Buffer.from('x-access-token:fixture-upstream-token').toString('base64');
|
|
130
|
+
upstream.push({method, path: pathInfo, query, authorization});
|
|
131
|
+
const output = execFileSync(backend, [], {
|
|
132
|
+
input,
|
|
133
|
+
env: {...gitEnv, GIT_PROJECT_ROOT: root, GIT_HTTP_EXPORT_ALL: '1', REQUEST_METHOD: method,
|
|
134
|
+
PATH_INFO: pathInfo, QUERY_STRING: query, REMOTE_USER: 'fixture-user',
|
|
135
|
+
CONTENT_TYPE: headers['content-type'] ?? '', CONTENT_LENGTH: headers['content-length'] ?? '0'}
|
|
136
|
+
});
|
|
137
|
+
const dropPush = fixture.dropPush && method === 'POST' && service === 'git-receive-pack';
|
|
138
|
+
if (dropPush && fixture.dropPush !== 'after-headers') {
|
|
139
|
+
res.destroy();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const end = output.indexOf('\r\n\r\n');
|
|
143
|
+
if (end < 0) throw new Error('Git CGI response had no header boundary');
|
|
144
|
+
const lines = output.subarray(0, end).toString().split('\r\n');
|
|
145
|
+
const statusLine = lines.find(line => line.startsWith('Status: '));
|
|
146
|
+
const contentType = lines.find(line => line.startsWith('Content-Type: '));
|
|
147
|
+
if (contentType) res.setHeader('Content-Type', contentType.slice(14));
|
|
148
|
+
if (dropPush) {
|
|
149
|
+
res.setHeader('Content-Length', output.length - end - 3);
|
|
150
|
+
res.setHeader('Connection', 'close');
|
|
151
|
+
}
|
|
152
|
+
res.writeHead(statusLine ? Number(statusLine.slice(8, 11)) : 200);
|
|
153
|
+
res.end(output.subarray(end + 4));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const server = createServer((req, res) => {
|
|
157
|
+
handle(req, res).catch(() => {
|
|
158
|
+
if (!res.headersSent) reply(res, 502, 'UPSTREAM_FAILURE');
|
|
159
|
+
else res.destroy();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
server.listen(0, '127.0.0.1');
|
|
163
|
+
await once(server, 'listening');
|
|
164
|
+
t.after(() => new Promise(resolve => {server.close(resolve); server.closeAllConnections();}));
|
|
165
|
+
fixture.origin = 'http://127.0.0.1:' + server.address().port;
|
|
166
|
+
return fixture;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const proxyPath = path => routePrefix + path;
|
|
170
|
+
|
|
171
|
+
export function adapterOptions(fixture, bearer = 'fixture-write') {
|
|
172
|
+
const connection = {mode: 'desktop', proxyOrigin: fixture.origin};
|
|
173
|
+
return {
|
|
174
|
+
remote, connection,
|
|
175
|
+
getSession: async () => ({connection, bearer, expiresAt: Date.now() + 60_000}),
|
|
176
|
+
fetch: (url, init) => fetch(url, {...init, headers: {...Object.fromEntries(init.headers), Origin: 'https://editor.example.invalid'}})
|
|
177
|
+
};
|
|
178
|
+
}
|