@optique/git 0.9.0-dev.215 → 0.9.0-dev.217

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/dist/index.js CHANGED
@@ -2,97 +2,43 @@ import { message, text } from "@optique/core/message";
2
2
  import { ensureNonEmptyString } from "@optique/core/nonempty";
3
3
  import * as git from "isomorphic-git";
4
4
  import { expandOid, listBranches, listRemotes, listTags, readObject, resolveRef } from "isomorphic-git";
5
+ import * as fs from "node:fs/promises";
5
6
  import process from "node:process";
6
7
 
7
8
  //#region src/index.ts
9
+ const gitFs = {
10
+ readFile: fs.readFile,
11
+ writeFile: fs.writeFile,
12
+ mkdir: fs.mkdir,
13
+ rmdir: fs.rmdir,
14
+ unlink: fs.unlink,
15
+ readdir: fs.readdir,
16
+ readlink: fs.readlink,
17
+ symlink: fs.symlink,
18
+ stat: fs.stat,
19
+ lstat: fs.lstat
20
+ };
8
21
  const METAVAR_BRANCH = "BRANCH";
9
22
  const METAVAR_TAG = "TAG";
10
23
  const METAVAR_REMOTE = "REMOTE";
11
- let defaultFs = null;
12
- let fsLoading = null;
13
- async function getDefaultFs() {
14
- if (defaultFs) return await defaultFs;
15
- if (fsLoading) return await fsLoading;
16
- fsLoading = (async () => {
17
- const nodeFs = await import("node:fs/promises");
18
- const { TextDecoder } = await import("node:util");
19
- const decoder = new TextDecoder();
20
- defaultFs = {
21
- async readFile(path) {
22
- const data = await nodeFs.readFile(path);
23
- if (path.endsWith("/index") || path.endsWith(".idx")) return data;
24
- return decoder.decode(data);
25
- },
26
- async writeFile(path, data) {
27
- await nodeFs.writeFile(path, data);
28
- },
29
- async mkdir(path, options) {
30
- await nodeFs.mkdir(path, options);
31
- },
32
- async rmdir(path, options) {
33
- await nodeFs.rmdir(path, options);
34
- },
35
- async unlink(path) {
36
- await nodeFs.unlink(path);
37
- },
38
- async readdir(path) {
39
- const entries = await nodeFs.readdir(path, { withFileTypes: false });
40
- return entries.filter((e) => typeof e === "string");
41
- },
42
- async lstat(path) {
43
- return await nodeFs.lstat(path);
44
- },
45
- async stat(path) {
46
- return await nodeFs.stat(path);
47
- },
48
- async readlink(path) {
49
- return await nodeFs.readlink(path);
50
- },
51
- async symlink(target, path) {
52
- await nodeFs.symlink(target, path, "file");
53
- },
54
- async chmod(path, mode) {
55
- await nodeFs.chmod(path, mode);
56
- },
57
- async chown(path, uid, gid) {
58
- await nodeFs.chown(path, uid, gid);
59
- },
60
- async rename(oldPath, newPath) {
61
- await nodeFs.rename(oldPath, newPath);
62
- },
63
- async copyFile(srcPath, destPath) {
64
- await nodeFs.copyFile(srcPath, destPath);
65
- },
66
- async exists(path) {
67
- try {
68
- await nodeFs.stat(path);
69
- return true;
70
- } catch {
71
- return false;
72
- }
73
- }
74
- };
75
- return defaultFs;
76
- })();
77
- return fsLoading;
24
+ function getRepoDir(dirOption) {
25
+ return dirOption ?? (typeof process !== "undefined" ? process.cwd() : ".");
78
26
  }
79
27
  function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
80
28
  return {
81
29
  $mode: "async",
82
30
  metavar,
83
- async parse(input) {
84
- const fs = options?.fs ?? await getDefaultFs();
85
- const dir = options?.dir ?? (typeof process !== "undefined" ? process.cwd() : ".");
31
+ parse(input) {
32
+ const dir = getRepoDir(options?.dir);
86
33
  ensureNonEmptyString(metavar);
87
- return parseFn(fs, dir, input);
34
+ return parseFn(dir, input);
88
35
  },
89
36
  format(value) {
90
37
  return value;
91
38
  },
92
39
  async *suggest(prefix) {
93
- const fs = options?.fs ?? await getDefaultFs();
94
- const dir = options?.dir ?? (typeof process !== "undefined" ? process.cwd() : ".");
95
- if (suggestFn) yield* suggestFn(fs, dir, prefix);
40
+ const dir = getRepoDir(options?.dir);
41
+ if (suggestFn) yield* suggestFn(dir, prefix);
96
42
  }
97
43
  };
98
44
  }
@@ -116,10 +62,10 @@ function createAsyncValueParser(options, metavar, parseFn, suggestFn) {
116
62
  */
117
63
  function gitBranch(options) {
118
64
  const metavar = options?.metavar ?? METAVAR_BRANCH;
119
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
65
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
120
66
  try {
121
67
  const branches = await git.listBranches({
122
- fs,
68
+ fs: gitFs,
123
69
  dir
124
70
  });
125
71
  if (branches.includes(input)) return {
@@ -136,10 +82,10 @@ function gitBranch(options) {
136
82
  error: message`Failed to list branches. Ensure ${text(dir)} is a valid git repository.`
137
83
  };
138
84
  }
139
- }, async function* suggestBranch(fs, dir, prefix) {
85
+ }, async function* suggestBranch(dir, prefix) {
140
86
  try {
141
87
  const branches = await git.listBranches({
142
- fs,
88
+ fs: gitFs,
143
89
  dir
144
90
  });
145
91
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
@@ -170,10 +116,10 @@ function gitBranch(options) {
170
116
  */
171
117
  function gitRemoteBranch(remote, options) {
172
118
  const metavar = options?.metavar ?? METAVAR_BRANCH;
173
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
119
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
174
120
  try {
175
121
  const branches = await git.listBranches({
176
- fs,
122
+ fs: gitFs,
177
123
  dir,
178
124
  remote
179
125
  });
@@ -191,10 +137,10 @@ function gitRemoteBranch(remote, options) {
191
137
  error: message`Failed to list remote branches. Ensure remote ${text(remote)} exists.`
192
138
  };
193
139
  }
194
- }, async function* suggestRemoteBranch(fs, dir, prefix) {
140
+ }, async function* suggestRemoteBranch(dir, prefix) {
195
141
  try {
196
142
  const branches = await git.listBranches({
197
- fs,
143
+ fs: gitFs,
198
144
  dir,
199
145
  remote
200
146
  });
@@ -208,27 +154,16 @@ function gitRemoteBranch(remote, options) {
208
154
  /**
209
155
  * Creates a value parser that validates tag names.
210
156
  *
211
- * This parser uses isomorphic-git to verify that the provided input
212
- * matches an existing tag in the repository.
213
- *
214
157
  * @param options Configuration options for the parser.
215
158
  * @returns A value parser that accepts existing tag names.
216
159
  * @since 0.9.0
217
- *
218
- * @example
219
- * ~~~~ typescript
220
- * import { gitTag } from "@optique/git";
221
- * import { option } from "@optique/core/primitives";
222
- *
223
- * const parser = option("-t", "--tag", gitTag());
224
- * ~~~~
225
160
  */
226
161
  function gitTag(options) {
227
162
  const metavar = options?.metavar ?? METAVAR_TAG;
228
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
163
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
229
164
  try {
230
165
  const tags = await git.listTags({
231
- fs,
166
+ fs: gitFs,
232
167
  dir
233
168
  });
234
169
  if (tags.includes(input)) return {
@@ -245,10 +180,10 @@ function gitTag(options) {
245
180
  error: message`Failed to list tags. Ensure ${text(dir)} is a valid git repository.`
246
181
  };
247
182
  }
248
- }, async function* suggestTag(fs, dir, prefix) {
183
+ }, async function* suggestTag(dir, prefix) {
249
184
  try {
250
185
  const tags = await git.listTags({
251
- fs,
186
+ fs: gitFs,
252
187
  dir
253
188
  });
254
189
  for (const tag of tags) if (tag.startsWith(prefix)) yield {
@@ -261,38 +196,26 @@ function gitTag(options) {
261
196
  /**
262
197
  * Creates a value parser that validates remote names.
263
198
  *
264
- * This parser uses isomorphic-git to verify that the provided input
265
- * matches an existing remote in the repository.
266
- *
267
199
  * @param options Configuration options for the parser.
268
200
  * @returns A value parser that accepts existing remote names.
269
201
  * @since 0.9.0
270
- *
271
- * @example
272
- * ~~~~ typescript
273
- * import { gitRemote } from "@optique/git";
274
- * import { option } from "@optique/core/primitives";
275
- *
276
- * const parser = option("-r", "--remote", gitRemote());
277
- * ~~~~
278
202
  */
279
203
  function gitRemote(options) {
280
204
  const metavar = options?.metavar ?? METAVAR_REMOTE;
281
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
205
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
282
206
  try {
283
207
  const remotes = await git.listRemotes({
284
- fs,
208
+ fs: gitFs,
285
209
  dir
286
210
  });
287
- const remoteNames = [];
288
- for (const r of remotes) if ("remote" in r && typeof r.remote === "string") remoteNames.push(r.remote);
289
- if (remoteNames.includes(input)) return {
211
+ const names = remotes.map((r) => r.remote);
212
+ if (names.includes(input)) return {
290
213
  success: true,
291
214
  value: input
292
215
  };
293
216
  return {
294
217
  success: false,
295
- error: message`Remote ${text(input)} does not exist. Available remotes: ${remoteNames.join(", ")}`
218
+ error: message`Remote ${text(input)} does not exist. Available remotes: ${names.join(", ")}`
296
219
  };
297
220
  } catch {
298
221
  return {
@@ -300,13 +223,13 @@ function gitRemote(options) {
300
223
  error: message`Failed to list remotes. Ensure ${text(dir)} is a valid git repository.`
301
224
  };
302
225
  }
303
- }, async function* suggestRemote(fs, dir, prefix) {
226
+ }, async function* suggestRemote(dir, prefix) {
304
227
  try {
305
228
  const remotes = await git.listRemotes({
306
- fs,
229
+ fs: gitFs,
307
230
  dir
308
231
  });
309
- for (const r of remotes) if ("remote" in r && typeof r.remote === "string" && r.remote.startsWith(prefix)) yield {
232
+ for (const r of remotes) if (r.remote.startsWith(prefix)) yield {
310
233
  kind: "literal",
311
234
  text: r.remote
312
235
  };
@@ -316,35 +239,34 @@ function gitRemote(options) {
316
239
  /**
317
240
  * Creates a value parser that validates commit SHAs.
318
241
  *
319
- * This parser uses isomorphic-git to verify that the provided input
320
- * is a valid commit SHA (full or shortened) that exists in the repository.
242
+ * This parser resolves the provided commit reference to its full 40-character
243
+ * OID.
321
244
  *
322
245
  * @param options Configuration options for the parser.
323
- * @returns A value parser that accepts valid commit SHAs.
246
+ * @returns A value parser that accepts existing commit SHAs.
324
247
  * @since 0.9.0
325
- *
326
- * @example
327
- * ~~~~ typescript
328
- * import { gitCommit } from "@optique/git";
329
- * import { option } from "@optique/core/primitives";
330
- *
331
- * const parser = option("-c", "--commit", gitCommit());
332
- * ~~~~
333
248
  */
334
249
  function gitCommit(options) {
335
250
  const metavar = options?.metavar ?? "COMMIT";
336
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
251
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
252
+ try {
253
+ ensureNonEmptyString(input);
254
+ } catch {
255
+ return {
256
+ success: false,
257
+ error: message`Invalid commit SHA: ${text(input)}`
258
+ };
259
+ }
260
+ if (input.length < 4 || input.length > 40) return {
261
+ success: false,
262
+ error: message`Commit ${text(input)} must be between 4 and 40 characters.`
263
+ };
337
264
  try {
338
265
  const oid = await git.expandOid({
339
- fs,
266
+ fs: gitFs,
340
267
  dir,
341
268
  oid: input
342
269
  });
343
- await git.readObject({
344
- fs,
345
- dir,
346
- oid
347
- });
348
270
  return {
349
271
  success: true,
350
272
  value: oid
@@ -355,80 +277,63 @@ function gitCommit(options) {
355
277
  error: message`Commit ${text(input)} does not exist. Provide a valid commit SHA.`
356
278
  };
357
279
  }
358
- }, async function* suggestCommit(fs, dir, prefix) {
359
- try {
360
- const branches = await git.listBranches({
361
- fs,
362
- dir
363
- });
364
- const commits = [];
365
- for (const branch of branches.slice(0, 10)) try {
366
- const oid = await git.resolveRef({
367
- fs,
368
- dir,
369
- ref: branch
370
- });
371
- if (oid.startsWith(prefix)) commits.push(oid);
372
- } catch {}
373
- for (const commit of [...new Set(commits)].slice(0, 10)) yield {
374
- kind: "literal",
375
- text: commit
376
- };
377
- } catch {}
378
280
  });
379
281
  }
380
282
  /**
381
- * Creates a value parser that validates any git reference
382
- * (branches, tags, or commits).
283
+ * Creates a value parser that validates any git reference.
383
284
  *
384
- * This parser uses isomorphic-git to verify that the provided input
385
- * resolves to a valid git reference (branch, tag, or commit SHA).
285
+ * Accepts branch names, tag names, or commit SHAs and resolves them to the
286
+ * corresponding commit OID.
386
287
  *
387
288
  * @param options Configuration options for the parser.
388
- * @returns A value parser that accepts branches, tags, or commit SHAs.
289
+ * @returns A value parser that accepts any git reference.
389
290
  * @since 0.9.0
390
- *
391
- * @example
392
- * ~~~~ typescript
393
- * import { gitRef } from "@optique/git";
394
- * import { option } from "@optique/core/primitives";
395
- *
396
- * const parser = option("--ref", gitRef());
397
- * ~~~~
398
291
  */
399
292
  function gitRef(options) {
400
293
  const metavar = options?.metavar ?? "REF";
401
- return createAsyncValueParser(options, metavar, async (fs, dir, input) => {
294
+ return createAsyncValueParser(options, metavar, async (dir, input) => {
402
295
  try {
403
- const oid = await git.resolveRef({
404
- fs,
296
+ const resolved = await git.resolveRef({
297
+ fs: gitFs,
405
298
  dir,
406
299
  ref: input
407
300
  });
408
301
  return {
409
302
  success: true,
410
- value: oid
303
+ value: resolved
411
304
  };
412
305
  } catch {
413
- return {
414
- success: false,
415
- error: message`Reference ${text(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
416
- };
306
+ try {
307
+ const oid = await git.expandOid({
308
+ fs: gitFs,
309
+ dir,
310
+ oid: input
311
+ });
312
+ return {
313
+ success: true,
314
+ value: oid
315
+ };
316
+ } catch {
317
+ return {
318
+ success: false,
319
+ error: message`Reference ${text(input)} does not exist. Provide a valid branch, tag, or commit SHA.`
320
+ };
321
+ }
417
322
  }
418
- }, async function* suggestRef(fs, dir, prefix) {
323
+ }, async function* suggestRef(dir, prefix) {
419
324
  try {
420
325
  const branches = await git.listBranches({
421
- fs,
326
+ fs: gitFs,
327
+ dir
328
+ });
329
+ const tags = await git.listTags({
330
+ fs: gitFs,
422
331
  dir
423
332
  });
424
333
  for (const branch of branches) if (branch.startsWith(prefix)) yield {
425
334
  kind: "literal",
426
335
  text: branch
427
336
  };
428
- const tags = await git.listTags({
429
- fs,
430
- dir
431
- });
432
337
  for (const tag of tags) if (tag.startsWith(prefix)) yield {
433
338
  kind: "literal",
434
339
  text: tag
@@ -437,51 +342,20 @@ function gitRef(options) {
437
342
  });
438
343
  }
439
344
  /**
440
- * Creates a factory for git parsers with shared configuration.
441
- *
442
- * This function returns an object with methods for creating individual git
443
- * parsers that share the same configuration (filesystem and directory).
345
+ * Creates a set of git parsers with shared configuration.
444
346
  *
445
- * @param options Shared configuration options for all parsers.
446
- * @returns An object with methods for creating individual git parsers.
347
+ * @param options Shared configuration for the parsers.
348
+ * @returns An object containing git parsers.
447
349
  * @since 0.9.0
448
- *
449
- * @example
450
- * ~~~~ typescript
451
- * import { createGitParsers } from "@optique/git";
452
- *
453
- * const git = createGitParsers({ dir: "/path/to/repo" });
454
- *
455
- * const branchParser = git.branch();
456
- * const tagParser = git.tag();
457
- * ~~~~
458
350
  */
459
351
  function createGitParsers(options) {
460
352
  return {
461
- branch: (parserOptions) => gitBranch({
462
- ...options,
463
- ...parserOptions
464
- }),
465
- remoteBranch: (remote, parserOptions) => gitRemoteBranch(remote, {
466
- ...options,
467
- ...parserOptions
468
- }),
469
- tag: (parserOptions) => gitTag({
470
- ...options,
471
- ...parserOptions
472
- }),
473
- remote: (parserOptions) => gitRemote({
474
- ...options,
475
- ...parserOptions
476
- }),
477
- commit: (parserOptions) => gitCommit({
478
- ...options,
479
- ...parserOptions
480
- }),
481
- ref: (parserOptions) => gitRef({
482
- ...options,
483
- ...parserOptions
484
- })
353
+ branch: (branchOptions) => gitBranch(branchOptions ?? options),
354
+ remoteBranch: (remote, branchOptions) => gitRemoteBranch(remote, branchOptions ?? options),
355
+ tag: (tagOptions) => gitTag(tagOptions ?? options),
356
+ remote: (remoteOptions) => gitRemote(remoteOptions ?? options),
357
+ commit: (commitOptions) => gitCommit(commitOptions ?? options),
358
+ ref: (refOptions) => gitRef(refOptions ?? options)
485
359
  };
486
360
  }
487
361
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/git",
3
- "version": "0.9.0-dev.215+13b302c0",
3
+ "version": "0.9.0-dev.217+7fc9df46",
4
4
  "description": "Git value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",