@git-stunts/git-warp 12.3.0 → 12.4.1
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 +5 -6
- package/bin/cli/commands/info.js +1 -5
- package/bin/cli/infrastructure.js +6 -9
- package/bin/cli/shared.js +8 -0
- package/bin/warp-graph.js +6 -6
- package/package.json +1 -1
- package/src/domain/WarpGraph.js +5 -35
- package/src/domain/crdt/VersionVector.js +1 -1
- package/src/domain/entities/GraphNode.js +1 -6
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/PatchError.js +1 -1
- package/src/domain/errors/PersistenceError.js +45 -0
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +1 -4
- package/src/domain/services/AnchorMessageCodec.js +1 -3
- package/src/domain/services/AuditMessageCodec.js +1 -5
- package/src/domain/services/AuditReceiptService.js +2 -17
- package/src/domain/services/AuditVerifierService.js +2 -7
- package/src/domain/services/BitmapIndexBuilder.js +6 -12
- package/src/domain/services/BitmapIndexReader.js +7 -20
- package/src/domain/services/BitmapNeighborProvider.js +1 -3
- package/src/domain/services/BoundaryTransitionRecord.js +6 -23
- package/src/domain/services/CheckpointMessageCodec.js +1 -6
- package/src/domain/services/CheckpointSerializerV5.js +8 -12
- package/src/domain/services/CheckpointService.js +9 -39
- package/src/domain/services/CommitDagTraversalService.js +1 -3
- package/src/domain/services/DagPathFinding.js +9 -59
- package/src/domain/services/DagTopology.js +4 -16
- package/src/domain/services/DagTraversal.js +7 -31
- package/src/domain/services/Frontier.js +4 -6
- package/src/domain/services/GitLogParser.js +1 -2
- package/src/domain/services/GraphTraversal.js +14 -114
- package/src/domain/services/HealthCheckService.js +3 -9
- package/src/domain/services/HookInstaller.js +2 -8
- package/src/domain/services/HttpSyncServer.js +24 -25
- package/src/domain/services/IncrementalIndexUpdater.js +4 -6
- package/src/domain/services/IndexRebuildService.js +6 -52
- package/src/domain/services/IndexStalenessChecker.js +2 -3
- package/src/domain/services/JoinReducer.js +39 -65
- package/src/domain/services/LogicalBitmapIndexBuilder.js +1 -2
- package/src/domain/services/LogicalIndexBuildService.js +2 -6
- package/src/domain/services/LogicalIndexReader.js +1 -2
- package/src/domain/services/LogicalTraversal.js +13 -64
- package/src/domain/services/MaterializedViewService.js +4 -18
- package/src/domain/services/MigrationService.js +1 -4
- package/src/domain/services/ObserverView.js +1 -7
- package/src/domain/services/PatchBuilderV2.js +6 -18
- package/src/domain/services/PatchMessageCodec.js +1 -6
- package/src/domain/services/PropertyIndexBuilder.js +1 -2
- package/src/domain/services/PropertyIndexReader.js +1 -4
- package/src/domain/services/ProvenanceIndex.js +5 -7
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -16
- package/src/domain/services/StateDiff.js +3 -9
- package/src/domain/services/StateSerializerV5.js +10 -10
- package/src/domain/services/StreamingBitmapIndexBuilder.js +13 -41
- package/src/domain/services/SyncAuthService.js +5 -32
- package/src/domain/services/SyncController.js +5 -25
- package/src/domain/services/SyncProtocol.js +4 -8
- package/src/domain/services/SyncTrustGate.js +4 -9
- package/src/domain/services/TemporalQuery.js +9 -27
- package/src/domain/services/TranslationCost.js +2 -8
- package/src/domain/services/WarpStateIndexBuilder.js +2 -4
- package/src/domain/services/WormholeService.js +9 -25
- package/src/domain/trust/TrustCrypto.js +1 -5
- package/src/domain/trust/TrustEvaluator.js +1 -8
- package/src/domain/trust/TrustRecordService.js +5 -10
- package/src/domain/types/TickReceipt.js +3 -7
- package/src/domain/types/WarpTypes.js +1 -5
- package/src/domain/types/WarpTypesV2.js +1 -8
- package/src/domain/utils/CachedValue.js +1 -4
- package/src/domain/utils/MinHeap.js +3 -3
- package/src/domain/utils/RefLayout.js +26 -0
- package/src/domain/utils/WriterId.js +2 -7
- package/src/domain/utils/canonicalCbor.js +1 -1
- package/src/domain/utils/defaultCodec.js +1 -1
- package/src/domain/utils/parseCursorBlob.js +4 -4
- package/src/domain/warp/PatchSession.js +3 -8
- package/src/domain/warp/Writer.js +3 -12
- package/src/domain/warp/_wire.js +2 -2
- package/src/domain/warp/_wiredMethods.d.ts +5 -7
- package/src/domain/warp/checkpoint.methods.js +1 -1
- package/src/domain/warp/fork.methods.js +1 -5
- package/src/domain/warp/materializeAdvanced.methods.js +3 -3
- package/src/domain/warp/patch.methods.js +6 -8
- package/src/domain/warp/provenance.methods.js +5 -5
- package/src/domain/warp/query.methods.js +9 -18
- package/src/domain/warp/subscribe.methods.js +2 -8
- package/src/globals.d.ts +7 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -18
- package/src/infrastructure/adapters/ConsoleLogger.js +2 -9
- package/src/infrastructure/adapters/DenoHttpAdapter.js +15 -15
- package/src/infrastructure/adapters/GitGraphAdapter.js +234 -58
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +9 -2
- package/src/infrastructure/adapters/NodeHttpAdapter.js +14 -14
- package/src/infrastructure/adapters/WebCryptoAdapter.js +1 -2
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/HttpServerPort.js +24 -2
- package/src/ports/RefPort.js +2 -1
- package/src/visualization/renderers/ascii/box.js +1 -1
- package/src/visualization/renderers/ascii/check.js +1 -5
- package/src/visualization/renderers/ascii/history.js +1 -6
- package/src/visualization/renderers/ascii/path.js +4 -22
- package/src/visualization/renderers/ascii/progress.js +1 -4
- package/src/visualization/renderers/ascii/seek.js +1 -5
- package/src/visualization/renderers/ascii/table.js +1 -3
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { Buffer } from 'node:buffer';
|
|
47
47
|
import { retry } from '@git-stunts/alfred';
|
|
48
|
+
import PersistenceError from '../../domain/errors/PersistenceError.js';
|
|
48
49
|
import GraphPersistencePort from '../../ports/GraphPersistencePort.js';
|
|
49
50
|
import { validateOid, validateRef, validateLimit, validateConfigKey } from './adapterValidation.js';
|
|
50
51
|
|
|
@@ -79,6 +80,17 @@ const TRANSIENT_ERROR_PATTERNS = [
|
|
|
79
80
|
* @typedef {Error & { details?: { stderr?: string, code?: number }, exitCode?: number, code?: number }} GitError
|
|
80
81
|
*/
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {{ collect(opts?: { asString?: boolean }): Promise<Buffer | string> } & import('node:stream').Readable} CollectableStream
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {object} GitPlumbingLike
|
|
89
|
+
* @property {string} emptyTree - The well-known SHA for Git's empty tree
|
|
90
|
+
* @property {(options: { args: string[], input?: string | Buffer }) => Promise<string>} execute - Execute a git command
|
|
91
|
+
* @property {(options: { args: string[] }) => Promise<CollectableStream>} executeStream - Execute a git command returning a stream
|
|
92
|
+
*/
|
|
93
|
+
|
|
82
94
|
/**
|
|
83
95
|
* Determines if an error is transient and safe to retry.
|
|
84
96
|
* @param {GitError} error - The error to check
|
|
@@ -91,10 +103,12 @@ function isTransientError(error) {
|
|
|
91
103
|
return TRANSIENT_ERROR_PATTERNS.some(pattern => searchText.includes(pattern));
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
/** @typedef {import('@git-stunts/alfred').RetryOptions} RetryOptions */
|
|
107
|
+
|
|
94
108
|
/**
|
|
95
109
|
* Default retry options for git operations.
|
|
96
110
|
* Uses exponential backoff with decorrelated jitter.
|
|
97
|
-
* @type {
|
|
111
|
+
* @type {RetryOptions}
|
|
98
112
|
*/
|
|
99
113
|
const DEFAULT_RETRY_OPTIONS = {
|
|
100
114
|
retries: 3,
|
|
@@ -135,6 +149,122 @@ function isDanglingObjectError(err) {
|
|
|
135
149
|
);
|
|
136
150
|
}
|
|
137
151
|
|
|
152
|
+
/** @type {string[]} Stderr/message patterns indicating a missing Git object. */
|
|
153
|
+
const MISSING_OBJECT_PATTERNS = [
|
|
154
|
+
'bad object',
|
|
155
|
+
'not a valid object name',
|
|
156
|
+
'does not point to a valid object',
|
|
157
|
+
'missing object',
|
|
158
|
+
'not a commit',
|
|
159
|
+
'could not read',
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/** @type {string[]} Stderr/message patterns indicating a ref was not found. */
|
|
163
|
+
const REF_NOT_FOUND_PATTERNS = [
|
|
164
|
+
'not found',
|
|
165
|
+
'does not exist',
|
|
166
|
+
'unknown revision',
|
|
167
|
+
'bad revision',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/** @type {string[]} Stderr/message patterns indicating a ref I/O failure. */
|
|
171
|
+
const REF_IO_PATTERNS = [
|
|
172
|
+
'cannot lock ref',
|
|
173
|
+
'unable to create',
|
|
174
|
+
'permission denied',
|
|
175
|
+
'failed to lock',
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Builds a combined search string from an error's message and stderr.
|
|
180
|
+
* @param {GitError} err
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
function errorSearchText(err) {
|
|
184
|
+
const message = (err.message || '').toLowerCase();
|
|
185
|
+
const stderr = (err.details?.stderr || '').toLowerCase();
|
|
186
|
+
return `${message} ${stderr}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a Git error indicates a missing object (commit, blob, tree).
|
|
191
|
+
* Covers exit code 128 with object-related stderr patterns.
|
|
192
|
+
* @param {GitError} err
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function isMissingObjectError(err) {
|
|
196
|
+
const code = getExitCode(err);
|
|
197
|
+
if (code !== 128 && code !== 1) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const text = errorSearchText(err);
|
|
201
|
+
return MISSING_OBJECT_PATTERNS.some(p => text.includes(p));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Checks if a Git error indicates a ref not found condition.
|
|
206
|
+
* Covers patterns like "not found", "does not exist", "unknown revision".
|
|
207
|
+
* Gated on exit codes 1 (rev-parse --verify --quiet) and 128 (fatal).
|
|
208
|
+
* @param {GitError} err
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
function isRefNotFoundError(err) {
|
|
212
|
+
const code = getExitCode(err);
|
|
213
|
+
if (code !== 128 && code !== 1) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const text = errorSearchText(err);
|
|
217
|
+
return REF_NOT_FOUND_PATTERNS.some(p => text.includes(p));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Checks if a Git error indicates a ref I/O failure
|
|
222
|
+
* (lock contention that exhausted retries, permission errors, etc.).
|
|
223
|
+
* Gated on exit code 128 (fatal).
|
|
224
|
+
* @param {GitError} err
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
function isRefIoError(err) {
|
|
228
|
+
if (getExitCode(err) !== 128) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const text = errorSearchText(err);
|
|
232
|
+
return REF_IO_PATTERNS.some(p => text.includes(p));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Wraps a raw Git error in a typed PersistenceError when the failure
|
|
237
|
+
* matches a known pattern. Returns the original error unchanged if
|
|
238
|
+
* no pattern matches.
|
|
239
|
+
* @param {GitError} err - The raw error from Git plumbing
|
|
240
|
+
* @param {{ ref?: string, oid?: string }} [hint={}] - Optional context hints
|
|
241
|
+
* @returns {PersistenceError|GitError}
|
|
242
|
+
*/
|
|
243
|
+
function wrapGitError(err, hint = {}) {
|
|
244
|
+
if (isMissingObjectError(err)) {
|
|
245
|
+
return new PersistenceError(
|
|
246
|
+
hint.oid ? `Missing Git object: ${hint.oid}` : err.message,
|
|
247
|
+
PersistenceError.E_MISSING_OBJECT,
|
|
248
|
+
{ cause: /** @type {Error} */ (err), context: { ...hint } },
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (isRefNotFoundError(err)) {
|
|
252
|
+
return new PersistenceError(
|
|
253
|
+
hint.ref ? `Ref not found: ${hint.ref}` : err.message,
|
|
254
|
+
PersistenceError.E_REF_NOT_FOUND,
|
|
255
|
+
{ cause: /** @type {Error} */ (err), context: { ...hint } },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (isRefIoError(err)) {
|
|
259
|
+
return new PersistenceError(
|
|
260
|
+
hint.ref ? `Ref I/O error: ${hint.ref}` : err.message,
|
|
261
|
+
PersistenceError.E_REF_IO,
|
|
262
|
+
{ cause: /** @type {Error} */ (err), context: { ...hint } },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return err;
|
|
266
|
+
}
|
|
267
|
+
|
|
138
268
|
/**
|
|
139
269
|
* Concrete implementation of {@link GraphPersistencePort} using Git plumbing commands.
|
|
140
270
|
*
|
|
@@ -200,7 +330,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
200
330
|
/**
|
|
201
331
|
* Creates a new GitGraphAdapter instance.
|
|
202
332
|
*
|
|
203
|
-
* @param {{ plumbing:
|
|
333
|
+
* @param {{ plumbing: GitPlumbingLike, retryOptions?: Partial<RetryOptions> }} options - Configuration options
|
|
204
334
|
*
|
|
205
335
|
* @throws {Error} If plumbing is not provided
|
|
206
336
|
*
|
|
@@ -221,7 +351,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
221
351
|
|
|
222
352
|
/**
|
|
223
353
|
* Executes a git command with retry logic.
|
|
224
|
-
* @param {
|
|
354
|
+
* @param {{ args: string[], input?: string | Buffer }} options - Options to pass to plumbing.execute
|
|
225
355
|
* @returns {Promise<string>} Command output
|
|
226
356
|
* @private
|
|
227
357
|
*/
|
|
@@ -259,10 +389,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
259
389
|
|
|
260
390
|
/**
|
|
261
391
|
* Creates a commit pointing to the empty tree.
|
|
262
|
-
* @param {
|
|
263
|
-
* @param {string} options.message - The commit message (typically CBOR-encoded patch data)
|
|
264
|
-
* @param {string[]} [options.parents=[]] - Parent commit SHAs
|
|
265
|
-
* @param {boolean} [options.sign=false] - Whether to GPG-sign the commit
|
|
392
|
+
* @param {{ message: string, parents?: string[], sign?: boolean }} options
|
|
266
393
|
* @returns {Promise<string>} The SHA of the created commit
|
|
267
394
|
* @throws {Error} If any parent OID is invalid
|
|
268
395
|
*/
|
|
@@ -273,11 +400,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
273
400
|
/**
|
|
274
401
|
* Creates a commit pointing to a custom tree (not the empty tree).
|
|
275
402
|
* Used for WARP patch commits that have attachment trees.
|
|
276
|
-
* @param {
|
|
277
|
-
* @param {string} options.treeOid - The tree OID to point to
|
|
278
|
-
* @param {string[]} [options.parents=[]] - Parent commit SHAs
|
|
279
|
-
* @param {string} options.message - Commit message
|
|
280
|
-
* @param {boolean} [options.sign=false] - Whether to GPG sign
|
|
403
|
+
* @param {{ treeOid: string, parents?: string[], message: string, sign?: boolean }} options
|
|
281
404
|
* @returns {Promise<string>} The created commit SHA
|
|
282
405
|
*/
|
|
283
406
|
async commitNodeWithTree({ treeOid, parents = [], message, sign = false }) {
|
|
@@ -293,7 +416,11 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
293
416
|
*/
|
|
294
417
|
async showNode(sha) {
|
|
295
418
|
this._validateOid(sha);
|
|
296
|
-
|
|
419
|
+
try {
|
|
420
|
+
return await this._executeWithRetry({ args: ['show', '-s', '--format=%B', sha] });
|
|
421
|
+
} catch (err) {
|
|
422
|
+
throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
|
|
423
|
+
}
|
|
297
424
|
}
|
|
298
425
|
|
|
299
426
|
/**
|
|
@@ -308,13 +435,24 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
308
435
|
// Format: SHA, author, date, parents (space-separated), then message
|
|
309
436
|
// Using %x00 to separate fields for reliable parsing
|
|
310
437
|
const format = '%H%x00%an <%ae>%x00%aI%x00%P%x00%B';
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
438
|
+
let output;
|
|
439
|
+
try {
|
|
440
|
+
output = await this._executeWithRetry({
|
|
441
|
+
args: ['show', '-s', `--format=${format}`, sha]
|
|
442
|
+
});
|
|
443
|
+
} catch (err) {
|
|
444
|
+
throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
|
|
445
|
+
}
|
|
314
446
|
|
|
315
447
|
const parts = output.split('\x00');
|
|
316
448
|
if (parts.length < 5) {
|
|
317
|
-
|
|
449
|
+
// Object exists but output is malformed — semantically closest to
|
|
450
|
+
// E_MISSING_OBJECT since the commit is unusable for data extraction.
|
|
451
|
+
throw new PersistenceError(
|
|
452
|
+
`Invalid commit format for SHA ${sha}`,
|
|
453
|
+
PersistenceError.E_MISSING_OBJECT,
|
|
454
|
+
{ context: { oid: sha } },
|
|
455
|
+
);
|
|
318
456
|
}
|
|
319
457
|
|
|
320
458
|
const [commitSha, author, date, parentsStr, ...messageParts] = parts;
|
|
@@ -338,18 +476,19 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
338
476
|
*/
|
|
339
477
|
async getCommitTree(sha) {
|
|
340
478
|
this._validateOid(sha);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
479
|
+
try {
|
|
480
|
+
const output = await this._executeWithRetry({
|
|
481
|
+
args: ['rev-parse', `${sha}^{tree}`]
|
|
482
|
+
});
|
|
483
|
+
return output.trim();
|
|
484
|
+
} catch (err) {
|
|
485
|
+
throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
|
|
486
|
+
}
|
|
345
487
|
}
|
|
346
488
|
|
|
347
489
|
/**
|
|
348
490
|
* Returns raw git log output for a ref.
|
|
349
|
-
* @param {
|
|
350
|
-
* @param {string} options.ref - The Git ref to log from
|
|
351
|
-
* @param {number} [options.limit=50] - Maximum number of commits to return
|
|
352
|
-
* @param {string} [options.format] - Custom format string for git log
|
|
491
|
+
* @param {{ ref: string, limit?: number, format?: string }} options
|
|
353
492
|
* @returns {Promise<string>} The raw log output
|
|
354
493
|
* @throws {Error} If the ref is invalid or the limit is out of range
|
|
355
494
|
*/
|
|
@@ -361,7 +500,11 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
361
500
|
args.push(`--format=${format}`);
|
|
362
501
|
}
|
|
363
502
|
args.push(ref);
|
|
364
|
-
|
|
503
|
+
try {
|
|
504
|
+
return await this._executeWithRetry({ args });
|
|
505
|
+
} catch (err) {
|
|
506
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref });
|
|
507
|
+
}
|
|
365
508
|
}
|
|
366
509
|
|
|
367
510
|
/**
|
|
@@ -369,10 +512,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
369
512
|
* Uses the -z flag to produce NUL-terminated output, which:
|
|
370
513
|
* - Ensures reliable parsing of commits with special characters in messages
|
|
371
514
|
* - Ignores the i18n.logOutputEncoding config setting for consistent output
|
|
372
|
-
* @param {
|
|
373
|
-
* @param {string} options.ref - The ref to log from
|
|
374
|
-
* @param {number} [options.limit=1000000] - Maximum number of commits to return
|
|
375
|
-
* @param {string} [options.format] - Custom format string for git log
|
|
515
|
+
* @param {{ ref: string, limit?: number, format?: string }} options
|
|
376
516
|
* @returns {Promise<import('node:stream').Readable>} A readable stream of git log output (NUL-terminated records)
|
|
377
517
|
* @throws {Error} If the ref is invalid or the limit is out of range
|
|
378
518
|
*/
|
|
@@ -469,9 +609,14 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
469
609
|
*/
|
|
470
610
|
async readTreeOids(treeOid) {
|
|
471
611
|
this._validateOid(treeOid);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
612
|
+
let output;
|
|
613
|
+
try {
|
|
614
|
+
output = await this._executeWithRetry({
|
|
615
|
+
args: ['ls-tree', '-r', '-z', treeOid]
|
|
616
|
+
});
|
|
617
|
+
} catch (err) {
|
|
618
|
+
throw wrapGitError(/** @type {GitError} */ (err), { oid: treeOid });
|
|
619
|
+
}
|
|
475
620
|
|
|
476
621
|
/** @type {Record<string, string>} */
|
|
477
622
|
const oids = {};
|
|
@@ -502,12 +647,16 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
502
647
|
*/
|
|
503
648
|
async readBlob(oid) {
|
|
504
649
|
this._validateOid(oid);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
650
|
+
try {
|
|
651
|
+
const stream = await this.plumbing.executeStream({
|
|
652
|
+
args: ['cat-file', 'blob', oid]
|
|
653
|
+
});
|
|
654
|
+
const raw = await stream.collect({ asString: false });
|
|
655
|
+
// Ensure a real Node Buffer (plumbing may return Uint8Array)
|
|
656
|
+
return Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
throw wrapGitError(/** @type {GitError} */ (err), { oid });
|
|
659
|
+
}
|
|
511
660
|
}
|
|
512
661
|
|
|
513
662
|
/**
|
|
@@ -520,9 +669,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
520
669
|
async updateRef(ref, oid) {
|
|
521
670
|
this._validateRef(ref);
|
|
522
671
|
this._validateOid(oid);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
672
|
+
try {
|
|
673
|
+
await this._executeWithRetry({
|
|
674
|
+
args: ['update-ref', ref, oid]
|
|
675
|
+
});
|
|
676
|
+
} catch (err) {
|
|
677
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref, oid });
|
|
678
|
+
}
|
|
526
679
|
}
|
|
527
680
|
|
|
528
681
|
/**
|
|
@@ -551,7 +704,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
551
704
|
if (isDanglingObjectError(gitErr)) {
|
|
552
705
|
return null;
|
|
553
706
|
}
|
|
554
|
-
throw
|
|
707
|
+
throw wrapGitError(gitErr, { ref });
|
|
555
708
|
}
|
|
556
709
|
}
|
|
557
710
|
|
|
@@ -576,9 +729,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
576
729
|
this._validateOid(expectedOid);
|
|
577
730
|
}
|
|
578
731
|
// Direct call — CAS failures are semantically expected and must NOT be retried.
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
732
|
+
try {
|
|
733
|
+
await this.plumbing.execute({
|
|
734
|
+
args: ['update-ref', ref, newOid, oldArg],
|
|
735
|
+
});
|
|
736
|
+
} catch (err) {
|
|
737
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref, oid: newOid });
|
|
738
|
+
}
|
|
582
739
|
}
|
|
583
740
|
|
|
584
741
|
/**
|
|
@@ -589,9 +746,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
589
746
|
*/
|
|
590
747
|
async deleteRef(ref) {
|
|
591
748
|
this._validateRef(ref);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
749
|
+
try {
|
|
750
|
+
await this._executeWithRetry({
|
|
751
|
+
args: ['update-ref', '-d', ref]
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref });
|
|
755
|
+
}
|
|
595
756
|
}
|
|
596
757
|
|
|
597
758
|
/**
|
|
@@ -645,14 +806,25 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
645
806
|
/**
|
|
646
807
|
* Lists refs matching a prefix.
|
|
647
808
|
* @param {string} prefix - The ref prefix to match (e.g., 'refs/warp/events/writers/')
|
|
809
|
+
* @param {{ limit?: number }} [options] - Optional parameters. When `limit` is omitted or 0, all matching refs are returned.
|
|
648
810
|
* @returns {Promise<string[]>} Array of matching ref paths
|
|
649
|
-
* @throws {Error} If the prefix is invalid
|
|
811
|
+
* @throws {Error} If the prefix is invalid or the limit is out of range
|
|
650
812
|
*/
|
|
651
|
-
async listRefs(prefix) {
|
|
813
|
+
async listRefs(prefix, options) {
|
|
652
814
|
this._validateRef(prefix);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
815
|
+
const limit = options?.limit;
|
|
816
|
+
const args = ['for-each-ref', '--format=%(refname)'];
|
|
817
|
+
if (limit) {
|
|
818
|
+
this._validateLimit(limit);
|
|
819
|
+
args.push(`--count=${limit}`);
|
|
820
|
+
}
|
|
821
|
+
args.push(prefix);
|
|
822
|
+
let output;
|
|
823
|
+
try {
|
|
824
|
+
output = await this._executeWithRetry({ args });
|
|
825
|
+
} catch (err) {
|
|
826
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref: prefix });
|
|
827
|
+
}
|
|
656
828
|
// Parse output - one ref per line, filter empty lines
|
|
657
829
|
return output.split('\n').filter(line => line.trim());
|
|
658
830
|
}
|
|
@@ -687,10 +859,14 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
687
859
|
*/
|
|
688
860
|
async countNodes(ref) {
|
|
689
861
|
this._validateRef(ref);
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
862
|
+
try {
|
|
863
|
+
const output = await this._executeWithRetry({
|
|
864
|
+
args: ['rev-list', '--count', ref]
|
|
865
|
+
});
|
|
866
|
+
return parseInt(output.trim(), 10);
|
|
867
|
+
} catch (err) {
|
|
868
|
+
throw wrapGitError(/** @type {GitError} */ (err), { ref });
|
|
869
|
+
}
|
|
694
870
|
}
|
|
695
871
|
|
|
696
872
|
/**
|
|
@@ -394,9 +394,10 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
|
|
|
394
394
|
|
|
395
395
|
/**
|
|
396
396
|
* @param {string} prefix
|
|
397
|
+
* @param {{ limit?: number }} [options]
|
|
397
398
|
* @returns {Promise<string[]>}
|
|
398
399
|
*/
|
|
399
|
-
async listRefs(prefix) {
|
|
400
|
+
async listRefs(prefix, options) {
|
|
400
401
|
validateRef(prefix);
|
|
401
402
|
const result = [];
|
|
402
403
|
for (const key of this._refs.keys()) {
|
|
@@ -404,7 +405,13 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
|
|
|
404
405
|
result.push(key);
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
|
-
|
|
408
|
+
const sorted = result.sort();
|
|
409
|
+
const limit = options?.limit;
|
|
410
|
+
if (limit) {
|
|
411
|
+
validateLimit(limit);
|
|
412
|
+
return sorted.slice(0, limit);
|
|
413
|
+
}
|
|
414
|
+
return sorted;
|
|
408
415
|
}
|
|
409
416
|
|
|
410
417
|
// ── ConfigPort ──────────────────────────────────────────────────────
|
|
@@ -9,7 +9,7 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
|
9
9
|
* a 500 response if the handler throws.
|
|
10
10
|
* @param {import('node:http').IncomingMessage} req
|
|
11
11
|
* @param {import('node:http').ServerResponse} res
|
|
12
|
-
* @param {{ handler:
|
|
12
|
+
* @param {{ handler: (request: import('../../ports/HttpServerPort.js').HttpRequest) => Promise<import('../../ports/HttpServerPort.js').HttpResponse>, logger: { error: (...args: unknown[]) => void } }} options
|
|
13
13
|
*/
|
|
14
14
|
async function dispatch(req, res, { handler, logger }) {
|
|
15
15
|
try {
|
|
@@ -27,12 +27,12 @@ async function dispatch(req, res, { handler, logger }) {
|
|
|
27
27
|
}
|
|
28
28
|
const body = Buffer.concat(chunks);
|
|
29
29
|
|
|
30
|
-
const response = await handler({
|
|
31
|
-
method: req.method,
|
|
32
|
-
url: req.url,
|
|
33
|
-
headers: req.headers,
|
|
30
|
+
const response = await handler(/** @type {import('../../ports/HttpServerPort.js').HttpRequest} */ ({
|
|
31
|
+
method: req.method || 'GET',
|
|
32
|
+
url: req.url || '/',
|
|
33
|
+
headers: /** @type {Record<string, string>} */ (req.headers),
|
|
34
34
|
body: body.length > 0 ? body : undefined,
|
|
35
|
-
});
|
|
35
|
+
}));
|
|
36
36
|
|
|
37
37
|
res.writeHead(response.status || 200, response.headers || {});
|
|
38
38
|
res.end(response.body);
|
|
@@ -56,7 +56,7 @@ const noopLogger = { error() {} };
|
|
|
56
56
|
*/
|
|
57
57
|
export default class NodeHttpAdapter extends HttpServerPort {
|
|
58
58
|
/**
|
|
59
|
-
* @param {{ logger?: { error:
|
|
59
|
+
* @param {{ logger?: { error: (...args: unknown[]) => void } }} [options]
|
|
60
60
|
*/
|
|
61
61
|
constructor({ logger } = {}) {
|
|
62
62
|
super();
|
|
@@ -64,8 +64,8 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* @param {
|
|
68
|
-
* @returns {
|
|
67
|
+
* @param {(request: import('../../ports/HttpServerPort.js').HttpRequest) => Promise<import('../../ports/HttpServerPort.js').HttpResponse>} requestHandler
|
|
68
|
+
* @returns {import('../../ports/HttpServerPort.js').HttpServerHandle}
|
|
69
69
|
*/
|
|
70
70
|
createServer(requestHandler) {
|
|
71
71
|
const logger = this._logger;
|
|
@@ -79,8 +79,8 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
79
79
|
return {
|
|
80
80
|
/**
|
|
81
81
|
* @param {number} port
|
|
82
|
-
* @param {string|
|
|
83
|
-
* @param {
|
|
82
|
+
* @param {string|((err?: Error | null) => void)} [host]
|
|
83
|
+
* @param {(err?: Error | null) => void} [callback]
|
|
84
84
|
*/
|
|
85
85
|
listen(port, host, callback) {
|
|
86
86
|
const cb = typeof host === 'function' ? host : callback;
|
|
@@ -88,7 +88,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
88
88
|
/** @param {unknown} err */
|
|
89
89
|
const onError = (err) => {
|
|
90
90
|
if (cb) {
|
|
91
|
-
cb(err);
|
|
91
|
+
cb(err instanceof Error ? err : new Error(String(err)));
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
server.once('error', onError);
|
|
@@ -108,12 +108,12 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
|
-
/** @param {(
|
|
111
|
+
/** @param {(err?: Error) => void} [callback] */
|
|
112
112
|
close(callback) {
|
|
113
113
|
server.close(callback);
|
|
114
114
|
},
|
|
115
115
|
address() {
|
|
116
|
-
return server.address();
|
|
116
|
+
return /** @type {{ address: string, port: number, family: string } | null} */ (server.address());
|
|
117
117
|
},
|
|
118
118
|
};
|
|
119
119
|
}
|
|
@@ -70,8 +70,7 @@ function bufToHex(buf) {
|
|
|
70
70
|
export default class WebCryptoAdapter extends CryptoPort {
|
|
71
71
|
/**
|
|
72
72
|
* Creates a new WebCryptoAdapter.
|
|
73
|
-
* @param {
|
|
74
|
-
* @param {SubtleCrypto} [options.subtle] - SubtleCrypto instance (defaults to globalThis.crypto.subtle)
|
|
73
|
+
* @param {{ subtle?: SubtleCrypto }} [options] - Configuration options
|
|
75
74
|
*/
|
|
76
75
|
constructor({ subtle } = {}) {
|
|
77
76
|
super();
|
package/src/ports/BlobPort.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export default class BlobPort {
|
|
11
11
|
/**
|
|
12
12
|
* Writes content as a Git blob and returns its OID.
|
|
13
|
-
* @param {Uint8Array|
|
|
13
|
+
* @param {Uint8Array | string} _content - The blob content to write
|
|
14
14
|
* @returns {Promise<string>} The Git OID of the created blob
|
|
15
15
|
* @throws {Error} If not implemented by a concrete adapter
|
|
16
16
|
*/
|
|
@@ -21,7 +21,7 @@ export default class BlobPort {
|
|
|
21
21
|
/**
|
|
22
22
|
* Reads the content of a Git blob.
|
|
23
23
|
* @param {string} _oid - The blob OID to read
|
|
24
|
-
* @returns {Promise<
|
|
24
|
+
* @returns {Promise<Uint8Array>} The blob content
|
|
25
25
|
* @throws {Error} If not implemented by a concrete adapter
|
|
26
26
|
*/
|
|
27
27
|
async readBlob(_oid) {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} HttpRequest
|
|
3
|
+
* @property {string} method - HTTP method (GET, POST, etc.)
|
|
4
|
+
* @property {string} url - Request URL path + query string
|
|
5
|
+
* @property {Record<string, string>} headers - Lowercased header map
|
|
6
|
+
* @property {Buffer | Uint8Array | undefined} body - Raw body bytes (undefined for bodiless requests)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} HttpResponse
|
|
11
|
+
* @property {number} [status] - HTTP status code (defaults to 200)
|
|
12
|
+
* @property {Record<string, string>} [headers] - Response headers
|
|
13
|
+
* @property {string | Buffer | Uint8Array | null} [body] - Response body
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} HttpServerHandle
|
|
18
|
+
* @property {(port: number, host?: string | ((err?: Error | null) => void), callback?: (err?: Error | null) => void) => void} listen
|
|
19
|
+
* @property {(callback?: (err?: Error) => void) => void} close
|
|
20
|
+
* @property {() => { address: string, port: number, family: string } | null} address
|
|
21
|
+
*/
|
|
22
|
+
|
|
1
23
|
/**
|
|
2
24
|
* Port for HTTP server creation.
|
|
3
25
|
*
|
|
@@ -12,8 +34,8 @@ export default class HttpServerPort {
|
|
|
12
34
|
* and must return `{ status, headers, body }`. No raw req/res objects
|
|
13
35
|
* are exposed to the domain.
|
|
14
36
|
*
|
|
15
|
-
* @param {(request:
|
|
16
|
-
* @returns {
|
|
37
|
+
* @param {(request: HttpRequest) => Promise<HttpResponse>} _requestHandler - Async function (request) => response
|
|
38
|
+
* @returns {HttpServerHandle} Server with listen(port, [host], cb(err)), close(cb), and address()
|
|
17
39
|
*/
|
|
18
40
|
createServer(_requestHandler) {
|
|
19
41
|
throw new Error('HttpServerPort.createServer() not implemented');
|
package/src/ports/RefPort.js
CHANGED
|
@@ -42,10 +42,11 @@ export default class RefPort {
|
|
|
42
42
|
/**
|
|
43
43
|
* Lists refs matching a prefix.
|
|
44
44
|
* @param {string} _prefix - The ref prefix to match (e.g., 'refs/warp/events/writers/')
|
|
45
|
+
* @param {{ limit?: number }} [_options] - Optional parameters. When `limit` is omitted or 0, all matching refs are returned.
|
|
45
46
|
* @returns {Promise<string[]>} Array of matching ref names
|
|
46
47
|
* @throws {Error} If not implemented by a concrete adapter
|
|
47
48
|
*/
|
|
48
|
-
async listRefs(_prefix) {
|
|
49
|
+
async listRefs(_prefix, _options) {
|
|
49
50
|
throw new Error('RefPort.listRefs() not implemented');
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -4,7 +4,7 @@ import boxen from 'boxen';
|
|
|
4
4
|
* Wraps content in a bordered box using boxen.
|
|
5
5
|
*
|
|
6
6
|
* @param {string} content - The text content to display inside the box
|
|
7
|
-
* @param {
|
|
7
|
+
* @param {Record<string, unknown>} [options] - Options forwarded to boxen (e.g. title, borderColor)
|
|
8
8
|
* @returns {string} The boxed content string
|
|
9
9
|
*/
|
|
10
10
|
export function createBox(content, options = {}) {
|
|
@@ -227,11 +227,7 @@ function buildStateLines(status, gc) {
|
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* Build the metadata section lines (writers, checkpoint, coverage, hooks).
|
|
230
|
-
* @param {
|
|
231
|
-
* @param {WritersInfo} opts.writers - Writers info
|
|
232
|
-
* @param {CheckpointInfo} opts.checkpoint - Checkpoint info
|
|
233
|
-
* @param {CoverageInfo} opts.coverage - Coverage info
|
|
234
|
-
* @param {HookInfo | null} opts.hook - Hook status
|
|
230
|
+
* @param {{ writers: WritersInfo, checkpoint: CheckpointInfo, coverage: CoverageInfo, hook: HookInfo | null }} opts - Metadata options
|
|
235
231
|
* @returns {string[]}
|
|
236
232
|
*/
|
|
237
233
|
function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
|
|
@@ -67,12 +67,7 @@ function renderTruncationIndicator(truncated, hiddenCount) {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Renders a single patch entry line.
|
|
70
|
-
* @param {
|
|
71
|
-
* @param {PatchEntry} params.entry - Patch entry
|
|
72
|
-
* @param {boolean} params.isLast - Whether this is the last entry
|
|
73
|
-
* @param {number} params.lamportWidth - Width for lamport timestamp padding
|
|
74
|
-
* @param {string} [params.writerStr] - Writer string
|
|
75
|
-
* @param {number} [params.maxWriterIdLen] - Max writer ID length for padding
|
|
70
|
+
* @param {{ entry: PatchEntry, isLast: boolean, lamportWidth: number, writerStr?: string, maxWriterIdLen?: number }} params - Entry parameters
|
|
76
71
|
* @returns {string} Formatted entry line
|
|
77
72
|
*/
|
|
78
73
|
function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {
|