@pierre/storage 0.2.0 → 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/dist/index.js CHANGED
@@ -148,257 +148,7 @@ var errorEnvelopeSchema = z.object({
148
148
  error: z.string()
149
149
  });
150
150
 
151
- // src/commit.ts
152
- var MAX_CHUNK_BYTES = 4 * 1024 * 1024;
153
- var DEFAULT_TTL_SECONDS = 60 * 60;
154
- var HEADS_REF_PREFIX = "refs/heads/";
155
- var BufferCtor = globalThis.Buffer;
156
- var CommitBuilderImpl = class {
157
- options;
158
- getAuthToken;
159
- transport;
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
- if (this.options.ephemeralBase && !this.options.baseBranch) {
195
- throw new Error("createCommit ephemeralBase requires baseBranch");
196
- }
197
- }
198
- addFile(path, source, options) {
199
- this.ensureNotSent();
200
- const normalizedPath = this.normalizePath(path);
201
- const contentId = randomContentId();
202
- const mode = options?.mode ?? "100644";
203
- this.operations.push({
204
- path: normalizedPath,
205
- contentId,
206
- mode,
207
- operation: "upsert",
208
- streamFactory: () => toAsyncIterable(source)
209
- });
210
- return this;
211
- }
212
- addFileFromString(path, contents, options) {
213
- const encoding = options?.encoding ?? "utf8";
214
- const normalizedEncoding = encoding === "utf-8" ? "utf8" : encoding;
215
- let data;
216
- if (normalizedEncoding === "utf8") {
217
- data = new TextEncoder().encode(contents);
218
- } else if (BufferCtor) {
219
- data = BufferCtor.from(
220
- contents,
221
- normalizedEncoding
222
- );
223
- } else {
224
- throw new Error(
225
- `Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`
226
- );
227
- }
228
- return this.addFile(path, data, options);
229
- }
230
- deletePath(path) {
231
- this.ensureNotSent();
232
- const normalizedPath = this.normalizePath(path);
233
- this.operations.push({
234
- path: normalizedPath,
235
- contentId: randomContentId(),
236
- operation: "delete"
237
- });
238
- return this;
239
- }
240
- async send() {
241
- this.ensureNotSent();
242
- this.sent = true;
243
- const metadata = this.buildMetadata();
244
- const blobEntries = this.operations.filter((op) => op.operation === "upsert" && op.streamFactory).map((op) => ({
245
- contentId: op.contentId,
246
- chunks: chunkify(op.streamFactory())
247
- }));
248
- const authorization = await this.getAuthToken();
249
- const ack = await this.transport.send({
250
- authorization,
251
- signal: this.options.signal,
252
- metadata,
253
- blobs: blobEntries
254
- });
255
- return buildCommitResult(ack);
256
- }
257
- buildMetadata() {
258
- const files = this.operations.map((op) => {
259
- const entry = {
260
- path: op.path,
261
- content_id: op.contentId,
262
- operation: op.operation
263
- };
264
- if (op.mode) {
265
- entry.mode = op.mode;
266
- }
267
- return entry;
268
- });
269
- const metadata = {
270
- target_branch: this.options.targetBranch,
271
- commit_message: this.options.commitMessage,
272
- author: {
273
- name: this.options.author.name,
274
- email: this.options.author.email
275
- },
276
- files
277
- };
278
- if (this.options.expectedHeadSha) {
279
- metadata.expected_head_sha = this.options.expectedHeadSha;
280
- }
281
- if (this.options.baseBranch) {
282
- metadata.base_branch = this.options.baseBranch;
283
- }
284
- if (this.options.committer) {
285
- metadata.committer = {
286
- name: this.options.committer.name,
287
- email: this.options.committer.email
288
- };
289
- }
290
- if (this.options.ephemeral) {
291
- metadata.ephemeral = true;
292
- }
293
- if (this.options.ephemeralBase) {
294
- metadata.ephemeral_base = true;
295
- }
296
- return metadata;
297
- }
298
- ensureNotSent() {
299
- if (this.sent) {
300
- throw new Error("createCommit builder cannot be reused after send()");
301
- }
302
- }
303
- normalizePath(path) {
304
- if (!path || typeof path !== "string" || path.trim() === "") {
305
- throw new Error("File path must be a non-empty string");
306
- }
307
- return path.replace(/^\//, "");
308
- }
309
- };
310
- var FetchCommitTransport = class {
311
- url;
312
- constructor(config) {
313
- const trimmedBase = config.baseUrl.replace(/\/+$/, "");
314
- this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
315
- }
316
- async send(request) {
317
- const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
318
- const body = toRequestBody(bodyIterable);
319
- const init = {
320
- method: "POST",
321
- headers: {
322
- Authorization: `Bearer ${request.authorization}`,
323
- "Content-Type": "application/x-ndjson",
324
- Accept: "application/json"
325
- },
326
- body,
327
- signal: request.signal
328
- };
329
- if (requiresDuplex(body)) {
330
- init.duplex = "half";
331
- }
332
- const response = await fetch(this.url, init);
333
- if (!response.ok) {
334
- const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(response);
335
- throw new RefUpdateError(statusMessage, {
336
- status: statusLabel,
337
- message: statusMessage,
338
- refUpdate
339
- });
340
- }
341
- const ack = commitPackAckSchema.parse(await response.json());
342
- return ack;
343
- }
344
- };
345
- function toRequestBody(iterable) {
346
- const readableStreamCtor = globalThis.ReadableStream;
347
- if (typeof readableStreamCtor === "function") {
348
- const iterator = iterable[Symbol.asyncIterator]();
349
- return new readableStreamCtor({
350
- async pull(controller) {
351
- const { value, done } = await iterator.next();
352
- if (done) {
353
- controller.close();
354
- return;
355
- }
356
- controller.enqueue(value);
357
- },
358
- async cancel(reason) {
359
- if (typeof iterator.return === "function") {
360
- await iterator.return(reason);
361
- }
362
- }
363
- });
364
- }
365
- return iterable;
366
- }
367
- function buildMessageIterable(metadata, blobs) {
368
- const encoder = new TextEncoder();
369
- return {
370
- async *[Symbol.asyncIterator]() {
371
- yield encoder.encode(`${JSON.stringify({ metadata })}
372
- `);
373
- for (const blob of blobs) {
374
- for await (const segment of blob.chunks) {
375
- const payload = {
376
- blob_chunk: {
377
- content_id: blob.contentId,
378
- data: base64Encode(segment.chunk),
379
- eof: segment.eof
380
- }
381
- };
382
- yield encoder.encode(`${JSON.stringify(payload)}
383
- `);
384
- }
385
- }
386
- }
387
- };
388
- }
389
- function requiresDuplex(body) {
390
- if (!body || typeof body !== "object") {
391
- return false;
392
- }
393
- if (typeof body[Symbol.asyncIterator] === "function") {
394
- return true;
395
- }
396
- const readableStreamCtor = globalThis.ReadableStream;
397
- if (readableStreamCtor && body instanceof readableStreamCtor) {
398
- return true;
399
- }
400
- return false;
401
- }
151
+ // src/commit-pack.ts
402
152
  function buildCommitResult(ack) {
403
153
  const refUpdate = toRefUpdate(ack.result);
404
154
  if (!ack.result.success) {
@@ -409,24 +159,98 @@ function buildCommitResult(ack) {
409
159
  message: ack.result.message,
410
160
  refUpdate
411
161
  }
412
- );
162
+ );
163
+ }
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
+ };
172
+ }
173
+ function toRefUpdate(result) {
174
+ return {
175
+ branch: result.branch,
176
+ oldSha: result.old_sha,
177
+ newSha: result.new_sha
178
+ };
179
+ }
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;
187
+ }
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
+ }
224
+ }
225
+ if (!message && typeof jsonBody === "string" && jsonBody.trim() !== "") {
226
+ message = jsonBody.trim();
227
+ }
228
+ if (!message && textBody && textBody.trim() !== "") {
229
+ message = textBody.trim();
413
230
  }
414
231
  return {
415
- commitSha: ack.commit.commit_sha,
416
- treeSha: ack.commit.tree_sha,
417
- targetBranch: ack.commit.target_branch,
418
- packBytes: ack.commit.pack_bytes,
419
- blobCount: ack.commit.blob_count,
232
+ statusMessage: message ?? fallbackMessage,
233
+ statusLabel,
420
234
  refUpdate
421
235
  };
422
236
  }
423
- function toRefUpdate(result) {
424
- return {
425
- branch: result.branch,
426
- oldSha: result.old_sha,
427
- newSha: result.new_sha
428
- };
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;
429
249
  }
250
+
251
+ // src/stream-utils.ts
252
+ var BufferCtor = globalThis.Buffer;
253
+ var MAX_CHUNK_BYTES = 4 * 1024 * 1024;
430
254
  async function* chunkify(source) {
431
255
  let pending = null;
432
256
  let produced = false;
@@ -502,7 +326,56 @@ async function* toAsyncIterable(source) {
502
326
  }
503
327
  return;
504
328
  }
505
- throw new Error("Unsupported file source for createCommit");
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;
506
379
  }
507
380
  async function* readReadableStream(stream) {
508
381
  const reader = stream.getReader();
@@ -562,19 +435,225 @@ function concatChunks(a, b) {
562
435
  merged.set(b, a.byteLength);
563
436
  return merged;
564
437
  }
565
- function base64Encode(bytes) {
566
- if (BufferCtor) {
567
- return BufferCtor.from(bytes).toString("base64");
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
+ }
568
484
  }
569
- let binary = "";
570
- for (let i = 0; i < bytes.byteLength; i++) {
571
- binary += String.fromCharCode(bytes[i]);
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`;
572
602
  }
573
- const btoaFn = globalThis.btoa;
574
- if (typeof btoaFn === "function") {
575
- return btoaFn(binary);
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;
576
634
  }
577
- throw new Error("Base64 encoding is not supported in this environment");
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
+ };
578
657
  }
579
658
  function randomContentId() {
580
659
  const cryptoObj = globalThis.crypto;
@@ -651,76 +730,208 @@ function resolveCommitTtlSeconds(options) {
651
730
  }
652
731
  return DEFAULT_TTL_SECONDS;
653
732
  }
654
- async function parseCommitPackError(response) {
655
- const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
656
- const cloned = response.clone();
657
- let jsonBody;
658
- try {
659
- jsonBody = await cloned.json();
660
- } catch {
661
- jsonBody = void 0;
662
- }
663
- let textBody;
664
- if (jsonBody === void 0) {
665
- try {
666
- textBody = await response.text();
667
- } catch {
668
- textBody = void 0;
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");
669
750
  }
670
- }
671
- const defaultStatus = (() => {
672
- const inferred = inferRefUpdateReason(String(response.status));
673
- return inferred === "unknown" ? "failed" : inferred;
674
- })();
675
- let statusLabel = defaultStatus;
676
- let refUpdate;
677
- let message;
678
- if (jsonBody !== void 0) {
679
- const parsedResponse = commitPackResponseSchema.safeParse(jsonBody);
680
- if (parsedResponse.success) {
681
- const result = parsedResponse.data.result;
682
- if (typeof result.status === "string" && result.status.trim() !== "") {
683
- statusLabel = result.status.trim();
684
- }
685
- refUpdate = toPartialRefUpdateFields(result.branch, result.old_sha, result.new_sha);
686
- if (typeof result.message === "string" && result.message.trim() !== "") {
687
- message = result.message.trim();
688
- }
751
+ if (!trimmedAuthorName || !trimmedAuthorEmail) {
752
+ throw new Error("createCommitFromDiff author name and email are required");
689
753
  }
690
- if (!message) {
691
- const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
692
- if (parsedError.success) {
693
- const trimmed = parsedError.data.error.trim();
694
- if (trimmed) {
695
- message = trimmed;
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");
696
769
  }
770
+ this.options.baseBranch = trimmedBase;
697
771
  }
698
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);
699
777
  }
700
- if (!message && typeof jsonBody === "string" && jsonBody.trim() !== "") {
701
- message = jsonBody.trim();
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);
702
791
  }
703
- if (!message && textBody && textBody.trim() !== "") {
704
- message = textBody.trim();
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());
705
863
  }
864
+ };
865
+ function buildMessageIterable2(metadata, diffChunks) {
866
+ const encoder = new TextEncoder();
706
867
  return {
707
- statusMessage: message ?? fallbackMessage,
708
- statusLabel,
709
- refUpdate
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
+ }
710
882
  };
711
883
  }
712
- function toPartialRefUpdateFields(branch, oldSha, newSha) {
713
- const refUpdate = {};
714
- if (typeof branch === "string" && branch.trim() !== "") {
715
- refUpdate.branch = branch.trim();
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 };
716
900
  }
717
- if (typeof oldSha === "string" && oldSha.trim() !== "") {
718
- refUpdate.oldSha = oldSha.trim();
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");
719
919
  }
720
- if (typeof newSha === "string" && newSha.trim() !== "") {
721
- refUpdate.newSha = newSha.trim();
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;
722
926
  }
723
- return Object.keys(refUpdate).length > 0 ? refUpdate : void 0;
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();
724
935
  }
725
936
 
726
937
  // src/fetch.ts
@@ -1513,6 +1724,25 @@ var RepoImpl = class {
1513
1724
  transport
1514
1725
  });
1515
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
+ }
1516
1746
  };
1517
1747
  var GitStorage = class _GitStorage {
1518
1748
  static overrides = {};