@pierre/storage 0.9.2 → 1.0.0

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
@@ -3,22 +3,22 @@ import { RefUpdateError } from './errors';
3
3
  import type { CommitPackAckRaw } from './schemas';
4
4
  import { commitPackAckSchema } from './schemas';
5
5
  import {
6
- base64Encode,
7
- type ChunkSegment,
8
- chunkify,
9
- requiresDuplex,
10
- toAsyncIterable,
11
- toRequestBody,
6
+ type ChunkSegment,
7
+ base64Encode,
8
+ chunkify,
9
+ requiresDuplex,
10
+ toAsyncIterable,
11
+ toRequestBody,
12
12
  } from './stream-utils';
13
13
  import type {
14
- CommitBuilder,
15
- CommitFileOptions,
16
- CommitFileSource,
17
- CommitResult,
18
- CommitSignature,
19
- CommitTextFileOptions,
20
- CreateCommitOptions,
21
- LegacyCreateCommitOptions,
14
+ CommitBuilder,
15
+ CommitFileOptions,
16
+ CommitFileSource,
17
+ CommitResult,
18
+ CommitSignature,
19
+ CommitTextFileOptions,
20
+ CreateCommitOptions,
21
+ LegacyCreateCommitOptions,
22
22
  } from './types';
23
23
  import { getUserAgent } from './version';
24
24
 
@@ -27,408 +27,416 @@ const HEADS_REF_PREFIX = 'refs/heads/';
27
27
 
28
28
  type NodeBuffer = Uint8Array & { toString(encoding?: string): string };
29
29
  interface NodeBufferConstructor {
30
- from(data: Uint8Array): NodeBuffer;
31
- from(data: string, encoding?: string): NodeBuffer;
32
- isBuffer(value: unknown): value is NodeBuffer;
30
+ from(data: Uint8Array): NodeBuffer;
31
+ from(data: string, encoding?: string): NodeBuffer;
32
+ isBuffer(value: unknown): value is NodeBuffer;
33
33
  }
34
34
 
35
35
  const BufferCtor: NodeBufferConstructor | undefined = (
36
- globalThis as { Buffer?: NodeBufferConstructor }
36
+ globalThis as { Buffer?: NodeBufferConstructor }
37
37
  ).Buffer;
38
38
 
39
39
  interface CommitMetadataPayload {
40
- target_branch: string;
41
- expected_head_sha?: string;
42
- base_branch?: string;
43
- commit_message: string;
44
- ephemeral?: boolean;
45
- ephemeral_base?: boolean;
46
- author: {
47
- name: string;
48
- email: string;
49
- };
50
- committer?: {
51
- name: string;
52
- email: string;
53
- };
54
- files: Array<{
55
- path: string;
56
- content_id: string;
57
- operation?: 'upsert' | 'delete';
58
- mode?: string;
59
- }>;
40
+ target_branch: string;
41
+ expected_head_sha?: string;
42
+ base_branch?: string;
43
+ commit_message: string;
44
+ ephemeral?: boolean;
45
+ ephemeral_base?: boolean;
46
+ author: {
47
+ name: string;
48
+ email: string;
49
+ };
50
+ committer?: {
51
+ name: string;
52
+ email: string;
53
+ };
54
+ files: Array<{
55
+ path: string;
56
+ content_id: string;
57
+ operation?: 'upsert' | 'delete';
58
+ mode?: string;
59
+ }>;
60
60
  }
61
61
 
62
62
  interface CommitTransportRequest {
63
- authorization: string;
64
- signal?: AbortSignal;
65
- metadata: CommitMetadataPayload;
66
- blobs: Array<{ contentId: string; chunks: AsyncIterable<ChunkSegment> }>;
63
+ authorization: string;
64
+ signal?: AbortSignal;
65
+ metadata: CommitMetadataPayload;
66
+ blobs: Array<{ contentId: string; chunks: AsyncIterable<ChunkSegment> }>;
67
67
  }
68
68
 
69
69
  interface CommitTransport {
70
- send(request: CommitTransportRequest): Promise<CommitPackAckRaw>;
70
+ send(request: CommitTransportRequest): Promise<CommitPackAckRaw>;
71
71
  }
72
72
 
73
73
  type NormalizedCommitOptions = {
74
- targetBranch: string;
75
- commitMessage: string;
76
- expectedHeadSha?: string;
77
- baseBranch?: string;
78
- ephemeral?: boolean;
79
- ephemeralBase?: boolean;
80
- author: CommitSignature;
81
- committer?: CommitSignature;
82
- signal?: AbortSignal;
83
- ttl?: number;
74
+ targetBranch: string;
75
+ commitMessage: string;
76
+ expectedHeadSha?: string;
77
+ baseBranch?: string;
78
+ ephemeral?: boolean;
79
+ ephemeralBase?: boolean;
80
+ author: CommitSignature;
81
+ committer?: CommitSignature;
82
+ signal?: AbortSignal;
83
+ ttl?: number;
84
84
  };
85
85
 
86
86
  interface CommitBuilderDeps {
87
- options: CreateCommitOptions;
88
- getAuthToken: () => Promise<string>;
89
- transport: CommitTransport;
87
+ options: CreateCommitOptions;
88
+ getAuthToken: () => Promise<string>;
89
+ transport: CommitTransport;
90
90
  }
91
91
 
92
92
  type FileOperationState = {
93
- path: string;
94
- contentId: string;
95
- mode?: string;
96
- operation: 'upsert' | 'delete';
97
- streamFactory?: () => AsyncIterable<Uint8Array>;
93
+ path: string;
94
+ contentId: string;
95
+ mode?: string;
96
+ operation: 'upsert' | 'delete';
97
+ streamFactory?: () => AsyncIterable<Uint8Array>;
98
98
  };
99
99
 
100
100
  export class CommitBuilderImpl implements CommitBuilder {
101
- private readonly options: NormalizedCommitOptions;
102
- private readonly getAuthToken: () => Promise<string>;
103
- private readonly transport: CommitTransport;
104
- private readonly operations: FileOperationState[] = [];
105
- private sent = false;
106
-
107
- constructor(deps: CommitBuilderDeps) {
108
- this.options = normalizeCommitOptions(deps.options);
109
- this.getAuthToken = deps.getAuthToken;
110
- this.transport = deps.transport;
111
-
112
- const trimmedMessage = this.options.commitMessage?.trim();
113
- const trimmedAuthorName = this.options.author?.name?.trim();
114
- const trimmedAuthorEmail = this.options.author?.email?.trim();
115
-
116
- if (!trimmedMessage) {
117
- throw new Error('createCommit commitMessage is required');
118
- }
119
- if (!trimmedAuthorName || !trimmedAuthorEmail) {
120
- throw new Error('createCommit author name and email are required');
121
- }
122
- this.options.commitMessage = trimmedMessage;
123
- this.options.author = {
124
- name: trimmedAuthorName,
125
- email: trimmedAuthorEmail,
126
- };
127
- if (typeof this.options.expectedHeadSha === 'string') {
128
- this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
129
- }
130
- if (typeof this.options.baseBranch === 'string') {
131
- const trimmedBase = this.options.baseBranch.trim();
132
- if (trimmedBase === '') {
133
- delete this.options.baseBranch;
134
- } else {
135
- if (trimmedBase.startsWith('refs/')) {
136
- throw new Error('createCommit baseBranch must not include refs/ prefix');
137
- }
138
- this.options.baseBranch = trimmedBase;
139
- }
140
- }
141
-
142
- if (this.options.ephemeralBase && !this.options.baseBranch) {
143
- throw new Error('createCommit ephemeralBase requires baseBranch');
144
- }
145
- }
146
-
147
- addFile(path: string, source: CommitFileSource, options?: CommitFileOptions): CommitBuilder {
148
- this.ensureNotSent();
149
- const normalizedPath = this.normalizePath(path);
150
- const contentId = randomContentId();
151
- const mode = options?.mode ?? '100644';
152
-
153
- this.operations.push({
154
- path: normalizedPath,
155
- contentId,
156
- mode,
157
- operation: 'upsert',
158
- streamFactory: () => toAsyncIterable(source),
159
- });
160
-
161
- return this;
162
- }
163
-
164
- addFileFromString(
165
- path: string,
166
- contents: string,
167
- options?: CommitTextFileOptions,
168
- ): CommitBuilder {
169
- const encoding = options?.encoding ?? 'utf8';
170
- const normalizedEncoding = encoding === 'utf-8' ? 'utf8' : encoding;
171
- let data: Uint8Array;
172
- if (normalizedEncoding === 'utf8') {
173
- data = new TextEncoder().encode(contents);
174
- } else if (BufferCtor) {
175
- data = BufferCtor.from(
176
- contents,
177
- normalizedEncoding as Parameters<NodeBufferConstructor['from']>[1],
178
- );
179
- } else {
180
- throw new Error(
181
- `Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`,
182
- );
183
- }
184
- return this.addFile(path, data, options);
185
- }
186
-
187
- deletePath(path: string): CommitBuilder {
188
- this.ensureNotSent();
189
- const normalizedPath = this.normalizePath(path);
190
- this.operations.push({
191
- path: normalizedPath,
192
- contentId: randomContentId(),
193
- operation: 'delete',
194
- });
195
- return this;
196
- }
197
-
198
- async send(): Promise<CommitResult> {
199
- this.ensureNotSent();
200
- this.sent = true;
201
-
202
- const metadata = this.buildMetadata();
203
- const blobEntries = this.operations
204
- .filter((op) => op.operation === 'upsert' && op.streamFactory)
205
- .map((op) => ({
206
- contentId: op.contentId,
207
- chunks: chunkify(op.streamFactory!()),
208
- }));
209
-
210
- const authorization = await this.getAuthToken();
211
- const ack = await this.transport.send({
212
- authorization,
213
- signal: this.options.signal,
214
- metadata,
215
- blobs: blobEntries,
216
- });
217
- return buildCommitResult(ack);
218
- }
219
-
220
- private buildMetadata(): CommitMetadataPayload {
221
- const files = this.operations.map((op) => {
222
- const entry: CommitMetadataPayload['files'][number] = {
223
- path: op.path,
224
- content_id: op.contentId,
225
- operation: op.operation,
226
- };
227
- if (op.mode) {
228
- entry.mode = op.mode;
229
- }
230
- return entry;
231
- });
232
-
233
- const metadata: CommitMetadataPayload = {
234
- target_branch: this.options.targetBranch,
235
- commit_message: this.options.commitMessage,
236
- author: {
237
- name: this.options.author.name,
238
- email: this.options.author.email,
239
- },
240
- files,
241
- };
242
-
243
- if (this.options.expectedHeadSha) {
244
- metadata.expected_head_sha = this.options.expectedHeadSha;
245
- }
246
- if (this.options.baseBranch) {
247
- metadata.base_branch = this.options.baseBranch;
248
- }
249
- if (this.options.committer) {
250
- metadata.committer = {
251
- name: this.options.committer.name,
252
- email: this.options.committer.email,
253
- };
254
- }
255
-
256
- if (this.options.ephemeral) {
257
- metadata.ephemeral = true;
258
- }
259
- if (this.options.ephemeralBase) {
260
- metadata.ephemeral_base = true;
261
- }
262
-
263
- return metadata;
264
- }
265
-
266
- private ensureNotSent(): void {
267
- if (this.sent) {
268
- throw new Error('createCommit builder cannot be reused after send()');
269
- }
270
- }
271
-
272
- private normalizePath(path: string): string {
273
- if (!path || typeof path !== 'string' || path.trim() === '') {
274
- throw new Error('File path must be a non-empty string');
275
- }
276
- return path.replace(/^\//, '');
277
- }
101
+ private readonly options: NormalizedCommitOptions;
102
+ private readonly getAuthToken: () => Promise<string>;
103
+ private readonly transport: CommitTransport;
104
+ private readonly operations: FileOperationState[] = [];
105
+ private sent = false;
106
+
107
+ constructor(deps: CommitBuilderDeps) {
108
+ this.options = normalizeCommitOptions(deps.options);
109
+ this.getAuthToken = deps.getAuthToken;
110
+ this.transport = deps.transport;
111
+
112
+ const trimmedMessage = this.options.commitMessage?.trim();
113
+ const trimmedAuthorName = this.options.author?.name?.trim();
114
+ const trimmedAuthorEmail = this.options.author?.email?.trim();
115
+
116
+ if (!trimmedMessage) {
117
+ throw new Error('createCommit commitMessage is required');
118
+ }
119
+ if (!trimmedAuthorName || !trimmedAuthorEmail) {
120
+ throw new Error('createCommit author name and email are required');
121
+ }
122
+ this.options.commitMessage = trimmedMessage;
123
+ this.options.author = {
124
+ name: trimmedAuthorName,
125
+ email: trimmedAuthorEmail,
126
+ };
127
+ if (typeof this.options.expectedHeadSha === 'string') {
128
+ this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
129
+ }
130
+ if (typeof this.options.baseBranch === 'string') {
131
+ const trimmedBase = this.options.baseBranch.trim();
132
+ if (trimmedBase === '') {
133
+ delete this.options.baseBranch;
134
+ } else {
135
+ if (trimmedBase.startsWith('refs/')) {
136
+ throw new Error(
137
+ 'createCommit baseBranch must not include refs/ prefix'
138
+ );
139
+ }
140
+ this.options.baseBranch = trimmedBase;
141
+ }
142
+ }
143
+
144
+ if (this.options.ephemeralBase && !this.options.baseBranch) {
145
+ throw new Error('createCommit ephemeralBase requires baseBranch');
146
+ }
147
+ }
148
+
149
+ addFile(
150
+ path: string,
151
+ source: CommitFileSource,
152
+ options?: CommitFileOptions
153
+ ): CommitBuilder {
154
+ this.ensureNotSent();
155
+ const normalizedPath = this.normalizePath(path);
156
+ const contentId = randomContentId();
157
+ const mode = options?.mode ?? '100644';
158
+
159
+ this.operations.push({
160
+ path: normalizedPath,
161
+ contentId,
162
+ mode,
163
+ operation: 'upsert',
164
+ streamFactory: () => toAsyncIterable(source),
165
+ });
166
+
167
+ return this;
168
+ }
169
+
170
+ addFileFromString(
171
+ path: string,
172
+ contents: string,
173
+ options?: CommitTextFileOptions
174
+ ): CommitBuilder {
175
+ const encoding = options?.encoding ?? 'utf8';
176
+ const normalizedEncoding = encoding === 'utf-8' ? 'utf8' : encoding;
177
+ let data: Uint8Array;
178
+ if (normalizedEncoding === 'utf8') {
179
+ data = new TextEncoder().encode(contents);
180
+ } else if (BufferCtor) {
181
+ data = BufferCtor.from(
182
+ contents,
183
+ normalizedEncoding as Parameters<NodeBufferConstructor['from']>[1]
184
+ );
185
+ } else {
186
+ throw new Error(
187
+ `Unsupported encoding "${encoding}" in this environment. Non-UTF encodings require Node.js Buffer support.`
188
+ );
189
+ }
190
+ return this.addFile(path, data, options);
191
+ }
192
+
193
+ deletePath(path: string): CommitBuilder {
194
+ this.ensureNotSent();
195
+ const normalizedPath = this.normalizePath(path);
196
+ this.operations.push({
197
+ path: normalizedPath,
198
+ contentId: randomContentId(),
199
+ operation: 'delete',
200
+ });
201
+ return this;
202
+ }
203
+
204
+ async send(): Promise<CommitResult> {
205
+ this.ensureNotSent();
206
+ this.sent = true;
207
+
208
+ const metadata = this.buildMetadata();
209
+ const blobEntries = this.operations
210
+ .filter((op) => op.operation === 'upsert' && op.streamFactory)
211
+ .map((op) => ({
212
+ contentId: op.contentId,
213
+ chunks: chunkify(op.streamFactory!()),
214
+ }));
215
+
216
+ const authorization = await this.getAuthToken();
217
+ const ack = await this.transport.send({
218
+ authorization,
219
+ signal: this.options.signal,
220
+ metadata,
221
+ blobs: blobEntries,
222
+ });
223
+ return buildCommitResult(ack);
224
+ }
225
+
226
+ private buildMetadata(): CommitMetadataPayload {
227
+ const files = this.operations.map((op) => {
228
+ const entry: CommitMetadataPayload['files'][number] = {
229
+ path: op.path,
230
+ content_id: op.contentId,
231
+ operation: op.operation,
232
+ };
233
+ if (op.mode) {
234
+ entry.mode = op.mode;
235
+ }
236
+ return entry;
237
+ });
238
+
239
+ const metadata: CommitMetadataPayload = {
240
+ target_branch: this.options.targetBranch,
241
+ commit_message: this.options.commitMessage,
242
+ author: {
243
+ name: this.options.author.name,
244
+ email: this.options.author.email,
245
+ },
246
+ files,
247
+ };
248
+
249
+ if (this.options.expectedHeadSha) {
250
+ metadata.expected_head_sha = this.options.expectedHeadSha;
251
+ }
252
+ if (this.options.baseBranch) {
253
+ metadata.base_branch = this.options.baseBranch;
254
+ }
255
+ if (this.options.committer) {
256
+ metadata.committer = {
257
+ name: this.options.committer.name,
258
+ email: this.options.committer.email,
259
+ };
260
+ }
261
+
262
+ if (this.options.ephemeral) {
263
+ metadata.ephemeral = true;
264
+ }
265
+ if (this.options.ephemeralBase) {
266
+ metadata.ephemeral_base = true;
267
+ }
268
+
269
+ return metadata;
270
+ }
271
+
272
+ private ensureNotSent(): void {
273
+ if (this.sent) {
274
+ throw new Error('createCommit builder cannot be reused after send()');
275
+ }
276
+ }
277
+
278
+ private normalizePath(path: string): string {
279
+ if (!path || typeof path !== 'string' || path.trim() === '') {
280
+ throw new Error('File path must be a non-empty string');
281
+ }
282
+ return path.replace(/^\//, '');
283
+ }
278
284
  }
279
285
 
280
286
  export class FetchCommitTransport implements CommitTransport {
281
- private readonly url: string;
282
-
283
- constructor(config: { baseUrl: string; version: number }) {
284
- const trimmedBase = config.baseUrl.replace(/\/+$/, '');
285
- this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
286
- }
287
-
288
- async send(request: CommitTransportRequest): Promise<CommitPackAckRaw> {
289
- const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
290
- const body = toRequestBody(bodyIterable);
291
-
292
- const init: RequestInit = {
293
- method: 'POST',
294
- headers: {
295
- Authorization: `Bearer ${request.authorization}`,
296
- 'Content-Type': 'application/x-ndjson',
297
- Accept: 'application/json',
298
- 'Code-Storage-Agent': getUserAgent(),
299
- },
300
- body: body as any,
301
- signal: request.signal,
302
- };
303
-
304
- if (requiresDuplex(body)) {
305
- (init as RequestInit & { duplex: 'half' }).duplex = 'half';
306
- }
307
-
308
- const response = await fetch(this.url, init);
309
-
310
- if (!response.ok) {
311
- const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
312
- const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
313
- response,
314
- fallbackMessage,
315
- );
316
- throw new RefUpdateError(statusMessage, {
317
- status: statusLabel,
318
- message: statusMessage,
319
- refUpdate,
320
- });
321
- }
322
-
323
- const ack = commitPackAckSchema.parse(await response.json());
324
- return ack;
325
- }
287
+ private readonly url: string;
288
+
289
+ constructor(config: { baseUrl: string; version: number }) {
290
+ const trimmedBase = config.baseUrl.replace(/\/+$/, '');
291
+ this.url = `${trimmedBase}/api/v${config.version}/repos/commit-pack`;
292
+ }
293
+
294
+ async send(request: CommitTransportRequest): Promise<CommitPackAckRaw> {
295
+ const bodyIterable = buildMessageIterable(request.metadata, request.blobs);
296
+ const body = toRequestBody(bodyIterable);
297
+
298
+ const init: RequestInit = {
299
+ method: 'POST',
300
+ headers: {
301
+ Authorization: `Bearer ${request.authorization}`,
302
+ 'Content-Type': 'application/x-ndjson',
303
+ Accept: 'application/json',
304
+ 'Code-Storage-Agent': getUserAgent(),
305
+ },
306
+ body: body as any,
307
+ signal: request.signal,
308
+ };
309
+
310
+ if (requiresDuplex(body)) {
311
+ (init as RequestInit & { duplex: 'half' }).duplex = 'half';
312
+ }
313
+
314
+ const response = await fetch(this.url, init);
315
+
316
+ if (!response.ok) {
317
+ const fallbackMessage = `createCommit request failed (${response.status} ${response.statusText})`;
318
+ const { statusMessage, statusLabel, refUpdate } =
319
+ await parseCommitPackError(response, fallbackMessage);
320
+ throw new RefUpdateError(statusMessage, {
321
+ status: statusLabel,
322
+ message: statusMessage,
323
+ refUpdate,
324
+ });
325
+ }
326
+
327
+ const ack = commitPackAckSchema.parse(await response.json());
328
+ return ack;
329
+ }
326
330
  }
327
331
 
328
332
  function buildMessageIterable(
329
- metadata: CommitMetadataPayload,
330
- blobs: Array<{ contentId: string; chunks: AsyncIterable<ChunkSegment> }>,
333
+ metadata: CommitMetadataPayload,
334
+ blobs: Array<{ contentId: string; chunks: AsyncIterable<ChunkSegment> }>
331
335
  ): AsyncIterable<Uint8Array> {
332
- const encoder = new TextEncoder();
333
- return {
334
- async *[Symbol.asyncIterator]() {
335
- yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
336
- for (const blob of blobs) {
337
- for await (const segment of blob.chunks) {
338
- const payload = {
339
- blob_chunk: {
340
- content_id: blob.contentId,
341
- data: base64Encode(segment.chunk),
342
- eof: segment.eof,
343
- },
344
- };
345
- yield encoder.encode(`${JSON.stringify(payload)}\n`);
346
- }
347
- }
348
- },
349
- };
336
+ const encoder = new TextEncoder();
337
+ return {
338
+ async *[Symbol.asyncIterator]() {
339
+ yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
340
+ for (const blob of blobs) {
341
+ for await (const segment of blob.chunks) {
342
+ const payload = {
343
+ blob_chunk: {
344
+ content_id: blob.contentId,
345
+ data: base64Encode(segment.chunk),
346
+ eof: segment.eof,
347
+ },
348
+ };
349
+ yield encoder.encode(`${JSON.stringify(payload)}\n`);
350
+ }
351
+ }
352
+ },
353
+ };
350
354
  }
351
355
 
352
356
  function randomContentId(): string {
353
- const cryptoObj = globalThis.crypto;
354
- if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
355
- return cryptoObj.randomUUID();
356
- }
357
- const random = Math.random().toString(36).slice(2);
358
- return `cid-${Date.now().toString(36)}-${random}`;
357
+ const cryptoObj = globalThis.crypto;
358
+ if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
359
+ return cryptoObj.randomUUID();
360
+ }
361
+ const random = Math.random().toString(36).slice(2);
362
+ return `cid-${Date.now().toString(36)}-${random}`;
359
363
  }
360
364
 
361
- function normalizeCommitOptions(options: CreateCommitOptions): NormalizedCommitOptions {
362
- return {
363
- targetBranch: resolveTargetBranch(options),
364
- commitMessage: options.commitMessage,
365
- expectedHeadSha: options.expectedHeadSha,
366
- baseBranch: options.baseBranch,
367
- ephemeral: options.ephemeral === true,
368
- ephemeralBase: options.ephemeralBase === true,
369
- author: options.author,
370
- committer: options.committer,
371
- signal: options.signal,
372
- ttl: options.ttl,
373
- };
365
+ function normalizeCommitOptions(
366
+ options: CreateCommitOptions
367
+ ): NormalizedCommitOptions {
368
+ return {
369
+ targetBranch: resolveTargetBranch(options),
370
+ commitMessage: options.commitMessage,
371
+ expectedHeadSha: options.expectedHeadSha,
372
+ baseBranch: options.baseBranch,
373
+ ephemeral: options.ephemeral === true,
374
+ ephemeralBase: options.ephemeralBase === true,
375
+ author: options.author,
376
+ committer: options.committer,
377
+ signal: options.signal,
378
+ ttl: options.ttl,
379
+ };
374
380
  }
375
381
 
376
382
  function resolveTargetBranch(options: CreateCommitOptions): string {
377
- const branchCandidate =
378
- typeof options.targetBranch === 'string' ? options.targetBranch.trim() : '';
379
- if (branchCandidate) {
380
- return normalizeBranchName(branchCandidate);
381
- }
382
- if (hasLegacyTargetRef(options)) {
383
- return normalizeLegacyTargetRef(options.targetRef);
384
- }
385
- throw new Error('createCommit targetBranch is required');
383
+ const branchCandidate =
384
+ typeof options.targetBranch === 'string' ? options.targetBranch.trim() : '';
385
+ if (branchCandidate) {
386
+ return normalizeBranchName(branchCandidate);
387
+ }
388
+ if (hasLegacyTargetRef(options)) {
389
+ return normalizeLegacyTargetRef(options.targetRef);
390
+ }
391
+ throw new Error('createCommit targetBranch is required');
386
392
  }
387
393
 
388
394
  function normalizeBranchName(value: string): string {
389
- const trimmed = value.trim();
390
- if (!trimmed) {
391
- throw new Error('createCommit targetBranch is required');
392
- }
393
- if (trimmed.startsWith(HEADS_REF_PREFIX)) {
394
- const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
395
- if (!branch) {
396
- throw new Error('createCommit targetBranch is required');
397
- }
398
- return branch;
399
- }
400
- if (trimmed.startsWith('refs/')) {
401
- throw new Error('createCommit targetBranch must not include refs/ prefix');
402
- }
403
- return trimmed;
395
+ const trimmed = value.trim();
396
+ if (!trimmed) {
397
+ throw new Error('createCommit targetBranch is required');
398
+ }
399
+ if (trimmed.startsWith(HEADS_REF_PREFIX)) {
400
+ const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
401
+ if (!branch) {
402
+ throw new Error('createCommit targetBranch is required');
403
+ }
404
+ return branch;
405
+ }
406
+ if (trimmed.startsWith('refs/')) {
407
+ throw new Error('createCommit targetBranch must not include refs/ prefix');
408
+ }
409
+ return trimmed;
404
410
  }
405
411
 
406
412
  function normalizeLegacyTargetRef(ref: string): string {
407
- const trimmed = ref.trim();
408
- if (!trimmed) {
409
- throw new Error('createCommit targetRef is required');
410
- }
411
- if (!trimmed.startsWith(HEADS_REF_PREFIX)) {
412
- throw new Error('createCommit targetRef must start with refs/heads/');
413
- }
414
- const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
415
- if (!branch) {
416
- throw new Error('createCommit targetRef must include a branch name');
417
- }
418
- return branch;
413
+ const trimmed = ref.trim();
414
+ if (!trimmed) {
415
+ throw new Error('createCommit targetRef is required');
416
+ }
417
+ if (!trimmed.startsWith(HEADS_REF_PREFIX)) {
418
+ throw new Error('createCommit targetRef must start with refs/heads/');
419
+ }
420
+ const branch = trimmed.slice(HEADS_REF_PREFIX.length).trim();
421
+ if (!branch) {
422
+ throw new Error('createCommit targetRef must include a branch name');
423
+ }
424
+ return branch;
419
425
  }
420
426
 
421
- function hasLegacyTargetRef(options: CreateCommitOptions): options is LegacyCreateCommitOptions {
422
- return typeof (options as LegacyCreateCommitOptions).targetRef === 'string';
427
+ function hasLegacyTargetRef(
428
+ options: CreateCommitOptions
429
+ ): options is LegacyCreateCommitOptions {
430
+ return typeof (options as LegacyCreateCommitOptions).targetRef === 'string';
423
431
  }
424
432
 
425
433
  export function createCommitBuilder(deps: CommitBuilderDeps): CommitBuilder {
426
- return new CommitBuilderImpl(deps);
434
+ return new CommitBuilderImpl(deps);
427
435
  }
428
436
 
429
437
  export function resolveCommitTtlSeconds(options?: { ttl?: number }): number {
430
- if (typeof options?.ttl === 'number' && options.ttl > 0) {
431
- return options.ttl;
432
- }
433
- return DEFAULT_TTL_SECONDS;
438
+ if (typeof options?.ttl === 'number' && options.ttl > 0) {
439
+ return options.ttl;
440
+ }
441
+ return DEFAULT_TTL_SECONDS;
434
442
  }