@withtruing/companion 0.13.2 → 0.14.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 +25 -26
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +837 -207
- package/dist/codex-binary.js +96 -0
- package/dist/codex-cli-caller.js +90 -16
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +95 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { configuredManagedRepository, githubAuthEnvironment, githubCloneUrl, githubLabel, networkGitEnvironment, } from './managed-repo.js';
|
|
5
|
+
import { checkRepositoryReadiness } from './local-scan.js';
|
|
6
|
+
import { AttemptSourceError, } from './whole-read-temp.js';
|
|
7
|
+
const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
8
|
+
const MAX_LEASE_MS = 30 * 60_000;
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 120_000;
|
|
10
|
+
const OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
11
|
+
export class RepositoryProviderError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
cleanup;
|
|
14
|
+
constructor(code, cleanup) {
|
|
15
|
+
super(code);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.cleanup = cleanup;
|
|
18
|
+
this.name = 'RepositoryProviderError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class GitCommandFailure extends Error {
|
|
22
|
+
}
|
|
23
|
+
function runGitCommand(input) {
|
|
24
|
+
if (input.signal.aborted)
|
|
25
|
+
return Promise.reject(new GitCommandFailure());
|
|
26
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
27
|
+
let child;
|
|
28
|
+
try {
|
|
29
|
+
child = spawn('git', [...input.args], {
|
|
30
|
+
cwd: input.cwd,
|
|
31
|
+
env: input.env,
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
rejectRun(new GitCommandFailure());
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
let settled = false;
|
|
43
|
+
let closeProcess;
|
|
44
|
+
const processClosed = new Promise((resolveClosed) => { closeProcess = resolveClosed; });
|
|
45
|
+
const unregisterReader = input.registerReader({
|
|
46
|
+
cancel: async () => {
|
|
47
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
48
|
+
child.kill('SIGKILL');
|
|
49
|
+
await processClosed;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const finish = (error) => {
|
|
53
|
+
if (settled)
|
|
54
|
+
return;
|
|
55
|
+
settled = true;
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
input.signal.removeEventListener('abort', abort);
|
|
58
|
+
unregisterReader();
|
|
59
|
+
if (error)
|
|
60
|
+
rejectRun(error);
|
|
61
|
+
else
|
|
62
|
+
resolveRun({ stdout: Buffer.concat(chunks, bytes).toString('utf8') });
|
|
63
|
+
};
|
|
64
|
+
const stop = () => {
|
|
65
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
66
|
+
child.kill('SIGKILL');
|
|
67
|
+
};
|
|
68
|
+
const abort = () => stop();
|
|
69
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
70
|
+
child.stdout?.on('data', (chunk) => {
|
|
71
|
+
bytes += chunk.length;
|
|
72
|
+
if (bytes > OUTPUT_LIMIT)
|
|
73
|
+
stop();
|
|
74
|
+
else
|
|
75
|
+
chunks.push(chunk);
|
|
76
|
+
});
|
|
77
|
+
// Git diagnostics can contain source paths or credentials, so they are drained, not retained.
|
|
78
|
+
child.stderr?.on('data', () => undefined);
|
|
79
|
+
child.on('error', () => {
|
|
80
|
+
closeProcess();
|
|
81
|
+
finish(new GitCommandFailure());
|
|
82
|
+
});
|
|
83
|
+
child.on('close', (code, signal) => {
|
|
84
|
+
closeProcess();
|
|
85
|
+
finish(code === 0 && signal === null && bytes <= OUTPUT_LIMIT
|
|
86
|
+
? undefined : new GitCommandFailure());
|
|
87
|
+
});
|
|
88
|
+
const timeout = setTimeout(stop, COMMAND_TIMEOUT_MS);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function runReadinessProbe(cwd, args, env) {
|
|
92
|
+
return new Promise((resolveProbe, rejectProbe) => {
|
|
93
|
+
let child;
|
|
94
|
+
try {
|
|
95
|
+
child = spawn('git', [...args], {
|
|
96
|
+
cwd, env, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
rejectProbe(new GitCommandFailure());
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let bytes = 0;
|
|
105
|
+
let settled = false;
|
|
106
|
+
const finish = (error) => {
|
|
107
|
+
if (settled)
|
|
108
|
+
return;
|
|
109
|
+
settled = true;
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
if (error)
|
|
112
|
+
rejectProbe(error);
|
|
113
|
+
else
|
|
114
|
+
resolveProbe(Buffer.concat(chunks, bytes).toString('utf8'));
|
|
115
|
+
};
|
|
116
|
+
child.stdout?.on('data', (chunk) => {
|
|
117
|
+
bytes += chunk.length;
|
|
118
|
+
if (bytes > 64 * 1024) {
|
|
119
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
120
|
+
child.kill('SIGKILL');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
chunks.push(chunk);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
child.on('error', () => finish(new GitCommandFailure()));
|
|
127
|
+
child.on('close', (code, signal) => finish(code === 0 && signal === null && bytes <= 64 * 1024
|
|
128
|
+
? undefined : new GitCommandFailure()));
|
|
129
|
+
const timeout = setTimeout(() => {
|
|
130
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
131
|
+
child.kill('SIGKILL');
|
|
132
|
+
finish(new GitCommandFailure());
|
|
133
|
+
}, 15_000);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function validBranchSelector(branch) {
|
|
137
|
+
return branch.length > 0 && branch.length <= 255 && !branch.startsWith('-')
|
|
138
|
+
&& !/[\x00-\x20\x7f~^:?*[\\]/.test(branch) && !branch.includes('..')
|
|
139
|
+
&& !branch.includes('@{') && !branch.includes('//') && !branch.endsWith('/')
|
|
140
|
+
&& branch.split('/').every((part) => part !== '' && !part.startsWith('.') && !part.endsWith('.lock'));
|
|
141
|
+
}
|
|
142
|
+
/** Bounded, non-interactive proof used by `connect --repo` before it records an identity. */
|
|
143
|
+
export async function proveManagedRepositoryAccess(input, dependencies = {}) {
|
|
144
|
+
if (!input.credential)
|
|
145
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
146
|
+
if (input.branch !== undefined && !validBranchSelector(input.branch)) {
|
|
147
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
148
|
+
}
|
|
149
|
+
const source = (dependencies.managedRepositoryUrl ?? githubCloneUrl)(input.repository);
|
|
150
|
+
if (!source || source.startsWith('-') || source.includes('\0')) {
|
|
151
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
152
|
+
}
|
|
153
|
+
const environment = githubAuthEnvironment(input.credential);
|
|
154
|
+
const requested = input.branch ? `refs/heads/${input.branch}` : 'HEAD';
|
|
155
|
+
const args = ['ls-remote', '--symref', '--exit-code', source, requested];
|
|
156
|
+
dependencies.observeGit?.(args, environment);
|
|
157
|
+
let stdout;
|
|
158
|
+
try {
|
|
159
|
+
stdout = await runReadinessProbe(process.cwd(), args, environment);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
163
|
+
}
|
|
164
|
+
const lines = stdout.trimEnd().split(/\r?\n/);
|
|
165
|
+
const objectLine = lines.find((line) => /^(?:[0-9a-f]{40}|[0-9a-f]{64})\t/.test(line)
|
|
166
|
+
&& line.endsWith(`\t${requested}`));
|
|
167
|
+
if (!objectLine)
|
|
168
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
169
|
+
if (input.branch)
|
|
170
|
+
return { defaultBranch: input.branch };
|
|
171
|
+
const symref = lines.find((line) => line.startsWith('ref: refs/heads/') && line.endsWith('\tHEAD'));
|
|
172
|
+
const defaultBranch = symref?.slice('ref: refs/heads/'.length, -'\tHEAD'.length);
|
|
173
|
+
if (!defaultBranch || !validBranchSelector(defaultBranch)) {
|
|
174
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
175
|
+
}
|
|
176
|
+
return { defaultBranch };
|
|
177
|
+
}
|
|
178
|
+
export async function checkConnectionRepositoryReadiness(connection, temporaryRepositories, dependencies) {
|
|
179
|
+
if (!temporaryRepositories || connection.managed !== true) {
|
|
180
|
+
return checkRepositoryReadiness(connection.repoPath);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const repository = configuredManagedRepository(connection);
|
|
184
|
+
const credential = await dependencies.getCredential(connection, repository);
|
|
185
|
+
if (!credential)
|
|
186
|
+
throw new GitCommandFailure();
|
|
187
|
+
const source = (dependencies.managedRepositoryUrl ?? githubCloneUrl)(repository, connection);
|
|
188
|
+
if (!source || source.startsWith('-') || source.includes('\0'))
|
|
189
|
+
throw new GitCommandFailure();
|
|
190
|
+
const environment = githubAuthEnvironment(credential);
|
|
191
|
+
const args = ['ls-remote', '--exit-code', source,
|
|
192
|
+
connection.branch ? `refs/heads/${connection.branch}` : 'HEAD'];
|
|
193
|
+
dependencies.observeGit?.(args, environment);
|
|
194
|
+
await runReadinessProbe(process.cwd(), args, environment);
|
|
195
|
+
return { state: 'ready' };
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return {
|
|
199
|
+
state: 'repository_access_denied',
|
|
200
|
+
reason: 'GitHub access is not ready for this repository.',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function normalizedExpiry(value, now) {
|
|
205
|
+
const requested = Date.parse(value);
|
|
206
|
+
if (!Number.isFinite(requested) || requested <= now.getTime())
|
|
207
|
+
return null;
|
|
208
|
+
return new Date(Math.min(requested, now.getTime() + MAX_LEASE_MS)).toISOString();
|
|
209
|
+
}
|
|
210
|
+
function nonBlank(value) {
|
|
211
|
+
return value.trim().length > 0;
|
|
212
|
+
}
|
|
213
|
+
function providerFailure(error, fallback) {
|
|
214
|
+
return error instanceof RepositoryProviderError ? error : new RepositoryProviderError(fallback);
|
|
215
|
+
}
|
|
216
|
+
function checkoutArgs(gitDirectory, path, commit) {
|
|
217
|
+
return [
|
|
218
|
+
'-c', 'core.longpaths=true',
|
|
219
|
+
'-c', 'core.symlinks=false',
|
|
220
|
+
'-c', 'core.autocrlf=false',
|
|
221
|
+
'--git-dir', gitDirectory,
|
|
222
|
+
'worktree', 'add', '--detach', path, commit,
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
export function createRepositoryProvider(dependencies) {
|
|
226
|
+
const clock = dependencies.now ?? (() => new Date());
|
|
227
|
+
const scheduleTimeout = dependencies.setTimeout ?? setTimeout;
|
|
228
|
+
const cancelTimeout = dependencies.clearTimeout ?? clearTimeout;
|
|
229
|
+
const gitRunner = dependencies.runGit ?? runGitCommand;
|
|
230
|
+
const activeOwners = new Set();
|
|
231
|
+
const runGit = (sourceLease, args, env, signal) => {
|
|
232
|
+
dependencies.observeGit?.(args, env);
|
|
233
|
+
return gitRunner({
|
|
234
|
+
cwd: sourceLease.path,
|
|
235
|
+
args,
|
|
236
|
+
env,
|
|
237
|
+
signal,
|
|
238
|
+
registerReader: (reader) => sourceLease.registerReader(reader),
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const acquireRepository = async (input) => {
|
|
242
|
+
const snapshotSha = input.target.kind === 'snapshot' ? input.target.sha : null;
|
|
243
|
+
if (!nonBlank(input.owner.attemptId) || !nonBlank(input.owner.claimToken)
|
|
244
|
+
|| (snapshotSha !== null && !SHA.test(snapshotSha))
|
|
245
|
+
|| (input.target.kind === 'range'
|
|
246
|
+
&& (!(input.target.base === null || SHA.test(input.target.base))
|
|
247
|
+
|| !nonBlank(input.target.head) || input.target.head.startsWith('-')
|
|
248
|
+
|| input.target.head.includes('\0')))) {
|
|
249
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
250
|
+
}
|
|
251
|
+
const firstExpiry = normalizedExpiry(input.owner.expiresAt, clock());
|
|
252
|
+
if (firstExpiry === null)
|
|
253
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
254
|
+
let claimLive = false;
|
|
255
|
+
try {
|
|
256
|
+
claimLive = await dependencies.isClaimLive(input.owner, input.connection);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// A claim that cannot be confirmed is not live.
|
|
260
|
+
}
|
|
261
|
+
const ownerKey = JSON.stringify([
|
|
262
|
+
input.owner.attemptId,
|
|
263
|
+
input.connection.connectionId,
|
|
264
|
+
input.target,
|
|
265
|
+
]);
|
|
266
|
+
if (!claimLive || activeOwners.has(ownerKey)) {
|
|
267
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
268
|
+
}
|
|
269
|
+
activeOwners.add(ownerKey);
|
|
270
|
+
const abandonOwner = (code) => {
|
|
271
|
+
activeOwners.delete(ownerKey);
|
|
272
|
+
throw new RepositoryProviderError(code);
|
|
273
|
+
};
|
|
274
|
+
let repositoryIdentity;
|
|
275
|
+
let sourceLocation;
|
|
276
|
+
let environment = networkGitEnvironment();
|
|
277
|
+
if (input.connection.managed === true) {
|
|
278
|
+
let repository;
|
|
279
|
+
try {
|
|
280
|
+
repository = configuredManagedRepository(input.connection);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return abandonOwner('repository_access_denied');
|
|
284
|
+
}
|
|
285
|
+
let credential = null;
|
|
286
|
+
try {
|
|
287
|
+
credential = await dependencies.getCredential(input.connection, repository);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Credentials are deliberately represented only by the fixed failure below.
|
|
291
|
+
}
|
|
292
|
+
if (!credential)
|
|
293
|
+
return abandonOwner('repository_access_denied');
|
|
294
|
+
repositoryIdentity = `github:${githubLabel(repository)}`;
|
|
295
|
+
try {
|
|
296
|
+
sourceLocation = (dependencies.managedRepositoryUrl ?? githubCloneUrl)(repository, input.connection);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return abandonOwner('repository_access_denied');
|
|
300
|
+
}
|
|
301
|
+
if (!sourceLocation || sourceLocation.startsWith('-') || sourceLocation.includes('\0')) {
|
|
302
|
+
return abandonOwner('repository_access_denied');
|
|
303
|
+
}
|
|
304
|
+
environment = githubAuthEnvironment(credential);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
if (!nonBlank(input.connection.repoPath)) {
|
|
308
|
+
return abandonOwner('repository_access_denied');
|
|
309
|
+
}
|
|
310
|
+
sourceLocation = resolve(input.connection.repoPath);
|
|
311
|
+
repositoryIdentity = `developer:${sourceLocation}`;
|
|
312
|
+
}
|
|
313
|
+
let sourceLease;
|
|
314
|
+
try {
|
|
315
|
+
sourceLease = await dependencies.sourceManager.acquireAttemptSource({
|
|
316
|
+
workspaceId: dependencies.workspaceId,
|
|
317
|
+
buildId: `repository-task:${input.connection.connectionId}`,
|
|
318
|
+
attemptId: input.owner.attemptId,
|
|
319
|
+
hostId: dependencies.hostId,
|
|
320
|
+
leaseExpiresAt: firstExpiry,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
activeOwners.delete(ownerKey);
|
|
325
|
+
throw error instanceof AttemptSourceError && error.code === 'source_lease_lost'
|
|
326
|
+
? new RepositoryProviderError('repository_lease_lost')
|
|
327
|
+
: new RepositoryProviderError('repository_access_denied');
|
|
328
|
+
}
|
|
329
|
+
const releaseSource = async () => {
|
|
330
|
+
let cleanup;
|
|
331
|
+
try {
|
|
332
|
+
cleanup = await sourceLease.release();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
throw new RepositoryProviderError('cleanup_pending');
|
|
336
|
+
}
|
|
337
|
+
if (cleanup.state === 'cleanup_pending') {
|
|
338
|
+
throw new RepositoryProviderError('cleanup_pending', cleanup);
|
|
339
|
+
}
|
|
340
|
+
return cleanup;
|
|
341
|
+
};
|
|
342
|
+
let failureCode = 'repository_access_denied';
|
|
343
|
+
let resolvedSource = snapshotSha;
|
|
344
|
+
let acquiredRange;
|
|
345
|
+
try {
|
|
346
|
+
if (!input.connection.managed) {
|
|
347
|
+
const top = await runGit(sourceLease, ['-C', sourceLocation, 'rev-parse', '--show-toplevel'], environment, input.signal);
|
|
348
|
+
if (resolve(top.stdout.trim()) !== sourceLocation) {
|
|
349
|
+
throw new RepositoryProviderError('repository_access_denied');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
await runGit(sourceLease, ['init', '--bare', sourceLease.bareRepositoryPath], networkGitEnvironment(), input.signal);
|
|
353
|
+
if (input.connection.managed) {
|
|
354
|
+
await runGit(sourceLease, ['ls-remote', '--exit-code', sourceLocation], environment, input.signal);
|
|
355
|
+
}
|
|
356
|
+
failureCode = 'snapshot_unavailable';
|
|
357
|
+
if (input.target.kind === 'snapshot') {
|
|
358
|
+
await runGit(sourceLease, [
|
|
359
|
+
'--git-dir', sourceLease.bareRepositoryPath,
|
|
360
|
+
'fetch', '--no-tags', '--depth=1', sourceLocation, input.target.sha,
|
|
361
|
+
], environment, input.signal);
|
|
362
|
+
const type = await runGit(sourceLease, [
|
|
363
|
+
'--git-dir', sourceLease.bareRepositoryPath, 'cat-file', '-t', input.target.sha,
|
|
364
|
+
], networkGitEnvironment(), input.signal);
|
|
365
|
+
const resolvedCommit = await runGit(sourceLease, [
|
|
366
|
+
'--git-dir', sourceLease.bareRepositoryPath,
|
|
367
|
+
'rev-parse', '--verify', `${input.target.sha}^{commit}`,
|
|
368
|
+
], networkGitEnvironment(), input.signal);
|
|
369
|
+
if (type.stdout.trim() !== 'commit' || resolvedCommit.stdout.trim() !== input.target.sha) {
|
|
370
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const branchRef = `refs/heads/${input.target.head}`;
|
|
375
|
+
await runGit(sourceLease, ['check-ref-format', '--branch', input.target.head], networkGitEnvironment(), input.signal);
|
|
376
|
+
const advertised = await runGit(sourceLease, ['ls-remote', '--exit-code', sourceLocation, branchRef], environment, input.signal);
|
|
377
|
+
const advertisedLines = advertised.stdout.trimEnd().split(/\r?\n/)
|
|
378
|
+
.filter((line) => line.endsWith(`\t${branchRef}`));
|
|
379
|
+
if (advertisedLines.length !== 1)
|
|
380
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
381
|
+
const advertisedHead = advertisedLines[0].split('\t')[0];
|
|
382
|
+
if (!SHA.test(advertisedHead))
|
|
383
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
384
|
+
const operation = randomUUID();
|
|
385
|
+
const headRef = `refs/product-console/repository-provider/${operation}/head`;
|
|
386
|
+
const baseRef = `refs/product-console/repository-provider/${operation}/base`;
|
|
387
|
+
const remote = `repository-provider-${operation}`;
|
|
388
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
389
|
+
'remote', 'add', remote, sourceLocation], networkGitEnvironment(), input.signal);
|
|
390
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
391
|
+
'config', `remote.${remote}.promisor`, 'true'], networkGitEnvironment(), input.signal);
|
|
392
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
393
|
+
'config', `remote.${remote}.partialclonefilter`, 'blob:none'], networkGitEnvironment(), input.signal);
|
|
394
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
395
|
+
'fetch', '--filter=blob:none', '--depth=64', '--update-shallow', '--no-tags', '--refmap=', '--no-write-fetch-head',
|
|
396
|
+
'--no-recurse-submodules', remote, `+${branchRef}:${headRef}`], environment, input.signal);
|
|
397
|
+
const resolvedHead = (await runGit(sourceLease, ['--no-replace-objects',
|
|
398
|
+
'--git-dir', sourceLease.bareRepositoryPath, 'rev-parse', '--verify', `${headRef}^{commit}`], networkGitEnvironment(), input.signal)).stdout.trim();
|
|
399
|
+
if (resolvedHead !== advertisedHead)
|
|
400
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
401
|
+
resolvedSource = resolvedHead;
|
|
402
|
+
const objectEnvironment = { ...networkGitEnvironment(), GIT_NO_LAZY_FETCH: '1' };
|
|
403
|
+
const isShallow = async () => (await runGit(sourceLease, ['--no-replace-objects',
|
|
404
|
+
'--git-dir', sourceLease.bareRepositoryPath, 'rev-parse', '--is-shallow-repository'], objectEnvironment, input.signal)).stdout.trim() === 'true';
|
|
405
|
+
const hasBase = async (base) => {
|
|
406
|
+
try {
|
|
407
|
+
await runGit(sourceLease, ['--no-replace-objects', '--git-dir', sourceLease.bareRepositoryPath,
|
|
408
|
+
'cat-file', '-e', `${base}^{commit}`], objectEnvironment, input.signal);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const baseIsAncestor = async (base) => {
|
|
416
|
+
try {
|
|
417
|
+
return (await runGit(sourceLease, ['--no-replace-objects',
|
|
418
|
+
'--git-dir', sourceLease.bareRepositoryPath, 'merge-base', base, resolvedHead], objectEnvironment, input.signal)).stdout.trim() === base;
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
let shallow = await isShallow();
|
|
425
|
+
let unavailable;
|
|
426
|
+
const base = input.target.base;
|
|
427
|
+
if (base !== null && base !== resolvedHead) {
|
|
428
|
+
let basePresent = await hasBase(base);
|
|
429
|
+
let ancestryComplete = basePresent && await baseIsAncestor(base);
|
|
430
|
+
for (const deepen of [256, 1_024, 4_096]) {
|
|
431
|
+
if (ancestryComplete || !shallow)
|
|
432
|
+
break;
|
|
433
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
434
|
+
'fetch', '--filter=blob:none', `--deepen=${deepen}`, '--update-shallow', '--no-tags',
|
|
435
|
+
'--refmap=', '--no-write-fetch-head', '--no-recurse-submodules', remote,
|
|
436
|
+
`+${branchRef}:${headRef}`], environment, input.signal);
|
|
437
|
+
shallow = await isShallow();
|
|
438
|
+
basePresent = await hasBase(base);
|
|
439
|
+
ancestryComplete = basePresent && await baseIsAncestor(base);
|
|
440
|
+
}
|
|
441
|
+
if (!ancestryComplete && !basePresent) {
|
|
442
|
+
try {
|
|
443
|
+
await runGit(sourceLease, ['--git-dir', sourceLease.bareRepositoryPath,
|
|
444
|
+
'fetch', '--filter=blob:none', '--no-tags', '--refmap=', '--no-write-fetch-head',
|
|
445
|
+
'--no-recurse-submodules', remote, `+${base}:${baseRef}`], environment, input.signal);
|
|
446
|
+
basePresent = await hasBase(base);
|
|
447
|
+
if (!basePresent)
|
|
448
|
+
unavailable = 'object_missing';
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
unavailable = shallow ? 'shallow_history' : 'object_missing';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!unavailable && !ancestryComplete && basePresent) {
|
|
455
|
+
unavailable = shallow ? 'shallow_history'
|
|
456
|
+
: await baseIsAncestor(base) ? undefined : 'not_ancestor';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
acquiredRange = {
|
|
460
|
+
base, head: resolvedHead, historyShallow: shallow,
|
|
461
|
+
...(unavailable ? { baseUnavailableReason: unavailable } : {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
try {
|
|
467
|
+
await releaseSource();
|
|
468
|
+
}
|
|
469
|
+
catch (cleanupError) {
|
|
470
|
+
throw providerFailure(cleanupError, 'cleanup_pending');
|
|
471
|
+
}
|
|
472
|
+
activeOwners.delete(ownerKey);
|
|
473
|
+
throw providerFailure(error, input.signal.aborted ? 'repository_lease_lost' : failureCode);
|
|
474
|
+
}
|
|
475
|
+
const publicOwner = { ...input.owner, expiresAt: firstExpiry };
|
|
476
|
+
const identities = {
|
|
477
|
+
connectionId: input.connection.connectionId,
|
|
478
|
+
repository: repositoryIdentity,
|
|
479
|
+
source: `commit:${resolvedSource}`,
|
|
480
|
+
target: input.target.kind === 'snapshot'
|
|
481
|
+
? { ...input.target }
|
|
482
|
+
: { kind: 'range', base: input.target.base, head: resolvedSource },
|
|
483
|
+
};
|
|
484
|
+
let state = 'live';
|
|
485
|
+
let releasePromise = null;
|
|
486
|
+
let checkoutPromise = null;
|
|
487
|
+
let expiryTimer = null;
|
|
488
|
+
const assertLive = () => {
|
|
489
|
+
if (state !== 'live' || clock().getTime() >= Date.parse(publicOwner.expiresAt)) {
|
|
490
|
+
if (state === 'live')
|
|
491
|
+
void release().catch(() => undefined);
|
|
492
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const scheduleExpiry = (expiresAt) => {
|
|
496
|
+
if (expiryTimer !== null)
|
|
497
|
+
cancelTimeout(expiryTimer);
|
|
498
|
+
expiryTimer = scheduleTimeout(() => { void release().catch(() => undefined); }, Math.max(0, Date.parse(expiresAt) - clock().getTime()));
|
|
499
|
+
if (typeof expiryTimer === 'object' && expiryTimer !== null && 'unref' in expiryTimer) {
|
|
500
|
+
expiryTimer.unref();
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const abort = () => { void release().catch(() => undefined); };
|
|
504
|
+
const release = () => {
|
|
505
|
+
if (releasePromise !== null)
|
|
506
|
+
return releasePromise;
|
|
507
|
+
state = 'releasing';
|
|
508
|
+
if (expiryTimer !== null)
|
|
509
|
+
cancelTimeout(expiryTimer);
|
|
510
|
+
input.signal.removeEventListener('abort', abort);
|
|
511
|
+
releasePromise = releaseSource().then((cleanup) => {
|
|
512
|
+
activeOwners.delete(ownerKey);
|
|
513
|
+
return cleanup;
|
|
514
|
+
}).finally(() => { state = 'released'; });
|
|
515
|
+
return releasePromise;
|
|
516
|
+
};
|
|
517
|
+
const loseLease = async () => {
|
|
518
|
+
await release();
|
|
519
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
520
|
+
};
|
|
521
|
+
const lease = {
|
|
522
|
+
identities,
|
|
523
|
+
owner: publicOwner,
|
|
524
|
+
temporaryPath: sourceLease.path,
|
|
525
|
+
gitDirectory: sourceLease.bareRepositoryPath,
|
|
526
|
+
...(acquiredRange ? { range: acquiredRange } : {}),
|
|
527
|
+
assertLive,
|
|
528
|
+
gitEnvironment() {
|
|
529
|
+
assertLive();
|
|
530
|
+
return { ...environment };
|
|
531
|
+
},
|
|
532
|
+
checkout() {
|
|
533
|
+
assertLive();
|
|
534
|
+
checkoutPromise ??= (async () => {
|
|
535
|
+
const path = resolve(sourceLease.path, 'checkout');
|
|
536
|
+
try {
|
|
537
|
+
await runGit(sourceLease, checkoutArgs(sourceLease.bareRepositoryPath, path, resolvedSource), networkGitEnvironment(), input.signal);
|
|
538
|
+
const observed = await runGit(sourceLease, ['-C', path, 'rev-parse', '--verify', 'HEAD^{commit}'], networkGitEnvironment(), input.signal);
|
|
539
|
+
if (observed.stdout.trim() !== resolvedSource) {
|
|
540
|
+
throw new RepositoryProviderError('snapshot_unavailable');
|
|
541
|
+
}
|
|
542
|
+
return { path, commit: resolvedSource };
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
throw providerFailure(error, state === 'live'
|
|
546
|
+
? 'snapshot_unavailable' : 'repository_lease_lost');
|
|
547
|
+
}
|
|
548
|
+
})();
|
|
549
|
+
return checkoutPromise;
|
|
550
|
+
},
|
|
551
|
+
registerReader(reader) {
|
|
552
|
+
assertLive();
|
|
553
|
+
try {
|
|
554
|
+
return sourceLease.registerReader(reader);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
throw new RepositoryProviderError('repository_lease_lost');
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
async heartbeat(heartbeat) {
|
|
561
|
+
if (heartbeat.claimToken !== publicOwner.claimToken || state !== 'live'
|
|
562
|
+
|| clock().getTime() >= Date.parse(publicOwner.expiresAt))
|
|
563
|
+
return loseLease();
|
|
564
|
+
let live = false;
|
|
565
|
+
try {
|
|
566
|
+
live = await dependencies.isClaimLive({
|
|
567
|
+
attemptId: publicOwner.attemptId,
|
|
568
|
+
claimToken: heartbeat.claimToken,
|
|
569
|
+
expiresAt: heartbeat.expiresAt,
|
|
570
|
+
}, input.connection);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// A failed ownership check is fenced just like an explicit stale response.
|
|
574
|
+
}
|
|
575
|
+
const expiresAt = normalizedExpiry(heartbeat.expiresAt, clock());
|
|
576
|
+
if (!live || expiresAt === null || Date.parse(expiresAt) < Date.parse(publicOwner.expiresAt)) {
|
|
577
|
+
return loseLease();
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
await sourceLease.renew(expiresAt);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return loseLease();
|
|
584
|
+
}
|
|
585
|
+
publicOwner.expiresAt = expiresAt;
|
|
586
|
+
scheduleExpiry(expiresAt);
|
|
587
|
+
return { expiresAt };
|
|
588
|
+
},
|
|
589
|
+
release,
|
|
590
|
+
};
|
|
591
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
592
|
+
scheduleExpiry(firstExpiry);
|
|
593
|
+
if (input.signal.aborted)
|
|
594
|
+
await loseLease();
|
|
595
|
+
return lease;
|
|
596
|
+
};
|
|
597
|
+
return {
|
|
598
|
+
acquireRepository,
|
|
599
|
+
async sweepRepositories(at = clock().toISOString()) {
|
|
600
|
+
try {
|
|
601
|
+
return await dependencies.sourceManager.sweepAttemptSources(at);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
throw new RepositoryProviderError('cleanup_pending');
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|