@pierre/storage 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/index.cjs +576 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +576 -323
- package/dist/index.js.map +1 -1
- package/package.json +38 -39
- package/src/commit-pack.ts +128 -0
- package/src/commit.ts +35 -360
- package/src/diff-commit.ts +300 -0
- package/src/index.ts +39 -4
- package/src/stream-utils.ts +255 -0
- package/src/types.ts +20 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { buildCommitResult, parseCommitPackError } from './commit-pack';
|
|
2
|
+
import { RefUpdateError } from './errors';
|
|
3
|
+
import type { CommitPackAckRaw } from './schemas';
|
|
4
|
+
import { commitPackAckSchema } from './schemas';
|
|
5
|
+
import {
|
|
6
|
+
base64Encode,
|
|
7
|
+
type ChunkSegment,
|
|
8
|
+
chunkify,
|
|
9
|
+
requiresDuplex,
|
|
10
|
+
toAsyncIterable,
|
|
11
|
+
toRequestBody,
|
|
12
|
+
} from './stream-utils';
|
|
13
|
+
import type {
|
|
14
|
+
CommitResult,
|
|
15
|
+
CommitSignature,
|
|
16
|
+
CreateCommitFromDiffOptions,
|
|
17
|
+
DiffSource,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
interface DiffCommitMetadataPayload {
|
|
21
|
+
target_branch: string;
|
|
22
|
+
expected_head_sha?: string;
|
|
23
|
+
base_branch?: string;
|
|
24
|
+
commit_message: string;
|
|
25
|
+
ephemeral?: boolean;
|
|
26
|
+
ephemeral_base?: boolean;
|
|
27
|
+
author: {
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
};
|
|
31
|
+
committer?: {
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DiffCommitTransportRequest {
|
|
38
|
+
authorization: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
metadata: DiffCommitMetadataPayload;
|
|
41
|
+
diffChunks: AsyncIterable<ChunkSegment>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DiffCommitTransport {
|
|
45
|
+
send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type NormalizedDiffCommitOptions = {
|
|
49
|
+
targetBranch: string;
|
|
50
|
+
commitMessage: string;
|
|
51
|
+
expectedHeadSha?: string;
|
|
52
|
+
baseBranch?: string;
|
|
53
|
+
ephemeral?: boolean;
|
|
54
|
+
ephemeralBase?: boolean;
|
|
55
|
+
author: CommitSignature;
|
|
56
|
+
committer?: CommitSignature;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
ttl?: number;
|
|
59
|
+
initialDiff: DiffSource;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
interface CommitFromDiffSendDeps {
|
|
63
|
+
options: CreateCommitFromDiffOptions;
|
|
64
|
+
getAuthToken: () => Promise<string>;
|
|
65
|
+
transport: DiffCommitTransport;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class DiffCommitExecutor {
|
|
69
|
+
private readonly options: NormalizedDiffCommitOptions;
|
|
70
|
+
private readonly getAuthToken: () => Promise<string>;
|
|
71
|
+
private readonly transport: DiffCommitTransport;
|
|
72
|
+
private readonly diffFactory: () => AsyncIterable<Uint8Array>;
|
|
73
|
+
private sent = false;
|
|
74
|
+
|
|
75
|
+
constructor(deps: CommitFromDiffSendDeps) {
|
|
76
|
+
this.options = normalizeDiffCommitOptions(deps.options);
|
|
77
|
+
this.getAuthToken = deps.getAuthToken;
|
|
78
|
+
this.transport = deps.transport;
|
|
79
|
+
|
|
80
|
+
const trimmedMessage = this.options.commitMessage?.trim();
|
|
81
|
+
const trimmedAuthorName = this.options.author?.name?.trim();
|
|
82
|
+
const trimmedAuthorEmail = this.options.author?.email?.trim();
|
|
83
|
+
|
|
84
|
+
if (!trimmedMessage) {
|
|
85
|
+
throw new Error('createCommitFromDiff commitMessage is required');
|
|
86
|
+
}
|
|
87
|
+
if (!trimmedAuthorName || !trimmedAuthorEmail) {
|
|
88
|
+
throw new Error('createCommitFromDiff author name and email are required');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.options.commitMessage = trimmedMessage;
|
|
92
|
+
this.options.author = {
|
|
93
|
+
name: trimmedAuthorName,
|
|
94
|
+
email: trimmedAuthorEmail,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (typeof this.options.expectedHeadSha === 'string') {
|
|
98
|
+
this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
|
|
99
|
+
}
|
|
100
|
+
if (typeof this.options.baseBranch === 'string') {
|
|
101
|
+
const trimmedBase = this.options.baseBranch.trim();
|
|
102
|
+
if (trimmedBase === '') {
|
|
103
|
+
delete this.options.baseBranch;
|
|
104
|
+
} else {
|
|
105
|
+
if (trimmedBase.startsWith('refs/')) {
|
|
106
|
+
throw new Error('createCommitFromDiff baseBranch must not include refs/ prefix');
|
|
107
|
+
}
|
|
108
|
+
this.options.baseBranch = trimmedBase;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (this.options.ephemeralBase && !this.options.baseBranch) {
|
|
112
|
+
throw new Error('createCommitFromDiff ephemeralBase requires baseBranch');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.diffFactory = () => toAsyncIterable(this.options.initialDiff);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async send(): Promise<CommitResult> {
|
|
119
|
+
this.ensureNotSent();
|
|
120
|
+
this.sent = true;
|
|
121
|
+
|
|
122
|
+
const metadata = this.buildMetadata();
|
|
123
|
+
const diffIterable = chunkify(this.diffFactory());
|
|
124
|
+
|
|
125
|
+
const authorization = await this.getAuthToken();
|
|
126
|
+
const ack = await this.transport.send({
|
|
127
|
+
authorization,
|
|
128
|
+
signal: this.options.signal,
|
|
129
|
+
metadata,
|
|
130
|
+
diffChunks: diffIterable,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return buildCommitResult(ack);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private buildMetadata(): DiffCommitMetadataPayload {
|
|
137
|
+
const metadata: DiffCommitMetadataPayload = {
|
|
138
|
+
target_branch: this.options.targetBranch,
|
|
139
|
+
commit_message: this.options.commitMessage,
|
|
140
|
+
author: {
|
|
141
|
+
name: this.options.author.name,
|
|
142
|
+
email: this.options.author.email,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (this.options.expectedHeadSha) {
|
|
147
|
+
metadata.expected_head_sha = this.options.expectedHeadSha;
|
|
148
|
+
}
|
|
149
|
+
if (this.options.baseBranch) {
|
|
150
|
+
metadata.base_branch = this.options.baseBranch;
|
|
151
|
+
}
|
|
152
|
+
if (this.options.committer) {
|
|
153
|
+
metadata.committer = {
|
|
154
|
+
name: this.options.committer.name,
|
|
155
|
+
email: this.options.committer.email,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (this.options.ephemeral) {
|
|
159
|
+
metadata.ephemeral = true;
|
|
160
|
+
}
|
|
161
|
+
if (this.options.ephemeralBase) {
|
|
162
|
+
metadata.ephemeral_base = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return metadata;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private ensureNotSent(): void {
|
|
169
|
+
if (this.sent) {
|
|
170
|
+
throw new Error('createCommitFromDiff cannot be reused after send()');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class FetchDiffCommitTransport implements DiffCommitTransport {
|
|
176
|
+
private readonly url: string;
|
|
177
|
+
|
|
178
|
+
constructor(config: { baseUrl: string; version: number }) {
|
|
179
|
+
const trimmedBase = config.baseUrl.replace(/\/+$/, '');
|
|
180
|
+
this.url = `${trimmedBase}/api/v${config.version}/repos/diff-commit`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw> {
|
|
184
|
+
const bodyIterable = buildMessageIterable(request.metadata, request.diffChunks);
|
|
185
|
+
const body = toRequestBody(bodyIterable);
|
|
186
|
+
|
|
187
|
+
const init: RequestInit = {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Bearer ${request.authorization}`,
|
|
191
|
+
'Content-Type': 'application/x-ndjson',
|
|
192
|
+
Accept: 'application/json',
|
|
193
|
+
},
|
|
194
|
+
body: body as any,
|
|
195
|
+
signal: request.signal,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (requiresDuplex(body)) {
|
|
199
|
+
(init as RequestInit & { duplex: 'half' }).duplex = 'half';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const response = await fetch(this.url, init);
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
const fallbackMessage = `createCommitFromDiff request failed (${response.status} ${response.statusText})`;
|
|
205
|
+
const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
|
|
206
|
+
response,
|
|
207
|
+
fallbackMessage,
|
|
208
|
+
);
|
|
209
|
+
throw new RefUpdateError(statusMessage, {
|
|
210
|
+
status: statusLabel,
|
|
211
|
+
message: statusMessage,
|
|
212
|
+
refUpdate,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return commitPackAckSchema.parse(await response.json());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildMessageIterable(
|
|
221
|
+
metadata: DiffCommitMetadataPayload,
|
|
222
|
+
diffChunks: AsyncIterable<ChunkSegment>,
|
|
223
|
+
): AsyncIterable<Uint8Array> {
|
|
224
|
+
const encoder = new TextEncoder();
|
|
225
|
+
return {
|
|
226
|
+
async *[Symbol.asyncIterator]() {
|
|
227
|
+
yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
|
|
228
|
+
for await (const segment of diffChunks) {
|
|
229
|
+
const payload = {
|
|
230
|
+
diff_chunk: {
|
|
231
|
+
data: base64Encode(segment.chunk),
|
|
232
|
+
eof: segment.eof,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
yield encoder.encode(`${JSON.stringify(payload)}\n`);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeDiffCommitOptions(
|
|
242
|
+
options: CreateCommitFromDiffOptions,
|
|
243
|
+
): NormalizedDiffCommitOptions {
|
|
244
|
+
if (!options || typeof options !== 'object') {
|
|
245
|
+
throw new Error('createCommitFromDiff options are required');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (options.diff === undefined || options.diff === null) {
|
|
249
|
+
throw new Error('createCommitFromDiff diff is required');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const targetBranch = normalizeBranchName(options.targetBranch);
|
|
253
|
+
|
|
254
|
+
let committer: CommitSignature | undefined;
|
|
255
|
+
if (options.committer) {
|
|
256
|
+
const name = options.committer.name?.trim();
|
|
257
|
+
const email = options.committer.email?.trim();
|
|
258
|
+
if (!name || !email) {
|
|
259
|
+
throw new Error('createCommitFromDiff committer name and email are required when provided');
|
|
260
|
+
}
|
|
261
|
+
committer = { name, email };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
targetBranch,
|
|
266
|
+
commitMessage: options.commitMessage,
|
|
267
|
+
expectedHeadSha: options.expectedHeadSha,
|
|
268
|
+
baseBranch: options.baseBranch,
|
|
269
|
+
ephemeral: options.ephemeral === true,
|
|
270
|
+
ephemeralBase: options.ephemeralBase === true,
|
|
271
|
+
author: options.author,
|
|
272
|
+
committer,
|
|
273
|
+
signal: options.signal,
|
|
274
|
+
ttl: options.ttl,
|
|
275
|
+
initialDiff: options.diff,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeBranchName(value: string | undefined): string {
|
|
280
|
+
const trimmed = value?.trim();
|
|
281
|
+
if (!trimmed) {
|
|
282
|
+
throw new Error('createCommitFromDiff targetBranch is required');
|
|
283
|
+
}
|
|
284
|
+
if (trimmed.startsWith('refs/heads/')) {
|
|
285
|
+
const branch = trimmed.slice('refs/heads/'.length).trim();
|
|
286
|
+
if (!branch) {
|
|
287
|
+
throw new Error('createCommitFromDiff targetBranch must include a branch name');
|
|
288
|
+
}
|
|
289
|
+
return branch;
|
|
290
|
+
}
|
|
291
|
+
if (trimmed.startsWith('refs/')) {
|
|
292
|
+
throw new Error('createCommitFromDiff targetBranch must not include refs/ prefix');
|
|
293
|
+
}
|
|
294
|
+
return trimmed;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function sendCommitFromDiff(deps: CommitFromDiffSendDeps): Promise<CommitResult> {
|
|
298
|
+
const executor = new DiffCommitExecutor(deps);
|
|
299
|
+
return executor.send();
|
|
300
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { importPKCS8, SignJWT } from 'jose';
|
|
8
8
|
import snakecaseKeys from 'snakecase-keys';
|
|
9
9
|
import { createCommitBuilder, FetchCommitTransport, resolveCommitTtlSeconds } from './commit';
|
|
10
|
+
import { FetchDiffCommitTransport, sendCommitFromDiff } from './diff-commit';
|
|
10
11
|
import { RefUpdateError } from './errors';
|
|
11
12
|
import { ApiFetcher } from './fetch';
|
|
12
13
|
import type { RestoreCommitAckRaw } from './schemas';
|
|
@@ -23,6 +24,8 @@ import type {
|
|
|
23
24
|
BranchInfo,
|
|
24
25
|
CommitBuilder,
|
|
25
26
|
CommitInfo,
|
|
27
|
+
CommitResult,
|
|
28
|
+
CreateCommitFromDiffOptions,
|
|
26
29
|
CreateCommitOptions,
|
|
27
30
|
CreateRepoOptions,
|
|
28
31
|
DiffFileState,
|
|
@@ -357,6 +360,9 @@ class RepoImpl implements Repo {
|
|
|
357
360
|
if (options.ref) {
|
|
358
361
|
params.ref = options.ref;
|
|
359
362
|
}
|
|
363
|
+
if (typeof options.ephemeral === 'boolean') {
|
|
364
|
+
params.ephemeral = String(options.ephemeral);
|
|
365
|
+
}
|
|
360
366
|
|
|
361
367
|
// Return the raw fetch Response for streaming
|
|
362
368
|
return this.api.get({ path: 'repos/file', params }, jwt);
|
|
@@ -369,10 +375,17 @@ class RepoImpl implements Repo {
|
|
|
369
375
|
ttl,
|
|
370
376
|
});
|
|
371
377
|
|
|
372
|
-
const params: Record<string, string>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
const params: Record<string, string> = {};
|
|
379
|
+
if (options?.ref) {
|
|
380
|
+
params.ref = options.ref;
|
|
381
|
+
}
|
|
382
|
+
if (typeof options?.ephemeral === 'boolean') {
|
|
383
|
+
params.ephemeral = String(options.ephemeral);
|
|
384
|
+
}
|
|
385
|
+
const response = await this.api.get(
|
|
386
|
+
{ path: 'repos/files', params: Object.keys(params).length ? params : undefined },
|
|
387
|
+
jwt,
|
|
388
|
+
);
|
|
376
389
|
|
|
377
390
|
const raw = listFilesResponseSchema.parse(await response.json());
|
|
378
391
|
return { paths: raw.paths, ref: raw.ref };
|
|
@@ -605,6 +618,28 @@ class RepoImpl implements Repo {
|
|
|
605
618
|
transport,
|
|
606
619
|
});
|
|
607
620
|
}
|
|
621
|
+
|
|
622
|
+
async createCommitFromDiff(options: CreateCommitFromDiffOptions): Promise<CommitResult> {
|
|
623
|
+
const version = this.options.apiVersion ?? API_VERSION;
|
|
624
|
+
const baseUrl = this.options.apiBaseUrl ?? API_BASE_URL;
|
|
625
|
+
const transport = new FetchDiffCommitTransport({ baseUrl, version });
|
|
626
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
627
|
+
const requestOptions: CreateCommitFromDiffOptions = {
|
|
628
|
+
...options,
|
|
629
|
+
ttl,
|
|
630
|
+
};
|
|
631
|
+
const getAuthToken = () =>
|
|
632
|
+
this.generateJWT(this.id, {
|
|
633
|
+
permissions: ['git:write'],
|
|
634
|
+
ttl,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return sendCommitFromDiff({
|
|
638
|
+
options: requestOptions,
|
|
639
|
+
getAuthToken,
|
|
640
|
+
transport,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
608
643
|
}
|
|
609
644
|
|
|
610
645
|
export class GitStorage {
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { BlobLike, FileLike, ReadableStreamLike } from './types';
|
|
2
|
+
|
|
3
|
+
type NodeBuffer = Uint8Array & { toString(encoding?: string): string };
|
|
4
|
+
interface NodeBufferConstructor {
|
|
5
|
+
from(data: Uint8Array): NodeBuffer;
|
|
6
|
+
from(data: string, encoding?: string): NodeBuffer;
|
|
7
|
+
isBuffer(value: unknown): value is NodeBuffer;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const BufferCtor: NodeBufferConstructor | undefined = (
|
|
11
|
+
globalThis as { Buffer?: NodeBufferConstructor }
|
|
12
|
+
).Buffer;
|
|
13
|
+
|
|
14
|
+
export const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export type ChunkSegment = {
|
|
17
|
+
chunk: Uint8Array;
|
|
18
|
+
eof: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function* chunkify(source: AsyncIterable<Uint8Array>): AsyncIterable<ChunkSegment> {
|
|
22
|
+
let pending: Uint8Array | null = null;
|
|
23
|
+
let produced = false;
|
|
24
|
+
|
|
25
|
+
for await (const value of source) {
|
|
26
|
+
const bytes = value;
|
|
27
|
+
|
|
28
|
+
if (pending && pending.byteLength === MAX_CHUNK_BYTES) {
|
|
29
|
+
yield { chunk: pending, eof: false };
|
|
30
|
+
produced = true;
|
|
31
|
+
pending = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const merged: Uint8Array = pending ? concatChunks(pending, bytes) : bytes;
|
|
35
|
+
pending = null;
|
|
36
|
+
|
|
37
|
+
let cursor: Uint8Array = merged;
|
|
38
|
+
while (cursor.byteLength > MAX_CHUNK_BYTES) {
|
|
39
|
+
const chunk: Uint8Array = cursor.slice(0, MAX_CHUNK_BYTES);
|
|
40
|
+
cursor = cursor.slice(MAX_CHUNK_BYTES);
|
|
41
|
+
yield { chunk, eof: false };
|
|
42
|
+
produced = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pending = cursor;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (pending) {
|
|
49
|
+
yield { chunk: pending, eof: true };
|
|
50
|
+
produced = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!produced) {
|
|
54
|
+
yield { chunk: new Uint8Array(0), eof: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function* toAsyncIterable(
|
|
59
|
+
source:
|
|
60
|
+
| string
|
|
61
|
+
| Uint8Array
|
|
62
|
+
| ArrayBuffer
|
|
63
|
+
| BlobLike
|
|
64
|
+
| FileLike
|
|
65
|
+
| ReadableStreamLike<Uint8Array | ArrayBuffer | ArrayBufferView | string>
|
|
66
|
+
| AsyncIterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>
|
|
67
|
+
| Iterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>,
|
|
68
|
+
): AsyncIterable<Uint8Array> {
|
|
69
|
+
if (typeof source === 'string') {
|
|
70
|
+
yield new TextEncoder().encode(source);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (source instanceof Uint8Array) {
|
|
74
|
+
yield source;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (source instanceof ArrayBuffer) {
|
|
78
|
+
yield new Uint8Array(source);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (ArrayBuffer.isView(source)) {
|
|
82
|
+
yield new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (isBlobLike(source)) {
|
|
86
|
+
const stream = source.stream();
|
|
87
|
+
if (isAsyncIterable(stream)) {
|
|
88
|
+
for await (const chunk of stream as AsyncIterable<unknown>) {
|
|
89
|
+
yield ensureUint8Array(chunk);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (isReadableStreamLike(stream)) {
|
|
94
|
+
yield* readReadableStream(stream);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (isReadableStreamLike(source)) {
|
|
99
|
+
yield* readReadableStream(source);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (isAsyncIterable(source)) {
|
|
103
|
+
for await (const chunk of source as AsyncIterable<unknown>) {
|
|
104
|
+
yield ensureUint8Array(chunk);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isIterable(source)) {
|
|
109
|
+
for (const chunk of source as Iterable<unknown>) {
|
|
110
|
+
yield ensureUint8Array(chunk);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
throw new Error('Unsupported content source; expected binary data');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function base64Encode(bytes: Uint8Array): string {
|
|
118
|
+
if (BufferCtor) {
|
|
119
|
+
return BufferCtor.from(bytes).toString('base64');
|
|
120
|
+
}
|
|
121
|
+
let binary = '';
|
|
122
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
123
|
+
binary += String.fromCharCode(bytes[i]);
|
|
124
|
+
}
|
|
125
|
+
const btoaFn = (globalThis as { btoa?: (data: string) => string }).btoa;
|
|
126
|
+
if (typeof btoaFn === 'function') {
|
|
127
|
+
return btoaFn(binary);
|
|
128
|
+
}
|
|
129
|
+
throw new Error('Base64 encoding is not supported in this environment');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function requiresDuplex(body: unknown): boolean {
|
|
133
|
+
if (!body || typeof body !== 'object') {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof (body as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function') {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const readableStreamCtor = (
|
|
142
|
+
globalThis as {
|
|
143
|
+
ReadableStream?: new (...args: unknown[]) => unknown;
|
|
144
|
+
}
|
|
145
|
+
).ReadableStream;
|
|
146
|
+
if (readableStreamCtor && body instanceof readableStreamCtor) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function toRequestBody(iterable: AsyncIterable<Uint8Array>): unknown {
|
|
154
|
+
const readableStreamCtor = (
|
|
155
|
+
globalThis as { ReadableStream?: new (underlyingSource: unknown) => unknown }
|
|
156
|
+
).ReadableStream;
|
|
157
|
+
if (typeof readableStreamCtor === 'function') {
|
|
158
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
159
|
+
return new readableStreamCtor({
|
|
160
|
+
async pull(controller: { enqueue(chunk: Uint8Array): void; close(): void }) {
|
|
161
|
+
const { value, done } = await iterator.next();
|
|
162
|
+
if (done) {
|
|
163
|
+
controller.close();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
controller.enqueue(value!);
|
|
167
|
+
},
|
|
168
|
+
async cancel(reason: unknown) {
|
|
169
|
+
if (typeof iterator.return === 'function') {
|
|
170
|
+
await iterator.return(reason);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return iterable;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function* readReadableStream(stream: ReadableStreamLike<unknown>): AsyncIterable<Uint8Array> {
|
|
179
|
+
const reader = stream.getReader();
|
|
180
|
+
try {
|
|
181
|
+
while (true) {
|
|
182
|
+
const { value, done } = await reader.read();
|
|
183
|
+
if (done) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (value !== undefined) {
|
|
187
|
+
yield ensureUint8Array(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
reader.releaseLock?.();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureUint8Array(value: unknown): Uint8Array {
|
|
196
|
+
if (value instanceof Uint8Array) {
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
if (value instanceof ArrayBuffer) {
|
|
200
|
+
return new Uint8Array(value);
|
|
201
|
+
}
|
|
202
|
+
if (ArrayBuffer.isView(value)) {
|
|
203
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
204
|
+
}
|
|
205
|
+
if (typeof value === 'string') {
|
|
206
|
+
return new TextEncoder().encode(value);
|
|
207
|
+
}
|
|
208
|
+
if (BufferCtor && BufferCtor.isBuffer(value)) {
|
|
209
|
+
return value as Uint8Array;
|
|
210
|
+
}
|
|
211
|
+
throw new Error('Unsupported chunk type; expected binary data');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isBlobLike(value: unknown): value is BlobLike {
|
|
215
|
+
return (
|
|
216
|
+
typeof value === 'object' && value !== null && typeof (value as BlobLike).stream === 'function'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isReadableStreamLike<T>(value: unknown): value is ReadableStreamLike<T> {
|
|
221
|
+
return (
|
|
222
|
+
typeof value === 'object' &&
|
|
223
|
+
value !== null &&
|
|
224
|
+
typeof (value as ReadableStreamLike<T>).getReader === 'function'
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
229
|
+
return (
|
|
230
|
+
typeof value === 'object' &&
|
|
231
|
+
value !== null &&
|
|
232
|
+
Symbol.asyncIterator in (value as Record<string, unknown>)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isIterable(value: unknown): value is Iterable<unknown> {
|
|
237
|
+
return (
|
|
238
|
+
typeof value === 'object' &&
|
|
239
|
+
value !== null &&
|
|
240
|
+
Symbol.iterator in (value as Record<string, unknown>)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function concatChunks(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
245
|
+
if (a.byteLength === 0) {
|
|
246
|
+
return b;
|
|
247
|
+
}
|
|
248
|
+
if (b.byteLength === 0) {
|
|
249
|
+
return a;
|
|
250
|
+
}
|
|
251
|
+
const merged = new Uint8Array(a.byteLength + b.byteLength);
|
|
252
|
+
merged.set(a, 0);
|
|
253
|
+
merged.set(b, a.byteLength);
|
|
254
|
+
return merged;
|
|
255
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface Repo {
|
|
|
47
47
|
pullUpstream(options: PullUpstreamOptions): Promise<void>;
|
|
48
48
|
restoreCommit(options: RestoreCommitOptions): Promise<RestoreCommitResult>;
|
|
49
49
|
createCommit(options: CreateCommitOptions): CommitBuilder;
|
|
50
|
+
createCommitFromDiff(options: CreateCommitFromDiffOptions): Promise<CommitResult>;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export type ValidMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -88,6 +89,7 @@ export interface CreateRepoOptions extends GitStorageInvocationOptions {
|
|
|
88
89
|
export interface GetFileOptions extends GitStorageInvocationOptions {
|
|
89
90
|
path: string;
|
|
90
91
|
ref?: string;
|
|
92
|
+
ephemeral?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export interface PullUpstreamOptions extends GitStorageInvocationOptions {
|
|
@@ -97,6 +99,7 @@ export interface PullUpstreamOptions extends GitStorageInvocationOptions {
|
|
|
97
99
|
// List Files API types
|
|
98
100
|
export interface ListFilesOptions extends GitStorageInvocationOptions {
|
|
99
101
|
ref?: string;
|
|
102
|
+
ephemeral?: boolean;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
export type ListFilesResponse = ListFilesResponseRaw;
|
|
@@ -228,6 +231,8 @@ interface CreateCommitBaseOptions extends GitStorageInvocationOptions {
|
|
|
228
231
|
commitMessage: string;
|
|
229
232
|
expectedHeadSha?: string;
|
|
230
233
|
baseBranch?: string;
|
|
234
|
+
ephemeral?: boolean;
|
|
235
|
+
ephemeralBase?: boolean;
|
|
231
236
|
author: CommitSignature;
|
|
232
237
|
committer?: CommitSignature;
|
|
233
238
|
signal?: AbortSignal;
|
|
@@ -312,6 +317,21 @@ export interface CommitBuilder {
|
|
|
312
317
|
send(): Promise<CommitResult>;
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
export type DiffSource = CommitFileSource;
|
|
321
|
+
|
|
322
|
+
export interface CreateCommitFromDiffOptions extends GitStorageInvocationOptions {
|
|
323
|
+
targetBranch: string;
|
|
324
|
+
commitMessage: string;
|
|
325
|
+
diff: DiffSource;
|
|
326
|
+
expectedHeadSha?: string;
|
|
327
|
+
baseBranch?: string;
|
|
328
|
+
ephemeral?: boolean;
|
|
329
|
+
ephemeralBase?: boolean;
|
|
330
|
+
author: CommitSignature;
|
|
331
|
+
committer?: CommitSignature;
|
|
332
|
+
signal?: AbortSignal;
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
export interface RefUpdate {
|
|
316
336
|
branch: string;
|
|
317
337
|
oldSha: string;
|