@pierre/storage 0.1.4 → 0.2.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.md +28 -0
- package/dist/index.cjs +576 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +576 -323
- package/dist/index.js.map +1 -1
- package/package.json +38 -39
- package/src/commit-pack.ts +128 -0
- package/src/commit.ts +35 -360
- package/src/diff-commit.ts +300 -0
- package/src/index.ts +39 -4
- package/src/stream-utils.ts +255 -0
- package/src/types.ts +20 -0
package/dist/index.js
CHANGED
|
@@ -148,276 +148,109 @@ var errorEnvelopeSchema = z.object({
|
|
|
148
148
|
error: z.string()
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
// src/commit.ts
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
operations = [];
|
|
161
|
-
sent = false;
|
|
162
|
-
constructor(deps) {
|
|
163
|
-
this.options = normalizeCommitOptions(deps.options);
|
|
164
|
-
this.getAuthToken = deps.getAuthToken;
|
|
165
|
-
this.transport = deps.transport;
|
|
166
|
-
const trimmedMessage = this.options.commitMessage?.trim();
|
|
167
|
-
const trimmedAuthorName = this.options.author?.name?.trim();
|
|
168
|
-
const trimmedAuthorEmail = this.options.author?.email?.trim();
|
|
169
|
-
if (!trimmedMessage) {
|
|
170
|
-
throw new Error("createCommit commitMessage is required");
|
|
171
|
-
}
|
|
172
|
-
if (!trimmedAuthorName || !trimmedAuthorEmail) {
|
|
173
|
-
throw new Error("createCommit author name and email are required");
|
|
174
|
-
}
|
|
175
|
-
this.options.commitMessage = trimmedMessage;
|
|
176
|
-
this.options.author = {
|
|
177
|
-
name: trimmedAuthorName,
|
|
178
|
-
email: trimmedAuthorEmail
|
|
179
|
-
};
|
|
180
|
-
if (typeof this.options.expectedHeadSha === "string") {
|
|
181
|
-
this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
|
|
182
|
-
}
|
|
183
|
-
if (typeof this.options.baseBranch === "string") {
|
|
184
|
-
const trimmedBase = this.options.baseBranch.trim();
|
|
185
|
-
if (trimmedBase === "") {
|
|
186
|
-
delete this.options.baseBranch;
|
|
187
|
-
} else {
|
|
188
|
-
if (trimmedBase.startsWith("refs/")) {
|
|
189
|
-
throw new Error("createCommit baseBranch must not include refs/ prefix");
|
|
190
|
-
}
|
|
191
|
-
this.options.baseBranch = trimmedBase;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
addFile(path, source, options) {
|
|
196
|
-
this.ensureNotSent();
|
|
197
|
-
const normalizedPath = this.normalizePath(path);
|
|
198
|
-
const contentId = randomContentId();
|
|
199
|
-
const mode = options?.mode ?? "100644";
|
|
200
|
-
this.operations.push({
|
|
201
|
-
path: normalizedPath,
|
|
202
|
-
contentId,
|
|
203
|
-
mode,
|
|
204
|
-
operation: "upsert",
|
|
205
|
-
streamFactory: () => toAsyncIterable(source)
|
|
206
|
-
});
|
|
207
|
-
return this;
|
|
208
|
-
}
|
|
209
|
-
addFileFromString(path, contents, options) {
|
|
210
|
-
const encoding = options?.encoding ?? "utf8";
|
|
211
|
-
const normalizedEncoding = encoding === "utf-8" ? "utf8" : encoding;
|
|
212
|
-
let data;
|
|
213
|
-
if (normalizedEncoding === "utf8") {
|
|
214
|
-
data = new TextEncoder().encode(contents);
|
|
215
|
-
} else if (BufferCtor) {
|
|
216
|
-
data = BufferCtor.from(
|
|
217
|
-
contents,
|
|
218
|
-
normalizedEncoding
|
|
219
|
-
);
|
|
220
|
-
} else {
|
|
221
|
-
throw new Error(
|
|
222
|
-
`Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
return this.addFile(path, data, options);
|
|
226
|
-
}
|
|
227
|
-
deletePath(path) {
|
|
228
|
-
this.ensureNotSent();
|
|
229
|
-
const normalizedPath = this.normalizePath(path);
|
|
230
|
-
this.operations.push({
|
|
231
|
-
path: normalizedPath,
|
|
232
|
-
contentId: randomContentId(),
|
|
233
|
-
operation: "delete"
|
|
234
|
-
});
|
|
235
|
-
return this;
|
|
236
|
-
}
|
|
237
|
-
async send() {
|
|
238
|
-
this.ensureNotSent();
|
|
239
|
-
this.sent = true;
|
|
240
|
-
const metadata = this.buildMetadata();
|
|
241
|
-
const blobEntries = this.operations.filter((op) => op.operation === "upsert" && op.streamFactory).map((op) => ({
|
|
242
|
-
contentId: op.contentId,
|
|
243
|
-
chunks: chunkify(op.streamFactory())
|
|
244
|
-
}));
|
|
245
|
-
const authorization = await this.getAuthToken();
|
|
246
|
-
const ack = await this.transport.send({
|
|
247
|
-
authorization,
|
|
248
|
-
signal: this.options.signal,
|
|
249
|
-
metadata,
|
|
250
|
-
blobs: blobEntries
|
|
251
|
-
});
|
|
252
|
-
return buildCommitResult(ack);
|
|
253
|
-
}
|
|
254
|
-
buildMetadata() {
|
|
255
|
-
const files = this.operations.map((op) => {
|
|
256
|
-
const entry = {
|
|
257
|
-
path: op.path,
|
|
258
|
-
content_id: op.contentId,
|
|
259
|
-
operation: op.operation
|
|
260
|
-
};
|
|
261
|
-
if (op.mode) {
|
|
262
|
-
entry.mode = op.mode;
|
|
263
|
-
}
|
|
264
|
-
return entry;
|
|
265
|
-
});
|
|
266
|
-
const metadata = {
|
|
267
|
-
target_branch: this.options.targetBranch,
|
|
268
|
-
commit_message: this.options.commitMessage,
|
|
269
|
-
author: {
|
|
270
|
-
name: this.options.author.name,
|
|
271
|
-
email: this.options.author.email
|
|
272
|
-
},
|
|
273
|
-
files
|
|
274
|
-
};
|
|
275
|
-
if (this.options.expectedHeadSha) {
|
|
276
|
-
metadata.expected_head_sha = this.options.expectedHeadSha;
|
|
277
|
-
}
|
|
278
|
-
if (this.options.baseBranch) {
|
|
279
|
-
metadata.base_branch = this.options.baseBranch;
|
|
280
|
-
}
|
|
281
|
-
if (this.options.committer) {
|
|
282
|
-
metadata.committer = {
|
|
283
|
-
name: this.options.committer.name,
|
|
284
|
-
email: this.options.committer.email
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return metadata;
|
|
288
|
-
}
|
|
289
|
-
ensureNotSent() {
|
|
290
|
-
if (this.sent) {
|
|
291
|
-
throw new Error("createCommit builder cannot be reused after send()");
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
normalizePath(path) {
|
|
295
|
-
if (!path || typeof path !== "string" || path.trim() === "") {
|
|
296
|
-
throw new Error("File path must be a non-empty string");
|
|
297
|
-
}
|
|
298
|
-
return path.replace(/^\//, "");
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
var FetchCommitTransport = class {
|
|
302
|
-
url;
|
|
303
|
-
constructor(config) {
|
|
304
|
-
const trimmedBase = config.baseUrl.replace(/\/+$/, "");
|
|
305
|
-
this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
|
|
306
|
-
}
|
|
307
|
-
async send(request) {
|
|
308
|
-
const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
|
|
309
|
-
const body = toRequestBody(bodyIterable);
|
|
310
|
-
const init = {
|
|
311
|
-
method: "POST",
|
|
312
|
-
headers: {
|
|
313
|
-
Authorization: `Bearer ${request.authorization}`,
|
|
314
|
-
"Content-Type": "application/x-ndjson",
|
|
315
|
-
Accept: "application/json"
|
|
316
|
-
},
|
|
317
|
-
body,
|
|
318
|
-
signal: request.signal
|
|
319
|
-
};
|
|
320
|
-
if (requiresDuplex(body)) {
|
|
321
|
-
init.duplex = "half";
|
|
322
|
-
}
|
|
323
|
-
const response = await fetch(this.url, init);
|
|
324
|
-
if (!response.ok) {
|
|
325
|
-
const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(response);
|
|
326
|
-
throw new RefUpdateError(statusMessage, {
|
|
327
|
-
status: statusLabel,
|
|
328
|
-
message: statusMessage,
|
|
151
|
+
// src/commit-pack.ts
|
|
152
|
+
function buildCommitResult(ack) {
|
|
153
|
+
const refUpdate = toRefUpdate(ack.result);
|
|
154
|
+
if (!ack.result.success) {
|
|
155
|
+
throw new RefUpdateError(
|
|
156
|
+
ack.result.message ?? `Commit failed with status ${ack.result.status}`,
|
|
157
|
+
{
|
|
158
|
+
status: ack.result.status,
|
|
159
|
+
message: ack.result.message,
|
|
329
160
|
refUpdate
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
const ack = commitPackAckSchema.parse(await response.json());
|
|
333
|
-
return ack;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
function toRequestBody(iterable) {
|
|
337
|
-
const readableStreamCtor = globalThis.ReadableStream;
|
|
338
|
-
if (typeof readableStreamCtor === "function") {
|
|
339
|
-
const iterator = iterable[Symbol.asyncIterator]();
|
|
340
|
-
return new readableStreamCtor({
|
|
341
|
-
async pull(controller) {
|
|
342
|
-
const { value, done } = await iterator.next();
|
|
343
|
-
if (done) {
|
|
344
|
-
controller.close();
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
controller.enqueue(value);
|
|
348
|
-
},
|
|
349
|
-
async cancel(reason) {
|
|
350
|
-
if (typeof iterator.return === "function") {
|
|
351
|
-
await iterator.return(reason);
|
|
352
|
-
}
|
|
353
161
|
}
|
|
354
|
-
|
|
162
|
+
);
|
|
355
163
|
}
|
|
356
|
-
return
|
|
164
|
+
return {
|
|
165
|
+
commitSha: ack.commit.commit_sha,
|
|
166
|
+
treeSha: ack.commit.tree_sha,
|
|
167
|
+
targetBranch: ack.commit.target_branch,
|
|
168
|
+
packBytes: ack.commit.pack_bytes,
|
|
169
|
+
blobCount: ack.commit.blob_count,
|
|
170
|
+
refUpdate
|
|
171
|
+
};
|
|
357
172
|
}
|
|
358
|
-
function
|
|
359
|
-
const encoder = new TextEncoder();
|
|
173
|
+
function toRefUpdate(result) {
|
|
360
174
|
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for (const blob of blobs) {
|
|
365
|
-
for await (const segment of blob.chunks) {
|
|
366
|
-
const payload = {
|
|
367
|
-
blob_chunk: {
|
|
368
|
-
content_id: blob.contentId,
|
|
369
|
-
data: base64Encode(segment.chunk),
|
|
370
|
-
eof: segment.eof
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
yield encoder.encode(`${JSON.stringify(payload)}
|
|
374
|
-
`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
175
|
+
branch: result.branch,
|
|
176
|
+
oldSha: result.old_sha,
|
|
177
|
+
newSha: result.new_sha
|
|
378
178
|
};
|
|
379
179
|
}
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
|
|
180
|
+
async function parseCommitPackError(response, fallbackMessage) {
|
|
181
|
+
const cloned = response.clone();
|
|
182
|
+
let jsonBody;
|
|
183
|
+
try {
|
|
184
|
+
jsonBody = await cloned.json();
|
|
185
|
+
} catch {
|
|
186
|
+
jsonBody = void 0;
|
|
383
187
|
}
|
|
384
|
-
|
|
385
|
-
|
|
188
|
+
let textBody;
|
|
189
|
+
if (jsonBody === void 0) {
|
|
190
|
+
try {
|
|
191
|
+
textBody = await response.text();
|
|
192
|
+
} catch {
|
|
193
|
+
textBody = void 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const defaultStatus = (() => {
|
|
197
|
+
const inferred = inferRefUpdateReason(String(response.status));
|
|
198
|
+
return inferred === "unknown" ? "failed" : inferred;
|
|
199
|
+
})();
|
|
200
|
+
let statusLabel = defaultStatus;
|
|
201
|
+
let refUpdate;
|
|
202
|
+
let message;
|
|
203
|
+
if (jsonBody !== void 0) {
|
|
204
|
+
const parsedResponse = commitPackResponseSchema.safeParse(jsonBody);
|
|
205
|
+
if (parsedResponse.success) {
|
|
206
|
+
const result = parsedResponse.data.result;
|
|
207
|
+
if (typeof result.status === "string" && result.status.trim() !== "") {
|
|
208
|
+
statusLabel = result.status.trim();
|
|
209
|
+
}
|
|
210
|
+
refUpdate = toPartialRefUpdateFields(result.branch, result.old_sha, result.new_sha);
|
|
211
|
+
if (typeof result.message === "string" && result.message.trim() !== "") {
|
|
212
|
+
message = result.message.trim();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!message) {
|
|
216
|
+
const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
|
|
217
|
+
if (parsedError.success) {
|
|
218
|
+
const trimmed = parsedError.data.error.trim();
|
|
219
|
+
if (trimmed) {
|
|
220
|
+
message = trimmed;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
386
224
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return true;
|
|
225
|
+
if (!message && typeof jsonBody === "string" && jsonBody.trim() !== "") {
|
|
226
|
+
message = jsonBody.trim();
|
|
390
227
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
function buildCommitResult(ack) {
|
|
394
|
-
const refUpdate = toRefUpdate(ack.result);
|
|
395
|
-
if (!ack.result.success) {
|
|
396
|
-
throw new RefUpdateError(
|
|
397
|
-
ack.result.message ?? `Commit failed with status ${ack.result.status}`,
|
|
398
|
-
{
|
|
399
|
-
status: ack.result.status,
|
|
400
|
-
message: ack.result.message,
|
|
401
|
-
refUpdate
|
|
402
|
-
}
|
|
403
|
-
);
|
|
228
|
+
if (!message && textBody && textBody.trim() !== "") {
|
|
229
|
+
message = textBody.trim();
|
|
404
230
|
}
|
|
405
231
|
return {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
targetBranch: ack.commit.target_branch,
|
|
409
|
-
packBytes: ack.commit.pack_bytes,
|
|
410
|
-
blobCount: ack.commit.blob_count,
|
|
232
|
+
statusMessage: message ?? fallbackMessage,
|
|
233
|
+
statusLabel,
|
|
411
234
|
refUpdate
|
|
412
235
|
};
|
|
413
236
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
237
|
+
function toPartialRefUpdateFields(branch, oldSha, newSha) {
|
|
238
|
+
const refUpdate = {};
|
|
239
|
+
if (typeof branch === "string" && branch.trim() !== "") {
|
|
240
|
+
refUpdate.branch = branch.trim();
|
|
241
|
+
}
|
|
242
|
+
if (typeof oldSha === "string" && oldSha.trim() !== "") {
|
|
243
|
+
refUpdate.oldSha = oldSha.trim();
|
|
244
|
+
}
|
|
245
|
+
if (typeof newSha === "string" && newSha.trim() !== "") {
|
|
246
|
+
refUpdate.newSha = newSha.trim();
|
|
247
|
+
}
|
|
248
|
+
return Object.keys(refUpdate).length > 0 ? refUpdate : void 0;
|
|
420
249
|
}
|
|
250
|
+
|
|
251
|
+
// src/stream-utils.ts
|
|
252
|
+
var BufferCtor = globalThis.Buffer;
|
|
253
|
+
var MAX_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
421
254
|
async function* chunkify(source) {
|
|
422
255
|
let pending = null;
|
|
423
256
|
let produced = false;
|
|
@@ -493,7 +326,56 @@ async function* toAsyncIterable(source) {
|
|
|
493
326
|
}
|
|
494
327
|
return;
|
|
495
328
|
}
|
|
496
|
-
throw new Error("Unsupported
|
|
329
|
+
throw new Error("Unsupported content source; expected binary data");
|
|
330
|
+
}
|
|
331
|
+
function base64Encode(bytes) {
|
|
332
|
+
if (BufferCtor) {
|
|
333
|
+
return BufferCtor.from(bytes).toString("base64");
|
|
334
|
+
}
|
|
335
|
+
let binary = "";
|
|
336
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
337
|
+
binary += String.fromCharCode(bytes[i]);
|
|
338
|
+
}
|
|
339
|
+
const btoaFn = globalThis.btoa;
|
|
340
|
+
if (typeof btoaFn === "function") {
|
|
341
|
+
return btoaFn(binary);
|
|
342
|
+
}
|
|
343
|
+
throw new Error("Base64 encoding is not supported in this environment");
|
|
344
|
+
}
|
|
345
|
+
function requiresDuplex(body) {
|
|
346
|
+
if (!body || typeof body !== "object") {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (typeof body[Symbol.asyncIterator] === "function") {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
const readableStreamCtor = globalThis.ReadableStream;
|
|
353
|
+
if (readableStreamCtor && body instanceof readableStreamCtor) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
function toRequestBody(iterable) {
|
|
359
|
+
const readableStreamCtor = globalThis.ReadableStream;
|
|
360
|
+
if (typeof readableStreamCtor === "function") {
|
|
361
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
362
|
+
return new readableStreamCtor({
|
|
363
|
+
async pull(controller) {
|
|
364
|
+
const { value, done } = await iterator.next();
|
|
365
|
+
if (done) {
|
|
366
|
+
controller.close();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
controller.enqueue(value);
|
|
370
|
+
},
|
|
371
|
+
async cancel(reason) {
|
|
372
|
+
if (typeof iterator.return === "function") {
|
|
373
|
+
await iterator.return(reason);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return iterable;
|
|
497
379
|
}
|
|
498
380
|
async function* readReadableStream(stream) {
|
|
499
381
|
const reader = stream.getReader();
|
|
@@ -553,19 +435,225 @@ function concatChunks(a, b) {
|
|
|
553
435
|
merged.set(b, a.byteLength);
|
|
554
436
|
return merged;
|
|
555
437
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
438
|
+
|
|
439
|
+
// src/commit.ts
|
|
440
|
+
var DEFAULT_TTL_SECONDS = 60 * 60;
|
|
441
|
+
var HEADS_REF_PREFIX = "refs/heads/";
|
|
442
|
+
var BufferCtor2 = globalThis.Buffer;
|
|
443
|
+
var CommitBuilderImpl = class {
|
|
444
|
+
options;
|
|
445
|
+
getAuthToken;
|
|
446
|
+
transport;
|
|
447
|
+
operations = [];
|
|
448
|
+
sent = false;
|
|
449
|
+
constructor(deps) {
|
|
450
|
+
this.options = normalizeCommitOptions(deps.options);
|
|
451
|
+
this.getAuthToken = deps.getAuthToken;
|
|
452
|
+
this.transport = deps.transport;
|
|
453
|
+
const trimmedMessage = this.options.commitMessage?.trim();
|
|
454
|
+
const trimmedAuthorName = this.options.author?.name?.trim();
|
|
455
|
+
const trimmedAuthorEmail = this.options.author?.email?.trim();
|
|
456
|
+
if (!trimmedMessage) {
|
|
457
|
+
throw new Error("createCommit commitMessage is required");
|
|
458
|
+
}
|
|
459
|
+
if (!trimmedAuthorName || !trimmedAuthorEmail) {
|
|
460
|
+
throw new Error("createCommit author name and email are required");
|
|
461
|
+
}
|
|
462
|
+
this.options.commitMessage = trimmedMessage;
|
|
463
|
+
this.options.author = {
|
|
464
|
+
name: trimmedAuthorName,
|
|
465
|
+
email: trimmedAuthorEmail
|
|
466
|
+
};
|
|
467
|
+
if (typeof this.options.expectedHeadSha === "string") {
|
|
468
|
+
this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
|
|
469
|
+
}
|
|
470
|
+
if (typeof this.options.baseBranch === "string") {
|
|
471
|
+
const trimmedBase = this.options.baseBranch.trim();
|
|
472
|
+
if (trimmedBase === "") {
|
|
473
|
+
delete this.options.baseBranch;
|
|
474
|
+
} else {
|
|
475
|
+
if (trimmedBase.startsWith("refs/")) {
|
|
476
|
+
throw new Error("createCommit baseBranch must not include refs/ prefix");
|
|
477
|
+
}
|
|
478
|
+
this.options.baseBranch = trimmedBase;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (this.options.ephemeralBase && !this.options.baseBranch) {
|
|
482
|
+
throw new Error("createCommit ephemeralBase requires baseBranch");
|
|
483
|
+
}
|
|
559
484
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
485
|
+
addFile(path, source, options) {
|
|
486
|
+
this.ensureNotSent();
|
|
487
|
+
const normalizedPath = this.normalizePath(path);
|
|
488
|
+
const contentId = randomContentId();
|
|
489
|
+
const mode = options?.mode ?? "100644";
|
|
490
|
+
this.operations.push({
|
|
491
|
+
path: normalizedPath,
|
|
492
|
+
contentId,
|
|
493
|
+
mode,
|
|
494
|
+
operation: "upsert",
|
|
495
|
+
streamFactory: () => toAsyncIterable(source)
|
|
496
|
+
});
|
|
497
|
+
return this;
|
|
498
|
+
}
|
|
499
|
+
addFileFromString(path, contents, options) {
|
|
500
|
+
const encoding = options?.encoding ?? "utf8";
|
|
501
|
+
const normalizedEncoding = encoding === "utf-8" ? "utf8" : encoding;
|
|
502
|
+
let data;
|
|
503
|
+
if (normalizedEncoding === "utf8") {
|
|
504
|
+
data = new TextEncoder().encode(contents);
|
|
505
|
+
} else if (BufferCtor2) {
|
|
506
|
+
data = BufferCtor2.from(
|
|
507
|
+
contents,
|
|
508
|
+
normalizedEncoding
|
|
509
|
+
);
|
|
510
|
+
} else {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return this.addFile(path, data, options);
|
|
516
|
+
}
|
|
517
|
+
deletePath(path) {
|
|
518
|
+
this.ensureNotSent();
|
|
519
|
+
const normalizedPath = this.normalizePath(path);
|
|
520
|
+
this.operations.push({
|
|
521
|
+
path: normalizedPath,
|
|
522
|
+
contentId: randomContentId(),
|
|
523
|
+
operation: "delete"
|
|
524
|
+
});
|
|
525
|
+
return this;
|
|
526
|
+
}
|
|
527
|
+
async send() {
|
|
528
|
+
this.ensureNotSent();
|
|
529
|
+
this.sent = true;
|
|
530
|
+
const metadata = this.buildMetadata();
|
|
531
|
+
const blobEntries = this.operations.filter((op) => op.operation === "upsert" && op.streamFactory).map((op) => ({
|
|
532
|
+
contentId: op.contentId,
|
|
533
|
+
chunks: chunkify(op.streamFactory())
|
|
534
|
+
}));
|
|
535
|
+
const authorization = await this.getAuthToken();
|
|
536
|
+
const ack = await this.transport.send({
|
|
537
|
+
authorization,
|
|
538
|
+
signal: this.options.signal,
|
|
539
|
+
metadata,
|
|
540
|
+
blobs: blobEntries
|
|
541
|
+
});
|
|
542
|
+
return buildCommitResult(ack);
|
|
543
|
+
}
|
|
544
|
+
buildMetadata() {
|
|
545
|
+
const files = this.operations.map((op) => {
|
|
546
|
+
const entry = {
|
|
547
|
+
path: op.path,
|
|
548
|
+
content_id: op.contentId,
|
|
549
|
+
operation: op.operation
|
|
550
|
+
};
|
|
551
|
+
if (op.mode) {
|
|
552
|
+
entry.mode = op.mode;
|
|
553
|
+
}
|
|
554
|
+
return entry;
|
|
555
|
+
});
|
|
556
|
+
const metadata = {
|
|
557
|
+
target_branch: this.options.targetBranch,
|
|
558
|
+
commit_message: this.options.commitMessage,
|
|
559
|
+
author: {
|
|
560
|
+
name: this.options.author.name,
|
|
561
|
+
email: this.options.author.email
|
|
562
|
+
},
|
|
563
|
+
files
|
|
564
|
+
};
|
|
565
|
+
if (this.options.expectedHeadSha) {
|
|
566
|
+
metadata.expected_head_sha = this.options.expectedHeadSha;
|
|
567
|
+
}
|
|
568
|
+
if (this.options.baseBranch) {
|
|
569
|
+
metadata.base_branch = this.options.baseBranch;
|
|
570
|
+
}
|
|
571
|
+
if (this.options.committer) {
|
|
572
|
+
metadata.committer = {
|
|
573
|
+
name: this.options.committer.name,
|
|
574
|
+
email: this.options.committer.email
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (this.options.ephemeral) {
|
|
578
|
+
metadata.ephemeral = true;
|
|
579
|
+
}
|
|
580
|
+
if (this.options.ephemeralBase) {
|
|
581
|
+
metadata.ephemeral_base = true;
|
|
582
|
+
}
|
|
583
|
+
return metadata;
|
|
584
|
+
}
|
|
585
|
+
ensureNotSent() {
|
|
586
|
+
if (this.sent) {
|
|
587
|
+
throw new Error("createCommit builder cannot be reused after send()");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
normalizePath(path) {
|
|
591
|
+
if (!path || typeof path !== "string" || path.trim() === "") {
|
|
592
|
+
throw new Error("File path must be a non-empty string");
|
|
593
|
+
}
|
|
594
|
+
return path.replace(/^\//, "");
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
var FetchCommitTransport = class {
|
|
598
|
+
url;
|
|
599
|
+
constructor(config) {
|
|
600
|
+
const trimmedBase = config.baseUrl.replace(/\/+$/, "");
|
|
601
|
+
this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
|
|
563
602
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
603
|
+
async send(request) {
|
|
604
|
+
const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
|
|
605
|
+
const body = toRequestBody(bodyIterable);
|
|
606
|
+
const init = {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${request.authorization}`,
|
|
610
|
+
"Content-Type": "application/x-ndjson",
|
|
611
|
+
Accept: "application/json"
|
|
612
|
+
},
|
|
613
|
+
body,
|
|
614
|
+
signal: request.signal
|
|
615
|
+
};
|
|
616
|
+
if (requiresDuplex(body)) {
|
|
617
|
+
init.duplex = "half";
|
|
618
|
+
}
|
|
619
|
+
const response = await fetch(this.url, init);
|
|
620
|
+
if (!response.ok) {
|
|
621
|
+
const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
|
|
622
|
+
const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
|
|
623
|
+
response,
|
|
624
|
+
fallbackMessage
|
|
625
|
+
);
|
|
626
|
+
throw new RefUpdateError(statusMessage, {
|
|
627
|
+
status: statusLabel,
|
|
628
|
+
message: statusMessage,
|
|
629
|
+
refUpdate
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const ack = commitPackAckSchema.parse(await response.json());
|
|
633
|
+
return ack;
|
|
567
634
|
}
|
|
568
|
-
|
|
635
|
+
};
|
|
636
|
+
function buildMessageIterable(metadata, blobs) {
|
|
637
|
+
const encoder = new TextEncoder();
|
|
638
|
+
return {
|
|
639
|
+
async *[Symbol.asyncIterator]() {
|
|
640
|
+
yield encoder.encode(`${JSON.stringify({ metadata })}
|
|
641
|
+
`);
|
|
642
|
+
for (const blob of blobs) {
|
|
643
|
+
for await (const segment of blob.chunks) {
|
|
644
|
+
const payload = {
|
|
645
|
+
blob_chunk: {
|
|
646
|
+
content_id: blob.contentId,
|
|
647
|
+
data: base64Encode(segment.chunk),
|
|
648
|
+
eof: segment.eof
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
yield encoder.encode(`${JSON.stringify(payload)}
|
|
652
|
+
`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
569
657
|
}
|
|
570
658
|
function randomContentId() {
|
|
571
659
|
const cryptoObj = globalThis.crypto;
|
|
@@ -581,6 +669,8 @@ function normalizeCommitOptions(options) {
|
|
|
581
669
|
commitMessage: options.commitMessage,
|
|
582
670
|
expectedHeadSha: options.expectedHeadSha,
|
|
583
671
|
baseBranch: options.baseBranch,
|
|
672
|
+
ephemeral: options.ephemeral === true,
|
|
673
|
+
ephemeralBase: options.ephemeralBase === true,
|
|
584
674
|
author: options.author,
|
|
585
675
|
committer: options.committer,
|
|
586
676
|
signal: options.signal,
|
|
@@ -640,76 +730,208 @@ function resolveCommitTtlSeconds(options) {
|
|
|
640
730
|
}
|
|
641
731
|
return DEFAULT_TTL_SECONDS;
|
|
642
732
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
733
|
+
|
|
734
|
+
// src/diff-commit.ts
|
|
735
|
+
var DiffCommitExecutor = class {
|
|
736
|
+
options;
|
|
737
|
+
getAuthToken;
|
|
738
|
+
transport;
|
|
739
|
+
diffFactory;
|
|
740
|
+
sent = false;
|
|
741
|
+
constructor(deps) {
|
|
742
|
+
this.options = normalizeDiffCommitOptions(deps.options);
|
|
743
|
+
this.getAuthToken = deps.getAuthToken;
|
|
744
|
+
this.transport = deps.transport;
|
|
745
|
+
const trimmedMessage = this.options.commitMessage?.trim();
|
|
746
|
+
const trimmedAuthorName = this.options.author?.name?.trim();
|
|
747
|
+
const trimmedAuthorEmail = this.options.author?.email?.trim();
|
|
748
|
+
if (!trimmedMessage) {
|
|
749
|
+
throw new Error("createCommitFromDiff commitMessage is required");
|
|
658
750
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const inferred = inferRefUpdateReason(String(response.status));
|
|
662
|
-
return inferred === "unknown" ? "failed" : inferred;
|
|
663
|
-
})();
|
|
664
|
-
let statusLabel = defaultStatus;
|
|
665
|
-
let refUpdate;
|
|
666
|
-
let message;
|
|
667
|
-
if (jsonBody !== void 0) {
|
|
668
|
-
const parsedResponse = commitPackResponseSchema.safeParse(jsonBody);
|
|
669
|
-
if (parsedResponse.success) {
|
|
670
|
-
const result = parsedResponse.data.result;
|
|
671
|
-
if (typeof result.status === "string" && result.status.trim() !== "") {
|
|
672
|
-
statusLabel = result.status.trim();
|
|
673
|
-
}
|
|
674
|
-
refUpdate = toPartialRefUpdateFields(result.branch, result.old_sha, result.new_sha);
|
|
675
|
-
if (typeof result.message === "string" && result.message.trim() !== "") {
|
|
676
|
-
message = result.message.trim();
|
|
677
|
-
}
|
|
751
|
+
if (!trimmedAuthorName || !trimmedAuthorEmail) {
|
|
752
|
+
throw new Error("createCommitFromDiff author name and email are required");
|
|
678
753
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
754
|
+
this.options.commitMessage = trimmedMessage;
|
|
755
|
+
this.options.author = {
|
|
756
|
+
name: trimmedAuthorName,
|
|
757
|
+
email: trimmedAuthorEmail
|
|
758
|
+
};
|
|
759
|
+
if (typeof this.options.expectedHeadSha === "string") {
|
|
760
|
+
this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
|
|
761
|
+
}
|
|
762
|
+
if (typeof this.options.baseBranch === "string") {
|
|
763
|
+
const trimmedBase = this.options.baseBranch.trim();
|
|
764
|
+
if (trimmedBase === "") {
|
|
765
|
+
delete this.options.baseBranch;
|
|
766
|
+
} else {
|
|
767
|
+
if (trimmedBase.startsWith("refs/")) {
|
|
768
|
+
throw new Error("createCommitFromDiff baseBranch must not include refs/ prefix");
|
|
685
769
|
}
|
|
770
|
+
this.options.baseBranch = trimmedBase;
|
|
686
771
|
}
|
|
687
772
|
}
|
|
773
|
+
if (this.options.ephemeralBase && !this.options.baseBranch) {
|
|
774
|
+
throw new Error("createCommitFromDiff ephemeralBase requires baseBranch");
|
|
775
|
+
}
|
|
776
|
+
this.diffFactory = () => toAsyncIterable(this.options.initialDiff);
|
|
688
777
|
}
|
|
689
|
-
|
|
690
|
-
|
|
778
|
+
async send() {
|
|
779
|
+
this.ensureNotSent();
|
|
780
|
+
this.sent = true;
|
|
781
|
+
const metadata = this.buildMetadata();
|
|
782
|
+
const diffIterable = chunkify(this.diffFactory());
|
|
783
|
+
const authorization = await this.getAuthToken();
|
|
784
|
+
const ack = await this.transport.send({
|
|
785
|
+
authorization,
|
|
786
|
+
signal: this.options.signal,
|
|
787
|
+
metadata,
|
|
788
|
+
diffChunks: diffIterable
|
|
789
|
+
});
|
|
790
|
+
return buildCommitResult(ack);
|
|
691
791
|
}
|
|
692
|
-
|
|
693
|
-
|
|
792
|
+
buildMetadata() {
|
|
793
|
+
const metadata = {
|
|
794
|
+
target_branch: this.options.targetBranch,
|
|
795
|
+
commit_message: this.options.commitMessage,
|
|
796
|
+
author: {
|
|
797
|
+
name: this.options.author.name,
|
|
798
|
+
email: this.options.author.email
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
if (this.options.expectedHeadSha) {
|
|
802
|
+
metadata.expected_head_sha = this.options.expectedHeadSha;
|
|
803
|
+
}
|
|
804
|
+
if (this.options.baseBranch) {
|
|
805
|
+
metadata.base_branch = this.options.baseBranch;
|
|
806
|
+
}
|
|
807
|
+
if (this.options.committer) {
|
|
808
|
+
metadata.committer = {
|
|
809
|
+
name: this.options.committer.name,
|
|
810
|
+
email: this.options.committer.email
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (this.options.ephemeral) {
|
|
814
|
+
metadata.ephemeral = true;
|
|
815
|
+
}
|
|
816
|
+
if (this.options.ephemeralBase) {
|
|
817
|
+
metadata.ephemeral_base = true;
|
|
818
|
+
}
|
|
819
|
+
return metadata;
|
|
820
|
+
}
|
|
821
|
+
ensureNotSent() {
|
|
822
|
+
if (this.sent) {
|
|
823
|
+
throw new Error("createCommitFromDiff cannot be reused after send()");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
var FetchDiffCommitTransport = class {
|
|
828
|
+
url;
|
|
829
|
+
constructor(config) {
|
|
830
|
+
const trimmedBase = config.baseUrl.replace(/\/+$/, "");
|
|
831
|
+
this.url = `${trimmedBase}/api/v${config.version}/repos/diff-commit`;
|
|
832
|
+
}
|
|
833
|
+
async send(request) {
|
|
834
|
+
const bodyIterable = buildMessageIterable2(request.metadata, request.diffChunks);
|
|
835
|
+
const body = toRequestBody(bodyIterable);
|
|
836
|
+
const init = {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: {
|
|
839
|
+
Authorization: `Bearer ${request.authorization}`,
|
|
840
|
+
"Content-Type": "application/x-ndjson",
|
|
841
|
+
Accept: "application/json"
|
|
842
|
+
},
|
|
843
|
+
body,
|
|
844
|
+
signal: request.signal
|
|
845
|
+
};
|
|
846
|
+
if (requiresDuplex(body)) {
|
|
847
|
+
init.duplex = "half";
|
|
848
|
+
}
|
|
849
|
+
const response = await fetch(this.url, init);
|
|
850
|
+
if (!response.ok) {
|
|
851
|
+
const fallbackMessage = `createCommitFromDiff request failed (${response.status} ${response.statusText})`;
|
|
852
|
+
const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
|
|
853
|
+
response,
|
|
854
|
+
fallbackMessage
|
|
855
|
+
);
|
|
856
|
+
throw new RefUpdateError(statusMessage, {
|
|
857
|
+
status: statusLabel,
|
|
858
|
+
message: statusMessage,
|
|
859
|
+
refUpdate
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return commitPackAckSchema.parse(await response.json());
|
|
694
863
|
}
|
|
864
|
+
};
|
|
865
|
+
function buildMessageIterable2(metadata, diffChunks) {
|
|
866
|
+
const encoder = new TextEncoder();
|
|
695
867
|
return {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
868
|
+
async *[Symbol.asyncIterator]() {
|
|
869
|
+
yield encoder.encode(`${JSON.stringify({ metadata })}
|
|
870
|
+
`);
|
|
871
|
+
for await (const segment of diffChunks) {
|
|
872
|
+
const payload = {
|
|
873
|
+
diff_chunk: {
|
|
874
|
+
data: base64Encode(segment.chunk),
|
|
875
|
+
eof: segment.eof
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
yield encoder.encode(`${JSON.stringify(payload)}
|
|
879
|
+
`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
699
882
|
};
|
|
700
883
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
884
|
+
function normalizeDiffCommitOptions(options) {
|
|
885
|
+
if (!options || typeof options !== "object") {
|
|
886
|
+
throw new Error("createCommitFromDiff options are required");
|
|
887
|
+
}
|
|
888
|
+
if (options.diff === void 0 || options.diff === null) {
|
|
889
|
+
throw new Error("createCommitFromDiff diff is required");
|
|
890
|
+
}
|
|
891
|
+
const targetBranch = normalizeBranchName2(options.targetBranch);
|
|
892
|
+
let committer;
|
|
893
|
+
if (options.committer) {
|
|
894
|
+
const name = options.committer.name?.trim();
|
|
895
|
+
const email = options.committer.email?.trim();
|
|
896
|
+
if (!name || !email) {
|
|
897
|
+
throw new Error("createCommitFromDiff committer name and email are required when provided");
|
|
898
|
+
}
|
|
899
|
+
committer = { name, email };
|
|
705
900
|
}
|
|
706
|
-
|
|
707
|
-
|
|
901
|
+
return {
|
|
902
|
+
targetBranch,
|
|
903
|
+
commitMessage: options.commitMessage,
|
|
904
|
+
expectedHeadSha: options.expectedHeadSha,
|
|
905
|
+
baseBranch: options.baseBranch,
|
|
906
|
+
ephemeral: options.ephemeral === true,
|
|
907
|
+
ephemeralBase: options.ephemeralBase === true,
|
|
908
|
+
author: options.author,
|
|
909
|
+
committer,
|
|
910
|
+
signal: options.signal,
|
|
911
|
+
ttl: options.ttl,
|
|
912
|
+
initialDiff: options.diff
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function normalizeBranchName2(value) {
|
|
916
|
+
const trimmed = value?.trim();
|
|
917
|
+
if (!trimmed) {
|
|
918
|
+
throw new Error("createCommitFromDiff targetBranch is required");
|
|
708
919
|
}
|
|
709
|
-
if (
|
|
710
|
-
|
|
920
|
+
if (trimmed.startsWith("refs/heads/")) {
|
|
921
|
+
const branch = trimmed.slice("refs/heads/".length).trim();
|
|
922
|
+
if (!branch) {
|
|
923
|
+
throw new Error("createCommitFromDiff targetBranch must include a branch name");
|
|
924
|
+
}
|
|
925
|
+
return branch;
|
|
711
926
|
}
|
|
712
|
-
|
|
927
|
+
if (trimmed.startsWith("refs/")) {
|
|
928
|
+
throw new Error("createCommitFromDiff targetBranch must not include refs/ prefix");
|
|
929
|
+
}
|
|
930
|
+
return trimmed;
|
|
931
|
+
}
|
|
932
|
+
async function sendCommitFromDiff(deps) {
|
|
933
|
+
const executor = new DiffCommitExecutor(deps);
|
|
934
|
+
return executor.send();
|
|
713
935
|
}
|
|
714
936
|
|
|
715
937
|
// src/fetch.ts
|
|
@@ -1293,6 +1515,9 @@ var RepoImpl = class {
|
|
|
1293
1515
|
if (options.ref) {
|
|
1294
1516
|
params.ref = options.ref;
|
|
1295
1517
|
}
|
|
1518
|
+
if (typeof options.ephemeral === "boolean") {
|
|
1519
|
+
params.ephemeral = String(options.ephemeral);
|
|
1520
|
+
}
|
|
1296
1521
|
return this.api.get({ path: "repos/file", params }, jwt);
|
|
1297
1522
|
}
|
|
1298
1523
|
async listFiles(options) {
|
|
@@ -1301,8 +1526,17 @@ var RepoImpl = class {
|
|
|
1301
1526
|
permissions: ["git:read"],
|
|
1302
1527
|
ttl
|
|
1303
1528
|
});
|
|
1304
|
-
const params =
|
|
1305
|
-
|
|
1529
|
+
const params = {};
|
|
1530
|
+
if (options?.ref) {
|
|
1531
|
+
params.ref = options.ref;
|
|
1532
|
+
}
|
|
1533
|
+
if (typeof options?.ephemeral === "boolean") {
|
|
1534
|
+
params.ephemeral = String(options.ephemeral);
|
|
1535
|
+
}
|
|
1536
|
+
const response = await this.api.get(
|
|
1537
|
+
{ path: "repos/files", params: Object.keys(params).length ? params : void 0 },
|
|
1538
|
+
jwt
|
|
1539
|
+
);
|
|
1306
1540
|
const raw = listFilesResponseSchema.parse(await response.json());
|
|
1307
1541
|
return { paths: raw.paths, ref: raw.ref };
|
|
1308
1542
|
}
|
|
@@ -1490,6 +1724,25 @@ var RepoImpl = class {
|
|
|
1490
1724
|
transport
|
|
1491
1725
|
});
|
|
1492
1726
|
}
|
|
1727
|
+
async createCommitFromDiff(options) {
|
|
1728
|
+
const version = this.options.apiVersion ?? API_VERSION;
|
|
1729
|
+
const baseUrl = this.options.apiBaseUrl ?? API_BASE_URL;
|
|
1730
|
+
const transport = new FetchDiffCommitTransport({ baseUrl, version });
|
|
1731
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
1732
|
+
const requestOptions = {
|
|
1733
|
+
...options,
|
|
1734
|
+
ttl
|
|
1735
|
+
};
|
|
1736
|
+
const getAuthToken = () => this.generateJWT(this.id, {
|
|
1737
|
+
permissions: ["git:write"],
|
|
1738
|
+
ttl
|
|
1739
|
+
});
|
|
1740
|
+
return sendCommitFromDiff({
|
|
1741
|
+
options: requestOptions,
|
|
1742
|
+
getAuthToken,
|
|
1743
|
+
transport
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1493
1746
|
};
|
|
1494
1747
|
var GitStorage = class _GitStorage {
|
|
1495
1748
|
static overrides = {};
|