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