-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
248 lines (228 loc) · 8.33 KB
/
index.js
File metadata and controls
248 lines (228 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
/**
* @fileoverview GitPlumbing - The primary domain service for Git plumbing operations
*/
import { RunnerOptionsSchema, DEFAULT_MAX_BUFFER_SIZE } from './src/ports/RunnerOptionsSchema.js';
// Value Objects
import GitSha from './src/domain/value-objects/GitSha.js';
import GitRef from './src/domain/value-objects/GitRef.js';
import GitSignature from './src/domain/value-objects/GitSignature.js';
import CommandRetryPolicy from './src/domain/value-objects/CommandRetryPolicy.js';
// Entities
import GitBlob from './src/domain/entities/GitBlob.js';
import GitTree from './src/domain/entities/GitTree.js';
// Services
import GitPlumbingError from './src/domain/errors/GitPlumbingError.js';
import InvalidArgumentError from './src/domain/errors/InvalidArgumentError.js';
import CommandSanitizer from './src/domain/services/CommandSanitizer.js';
import ShellRunnerFactory from './src/infrastructure/factories/ShellRunnerFactory.js';
import GitRepositoryService from './src/domain/services/GitRepositoryService.js';
import ExecutionOrchestrator from './src/domain/services/ExecutionOrchestrator.js';
import GitBinaryChecker from './src/domain/services/GitBinaryChecker.js';
import GitCommandBuilder from './src/domain/services/GitCommandBuilder.js';
import GitPersistenceService from './src/domain/services/GitPersistenceService.js';
// Infrastructure
import GitStream from './src/infrastructure/GitStream.js';
/**
* Named exports for public API
*/
export {
GitSha,
GitRef,
GitSignature,
GitBlob,
GitTree,
GitPersistenceService,
GitCommandBuilder,
ShellRunnerFactory,
GitPlumbingError,
InvalidArgumentError,
CommandRetryPolicy,
GitRepositoryService
};
/**
* GitPlumbing provides a low-level, robust interface for executing Git plumbing commands.
* Adheres to Hexagonal Architecture by defining its dependencies via ports (CommandRunner).
*/
export default class GitPlumbing {
/**
* @param {Object} options
* @param {import('./src/ports/CommandRunnerPort.js').CommandRunner} options.runner - The functional port for shell execution.
* @param {string} [options.cwd='.'] - The working directory for git operations.
* @param {CommandSanitizer} [options.sanitizer] - Injected sanitizer.
* @param {ExecutionOrchestrator} [options.orchestrator] - Injected orchestrator.
* @param {Object} [options.fsAdapter] - Optional filesystem adapter for CWD validation.
*/
constructor({
runner,
cwd = '.',
sanitizer = new CommandSanitizer(),
orchestrator = new ExecutionOrchestrator(),
fsAdapter = null
}) {
if (typeof runner !== 'function') {
throw new InvalidArgumentError('A functional runner port is required for GitPlumbing', 'GitPlumbing.constructor');
}
let resolvedCwd = cwd;
if (fsAdapter) {
try {
resolvedCwd = fsAdapter.resolve(cwd);
if (typeof fsAdapter.isDirectory === 'function' && !fsAdapter.isDirectory(resolvedCwd)) {
throw new Error('Not a directory');
}
} catch (err) {
throw new InvalidArgumentError(`Invalid working directory: ${cwd}`, 'GitPlumbing.constructor', { cwd, error: err.message });
}
}
/** @private */
this.runner = runner;
/** @private */
this.cwd = resolvedCwd;
/** @private */
this.sanitizer = sanitizer;
/** @private */
this.orchestrator = orchestrator;
/** @private */
this.checker = new GitBinaryChecker({ plumbing: this });
}
/**
* Orchestrates a full commit sequence from content to reference update.
* Delegates to GitRepositoryService.
* @param {Object} options
* @returns {Promise<GitSha>} The resulting commit SHA.
*/
async commit(options) {
const repo = new GitRepositoryService({ plumbing: this });
return repo.createCommitFromFiles(options);
}
/**
* Factory method to create an instance with the default shell runner for the current environment.
* @param {Object} [options]
* @param {string} [options.cwd]
* @param {string} [options.env] - Override environment detection.
* @param {CommandSanitizer} [options.sanitizer]
* @param {ExecutionOrchestrator} [options.orchestrator]
* @returns {GitPlumbing}
*/
static createDefault(options = {}) {
const env = options.env || globalThis.process?.env?.GIT_PLUMBING_ENV;
return new GitPlumbing({
runner: ShellRunnerFactory.create({ env }),
...options
});
}
/**
* Factory method to create a high-level GitRepositoryService.
* @param {Object} [options]
* @returns {GitRepositoryService}
*/
static createRepository(options = {}) {
const plumbing = GitPlumbing.createDefault(options);
return new GitRepositoryService({ plumbing });
}
/**
* Verifies that the git binary is available and the CWD is a valid repository.
* @throws {GitPlumbingError}
*/
async verifyInstallation() {
await this.checker.check();
const isInside = await this.checker.isInsideWorkTree();
if (!isInside) {
throw new GitPlumbingError('Not inside a git work tree', 'GitPlumbing.verifyInstallation', {
code: 'GIT_NOT_IN_WORK_TREE'
});
}
}
/**
* Executes a git command asynchronously and buffers the result.
* Includes retry logic for lock contention and telemetry (Trace ID, Latency).
* @param {Object} options
* @param {string[]} options.args - Array of git arguments.
* @param {string|Uint8Array} [options.input] - Optional stdin input.
* @param {number} [options.maxBytes=DEFAULT_MAX_BUFFER_SIZE] - Maximum buffer size.
* @param {string} [options.traceId] - Correlation ID for the command.
* @param {CommandRetryPolicy} [options.retryPolicy] - Strategy for retrying failed commands.
* @returns {Promise<string>} - The trimmed stdout.
* @throws {GitPlumbingError} - If the command fails or buffer is exceeded.
*/
async execute({
args,
input,
env,
maxBytes = DEFAULT_MAX_BUFFER_SIZE,
traceId = Math.random().toString(36).substring(7),
retryPolicy = CommandRetryPolicy.default()
}) {
return this.orchestrator.orchestrate({
execute: async () => {
const stream = await this.executeStream({ args, input, env });
const stdout = await stream.collect({ maxBytes, asString: true });
const result = await stream.finished;
return { stdout, result };
},
retryPolicy,
args,
traceId
});
}
/**
* Executes a git command asynchronously and returns a universal stream.
* @param {Object} options
* @param {string[]} options.args - Array of git arguments.
* @param {string|Uint8Array} [options.input] - Optional stdin input.
* @param {Object} [options.env] - Optional environment overrides.
* @returns {Promise<GitStream>} - The unified stdout stream wrapper.
* @throws {GitPlumbingError} - If command setup fails.
*/
async executeStream({ args, input, env }) {
this.sanitizer.sanitize(args);
const options = RunnerOptionsSchema.parse({
command: 'git',
args,
cwd: this.cwd,
input,
env
});
try {
const result = await this.runner(options);
return new GitStream(result.stdoutStream, result.exitPromise);
} catch (err) {
if (err instanceof GitPlumbingError) {
throw err;
}
throw new GitPlumbingError(err.message, 'GitPlumbing.executeStream', { args, originalError: err });
}
}
/**
* Executes a git command and returns both stdout and exit status without throwing on non-zero exit.
* @param {Object} options
* @param {string[]} options.args - Array of git arguments.
* @param {number} [options.maxBytes] - Maximum buffer size.
* @returns {Promise<{stdout: string, status: number}>}
*/
async executeWithStatus({ args, maxBytes }) {
const startTime = performance.now();
try {
const stream = await this.executeStream({ args });
const stdout = await stream.collect({ maxBytes, asString: true });
const result = await stream.finished;
return {
stdout: stdout.trim(),
status: result.code || 0,
latency: performance.now() - startTime
};
} catch (err) {
throw new GitPlumbingError(err.message, 'GitPlumbing.executeWithStatus', {
args,
originalError: err,
latency: performance.now() - startTime
});
}
}
/**
* Returns the SHA-1 of the empty tree.
* @returns {string}
*/
get emptyTree() {
return GitSha.EMPTY_TREE_VALUE;
}
}