@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/README.md +77 -43
- package/dist/index.cjs +148 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +148 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/commit-pack.ts +103 -99
- package/src/commit.ts +373 -365
- package/src/diff-commit.ts +272 -259
- package/src/errors.ts +34 -34
- package/src/fetch.ts +146 -141
- package/src/index.ts +1400 -1249
- package/src/schemas.ts +120 -114
- package/src/stream-utils.ts +225 -208
- package/src/types.ts +378 -354
- package/src/util.ts +41 -34
- package/src/version.ts +1 -1
- package/src/webhook.ts +244 -239
package/src/index.ts
CHANGED
|
@@ -3,86 +3,91 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A TypeScript SDK for interacting with Pierre's git storage system
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
import { importPKCS8, SignJWT } from 'jose';
|
|
6
|
+
import { SignJWT, importPKCS8 } from 'jose';
|
|
8
7
|
import snakecaseKeys from 'snakecase-keys';
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
FetchCommitTransport,
|
|
11
|
+
createCommitBuilder,
|
|
12
|
+
resolveCommitTtlSeconds,
|
|
13
|
+
} from './commit';
|
|
10
14
|
import { FetchDiffCommitTransport, sendCommitFromDiff } from './diff-commit';
|
|
11
15
|
import { RefUpdateError } from './errors';
|
|
12
16
|
import { ApiError, ApiFetcher } from './fetch';
|
|
13
17
|
import type { RestoreCommitAckRaw } from './schemas';
|
|
14
18
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
branchDiffResponseSchema,
|
|
20
|
+
commitDiffResponseSchema,
|
|
21
|
+
createBranchResponseSchema,
|
|
22
|
+
errorEnvelopeSchema,
|
|
23
|
+
grepResponseSchema,
|
|
24
|
+
listBranchesResponseSchema,
|
|
25
|
+
listCommitsResponseSchema,
|
|
26
|
+
listFilesResponseSchema,
|
|
27
|
+
listReposResponseSchema,
|
|
28
|
+
noteReadResponseSchema,
|
|
29
|
+
noteWriteResponseSchema,
|
|
30
|
+
restoreCommitAckSchema,
|
|
31
|
+
restoreCommitResponseSchema,
|
|
28
32
|
} from './schemas';
|
|
29
33
|
import type {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
34
|
+
AppendNoteOptions,
|
|
35
|
+
ArchiveOptions,
|
|
36
|
+
BranchInfo,
|
|
37
|
+
CommitBuilder,
|
|
38
|
+
CommitInfo,
|
|
39
|
+
CommitResult,
|
|
40
|
+
CreateBranchOptions,
|
|
41
|
+
CreateBranchResponse,
|
|
42
|
+
CreateBranchResult,
|
|
43
|
+
CreateCommitFromDiffOptions,
|
|
44
|
+
CreateCommitOptions,
|
|
45
|
+
CreateNoteOptions,
|
|
46
|
+
CreateRepoOptions,
|
|
47
|
+
DeleteNoteOptions,
|
|
48
|
+
DeleteRepoOptions,
|
|
49
|
+
DeleteRepoResult,
|
|
50
|
+
DiffFileState,
|
|
51
|
+
FileDiff,
|
|
52
|
+
FilteredFile,
|
|
53
|
+
FindOneOptions,
|
|
54
|
+
GetBranchDiffOptions,
|
|
55
|
+
GetBranchDiffResponse,
|
|
56
|
+
GetBranchDiffResult,
|
|
57
|
+
GetCommitDiffOptions,
|
|
58
|
+
GetCommitDiffResponse,
|
|
59
|
+
GetCommitDiffResult,
|
|
60
|
+
GetFileOptions,
|
|
61
|
+
GetNoteOptions,
|
|
62
|
+
GetNoteResult,
|
|
63
|
+
GetRemoteURLOptions,
|
|
64
|
+
GitStorageOptions,
|
|
65
|
+
GrepFileMatch,
|
|
66
|
+
GrepLine,
|
|
67
|
+
GrepOptions,
|
|
68
|
+
GrepResult,
|
|
69
|
+
ListBranchesOptions,
|
|
70
|
+
ListBranchesResponse,
|
|
71
|
+
ListBranchesResult,
|
|
72
|
+
ListCommitsOptions,
|
|
73
|
+
ListCommitsResponse,
|
|
74
|
+
ListCommitsResult,
|
|
75
|
+
ListFilesOptions,
|
|
76
|
+
ListFilesResult,
|
|
77
|
+
ListReposOptions,
|
|
78
|
+
ListReposResponse,
|
|
79
|
+
ListReposResult,
|
|
80
|
+
NoteWriteResult,
|
|
81
|
+
PullUpstreamOptions,
|
|
82
|
+
RawBranchInfo,
|
|
83
|
+
RawCommitInfo,
|
|
84
|
+
RawFileDiff,
|
|
85
|
+
RawFilteredFile,
|
|
86
|
+
RefUpdate,
|
|
87
|
+
Repo,
|
|
88
|
+
RestoreCommitOptions,
|
|
89
|
+
RestoreCommitResult,
|
|
90
|
+
ValidAPIVersion,
|
|
86
91
|
} from './types';
|
|
87
92
|
|
|
88
93
|
/**
|
|
@@ -95,7 +100,11 @@ export { ApiError } from './fetch';
|
|
|
95
100
|
export * from './types';
|
|
96
101
|
|
|
97
102
|
// Export webhook validation utilities
|
|
98
|
-
export {
|
|
103
|
+
export {
|
|
104
|
+
parseSignatureHeader,
|
|
105
|
+
validateWebhook,
|
|
106
|
+
validateWebhookSignature,
|
|
107
|
+
} from './webhook';
|
|
99
108
|
|
|
100
109
|
/**
|
|
101
110
|
* Git Storage API
|
|
@@ -111,1277 +120,1419 @@ const API_VERSION: ValidAPIVersion = 1;
|
|
|
111
120
|
const apiInstanceMap = new Map<string, ApiFetcher>();
|
|
112
121
|
const DEFAULT_TOKEN_TTL_SECONDS = 60 * 60; // 1 hour
|
|
113
122
|
const RESTORE_COMMIT_ALLOWED_STATUS = [
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
400, // Bad Request - validation errors
|
|
124
|
+
401, // Unauthorized - missing/invalid auth header
|
|
125
|
+
403, // Forbidden - missing git:write scope
|
|
126
|
+
404, // Not Found - repo lookup failures
|
|
127
|
+
408, // Request Timeout - client cancelled
|
|
128
|
+
409, // Conflict - concurrent ref updates
|
|
129
|
+
412, // Precondition Failed - optimistic concurrency
|
|
130
|
+
422, // Unprocessable Entity - metadata issues
|
|
131
|
+
429, // Too Many Requests - upstream throttling
|
|
132
|
+
499, // Client Closed Request - storage cancellation
|
|
133
|
+
500, // Internal Server Error - generic failure
|
|
134
|
+
502, // Bad Gateway - storage/gateway bridge issues
|
|
135
|
+
503, // Service Unavailable - storage selection failures
|
|
136
|
+
504, // Gateway Timeout - long-running storage operations
|
|
128
137
|
] as const;
|
|
129
138
|
|
|
130
139
|
const NOTE_WRITE_ALLOWED_STATUS = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
400, // Bad Request - validation errors
|
|
141
|
+
401, // Unauthorized - missing/invalid auth header
|
|
142
|
+
403, // Forbidden - missing git:write scope
|
|
143
|
+
404, // Not Found - repo or note lookup failures
|
|
144
|
+
408, // Request Timeout - client cancelled
|
|
145
|
+
409, // Conflict - concurrent ref updates
|
|
146
|
+
412, // Precondition Failed - optimistic concurrency
|
|
147
|
+
422, // Unprocessable Entity - metadata issues
|
|
148
|
+
429, // Too Many Requests - upstream throttling
|
|
149
|
+
499, // Client Closed Request - storage cancellation
|
|
150
|
+
500, // Internal Server Error - generic failure
|
|
151
|
+
502, // Bad Gateway - storage/gateway bridge issues
|
|
152
|
+
503, // Service Unavailable - storage selection failures
|
|
153
|
+
504, // Gateway Timeout - long-running storage operations
|
|
145
154
|
] as const;
|
|
146
155
|
|
|
147
156
|
function resolveInvocationTtlSeconds(
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
options?: { ttl?: number },
|
|
158
|
+
defaultValue: number = DEFAULT_TOKEN_TTL_SECONDS
|
|
150
159
|
): number {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
if (typeof options?.ttl === 'number' && options.ttl > 0) {
|
|
161
|
+
return options.ttl;
|
|
162
|
+
}
|
|
163
|
+
return defaultValue;
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
type RestoreCommitAck = RestoreCommitAckRaw;
|
|
158
167
|
|
|
159
168
|
function toRefUpdate(result: RestoreCommitAck['result']): RefUpdate {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
return {
|
|
170
|
+
branch: result.branch,
|
|
171
|
+
oldSha: result.old_sha,
|
|
172
|
+
newSha: result.new_sha,
|
|
173
|
+
};
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
function buildRestoreCommitResult(ack: RestoreCommitAck): RestoreCommitResult {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
const refUpdate = toRefUpdate(ack.result);
|
|
178
|
+
if (!ack.result.success) {
|
|
179
|
+
throw new RefUpdateError(
|
|
180
|
+
ack.result.message ??
|
|
181
|
+
`Restore commit failed with status ${ack.result.status}`,
|
|
182
|
+
{
|
|
183
|
+
status: ack.result.status,
|
|
184
|
+
message: ack.result.message,
|
|
185
|
+
refUpdate,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
commitSha: ack.commit.commit_sha,
|
|
191
|
+
treeSha: ack.commit.tree_sha,
|
|
192
|
+
targetBranch: ack.commit.target_branch,
|
|
193
|
+
packBytes: ack.commit.pack_bytes,
|
|
194
|
+
refUpdate,
|
|
195
|
+
};
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
interface RestoreCommitFailureInfo {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
status?: string;
|
|
200
|
+
message?: string;
|
|
201
|
+
refUpdate?: Partial<RefUpdate>;
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
function toPartialRefUpdate(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
branch?: unknown,
|
|
206
|
+
oldSha?: unknown,
|
|
207
|
+
newSha?: unknown
|
|
198
208
|
): Partial<RefUpdate> | undefined {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
const refUpdate: Partial<RefUpdate> = {};
|
|
210
|
+
if (typeof branch === 'string' && branch.trim() !== '') {
|
|
211
|
+
refUpdate.branch = branch;
|
|
212
|
+
}
|
|
213
|
+
if (typeof oldSha === 'string' && oldSha.trim() !== '') {
|
|
214
|
+
refUpdate.oldSha = oldSha;
|
|
215
|
+
}
|
|
216
|
+
if (typeof newSha === 'string' && newSha.trim() !== '') {
|
|
217
|
+
refUpdate.newSha = newSha;
|
|
218
|
+
}
|
|
219
|
+
return Object.keys(refUpdate).length > 0 ? refUpdate : undefined;
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
function parseRestoreCommitPayload(
|
|
213
|
-
|
|
223
|
+
payload: unknown
|
|
214
224
|
): { ack: RestoreCommitAck } | { failure: RestoreCommitFailureInfo } | null {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
225
|
+
const ack = restoreCommitAckSchema.safeParse(payload);
|
|
226
|
+
if (ack.success) {
|
|
227
|
+
return { ack: ack.data };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const failure = restoreCommitResponseSchema.safeParse(payload);
|
|
231
|
+
if (failure.success) {
|
|
232
|
+
const result = failure.data.result;
|
|
233
|
+
return {
|
|
234
|
+
failure: {
|
|
235
|
+
status: result.status,
|
|
236
|
+
message: result.message,
|
|
237
|
+
refUpdate: toPartialRefUpdate(
|
|
238
|
+
result.branch,
|
|
239
|
+
result.old_sha,
|
|
240
|
+
result.new_sha
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return null;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
function httpStatusToRestoreStatus(status: number): string {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
250
|
+
switch (status) {
|
|
251
|
+
case 409:
|
|
252
|
+
return 'conflict';
|
|
253
|
+
case 412:
|
|
254
|
+
return 'precondition_failed';
|
|
255
|
+
default:
|
|
256
|
+
return `${status}`;
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
|
|
246
260
|
function getApiInstance(baseUrl: string, version: ValidAPIVersion) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
261
|
+
if (!apiInstanceMap.has(`${baseUrl}--${version}`)) {
|
|
262
|
+
apiInstanceMap.set(
|
|
263
|
+
`${baseUrl}--${version}`,
|
|
264
|
+
new ApiFetcher(baseUrl, version)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return apiInstanceMap.get(`${baseUrl}--${version}`)!;
|
|
251
268
|
}
|
|
252
269
|
|
|
253
270
|
function transformBranchInfo(raw: RawBranchInfo): BranchInfo {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
return {
|
|
272
|
+
cursor: raw.cursor,
|
|
273
|
+
name: raw.name,
|
|
274
|
+
headSha: raw.head_sha,
|
|
275
|
+
createdAt: raw.created_at,
|
|
276
|
+
};
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
function transformListBranchesResult(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
279
|
+
function transformListBranchesResult(
|
|
280
|
+
raw: ListBranchesResponse
|
|
281
|
+
): ListBranchesResult {
|
|
282
|
+
return {
|
|
283
|
+
branches: raw.branches.map(transformBranchInfo),
|
|
284
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
285
|
+
hasMore: raw.has_more,
|
|
286
|
+
};
|
|
268
287
|
}
|
|
269
288
|
|
|
270
289
|
function transformCommitInfo(raw: RawCommitInfo): CommitInfo {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
290
|
+
const parsedDate = new Date(raw.date);
|
|
291
|
+
return {
|
|
292
|
+
sha: raw.sha,
|
|
293
|
+
message: raw.message,
|
|
294
|
+
authorName: raw.author_name,
|
|
295
|
+
authorEmail: raw.author_email,
|
|
296
|
+
committerName: raw.committer_name,
|
|
297
|
+
committerEmail: raw.committer_email,
|
|
298
|
+
date: parsedDate,
|
|
299
|
+
rawDate: raw.date,
|
|
300
|
+
};
|
|
282
301
|
}
|
|
283
302
|
|
|
284
|
-
function transformListCommitsResult(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
303
|
+
function transformListCommitsResult(
|
|
304
|
+
raw: ListCommitsResponse
|
|
305
|
+
): ListCommitsResult {
|
|
306
|
+
return {
|
|
307
|
+
commits: raw.commits.map(transformCommitInfo),
|
|
308
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
309
|
+
hasMore: raw.has_more,
|
|
310
|
+
};
|
|
290
311
|
}
|
|
291
312
|
|
|
292
313
|
function normalizeDiffState(rawState: string): DiffFileState {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
if (!rawState) {
|
|
315
|
+
return 'unknown';
|
|
316
|
+
}
|
|
317
|
+
const leading = rawState.trim()[0]?.toUpperCase();
|
|
318
|
+
switch (leading) {
|
|
319
|
+
case 'A':
|
|
320
|
+
return 'added';
|
|
321
|
+
case 'M':
|
|
322
|
+
return 'modified';
|
|
323
|
+
case 'D':
|
|
324
|
+
return 'deleted';
|
|
325
|
+
case 'R':
|
|
326
|
+
return 'renamed';
|
|
327
|
+
case 'C':
|
|
328
|
+
return 'copied';
|
|
329
|
+
case 'T':
|
|
330
|
+
return 'type_changed';
|
|
331
|
+
case 'U':
|
|
332
|
+
return 'unmerged';
|
|
333
|
+
default:
|
|
334
|
+
return 'unknown';
|
|
335
|
+
}
|
|
315
336
|
}
|
|
316
337
|
|
|
317
338
|
function transformFileDiff(raw: RawFileDiff): FileDiff {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
const normalizedState = normalizeDiffState(raw.state);
|
|
340
|
+
return {
|
|
341
|
+
path: raw.path,
|
|
342
|
+
state: normalizedState,
|
|
343
|
+
rawState: raw.state,
|
|
344
|
+
oldPath: raw.old_path ?? undefined,
|
|
345
|
+
raw: raw.raw,
|
|
346
|
+
bytes: raw.bytes,
|
|
347
|
+
isEof: raw.is_eof,
|
|
348
|
+
};
|
|
328
349
|
}
|
|
329
350
|
|
|
330
351
|
function transformFilteredFile(raw: RawFilteredFile): FilteredFile {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
const normalizedState = normalizeDiffState(raw.state);
|
|
353
|
+
return {
|
|
354
|
+
path: raw.path,
|
|
355
|
+
state: normalizedState,
|
|
356
|
+
rawState: raw.state,
|
|
357
|
+
oldPath: raw.old_path ?? undefined,
|
|
358
|
+
bytes: raw.bytes,
|
|
359
|
+
isEof: raw.is_eof,
|
|
360
|
+
};
|
|
340
361
|
}
|
|
341
362
|
|
|
342
|
-
function transformBranchDiffResult(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
function transformBranchDiffResult(
|
|
364
|
+
raw: GetBranchDiffResponse
|
|
365
|
+
): GetBranchDiffResult {
|
|
366
|
+
return {
|
|
367
|
+
branch: raw.branch,
|
|
368
|
+
base: raw.base,
|
|
369
|
+
stats: raw.stats,
|
|
370
|
+
files: raw.files.map(transformFileDiff),
|
|
371
|
+
filteredFiles: raw.filtered_files.map(transformFilteredFile),
|
|
372
|
+
};
|
|
350
373
|
}
|
|
351
374
|
|
|
352
|
-
function transformCommitDiffResult(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
375
|
+
function transformCommitDiffResult(
|
|
376
|
+
raw: GetCommitDiffResponse
|
|
377
|
+
): GetCommitDiffResult {
|
|
378
|
+
return {
|
|
379
|
+
sha: raw.sha,
|
|
380
|
+
stats: raw.stats,
|
|
381
|
+
files: raw.files.map(transformFileDiff),
|
|
382
|
+
filteredFiles: raw.filtered_files.map(transformFilteredFile),
|
|
383
|
+
};
|
|
359
384
|
}
|
|
360
385
|
|
|
361
|
-
function transformCreateBranchResult(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
386
|
+
function transformCreateBranchResult(
|
|
387
|
+
raw: CreateBranchResponse
|
|
388
|
+
): CreateBranchResult {
|
|
389
|
+
return {
|
|
390
|
+
message: raw.message,
|
|
391
|
+
targetBranch: raw.target_branch,
|
|
392
|
+
targetIsEphemeral: raw.target_is_ephemeral,
|
|
393
|
+
commitSha: raw.commit_sha ?? undefined,
|
|
394
|
+
};
|
|
368
395
|
}
|
|
369
396
|
|
|
370
397
|
function transformListReposResult(raw: ListReposResponse): ListReposResult {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
398
|
+
return {
|
|
399
|
+
repos: raw.repos.map((repo) => ({
|
|
400
|
+
repoId: repo.repo_id,
|
|
401
|
+
url: repo.url,
|
|
402
|
+
defaultBranch: repo.default_branch,
|
|
403
|
+
createdAt: repo.created_at,
|
|
404
|
+
baseRepo: repo.base_repo
|
|
405
|
+
? {
|
|
406
|
+
provider: repo.base_repo.provider,
|
|
407
|
+
owner: repo.base_repo.owner,
|
|
408
|
+
name: repo.base_repo.name,
|
|
409
|
+
}
|
|
410
|
+
: undefined,
|
|
411
|
+
})),
|
|
412
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
413
|
+
hasMore: raw.has_more,
|
|
414
|
+
};
|
|
388
415
|
}
|
|
389
416
|
|
|
390
|
-
function transformGrepLine(raw: {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
417
|
+
function transformGrepLine(raw: {
|
|
418
|
+
line_number: number;
|
|
419
|
+
text: string;
|
|
420
|
+
type: string;
|
|
421
|
+
}): GrepLine {
|
|
422
|
+
return {
|
|
423
|
+
lineNumber: raw.line_number,
|
|
424
|
+
text: raw.text,
|
|
425
|
+
type: raw.type,
|
|
426
|
+
};
|
|
396
427
|
}
|
|
397
428
|
|
|
398
429
|
function transformGrepFileMatch(raw: {
|
|
399
|
-
|
|
400
|
-
|
|
430
|
+
path: string;
|
|
431
|
+
lines: { line_number: number; text: string; type: string }[];
|
|
401
432
|
}): GrepFileMatch {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
433
|
+
return {
|
|
434
|
+
path: raw.path,
|
|
435
|
+
lines: raw.lines.map(transformGrepLine),
|
|
436
|
+
};
|
|
406
437
|
}
|
|
407
438
|
|
|
408
439
|
function transformNoteReadResult(raw: {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
440
|
+
sha: string;
|
|
441
|
+
note: string;
|
|
442
|
+
ref_sha: string;
|
|
412
443
|
}): GetNoteResult {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
444
|
+
return {
|
|
445
|
+
sha: raw.sha,
|
|
446
|
+
note: raw.note,
|
|
447
|
+
refSha: raw.ref_sha,
|
|
448
|
+
};
|
|
418
449
|
}
|
|
419
450
|
|
|
420
451
|
function transformNoteWriteResult(raw: {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
452
|
+
sha: string;
|
|
453
|
+
target_ref: string;
|
|
454
|
+
base_commit?: string;
|
|
455
|
+
new_ref_sha: string;
|
|
456
|
+
result: { success: boolean; status: string; message?: string };
|
|
426
457
|
}): NoteWriteResult {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
return {
|
|
459
|
+
sha: raw.sha,
|
|
460
|
+
targetRef: raw.target_ref,
|
|
461
|
+
baseCommit: raw.base_commit,
|
|
462
|
+
newRefSha: raw.new_ref_sha,
|
|
463
|
+
result: {
|
|
464
|
+
success: raw.result.success,
|
|
465
|
+
status: raw.result.status,
|
|
466
|
+
message: raw.result.message,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
438
469
|
}
|
|
439
470
|
|
|
440
471
|
function buildNoteWriteBody(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
472
|
+
sha: string,
|
|
473
|
+
note: string,
|
|
474
|
+
action: 'add' | 'append',
|
|
475
|
+
options: { expectedRefSha?: string; author?: { name: string; email: string } }
|
|
445
476
|
): Record<string, unknown> {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
477
|
+
const body: Record<string, unknown> = {
|
|
478
|
+
sha,
|
|
479
|
+
action,
|
|
480
|
+
note,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const expectedRefSha = options.expectedRefSha?.trim();
|
|
484
|
+
if (expectedRefSha) {
|
|
485
|
+
body.expected_ref_sha = expectedRefSha;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (options.author) {
|
|
489
|
+
const authorName = options.author.name?.trim();
|
|
490
|
+
const authorEmail = options.author.email?.trim();
|
|
491
|
+
if (!authorName || !authorEmail) {
|
|
492
|
+
throw new Error('note author name and email are required when provided');
|
|
493
|
+
}
|
|
494
|
+
body.author = {
|
|
495
|
+
name: authorName,
|
|
496
|
+
email: authorEmail,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return body;
|
|
470
501
|
}
|
|
471
502
|
|
|
472
503
|
async function parseNoteWriteResponse(
|
|
473
|
-
|
|
474
|
-
|
|
504
|
+
response: Response,
|
|
505
|
+
method: 'POST' | 'DELETE'
|
|
475
506
|
): Promise<NoteWriteResult> {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
507
|
+
let jsonBody: unknown;
|
|
508
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
509
|
+
try {
|
|
510
|
+
if (contentType.includes('application/json')) {
|
|
511
|
+
jsonBody = await response.json();
|
|
512
|
+
} else {
|
|
513
|
+
jsonBody = await response.text();
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
jsonBody = undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (jsonBody && typeof jsonBody === 'object') {
|
|
520
|
+
const parsed = noteWriteResponseSchema.safeParse(jsonBody);
|
|
521
|
+
if (parsed.success) {
|
|
522
|
+
return transformNoteWriteResult(parsed.data);
|
|
523
|
+
}
|
|
524
|
+
const parsedError = errorEnvelopeSchema.safeParse(jsonBody);
|
|
525
|
+
if (parsedError.success) {
|
|
526
|
+
throw new ApiError({
|
|
527
|
+
message: parsedError.data.error,
|
|
528
|
+
status: response.status,
|
|
529
|
+
statusText: response.statusText,
|
|
530
|
+
method,
|
|
531
|
+
url: response.url,
|
|
532
|
+
body: jsonBody,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const fallbackMessage =
|
|
538
|
+
typeof jsonBody === 'string' && jsonBody.trim() !== ''
|
|
539
|
+
? jsonBody.trim()
|
|
540
|
+
: `Request ${method} ${response.url} failed with status ${response.status} ${response.statusText}`;
|
|
541
|
+
|
|
542
|
+
throw new ApiError({
|
|
543
|
+
message: fallbackMessage,
|
|
544
|
+
status: response.status,
|
|
545
|
+
statusText: response.statusText,
|
|
546
|
+
method,
|
|
547
|
+
url: response.url,
|
|
548
|
+
body: jsonBody,
|
|
549
|
+
});
|
|
519
550
|
}
|
|
520
551
|
|
|
521
552
|
/**
|
|
522
553
|
* Implementation of the Repo interface
|
|
523
554
|
*/
|
|
524
555
|
class RepoImpl implements Repo {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
556
|
+
private readonly api: ApiFetcher;
|
|
557
|
+
|
|
558
|
+
constructor(
|
|
559
|
+
public readonly id: string,
|
|
560
|
+
public readonly defaultBranch: string,
|
|
561
|
+
private readonly options: GitStorageOptions,
|
|
562
|
+
private readonly generateJWT: (
|
|
563
|
+
repoId: string,
|
|
564
|
+
options?: GetRemoteURLOptions
|
|
565
|
+
) => Promise<string>
|
|
566
|
+
) {
|
|
567
|
+
this.api = getApiInstance(
|
|
568
|
+
this.options.apiBaseUrl ?? GitStorage.getDefaultAPIBaseUrl(options.name),
|
|
569
|
+
this.options.apiVersion ?? API_VERSION
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async getRemoteURL(urlOptions?: GetRemoteURLOptions): Promise<string> {
|
|
574
|
+
const url = new URL(
|
|
575
|
+
`https://${this.options.storageBaseUrl}/${this.id}.git`
|
|
576
|
+
);
|
|
577
|
+
url.username = `t`;
|
|
578
|
+
url.password = await this.generateJWT(this.id, urlOptions);
|
|
579
|
+
return url.toString();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async getEphemeralRemoteURL(
|
|
583
|
+
urlOptions?: GetRemoteURLOptions
|
|
584
|
+
): Promise<string> {
|
|
585
|
+
const url = new URL(
|
|
586
|
+
`https://${this.options.storageBaseUrl}/${this.id}+ephemeral.git`
|
|
587
|
+
);
|
|
588
|
+
url.username = `t`;
|
|
589
|
+
url.password = await this.generateJWT(this.id, urlOptions);
|
|
590
|
+
return url.toString();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async getFileStream(options: GetFileOptions): Promise<Response> {
|
|
594
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
595
|
+
const jwt = await this.generateJWT(this.id, {
|
|
596
|
+
permissions: ['git:read'],
|
|
597
|
+
ttl,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const params: Record<string, string> = {
|
|
601
|
+
path: options.path,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
if (options.ref) {
|
|
605
|
+
params.ref = options.ref;
|
|
606
|
+
}
|
|
607
|
+
if (typeof options.ephemeral === 'boolean') {
|
|
608
|
+
params.ephemeral = String(options.ephemeral);
|
|
609
|
+
}
|
|
610
|
+
if (typeof options.ephemeralBase === 'boolean') {
|
|
611
|
+
params.ephemeral_base = String(options.ephemeralBase);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Return the raw fetch Response for streaming
|
|
615
|
+
return this.api.get({ path: 'repos/file', params }, jwt);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async getArchiveStream(options: ArchiveOptions = {}): Promise<Response> {
|
|
619
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
620
|
+
const jwt = await this.generateJWT(this.id, {
|
|
621
|
+
permissions: ['git:read'],
|
|
622
|
+
ttl,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const body: Record<string, unknown> = {};
|
|
626
|
+
const ref = options.ref?.trim();
|
|
627
|
+
if (ref) {
|
|
628
|
+
body.ref = ref;
|
|
629
|
+
}
|
|
630
|
+
if (Array.isArray(options.includeGlobs) && options.includeGlobs.length > 0) {
|
|
631
|
+
body.include_globs = options.includeGlobs;
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(options.excludeGlobs) && options.excludeGlobs.length > 0) {
|
|
634
|
+
body.exclude_globs = options.excludeGlobs;
|
|
635
|
+
}
|
|
636
|
+
if (typeof options.archivePrefix === 'string') {
|
|
637
|
+
const prefix = options.archivePrefix.trim();
|
|
638
|
+
if (prefix) {
|
|
639
|
+
body.archive = { prefix };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const path =
|
|
644
|
+
Object.keys(body).length > 0 ? { path: 'repos/archive', body } : 'repos/archive';
|
|
645
|
+
|
|
646
|
+
return this.api.post(path, jwt);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async listFiles(options?: ListFilesOptions): Promise<ListFilesResult> {
|
|
650
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
651
|
+
const jwt = await this.generateJWT(this.id, {
|
|
652
|
+
permissions: ['git:read'],
|
|
653
|
+
ttl,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const params: Record<string, string> = {};
|
|
657
|
+
if (options?.ref) {
|
|
658
|
+
params.ref = options.ref;
|
|
659
|
+
}
|
|
660
|
+
if (typeof options?.ephemeral === 'boolean') {
|
|
661
|
+
params.ephemeral = String(options.ephemeral);
|
|
662
|
+
}
|
|
663
|
+
const response = await this.api.get(
|
|
664
|
+
{
|
|
665
|
+
path: 'repos/files',
|
|
666
|
+
params: Object.keys(params).length ? params : undefined,
|
|
667
|
+
},
|
|
668
|
+
jwt
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const raw = listFilesResponseSchema.parse(await response.json());
|
|
672
|
+
return { paths: raw.paths, ref: raw.ref };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async listBranches(
|
|
676
|
+
options?: ListBranchesOptions
|
|
677
|
+
): Promise<ListBranchesResult> {
|
|
678
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
679
|
+
const jwt = await this.generateJWT(this.id, {
|
|
680
|
+
permissions: ['git:read'],
|
|
681
|
+
ttl,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const cursor = options?.cursor;
|
|
685
|
+
const limit = options?.limit;
|
|
686
|
+
|
|
687
|
+
let params: Record<string, string> | undefined;
|
|
688
|
+
|
|
689
|
+
if (typeof cursor === 'string' || typeof limit === 'number') {
|
|
690
|
+
params = {};
|
|
691
|
+
if (typeof cursor === 'string') {
|
|
692
|
+
params.cursor = cursor;
|
|
693
|
+
}
|
|
694
|
+
if (typeof limit === 'number') {
|
|
695
|
+
params.limit = limit.toString();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const response = await this.api.get(
|
|
700
|
+
{ path: 'repos/branches', params },
|
|
701
|
+
jwt
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const raw = listBranchesResponseSchema.parse(await response.json());
|
|
705
|
+
return transformListBranchesResult({
|
|
706
|
+
...raw,
|
|
707
|
+
next_cursor: raw.next_cursor ?? undefined,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async listCommits(options?: ListCommitsOptions): Promise<ListCommitsResult> {
|
|
712
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
713
|
+
const jwt = await this.generateJWT(this.id, {
|
|
714
|
+
permissions: ['git:read'],
|
|
715
|
+
ttl,
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
let params: Record<string, string> | undefined;
|
|
719
|
+
|
|
720
|
+
if (options?.branch || options?.cursor || options?.limit) {
|
|
721
|
+
params = {};
|
|
722
|
+
if (options?.branch) {
|
|
723
|
+
params.branch = options.branch;
|
|
724
|
+
}
|
|
725
|
+
if (options?.cursor) {
|
|
726
|
+
params.cursor = options.cursor;
|
|
727
|
+
}
|
|
728
|
+
if (typeof options?.limit == 'number') {
|
|
729
|
+
params.limit = options.limit.toString();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const response = await this.api.get({ path: 'repos/commits', params }, jwt);
|
|
734
|
+
|
|
735
|
+
const raw = listCommitsResponseSchema.parse(await response.json());
|
|
736
|
+
return transformListCommitsResult({
|
|
737
|
+
...raw,
|
|
738
|
+
next_cursor: raw.next_cursor ?? undefined,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async getNote(options: GetNoteOptions): Promise<GetNoteResult> {
|
|
743
|
+
const sha = options?.sha?.trim();
|
|
744
|
+
if (!sha) {
|
|
745
|
+
throw new Error('getNote sha is required');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
749
|
+
const jwt = await this.generateJWT(this.id, {
|
|
750
|
+
permissions: ['git:read'],
|
|
751
|
+
ttl,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const response = await this.api.get(
|
|
755
|
+
{ path: 'repos/notes', params: { sha } },
|
|
756
|
+
jwt
|
|
757
|
+
);
|
|
758
|
+
const raw = noteReadResponseSchema.parse(await response.json());
|
|
759
|
+
return transformNoteReadResult(raw);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async createNote(options: CreateNoteOptions): Promise<NoteWriteResult> {
|
|
763
|
+
const sha = options?.sha?.trim();
|
|
764
|
+
if (!sha) {
|
|
765
|
+
throw new Error('createNote sha is required');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const note = options?.note?.trim();
|
|
769
|
+
if (!note) {
|
|
770
|
+
throw new Error('createNote note is required');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
774
|
+
const jwt = await this.generateJWT(this.id, {
|
|
775
|
+
permissions: ['git:write'],
|
|
776
|
+
ttl,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const body = buildNoteWriteBody(sha, note, 'add', {
|
|
780
|
+
expectedRefSha: options.expectedRefSha,
|
|
781
|
+
author: options.author,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const response = await this.api.post({ path: 'repos/notes', body }, jwt, {
|
|
785
|
+
allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS],
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
const result = await parseNoteWriteResponse(response, 'POST');
|
|
789
|
+
if (!result.result.success) {
|
|
790
|
+
throw new RefUpdateError(
|
|
791
|
+
result.result.message ??
|
|
792
|
+
`createNote failed with status ${result.result.status}`,
|
|
793
|
+
{
|
|
794
|
+
status: result.result.status,
|
|
795
|
+
message: result.result.message,
|
|
796
|
+
refUpdate: toPartialRefUpdate(
|
|
797
|
+
result.targetRef,
|
|
798
|
+
result.baseCommit,
|
|
799
|
+
result.newRefSha
|
|
800
|
+
),
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async appendNote(options: AppendNoteOptions): Promise<NoteWriteResult> {
|
|
808
|
+
const sha = options?.sha?.trim();
|
|
809
|
+
if (!sha) {
|
|
810
|
+
throw new Error('appendNote sha is required');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const note = options?.note?.trim();
|
|
814
|
+
if (!note) {
|
|
815
|
+
throw new Error('appendNote note is required');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
819
|
+
const jwt = await this.generateJWT(this.id, {
|
|
820
|
+
permissions: ['git:write'],
|
|
821
|
+
ttl,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const body = buildNoteWriteBody(sha, note, 'append', {
|
|
825
|
+
expectedRefSha: options.expectedRefSha,
|
|
826
|
+
author: options.author,
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const response = await this.api.post({ path: 'repos/notes', body }, jwt, {
|
|
830
|
+
allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS],
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const result = await parseNoteWriteResponse(response, 'POST');
|
|
834
|
+
if (!result.result.success) {
|
|
835
|
+
throw new RefUpdateError(
|
|
836
|
+
result.result.message ??
|
|
837
|
+
`appendNote failed with status ${result.result.status}`,
|
|
838
|
+
{
|
|
839
|
+
status: result.result.status,
|
|
840
|
+
message: result.result.message,
|
|
841
|
+
refUpdate: toPartialRefUpdate(
|
|
842
|
+
result.targetRef,
|
|
843
|
+
result.baseCommit,
|
|
844
|
+
result.newRefSha
|
|
845
|
+
),
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async deleteNote(options: DeleteNoteOptions): Promise<NoteWriteResult> {
|
|
853
|
+
const sha = options?.sha?.trim();
|
|
854
|
+
if (!sha) {
|
|
855
|
+
throw new Error('deleteNote sha is required');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
859
|
+
const jwt = await this.generateJWT(this.id, {
|
|
860
|
+
permissions: ['git:write'],
|
|
861
|
+
ttl,
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
const body: Record<string, unknown> = {
|
|
865
|
+
sha,
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const expectedRefSha = options.expectedRefSha?.trim();
|
|
869
|
+
if (expectedRefSha) {
|
|
870
|
+
body.expected_ref_sha = expectedRefSha;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (options.author) {
|
|
874
|
+
const authorName = options.author.name?.trim();
|
|
875
|
+
const authorEmail = options.author.email?.trim();
|
|
876
|
+
if (!authorName || !authorEmail) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
'deleteNote author name and email are required when provided'
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
body.author = {
|
|
882
|
+
name: authorName,
|
|
883
|
+
email: authorEmail,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const response = await this.api.delete({ path: 'repos/notes', body }, jwt, {
|
|
888
|
+
allowedStatus: [...NOTE_WRITE_ALLOWED_STATUS],
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const result = await parseNoteWriteResponse(response, 'DELETE');
|
|
892
|
+
if (!result.result.success) {
|
|
893
|
+
throw new RefUpdateError(
|
|
894
|
+
result.result.message ??
|
|
895
|
+
`deleteNote failed with status ${result.result.status}`,
|
|
896
|
+
{
|
|
897
|
+
status: result.result.status,
|
|
898
|
+
message: result.result.message,
|
|
899
|
+
refUpdate: toPartialRefUpdate(
|
|
900
|
+
result.targetRef,
|
|
901
|
+
result.baseCommit,
|
|
902
|
+
result.newRefSha
|
|
903
|
+
),
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async getBranchDiff(
|
|
911
|
+
options: GetBranchDiffOptions
|
|
912
|
+
): Promise<GetBranchDiffResult> {
|
|
913
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
914
|
+
const jwt = await this.generateJWT(this.id, {
|
|
915
|
+
permissions: ['git:read'],
|
|
916
|
+
ttl,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const params: Record<string, string | string[]> = {
|
|
920
|
+
branch: options.branch,
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
if (options.base) {
|
|
924
|
+
params.base = options.base;
|
|
925
|
+
}
|
|
926
|
+
if (typeof options.ephemeral === 'boolean') {
|
|
927
|
+
params.ephemeral = String(options.ephemeral);
|
|
928
|
+
}
|
|
929
|
+
if (typeof options.ephemeralBase === 'boolean') {
|
|
930
|
+
params.ephemeral_base = String(options.ephemeralBase);
|
|
931
|
+
}
|
|
932
|
+
if (options.paths && options.paths.length > 0) {
|
|
933
|
+
params.path = options.paths;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const response = await this.api.get(
|
|
937
|
+
{ path: 'repos/branches/diff', params },
|
|
938
|
+
jwt
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const raw = branchDiffResponseSchema.parse(await response.json());
|
|
942
|
+
return transformBranchDiffResult(raw);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async getCommitDiff(
|
|
946
|
+
options: GetCommitDiffOptions
|
|
947
|
+
): Promise<GetCommitDiffResult> {
|
|
948
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
949
|
+
const jwt = await this.generateJWT(this.id, {
|
|
950
|
+
permissions: ['git:read'],
|
|
951
|
+
ttl,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const params: Record<string, string | string[]> = {
|
|
955
|
+
sha: options.sha,
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
if (options.baseSha) {
|
|
959
|
+
params.baseSha = options.baseSha;
|
|
960
|
+
}
|
|
961
|
+
if (options.paths && options.paths.length > 0) {
|
|
962
|
+
params.path = options.paths;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const response = await this.api.get({ path: 'repos/diff', params }, jwt);
|
|
966
|
+
|
|
967
|
+
const raw = commitDiffResponseSchema.parse(await response.json());
|
|
968
|
+
return transformCommitDiffResult(raw);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async grep(options: GrepOptions): Promise<GrepResult> {
|
|
972
|
+
const pattern = options?.query?.pattern?.trim();
|
|
973
|
+
if (!pattern) {
|
|
974
|
+
throw new Error('grep query.pattern is required');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
978
|
+
const jwt = await this.generateJWT(this.id, {
|
|
979
|
+
permissions: ['git:read'],
|
|
980
|
+
ttl,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const body: Record<string, unknown> = {
|
|
984
|
+
query: {
|
|
985
|
+
pattern,
|
|
986
|
+
...(typeof options.query.caseSensitive === 'boolean'
|
|
987
|
+
? { case_sensitive: options.query.caseSensitive }
|
|
988
|
+
: {}),
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const ref = options.ref?.trim() || options.rev?.trim();
|
|
993
|
+
if (ref) {
|
|
994
|
+
body.ref = ref;
|
|
995
|
+
}
|
|
996
|
+
if (Array.isArray(options.paths) && options.paths.length > 0) {
|
|
997
|
+
body.paths = options.paths;
|
|
998
|
+
}
|
|
999
|
+
if (options.fileFilters) {
|
|
1000
|
+
body.file_filters = {
|
|
1001
|
+
...(options.fileFilters.includeGlobs
|
|
1002
|
+
? { include_globs: options.fileFilters.includeGlobs }
|
|
1003
|
+
: {}),
|
|
1004
|
+
...(options.fileFilters.excludeGlobs
|
|
1005
|
+
? { exclude_globs: options.fileFilters.excludeGlobs }
|
|
1006
|
+
: {}),
|
|
1007
|
+
...(options.fileFilters.extensionFilters
|
|
1008
|
+
? { extension_filters: options.fileFilters.extensionFilters }
|
|
1009
|
+
: {}),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
if (options.context) {
|
|
1013
|
+
body.context = {
|
|
1014
|
+
...(typeof options.context.before === 'number'
|
|
1015
|
+
? { before: options.context.before }
|
|
1016
|
+
: {}),
|
|
1017
|
+
...(typeof options.context.after === 'number'
|
|
1018
|
+
? { after: options.context.after }
|
|
1019
|
+
: {}),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
if (options.limits) {
|
|
1023
|
+
body.limits = {
|
|
1024
|
+
...(typeof options.limits.maxLines === 'number'
|
|
1025
|
+
? { max_lines: options.limits.maxLines }
|
|
1026
|
+
: {}),
|
|
1027
|
+
...(typeof options.limits.maxMatchesPerFile === 'number'
|
|
1028
|
+
? { max_matches_per_file: options.limits.maxMatchesPerFile }
|
|
1029
|
+
: {}),
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
if (options.pagination) {
|
|
1033
|
+
body.pagination = {
|
|
1034
|
+
...(typeof options.pagination.cursor === 'string' &&
|
|
1035
|
+
options.pagination.cursor.trim() !== ''
|
|
1036
|
+
? { cursor: options.pagination.cursor }
|
|
1037
|
+
: {}),
|
|
1038
|
+
...(typeof options.pagination.limit === 'number'
|
|
1039
|
+
? { limit: options.pagination.limit }
|
|
1040
|
+
: {}),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const response = await this.api.post({ path: 'repos/grep', body }, jwt);
|
|
1045
|
+
const raw = grepResponseSchema.parse(await response.json());
|
|
1046
|
+
|
|
1047
|
+
return {
|
|
1048
|
+
query: {
|
|
1049
|
+
pattern: raw.query.pattern,
|
|
1050
|
+
caseSensitive: raw.query.case_sensitive,
|
|
1051
|
+
},
|
|
1052
|
+
repo: {
|
|
1053
|
+
ref: raw.repo.ref,
|
|
1054
|
+
commit: raw.repo.commit,
|
|
1055
|
+
},
|
|
1056
|
+
matches: raw.matches.map(transformGrepFileMatch),
|
|
1057
|
+
nextCursor: raw.next_cursor ?? undefined,
|
|
1058
|
+
hasMore: raw.has_more,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async pullUpstream(options: PullUpstreamOptions = {}): Promise<void> {
|
|
1063
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
1064
|
+
const jwt = await this.generateJWT(this.id, {
|
|
1065
|
+
permissions: ['git:write'],
|
|
1066
|
+
ttl,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const body: Record<string, string> = {};
|
|
1070
|
+
|
|
1071
|
+
if (options.ref) {
|
|
1072
|
+
body.ref = options.ref;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const response = await this.api.post(
|
|
1076
|
+
{ path: 'repos/pull-upstream', body },
|
|
1077
|
+
jwt
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
if (response.status !== 202) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Pull Upstream failed: ${response.status} ${await response.text()}`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async createBranch(
|
|
1090
|
+
options: CreateBranchOptions
|
|
1091
|
+
): Promise<CreateBranchResult> {
|
|
1092
|
+
const baseBranch = options?.baseBranch?.trim();
|
|
1093
|
+
if (!baseBranch) {
|
|
1094
|
+
throw new Error('createBranch baseBranch is required');
|
|
1095
|
+
}
|
|
1096
|
+
const targetBranch = options?.targetBranch?.trim();
|
|
1097
|
+
if (!targetBranch) {
|
|
1098
|
+
throw new Error('createBranch targetBranch is required');
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
1102
|
+
const jwt = await this.generateJWT(this.id, {
|
|
1103
|
+
permissions: ['git:write'],
|
|
1104
|
+
ttl,
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
const body: Record<string, unknown> = {
|
|
1108
|
+
base_branch: baseBranch,
|
|
1109
|
+
target_branch: targetBranch,
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
if (options.baseIsEphemeral === true) {
|
|
1113
|
+
body.base_is_ephemeral = true;
|
|
1114
|
+
}
|
|
1115
|
+
if (options.targetIsEphemeral === true) {
|
|
1116
|
+
body.target_is_ephemeral = true;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const response = await this.api.post(
|
|
1120
|
+
{ path: 'repos/branches/create', body },
|
|
1121
|
+
jwt
|
|
1122
|
+
);
|
|
1123
|
+
const raw = createBranchResponseSchema.parse(await response.json());
|
|
1124
|
+
return transformCreateBranchResult(raw);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async restoreCommit(
|
|
1128
|
+
options: RestoreCommitOptions
|
|
1129
|
+
): Promise<RestoreCommitResult> {
|
|
1130
|
+
const targetBranch = options?.targetBranch?.trim();
|
|
1131
|
+
if (!targetBranch) {
|
|
1132
|
+
throw new Error('restoreCommit targetBranch is required');
|
|
1133
|
+
}
|
|
1134
|
+
if (targetBranch.startsWith('refs/')) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
'restoreCommit targetBranch must not include refs/ prefix'
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const targetCommitSha = options?.targetCommitSha?.trim();
|
|
1141
|
+
if (!targetCommitSha) {
|
|
1142
|
+
throw new Error('restoreCommit targetCommitSha is required');
|
|
1143
|
+
}
|
|
1144
|
+
const commitMessage = options?.commitMessage?.trim();
|
|
1145
|
+
|
|
1146
|
+
const authorName = options.author?.name?.trim();
|
|
1147
|
+
const authorEmail = options.author?.email?.trim();
|
|
1148
|
+
if (!authorName || !authorEmail) {
|
|
1149
|
+
throw new Error('restoreCommit author name and email are required');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
1153
|
+
const jwt = await this.generateJWT(this.id, {
|
|
1154
|
+
permissions: ['git:write'],
|
|
1155
|
+
ttl,
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
const metadata: Record<string, unknown> = {
|
|
1159
|
+
target_branch: targetBranch,
|
|
1160
|
+
target_commit_sha: targetCommitSha,
|
|
1161
|
+
author: {
|
|
1162
|
+
name: authorName,
|
|
1163
|
+
email: authorEmail,
|
|
1164
|
+
},
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
if (commitMessage) {
|
|
1168
|
+
metadata.commit_message = commitMessage;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const expectedHeadSha = options.expectedHeadSha?.trim();
|
|
1172
|
+
if (expectedHeadSha) {
|
|
1173
|
+
metadata.expected_head_sha = expectedHeadSha;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (options.committer) {
|
|
1177
|
+
const committerName = options.committer.name?.trim();
|
|
1178
|
+
const committerEmail = options.committer.email?.trim();
|
|
1179
|
+
if (!committerName || !committerEmail) {
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
'restoreCommit committer name and email are required when provided'
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
metadata.committer = {
|
|
1185
|
+
name: committerName,
|
|
1186
|
+
email: committerEmail,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const response = await this.api.post(
|
|
1191
|
+
{ path: 'repos/restore-commit', body: { metadata } },
|
|
1192
|
+
jwt,
|
|
1193
|
+
{
|
|
1194
|
+
allowedStatus: [...RESTORE_COMMIT_ALLOWED_STATUS],
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
const payload = await response.json();
|
|
1199
|
+
const parsed = parseRestoreCommitPayload(payload);
|
|
1200
|
+
if (parsed && 'ack' in parsed) {
|
|
1201
|
+
return buildRestoreCommitResult(parsed.ack);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const failure = parsed && 'failure' in parsed ? parsed.failure : undefined;
|
|
1205
|
+
const status =
|
|
1206
|
+
failure?.status ?? httpStatusToRestoreStatus(response.status);
|
|
1207
|
+
const message =
|
|
1208
|
+
failure?.message ??
|
|
1209
|
+
`Restore commit failed with HTTP ${response.status}` +
|
|
1210
|
+
(response.statusText ? ` ${response.statusText}` : '');
|
|
1211
|
+
|
|
1212
|
+
throw new RefUpdateError(message, {
|
|
1213
|
+
status,
|
|
1214
|
+
refUpdate: failure?.refUpdate,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
createCommit(options: CreateCommitOptions): CommitBuilder {
|
|
1219
|
+
const version = this.options.apiVersion ?? API_VERSION;
|
|
1220
|
+
const baseUrl =
|
|
1221
|
+
this.options.apiBaseUrl ??
|
|
1222
|
+
GitStorage.getDefaultAPIBaseUrl(this.options.name);
|
|
1223
|
+
const transport = new FetchCommitTransport({ baseUrl, version });
|
|
1224
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
1225
|
+
const builderOptions: CreateCommitOptions = {
|
|
1226
|
+
...options,
|
|
1227
|
+
ttl,
|
|
1228
|
+
};
|
|
1229
|
+
const getAuthToken = () =>
|
|
1230
|
+
this.generateJWT(this.id, {
|
|
1231
|
+
permissions: ['git:write'],
|
|
1232
|
+
ttl,
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
return createCommitBuilder({
|
|
1236
|
+
options: builderOptions,
|
|
1237
|
+
getAuthToken,
|
|
1238
|
+
transport,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async createCommitFromDiff(
|
|
1243
|
+
options: CreateCommitFromDiffOptions
|
|
1244
|
+
): Promise<CommitResult> {
|
|
1245
|
+
const version = this.options.apiVersion ?? API_VERSION;
|
|
1246
|
+
const baseUrl =
|
|
1247
|
+
this.options.apiBaseUrl ??
|
|
1248
|
+
GitStorage.getDefaultAPIBaseUrl(this.options.name);
|
|
1249
|
+
const transport = new FetchDiffCommitTransport({ baseUrl, version });
|
|
1250
|
+
const ttl = resolveCommitTtlSeconds(options);
|
|
1251
|
+
const requestOptions: CreateCommitFromDiffOptions = {
|
|
1252
|
+
...options,
|
|
1253
|
+
ttl,
|
|
1254
|
+
};
|
|
1255
|
+
const getAuthToken = () =>
|
|
1256
|
+
this.generateJWT(this.id, {
|
|
1257
|
+
permissions: ['git:write'],
|
|
1258
|
+
ttl,
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
return sendCommitFromDiff({
|
|
1262
|
+
options: requestOptions,
|
|
1263
|
+
getAuthToken,
|
|
1264
|
+
transport,
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1135
1267
|
}
|
|
1136
1268
|
|
|
1137
1269
|
export class GitStorage {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1270
|
+
private options: GitStorageOptions;
|
|
1271
|
+
private api: ApiFetcher;
|
|
1272
|
+
|
|
1273
|
+
constructor(options: GitStorageOptions) {
|
|
1274
|
+
if (
|
|
1275
|
+
!options ||
|
|
1276
|
+
options.name === undefined ||
|
|
1277
|
+
options.key === undefined ||
|
|
1278
|
+
options.name === null ||
|
|
1279
|
+
options.key === null
|
|
1280
|
+
) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
'GitStorage requires a name and key. Please check your configuration and try again.'
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (typeof options.name !== 'string' || options.name.trim() === '') {
|
|
1287
|
+
throw new Error('GitStorage name must be a non-empty string.');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (typeof options.key !== 'string' || options.key.trim() === '') {
|
|
1291
|
+
throw new Error('GitStorage key must be a non-empty string.');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const resolvedApiBaseUrl =
|
|
1295
|
+
options.apiBaseUrl ?? GitStorage.getDefaultAPIBaseUrl(options.name);
|
|
1296
|
+
const resolvedApiVersion = options.apiVersion ?? API_VERSION;
|
|
1297
|
+
const resolvedStorageBaseUrl =
|
|
1298
|
+
options.storageBaseUrl ??
|
|
1299
|
+
GitStorage.getDefaultStorageBaseUrl(options.name);
|
|
1300
|
+
const resolvedDefaultTtl = options.defaultTTL;
|
|
1301
|
+
|
|
1302
|
+
this.api = getApiInstance(resolvedApiBaseUrl, resolvedApiVersion);
|
|
1303
|
+
|
|
1304
|
+
this.options = {
|
|
1305
|
+
key: options.key,
|
|
1306
|
+
name: options.name,
|
|
1307
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
1308
|
+
apiVersion: resolvedApiVersion,
|
|
1309
|
+
storageBaseUrl: resolvedStorageBaseUrl,
|
|
1310
|
+
defaultTTL: resolvedDefaultTtl,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
static getDefaultAPIBaseUrl(name: string): string {
|
|
1315
|
+
return API_BASE_URL.replace('{{org}}', name);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
static getDefaultStorageBaseUrl(name: string): string {
|
|
1319
|
+
return STORAGE_BASE_URL.replace('{{org}}', name);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Create a new repository
|
|
1324
|
+
* @returns The created repository
|
|
1325
|
+
*/
|
|
1326
|
+
async createRepo(options?: CreateRepoOptions): Promise<Repo> {
|
|
1327
|
+
const repoId = options?.id || crypto.randomUUID();
|
|
1328
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
1329
|
+
const jwt = await this.generateJWT(repoId, {
|
|
1330
|
+
permissions: ['repo:write'],
|
|
1331
|
+
ttl,
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
const baseRepo = options?.baseRepo;
|
|
1335
|
+
const isFork = baseRepo ? 'id' in baseRepo : false;
|
|
1336
|
+
let baseRepoOptions: Record<string, unknown> | null = null;
|
|
1337
|
+
let resolvedDefaultBranch: string | undefined;
|
|
1338
|
+
|
|
1339
|
+
if (baseRepo) {
|
|
1340
|
+
if ('id' in baseRepo) {
|
|
1341
|
+
const baseRepoToken = await this.generateJWT(baseRepo.id, {
|
|
1342
|
+
permissions: ['git:read'],
|
|
1343
|
+
ttl,
|
|
1344
|
+
});
|
|
1345
|
+
baseRepoOptions = {
|
|
1346
|
+
provider: 'code',
|
|
1347
|
+
owner: this.options.name,
|
|
1348
|
+
name: baseRepo.id,
|
|
1349
|
+
operation: 'fork',
|
|
1350
|
+
auth: { token: baseRepoToken },
|
|
1351
|
+
...(baseRepo.ref ? { ref: baseRepo.ref } : {}),
|
|
1352
|
+
...(baseRepo.sha ? { sha: baseRepo.sha } : {}),
|
|
1353
|
+
};
|
|
1354
|
+
} else {
|
|
1355
|
+
baseRepoOptions = {
|
|
1356
|
+
provider: 'github',
|
|
1357
|
+
...snakecaseKeys(baseRepo as unknown as Record<string, unknown>),
|
|
1358
|
+
};
|
|
1359
|
+
resolvedDefaultBranch = baseRepo.defaultBranch;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Match backend priority: baseRepo.defaultBranch > options.defaultBranch > 'main'
|
|
1364
|
+
if (!resolvedDefaultBranch) {
|
|
1365
|
+
if (options?.defaultBranch) {
|
|
1366
|
+
resolvedDefaultBranch = options.defaultBranch;
|
|
1367
|
+
} else if (!isFork) {
|
|
1368
|
+
resolvedDefaultBranch = 'main';
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const createRepoPath =
|
|
1373
|
+
baseRepoOptions || resolvedDefaultBranch
|
|
1374
|
+
? {
|
|
1375
|
+
path: 'repos',
|
|
1376
|
+
body: {
|
|
1377
|
+
...(baseRepoOptions && { base_repo: baseRepoOptions }),
|
|
1378
|
+
...(resolvedDefaultBranch && {
|
|
1379
|
+
default_branch: resolvedDefaultBranch,
|
|
1380
|
+
}),
|
|
1381
|
+
},
|
|
1382
|
+
}
|
|
1383
|
+
: 'repos';
|
|
1384
|
+
|
|
1385
|
+
// Allow 409 so we can map it to a clearer error message
|
|
1386
|
+
const resp = await this.api.post(createRepoPath, jwt, {
|
|
1387
|
+
allowedStatus: [409],
|
|
1388
|
+
});
|
|
1389
|
+
if (resp.status === 409) {
|
|
1390
|
+
throw new Error('Repository already exists');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return new RepoImpl(
|
|
1394
|
+
repoId,
|
|
1395
|
+
resolvedDefaultBranch ?? 'main',
|
|
1396
|
+
this.options,
|
|
1397
|
+
this.generateJWT.bind(this)
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* List repositories for the authenticated organization
|
|
1403
|
+
* @returns Paginated repositories list
|
|
1404
|
+
*/
|
|
1405
|
+
async listRepos(options?: ListReposOptions): Promise<ListReposResult> {
|
|
1406
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
1407
|
+
const jwt = await this.generateJWT('org', {
|
|
1408
|
+
permissions: ['org:read'],
|
|
1409
|
+
ttl,
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
let params: Record<string, string> | undefined;
|
|
1413
|
+
if (options?.cursor || typeof options?.limit === 'number') {
|
|
1414
|
+
params = {};
|
|
1415
|
+
if (options.cursor) {
|
|
1416
|
+
params.cursor = options.cursor;
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof options.limit === 'number') {
|
|
1419
|
+
params.limit = options.limit.toString();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const response = await this.api.get({ path: 'repos', params }, jwt);
|
|
1424
|
+
const raw = listReposResponseSchema.parse(await response.json());
|
|
1425
|
+
return transformListReposResult({
|
|
1426
|
+
...raw,
|
|
1427
|
+
next_cursor: raw.next_cursor ?? undefined,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Find a repository by ID
|
|
1433
|
+
* @param options The search options
|
|
1434
|
+
* @returns The found repository
|
|
1435
|
+
*/
|
|
1436
|
+
async findOne(options: FindOneOptions): Promise<Repo | null> {
|
|
1437
|
+
const jwt = await this.generateJWT(options.id, {
|
|
1438
|
+
permissions: ['git:read'],
|
|
1439
|
+
ttl: DEFAULT_TOKEN_TTL_SECONDS,
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// Allow 404 to indicate "not found" without throwing
|
|
1443
|
+
const resp = await this.api.get('repo', jwt, { allowedStatus: [404] });
|
|
1444
|
+
if (resp.status === 404) {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
const body = (await resp.json()) as { default_branch?: string };
|
|
1448
|
+
const defaultBranch = body.default_branch ?? 'main';
|
|
1449
|
+
return new RepoImpl(
|
|
1450
|
+
options.id,
|
|
1451
|
+
defaultBranch,
|
|
1452
|
+
this.options,
|
|
1453
|
+
this.generateJWT.bind(this)
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Delete a repository by ID
|
|
1459
|
+
* @param options The delete options containing the repo ID
|
|
1460
|
+
* @returns The deletion result
|
|
1461
|
+
*/
|
|
1462
|
+
async deleteRepo(options: DeleteRepoOptions): Promise<DeleteRepoResult> {
|
|
1463
|
+
const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
|
|
1464
|
+
const jwt = await this.generateJWT(options.id, {
|
|
1465
|
+
permissions: ['repo:write'],
|
|
1466
|
+
ttl,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Allow 404 and 409 for clearer error handling
|
|
1470
|
+
const resp = await this.api.delete('repos/delete', jwt, {
|
|
1471
|
+
allowedStatus: [404, 409],
|
|
1472
|
+
});
|
|
1473
|
+
if (resp.status === 404) {
|
|
1474
|
+
throw new Error('Repository not found');
|
|
1475
|
+
}
|
|
1476
|
+
if (resp.status === 409) {
|
|
1477
|
+
throw new Error('Repository already deleted');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const body = (await resp.json()) as { repo_id: string; message: string };
|
|
1481
|
+
return {
|
|
1482
|
+
repoId: body.repo_id,
|
|
1483
|
+
message: body.message,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Get the current configuration
|
|
1489
|
+
* @returns The client configuration
|
|
1490
|
+
*/
|
|
1491
|
+
getConfig(): GitStorageOptions {
|
|
1492
|
+
return { ...this.options };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Generate a JWT token for git storage URL authentication
|
|
1497
|
+
* @private
|
|
1498
|
+
*/
|
|
1499
|
+
private async generateJWT(
|
|
1500
|
+
repoId: string,
|
|
1501
|
+
options?: GetRemoteURLOptions
|
|
1502
|
+
): Promise<string> {
|
|
1503
|
+
// Default permissions and TTL
|
|
1504
|
+
const permissions = options?.permissions || ['git:write', 'git:read'];
|
|
1505
|
+
const ttl = resolveInvocationTtlSeconds(
|
|
1506
|
+
options,
|
|
1507
|
+
this.options.defaultTTL ?? 365 * 24 * 60 * 60
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
// Create the JWT payload
|
|
1511
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1512
|
+
const payload = {
|
|
1513
|
+
iss: this.options.name,
|
|
1514
|
+
sub: '@pierre/storage',
|
|
1515
|
+
repo: repoId,
|
|
1516
|
+
scopes: permissions,
|
|
1517
|
+
iat: now,
|
|
1518
|
+
exp: now + ttl,
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
// Sign the JWT with the key as the secret
|
|
1522
|
+
// Using HS256 for symmetric signing with the key
|
|
1523
|
+
const key = await importPKCS8(this.options.key, 'ES256');
|
|
1524
|
+
// Sign the JWT with the key as the secret
|
|
1525
|
+
const jwt = await new SignJWT(payload)
|
|
1526
|
+
.setProtectedHeader({ alg: 'ES256', typ: 'JWT' })
|
|
1527
|
+
.sign(key);
|
|
1528
|
+
|
|
1529
|
+
return jwt;
|
|
1530
|
+
}
|
|
1380
1531
|
}
|
|
1381
1532
|
|
|
1382
1533
|
// Export a default client factory
|
|
1383
1534
|
export function createClient(options: GitStorageOptions): GitStorage {
|
|
1384
|
-
|
|
1535
|
+
return new GitStorage(options);
|
|
1385
1536
|
}
|
|
1386
1537
|
|
|
1387
1538
|
// Export CodeStorage as an alias for GitStorage
|