keyv-github 1.0.0 → 1.1.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/.github/workflows/release.yml +43 -0
- package/README.md +1 -1
- package/bun.lock +885 -0
- package/dist/index.d.mts +14 -1
- package/dist/index.mjs +50 -28
- package/examples/hello-today.ts +14 -12
- package/package.json +30 -25
- package/src/index.test.ts +545 -411
- package/src/index.ts +372 -295
- package/tsconfig.json +24 -24
package/src/index.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
import { Octokit } from "octokit";
|
|
3
2
|
import type { KeyvStoreAdapter, StoredData } from "keyv";
|
|
3
|
+
import { Octokit } from "octokit";
|
|
4
4
|
|
|
5
5
|
export interface KeyvGithubOptions {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
url: string;
|
|
7
|
+
branch?: string;
|
|
8
|
+
client?: Octokit | Octokit["rest"];
|
|
9
|
+
/** Customize the commit message. value is null for deletes. */
|
|
10
|
+
msg?: (key: string, value: string | null) => string;
|
|
11
|
+
/** clear() deletes every file in the repo and is disabled by default. Set to true to allow it. */
|
|
12
|
+
enableClear?: boolean;
|
|
13
|
+
/** Path prefix prepended to every key (e.g. 'data/'). Defaults to ''. */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
/** Path suffix appended to every key (e.g. '.json'). Defaults to ''. */
|
|
16
|
+
suffix?: string;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -18,291 +22,364 @@ export interface KeyvGithubOptions {
|
|
|
18
22
|
* Each key is a file path in the repo; the file content is the value.
|
|
19
23
|
* Example: new KeyvGithub("https://github.com/owner/repo/tree/main", { client })
|
|
20
24
|
*/
|
|
21
|
-
export default class KeyvGithub
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
25
|
+
export default class KeyvGithub
|
|
26
|
+
extends EventEmitter
|
|
27
|
+
implements KeyvStoreAdapter
|
|
28
|
+
{
|
|
29
|
+
opts: KeyvGithubOptions;
|
|
30
|
+
namespace?: string;
|
|
31
|
+
|
|
32
|
+
readonly owner: string;
|
|
33
|
+
readonly repo: string;
|
|
34
|
+
readonly ref: string;
|
|
35
|
+
/** Alias for {@link ref}. */
|
|
36
|
+
get branch(): string {
|
|
37
|
+
return this.ref;
|
|
38
|
+
}
|
|
39
|
+
rest: Octokit["rest"];
|
|
40
|
+
private msg: (key: string, value: string | null) => string;
|
|
41
|
+
readonly enableClear: boolean;
|
|
42
|
+
readonly prefix: string;
|
|
43
|
+
readonly suffix: string;
|
|
44
|
+
|
|
45
|
+
constructor(url: string, options: Omit<KeyvGithubOptions, "url"> = {}) {
|
|
46
|
+
super();
|
|
47
|
+
// github.com prefix is optional: "owner/repo/tree/branch" works too
|
|
48
|
+
// RegExp string avoids native-TS-preview parser issues with char classes containing ? or #
|
|
49
|
+
const match = url.match(
|
|
50
|
+
/(?:.*github\.com[/:])?([^/:]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^?#]+))?(?:[?#].*)?$/,
|
|
51
|
+
);
|
|
52
|
+
if (!match) throw new Error(`Invalid GitHub repo URL: ${url}`);
|
|
53
|
+
this.owner = match[1]!;
|
|
54
|
+
this.repo = match[2]!;
|
|
55
|
+
this.ref = options.branch ?? match[3] ?? "main";
|
|
56
|
+
this.opts = { url, ...options };
|
|
57
|
+
this.rest =
|
|
58
|
+
options.client instanceof Octokit
|
|
59
|
+
? options.client.rest
|
|
60
|
+
: (options.client ?? new Octokit().rest);
|
|
61
|
+
this.msg =
|
|
62
|
+
options.msg ??
|
|
63
|
+
((key, value) => (value === null ? `delete ${key}` : `update ${key}`));
|
|
64
|
+
this.enableClear = options.enableClear ?? false;
|
|
65
|
+
this.prefix = options.prefix ?? "";
|
|
66
|
+
this.suffix = options.suffix ?? "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Converts a user key to the GitHub file path. */
|
|
70
|
+
private toPath(key: string): string {
|
|
71
|
+
return this.prefix + key + this.suffix;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Converts a GitHub file path back to a user key.
|
|
76
|
+
* Returns null if the path does not match the configured prefix/suffix.
|
|
77
|
+
*/
|
|
78
|
+
private fromPath(path: string): string | null {
|
|
79
|
+
if (!path.startsWith(this.prefix) || !path.endsWith(this.suffix))
|
|
80
|
+
return null;
|
|
81
|
+
const end = this.suffix ? path.length - this.suffix.length : undefined;
|
|
82
|
+
return path.slice(this.prefix.length, end);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static isHttpError(e: unknown): e is { status: number } {
|
|
86
|
+
return (
|
|
87
|
+
typeof e === "object" &&
|
|
88
|
+
e !== null &&
|
|
89
|
+
"status" in e &&
|
|
90
|
+
typeof (e as Record<string, unknown>).status === "number"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private validatePath(path: string): void {
|
|
95
|
+
if (!path) throw new Error("Path must not be empty");
|
|
96
|
+
if (path.startsWith("/"))
|
|
97
|
+
throw new Error(`Path must not start with '/': ${path}`);
|
|
98
|
+
if (path.endsWith("/"))
|
|
99
|
+
throw new Error(`Path must not end with '/': ${path}`);
|
|
100
|
+
if (path.includes("//"))
|
|
101
|
+
throw new Error(`Path must not contain '//': ${path}`);
|
|
102
|
+
if (path.includes("\0"))
|
|
103
|
+
throw new Error(`Path must not contain null bytes: ${path}`);
|
|
104
|
+
if (path.split("/").some((seg) => seg === ".." || seg === "."))
|
|
105
|
+
throw new Error(`Path must not contain '.' or '..' segments: ${path}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async get<Value>(key: string): Promise<StoredData<Value> | undefined> {
|
|
109
|
+
this.validatePath(this.toPath(key));
|
|
110
|
+
try {
|
|
111
|
+
const { data } = await this.rest.repos.getContent({
|
|
112
|
+
owner: this.owner,
|
|
113
|
+
repo: this.repo,
|
|
114
|
+
path: this.toPath(key),
|
|
115
|
+
ref: this.ref,
|
|
116
|
+
});
|
|
117
|
+
if (Array.isArray(data) || data.type !== "file") return undefined;
|
|
118
|
+
return Buffer.from(data.content, "base64").toString(
|
|
119
|
+
"utf-8",
|
|
120
|
+
) as StoredData<Value>;
|
|
121
|
+
} catch (e: unknown) {
|
|
122
|
+
if (KeyvGithub.isHttpError(e) && e.status === 404) return undefined;
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async set(key: string, value: any, _ttl?: number): Promise<void> {
|
|
128
|
+
this.validatePath(this.toPath(key));
|
|
129
|
+
const path = this.toPath(key);
|
|
130
|
+
let sha: string | undefined;
|
|
131
|
+
try {
|
|
132
|
+
const { data } = await this.rest.repos.getContent({
|
|
133
|
+
owner: this.owner,
|
|
134
|
+
repo: this.repo,
|
|
135
|
+
path,
|
|
136
|
+
ref: this.ref,
|
|
137
|
+
});
|
|
138
|
+
if (!Array.isArray(data) && data.type === "file") sha = data.sha;
|
|
139
|
+
} catch (e: unknown) {
|
|
140
|
+
if (!KeyvGithub.isHttpError(e) || e.status !== 404) throw e;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await this.rest.repos.createOrUpdateFileContents({
|
|
144
|
+
owner: this.owner,
|
|
145
|
+
repo: this.repo,
|
|
146
|
+
path,
|
|
147
|
+
message: this.msg(path, value),
|
|
148
|
+
content: Buffer.from(String(value)).toString("base64"),
|
|
149
|
+
sha,
|
|
150
|
+
branch: this.ref,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async delete(key: string): Promise<boolean> {
|
|
155
|
+
this.validatePath(this.toPath(key));
|
|
156
|
+
const path = this.toPath(key);
|
|
157
|
+
try {
|
|
158
|
+
const { data } = await this.rest.repos.getContent({
|
|
159
|
+
owner: this.owner,
|
|
160
|
+
repo: this.repo,
|
|
161
|
+
path,
|
|
162
|
+
ref: this.ref,
|
|
163
|
+
});
|
|
164
|
+
if (Array.isArray(data) || data.type !== "file") return false;
|
|
165
|
+
await this.rest.repos.deleteFile({
|
|
166
|
+
owner: this.owner,
|
|
167
|
+
repo: this.repo,
|
|
168
|
+
path,
|
|
169
|
+
message: this.msg(path, null),
|
|
170
|
+
sha: data.sha,
|
|
171
|
+
branch: this.ref,
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
} catch (e: unknown) {
|
|
175
|
+
if (KeyvGithub.isHttpError(e) && e.status === 404) return false;
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async has(key: string): Promise<boolean> {
|
|
181
|
+
this.validatePath(this.toPath(key));
|
|
182
|
+
try {
|
|
183
|
+
const { data } = await this.rest.repos.getContent({
|
|
184
|
+
owner: this.owner,
|
|
185
|
+
repo: this.repo,
|
|
186
|
+
path: this.toPath(key),
|
|
187
|
+
ref: this.ref,
|
|
188
|
+
});
|
|
189
|
+
return !Array.isArray(data) && data.type === "file";
|
|
190
|
+
} catch (e: unknown) {
|
|
191
|
+
if (KeyvGithub.isHttpError(e) && e.status === 404) return false;
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Commit multiple file changes in one roundtrip: 5 API calls for any N.
|
|
198
|
+
* set entries: written inline into the tree (no separate blob creation).
|
|
199
|
+
* delete paths: removed by setting sha: null in the tree.
|
|
200
|
+
*/
|
|
201
|
+
private async _batchCommit(params: {
|
|
202
|
+
set?: [string, string][];
|
|
203
|
+
delete?: string[];
|
|
204
|
+
message: string;
|
|
205
|
+
}): Promise<void> {
|
|
206
|
+
const { set = [], delete: del = [], message } = params;
|
|
207
|
+
|
|
208
|
+
const { data: refData } = await this.rest.git.getRef({
|
|
209
|
+
owner: this.owner,
|
|
210
|
+
repo: this.repo,
|
|
211
|
+
ref: `heads/${this.ref}`,
|
|
212
|
+
});
|
|
213
|
+
const headSha = refData.object.sha;
|
|
214
|
+
|
|
215
|
+
const { data: commitData } = await this.rest.git.getCommit({
|
|
216
|
+
owner: this.owner,
|
|
217
|
+
repo: this.repo,
|
|
218
|
+
commit_sha: headSha,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const treeEntries = [
|
|
222
|
+
...set.map(([path, content]) => ({
|
|
223
|
+
path,
|
|
224
|
+
mode: "100644" as const,
|
|
225
|
+
type: "blob" as const,
|
|
226
|
+
content,
|
|
227
|
+
})),
|
|
228
|
+
...del.map((path) => ({
|
|
229
|
+
path,
|
|
230
|
+
mode: "100644" as const,
|
|
231
|
+
type: "blob" as const,
|
|
232
|
+
sha: null,
|
|
233
|
+
})),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const { data: newTree } = await this.rest.git.createTree({
|
|
237
|
+
owner: this.owner,
|
|
238
|
+
repo: this.repo,
|
|
239
|
+
base_tree: commitData.tree.sha,
|
|
240
|
+
tree: treeEntries,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const { data: newCommit } = await this.rest.git.createCommit({
|
|
244
|
+
owner: this.owner,
|
|
245
|
+
repo: this.repo,
|
|
246
|
+
message,
|
|
247
|
+
tree: newTree.sha,
|
|
248
|
+
parents: [headSha],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await this.rest.git.updateRef({
|
|
252
|
+
owner: this.owner,
|
|
253
|
+
repo: this.repo,
|
|
254
|
+
ref: `heads/${this.ref}`,
|
|
255
|
+
sha: newCommit.sha,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Keyv batch-set: writes multiple keys in a single commit (5 API calls total). */
|
|
260
|
+
async setMany(
|
|
261
|
+
values: Array<{ key: string; value: any; ttl?: number }>,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
if (values.length === 0) return;
|
|
264
|
+
for (const { key } of values) this.validatePath(this.toPath(key));
|
|
265
|
+
const entries: [string, string][] = values.map(({ key, value }) => [
|
|
266
|
+
this.toPath(key),
|
|
267
|
+
String(value),
|
|
268
|
+
]);
|
|
269
|
+
const message =
|
|
270
|
+
entries.length === 1
|
|
271
|
+
? this.msg(entries[0]![0], entries[0]![1])
|
|
272
|
+
: `batch update ${entries.length} files`;
|
|
273
|
+
await this._batchCommit({ set: entries, message });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Keyv batch-delete: deletes multiple keys in a single commit (7 API calls total).
|
|
278
|
+
* Returns true if any keys were deleted.
|
|
279
|
+
*/
|
|
280
|
+
async deleteMany(keys: string[]): Promise<boolean> {
|
|
281
|
+
if (keys.length === 0) return false;
|
|
282
|
+
for (const key of keys) this.validatePath(this.toPath(key));
|
|
283
|
+
|
|
284
|
+
const { data: refData } = await this.rest.git.getRef({
|
|
285
|
+
owner: this.owner,
|
|
286
|
+
repo: this.repo,
|
|
287
|
+
ref: `heads/${this.ref}`,
|
|
288
|
+
});
|
|
289
|
+
const { data: treeData } = await this.rest.git.getTree({
|
|
290
|
+
owner: this.owner,
|
|
291
|
+
repo: this.repo,
|
|
292
|
+
tree_sha: refData.object.sha,
|
|
293
|
+
recursive: "1",
|
|
294
|
+
});
|
|
295
|
+
const existingPaths = new Set(
|
|
296
|
+
treeData.tree
|
|
297
|
+
.filter(
|
|
298
|
+
(i: { type?: string; path?: string }) => i.type === "blob" && i.path,
|
|
299
|
+
)
|
|
300
|
+
.map((i: { path?: string }) => i.path!),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const toDelete = keys
|
|
304
|
+
.map((k) => this.toPath(k))
|
|
305
|
+
.filter((p) => existingPaths.has(p));
|
|
306
|
+
if (toDelete.length === 0) return false;
|
|
307
|
+
|
|
308
|
+
const message =
|
|
309
|
+
toDelete.length === 1
|
|
310
|
+
? this.msg(toDelete[0]!, null)
|
|
311
|
+
: `batch delete ${toDelete.length} files`;
|
|
312
|
+
await this._batchCommit({ delete: toDelete, message });
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async clear(): Promise<void> {
|
|
317
|
+
if (!this.enableClear)
|
|
318
|
+
throw new Error(
|
|
319
|
+
"clear() is disabled. Set enableClear: true in options to allow it.",
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const { data: refData } = await this.rest.git.getRef({
|
|
323
|
+
owner: this.owner,
|
|
324
|
+
repo: this.repo,
|
|
325
|
+
ref: `heads/${this.ref}`,
|
|
326
|
+
});
|
|
327
|
+
const { data: treeData } = await this.rest.git.getTree({
|
|
328
|
+
owner: this.owner,
|
|
329
|
+
repo: this.repo,
|
|
330
|
+
tree_sha: refData.object.sha,
|
|
331
|
+
recursive: "1",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const allPaths = treeData.tree
|
|
335
|
+
.filter(
|
|
336
|
+
(i) =>
|
|
337
|
+
i.type === "blob" &&
|
|
338
|
+
i.path &&
|
|
339
|
+
i.path.startsWith(this.prefix) &&
|
|
340
|
+
i.path.endsWith(this.suffix),
|
|
341
|
+
)
|
|
342
|
+
.map((i) => i.path!);
|
|
343
|
+
|
|
344
|
+
if (allPaths.length > 0) {
|
|
345
|
+
await this._batchCommit({
|
|
346
|
+
delete: allPaths,
|
|
347
|
+
message: `clear: remove ${allPaths.length} files`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async *iterator<Value>(
|
|
353
|
+
prefix?: string,
|
|
354
|
+
): AsyncGenerator<[string, Value | undefined]> {
|
|
355
|
+
const { data: refData } = await this.rest.git.getRef({
|
|
356
|
+
owner: this.owner,
|
|
357
|
+
repo: this.repo,
|
|
358
|
+
ref: `heads/${this.ref}`,
|
|
359
|
+
});
|
|
360
|
+
const { data: treeData } = await this.rest.git.getTree({
|
|
361
|
+
owner: this.owner,
|
|
362
|
+
repo: this.repo,
|
|
363
|
+
tree_sha: refData.object.sha,
|
|
364
|
+
recursive: "1",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const pathPrefix = this.prefix + (prefix ?? "");
|
|
368
|
+
const files = treeData.tree.filter(
|
|
369
|
+
(item) =>
|
|
370
|
+
item.type === "blob" &&
|
|
371
|
+
item.path &&
|
|
372
|
+
item.path.startsWith(pathPrefix) &&
|
|
373
|
+
item.path.endsWith(this.suffix),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
if (file.path) {
|
|
378
|
+
const key = this.fromPath(file.path);
|
|
379
|
+
if (key === null || key === "") continue;
|
|
380
|
+
const value = await this.get<Value>(key);
|
|
381
|
+
if (value !== undefined) yield [key, value as Value];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
308
385
|
}
|