@pierre/storage 0.1.2 → 0.1.4

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/src/commit.ts CHANGED
@@ -7,14 +7,17 @@ import type {
7
7
  CommitFileOptions,
8
8
  CommitFileSource,
9
9
  CommitResult,
10
+ CommitSignature,
10
11
  CommitTextFileOptions,
11
12
  CreateCommitOptions,
13
+ LegacyCreateCommitOptions,
12
14
  ReadableStreamLike,
13
15
  RefUpdate,
14
16
  } from './types';
15
17
 
16
18
  const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
17
19
  const DEFAULT_TTL_SECONDS = 60 * 60;
20
+ const HEADS_REF_PREFIX = 'refs/heads/';
18
21
 
19
22
  type NodeBuffer = Uint8Array & { toString(encoding?: string): string };
20
23
  interface NodeBufferConstructor {
@@ -35,6 +38,7 @@ type ChunkSegment = {
35
38
  interface CommitMetadataPayload {
36
39
  target_branch: string;
37
40
  expected_head_sha?: string;
41
+ base_branch?: string;
38
42
  commit_message: string;
39
43
  author: {
40
44
  name: string;
@@ -63,6 +67,17 @@ interface CommitTransport {
63
67
  send(request: CommitTransportRequest): Promise<CommitPackAck>;
64
68
  }
65
69
 
70
+ type NormalizedCommitOptions = {
71
+ targetBranch: string;
72
+ commitMessage: string;
73
+ expectedHeadSha?: string;
74
+ baseBranch?: string;
75
+ author: CommitSignature;
76
+ committer?: CommitSignature;
77
+ signal?: AbortSignal;
78
+ ttl?: number;
79
+ };
80
+
66
81
  interface CommitBuilderDeps {
67
82
  options: CreateCommitOptions;
68
83
  getAuthToken: () => Promise<string>;
@@ -80,35 +95,27 @@ type FileOperationState = {
80
95
  type CommitPackAck = CommitPackAckRaw;
81
96
 
82
97
  export class CommitBuilderImpl implements CommitBuilder {
83
- private readonly options: CreateCommitOptions;
98
+ private readonly options: NormalizedCommitOptions;
84
99
  private readonly getAuthToken: () => Promise<string>;
85
100
  private readonly transport: CommitTransport;
86
101
  private readonly operations: FileOperationState[] = [];
87
102
  private sent = false;
88
103
 
89
104
  constructor(deps: CommitBuilderDeps) {
90
- this.options = { ...deps.options };
105
+ this.options = normalizeCommitOptions(deps.options);
91
106
  this.getAuthToken = deps.getAuthToken;
92
107
  this.transport = deps.transport;
93
108
 
94
- const trimmedTarget = this.options.targetBranch?.trim();
95
109
  const trimmedMessage = this.options.commitMessage?.trim();
96
110
  const trimmedAuthorName = this.options.author?.name?.trim();
97
111
  const trimmedAuthorEmail = this.options.author?.email?.trim();
98
112
 
99
- if (!trimmedTarget) {
100
- throw new Error('createCommit targetBranch is required');
101
- }
102
- if (trimmedTarget.startsWith('refs/')) {
103
- throw new Error('createCommit targetBranch must not include refs/ prefix');
104
- }
105
113
  if (!trimmedMessage) {
106
114
  throw new Error('createCommit commitMessage is required');
107
115
  }
108
116
  if (!trimmedAuthorName || !trimmedAuthorEmail) {
109
117
  throw new Error('createCommit author name and email are required');
110
118
  }
111
- this.options.targetBranch = trimmedTarget;
112
119
  this.options.commitMessage = trimmedMessage;
113
120
  this.options.author = {
114
121
  name: trimmedAuthorName,
@@ -117,6 +124,17 @@ export class CommitBuilderImpl implements CommitBuilder {
117
124
  if (typeof this.options.expectedHeadSha === 'string') {
118
125
  this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
119
126
  }
127
+ if (typeof this.options.baseBranch === 'string') {
128
+ const trimmedBase = this.options.baseBranch.trim();
129
+ if (trimmedBase === '') {
130
+ delete this.options.baseBranch;
131
+ } else {
132
+ if (trimmedBase.startsWith('refs/')) {
133
+ throw new Error('createCommit baseBranch must not include refs/ prefix');
134
+ }
135
+ this.options.baseBranch = trimmedBase;
136
+ }
137
+ }
120
138
  }
121
139
 
122
140
  addFile(path: string, source: CommitFileSource, options?: CommitFileOptions): CommitBuilder {
@@ -218,6 +236,9 @@ export class CommitBuilderImpl implements CommitBuilder {
218
236
  if (this.options.expectedHeadSha) {
219
237
  metadata.expected_head_sha = this.options.expectedHeadSha;
220
238
  }
239
+ if (this.options.baseBranch) {
240
+ metadata.base_branch = this.options.baseBranch;
241
+ }
221
242
  if (this.options.committer) {
222
243
  metadata.committer = {
223
244
  name: this.options.committer.name,
@@ -254,7 +275,7 @@ export class FetchCommitTransport implements CommitTransport {
254
275
  const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
255
276
  const body = toRequestBody(bodyIterable);
256
277
 
257
- const response = await fetch(this.url, {
278
+ const init: RequestInit = {
258
279
  method: 'POST',
259
280
  headers: {
260
281
  Authorization: `Bearer ${request.authorization}`,
@@ -263,7 +284,13 @@ export class FetchCommitTransport implements CommitTransport {
263
284
  },
264
285
  body: body as any,
265
286
  signal: request.signal,
266
- });
287
+ };
288
+
289
+ if (requiresDuplex(body)) {
290
+ (init as RequestInit & { duplex: 'half' }).duplex = 'half';
291
+ }
292
+
293
+ const response = await fetch(this.url, init);
267
294
 
268
295
  if (!response.ok) {
269
296
  const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(response);
@@ -328,6 +355,25 @@ function buildMessageIterable(
328
355
  };
329
356
  }
330
357
 
358
+ function requiresDuplex(body: unknown): boolean {
359
+ if (!body || typeof body !== 'object') {
360
+ return false;
361
+ }
362
+
363
+ if (typeof (body as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function') {
364
+ return true;
365
+ }
366
+
367
+ const readableStreamCtor = (globalThis as {
368
+ ReadableStream?: new (...args: unknown[]) => unknown;
369
+ }).ReadableStream;
370
+ if (readableStreamCtor && body instanceof readableStreamCtor) {
371
+ return true;
372
+ }
373
+
374
+ return false;
375
+ }
376
+
331
377
  function buildCommitResult(ack: CommitPackAck): CommitResult {
332
378
  const refUpdate = toRefUpdate(ack.result);
333
379
  if (!ack.result.success) {
@@ -547,6 +593,68 @@ function randomContentId(): string {
547
593
  return `cid-${Date.now().toString(36)}-${random}`;
548
594
  }
549
595
 
596
+ function normalizeCommitOptions(options: CreateCommitOptions): NormalizedCommitOptions {
597
+ return {
598
+ targetBranch: resolveTargetBranch(options),
599
+ commitMessage: options.commitMessage,
600
+ expectedHeadSha: options.expectedHeadSha,
601
+ baseBranch: options.baseBranch,
602
+ author: options.author,
603
+ committer: options.committer,
604
+ signal: options.signal,
605
+ ttl: options.ttl,
606
+ };
607
+ }
608
+
609
+ function resolveTargetBranch(options: CreateCommitOptions): string {
610
+ const branchCandidate =
611
+ typeof options.targetBranch === 'string' ? options.targetBranch.trim() : '';
612
+ if (branchCandidate) {
613
+ return normalizeBranchName(branchCandidate);
614
+ }
615
+ if (hasLegacyTargetRef(options)) {
616
+ return normalizeLegacyTargetRef(options.targetRef);
617
+ }
618
+ throw new Error('createCommit targetBranch is required');
619
+ }
620
+
621
+ function normalizeBranchName(value: string): string {
622
+ const trimmed = value.trim();
623
+ if (!trimmed) {
624
+ throw new Error('createCommit targetBranch is required');
625
+ }
626
+ if (trimmed.startsWith(HEADS_REF_PREFIX)) {
627
+ const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
628
+ if (!branch) {
629
+ throw new Error('createCommit targetBranch is required');
630
+ }
631
+ return branch;
632
+ }
633
+ if (trimmed.startsWith('refs/')) {
634
+ throw new Error('createCommit targetBranch must not include refs/ prefix');
635
+ }
636
+ return trimmed;
637
+ }
638
+
639
+ function normalizeLegacyTargetRef(ref: string): string {
640
+ const trimmed = ref.trim();
641
+ if (!trimmed) {
642
+ throw new Error('createCommit targetRef is required');
643
+ }
644
+ if (!trimmed.startsWith(HEADS_REF_PREFIX)) {
645
+ throw new Error('createCommit targetRef must start with refs/heads/');
646
+ }
647
+ const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
648
+ if (!branch) {
649
+ throw new Error('createCommit targetRef must include a branch name');
650
+ }
651
+ return branch;
652
+ }
653
+
654
+ function hasLegacyTargetRef(options: CreateCommitOptions): options is LegacyCreateCommitOptions {
655
+ return typeof (options as LegacyCreateCommitOptions).targetRef === 'string';
656
+ }
657
+
550
658
  export function createCommitBuilder(deps: CommitBuilderDeps): CommitBuilder {
551
659
  return new CommitBuilderImpl(deps);
552
660
  }
package/src/types.ts CHANGED
@@ -224,15 +224,30 @@ export interface FileDiff extends DiffFileBase {
224
224
 
225
225
  export interface FilteredFile extends DiffFileBase {}
226
226
 
227
- export interface CreateCommitOptions extends GitStorageInvocationOptions {
228
- targetBranch: string;
227
+ interface CreateCommitBaseOptions extends GitStorageInvocationOptions {
229
228
  commitMessage: string;
230
229
  expectedHeadSha?: string;
230
+ baseBranch?: string;
231
231
  author: CommitSignature;
232
232
  committer?: CommitSignature;
233
233
  signal?: AbortSignal;
234
234
  }
235
235
 
236
+ export interface CreateCommitBranchOptions extends CreateCommitBaseOptions {
237
+ targetBranch: string;
238
+ targetRef?: never;
239
+ }
240
+
241
+ /**
242
+ * @deprecated Use {@link CreateCommitBranchOptions} instead.
243
+ */
244
+ export interface LegacyCreateCommitOptions extends CreateCommitBaseOptions {
245
+ targetBranch?: never;
246
+ targetRef: string;
247
+ }
248
+
249
+ export type CreateCommitOptions = CreateCommitBranchOptions | LegacyCreateCommitOptions;
250
+
236
251
  export interface CommitSignature {
237
252
  name: string;
238
253
  email: string;