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