Blame Raw
Cohee · e3f41666 · · 138 lines (3.6 KB)
1 contributor
1import fs from 'node:fs';
2
3import { sync as commandExistsSync } from 'command-exists';
4import git from 'isomorphic-git';
5import http from 'isomorphic-git/http/node';
6import simpleGit from 'simple-git';
7
8/** @type {{ AUTO: 'auto', SYSTEM: 'system', BUILTIN: 'builtin' }} */
9export const GIT_BACKENDS = {
10 AUTO: 'auto',
11 SYSTEM: 'system',
12 BUILTIN: 'builtin',
13};
14
15/**
16 * @param {string | undefined | null} preferredBackend
17 * @returns {'system' | 'builtin'}
18 */
19function resolveBackend(preferredBackend) {
20 const normalized = typeof preferredBackend === 'string' ? preferredBackend.trim().toLowerCase() : GIT_BACKENDS.AUTO;
21 const backend = normalized === GIT_BACKENDS.SYSTEM
22 ? GIT_BACKENDS.SYSTEM
23 : normalized === GIT_BACKENDS.BUILTIN
24 ? GIT_BACKENDS.BUILTIN
25 : GIT_BACKENDS.AUTO;
26 const systemGitAvailable = commandExistsSync('git');
27
28 if (backend === GIT_BACKENDS.SYSTEM && !systemGitAvailable) {
29 throw new Error('System git backend is configured, but no git binary was found in PATH.');
30 }
31
32 if (backend === GIT_BACKENDS.SYSTEM || (backend === GIT_BACKENDS.AUTO && systemGitAvailable)) {
33 return GIT_BACKENDS.SYSTEM;
34 }
35
36 return GIT_BACKENDS.BUILTIN;
37}
38
39/**
40 * @typedef {object} GitCloneOptions
41 * @property {number} [depth]
42 * @property {string} [branch]
43 */
44
45const SUPPORTED_CLONE_OPTIONS = new Set(['depth', 'branch']);
46
47/**
48 * @param {GitCloneOptions} [options]
49 * @returns {{ depth?: number, branch?: string }}
50 */
51function normalizeCloneOptions(options = {}) {
52 for (const key of Object.keys(options)) {
53 if (!SUPPORTED_CLONE_OPTIONS.has(key)) {
54 throw new Error(`Unsupported clone option: ${key}`);
55 }
56 }
57 return { depth: options.depth, branch: options.branch };
58}
59
60/**
61 * @typedef {object} GitClient
62 * @property {'system' | 'builtin'} backend
63 * @property {(url: string, localPath: string, options?: GitCloneOptions) => Promise<void>} clone
64 */
65
66/**
67 * @param {{ backend?: string }} [options]
68 * @returns {GitClient}
69 */
70export function createGitClient(options = {}) {
71 const backend = resolveBackend(options.backend);
72 if (backend === GIT_BACKENDS.SYSTEM) {
73 return new SimpleGitClient();
74 }
75
76 return new IsomorphicGitClient();
77}
78
79/**
80 * @implements {GitClient}
81 */
82class SimpleGitClient {
83 constructor() {
84 this.backend = GIT_BACKENDS.SYSTEM;
85 this.git = simpleGit();
86 }
87
88 /**
89 * @param {string} url
90 * @param {string} localPath
91 * @param {GitCloneOptions} [options]
92 * @returns {Promise<void>}
93 */
94 async clone(url, localPath, options = {}) {
95 const { depth, branch } = normalizeCloneOptions(options);
96 /** @type {Record<string, any>} */
97 const cloneOptions = {};
98
99 if (depth !== undefined) {
100 cloneOptions['--depth'] = depth;
101 }
102
103 if (branch) {
104 cloneOptions['--branch'] = branch;
105 }
106
107 await this.git.clone(url, localPath, cloneOptions);
108 }
109}
110
111/**
112 * @implements {GitClient}
113 */
114class IsomorphicGitClient {
115 constructor() {
116 this.backend = GIT_BACKENDS.BUILTIN;
117 }
118
119 /**
120 * @param {string} url
121 * @param {string} localPath
122 * @param {GitCloneOptions} [options]
123 * @returns {Promise<void>}
124 */
125 async clone(url, localPath, options = {}) {
126 const { depth, branch } = normalizeCloneOptions(options);
127
128 await git.clone({
129 fs,
130 http,
131 dir: localPath,
132 url,
133 depth,
134 ref: branch,
135 singleBranch: depth !== undefined || Boolean(branch),
136 });
137 }
138}